-
fanliangqin
Action.requireRecepient、Action.sendInline和Transaction.send
在Ultrain的合约体系中,我们是没有办法将其它合约的代码嵌入到当前代码中来执行的,但是这并不意味不能执行其它合约中的代码。我们提供了三个方法,允许你从自己的合约中调用其它合约的代码:Action.requireRecepient(),Action.sendInline(),Transaction.send()。这三个方法可以提供不同的调用其它合约的方法。
Action.requireRecepient
从这个方法的名字中我们也可以看出来,这是一个通知。它的原型是
Action.requireRecepient(to: account_name): void
这个方法被调用时,to
合约上部署的同名方法将被调用。假如我们编写了含有以下方法的两个合约:
合约1://... @action recepient(name: string): void { Log.s("hi, it is ").s(RNAME(this.receiver)).s(", I will call recepient with parameter: ").s(name).flush(); Action.requireRecipient(NAME("jack")); }
我们将这个合约部署到帐号
rose
上。合约2:
//... @action recepient(name: string): void { Action.requireAuth(NAME("rose")); Log.s("hi, it is ").s(RNAME(this.receiver)).s(", recepient was called with parameter: ").s(name).flush(); }
这个合约部署到了
jack
上。这样我们就可以发起一笔交易来测试一下:
clultrain push action rose recepient '["messi"]' -p rose
如果一切正常的话,会产生以下输出
executed transaction: 6d2cfdd6fa9de76f00b7f8a46eeafa7acef955f8890e5976ab4a42e0ac31ae8d 112 bytes 848 us # rose <= rose::recepient {"name":"messi"} >> hi, it is rose, I will call recepient with parameter: messi # jack <= rose::recepient {"name":"messi"} >> hi, it is jack, recepient was called with parameter: messi
从上面的小示例我们可以看到以下几个事实:
- jack的++同名方法recepient++也被调用了。
- 传递的参数和交易发起时的参数
messi
是一致,不需要明确的传递这个参数。 - rose和jack的recepient方法都在同一个transaction里被执行了。
- rose和jack的recepient方法都具有rose的权限。
Action.sendInline
从Action.requireRecepient()的测试结果中,我们看到,requireRecepient()方法只能用相同的参数调用同名方法,这个在很多时候是很受限制的。所以,我们需要另一种方式,能够调用任意的方法。
这个新的方法就是Action.sendInline(),它允许我们调用我们想调用的任意方法。同样的,我们用例子来说明。rose帐号的合约:
//... @action inline(name: string): void { Log.s("hi, it is ").s(RNAME(this.receiver)).s(", I will call sendInline with parameter: ").s(name).flush(); let pl = new PermissionLevel(this.receiver, NAME("active")); let params = new Parameters(); params.name = "messi"; Action.sendInline([pl], NAME("jack"), NEX("onInline"), params); } //...
jack帐号的合约:
//... @action onInline(name: string): void { Action.requireAuth(NAME("rose")); Log.s("hi, it is ").s(RNAME(this.receiver)).s(", onInline was called with parameter: ").s(name).flush(); } //...
代码编译完之后,分别将它们部署到rose和jack帐号上。然后执行下面的命令:
clultrain push action rose inline '["cr7"]' -p rose
成功执行之后,将会产生以下输出:executed transaction: 6a65a2ee39d35e469d2ea21e36665547090d2ee9795994511d3e17d48b500131 112 bytes 821 us # rose <= rose::inline {"name":"cr7"} >> hi, it is rose, I will call sendInline with parameter: cr7 # jack <= jack::onInline {"name":"messi"} >> hi, it is jack, onInline was called with parameter: messi
我们可以得到以下事实:
- rose的合约中可以调用jack任意的方法。
- 调用方法时,可以传递任意参数。
- jack中的方法被调用时,具有发起时一样的权限(rose)。
- 交易在同一个transaction中被执行。
Transaction.send
前面我们介绍了Action.requireRecepient()和Action.sendInline()的使用方法和它们的特点,其中一条就是它们都在++同一个transaction中++被执行,这也就意味着,整个执行链条上如果有一个action失败了,那么整个transaction也就失败了。有些情况下,我们并不想所有的actions作为一个事务处理,这时候我们就需要Transaction.send()。
我们来演示一下这个方法是怎么使用的。rose的合约:
//... @action deferred(name: string): void { Log.s("hi, it is ").s(RNAME(this.receiver)).s(", I will call Tx.send deferred with parameter: ").s(name).flush(); let p = new Parameters(); p.name = name; let act = new ActionImpl(); act.account = NAME("jack"); act.name = NEX("onDeferred"); act.data = SerializableToArray(p); act.authorization.push(new PermissionLevel(this.receiver, NAME("active"))); let tx = new Transaction(0); tx.actions.push(act); tx.header.delay_sec = 5; tx.send(1111, this.receiver, false); } //...
jack的合约:
//... @action onDeferred(name: string): void { Action.requireAuth(NAME("rose")); Log.s("hi, it is ").s(RNAME(this.receiver)).s(", onDeferred was called with parameter: ").s(name).flush(); } //...
把合约部署到链上之后,我们执行一下rose的deferred方法(需要将rose的active权限代理给utrio.code,否则这个方法执行时会失败,设置代理的命令参考
clultrain set account permission rose active '{"threshold": 1,"keys": [{"key":"pubkey_of_rose","weight": 1}],"accounts": [{"permission":{"actor":"rose","permission":"utrio.code"},"weight":1}]}' owner -p rose
):
clultrain push action rose deferred '["henry"]' -p rose
)这时候我们发现产生的返回信息:
executed transaction: 386b8c647cf6812586e7d7c2a711482eb5b9b25851f78d2a608932ffc513ffe5 152 bytes 1522 us # rose <= rose::deferred {"name":"henry"} >> hi, it is rose, I will call Tx.send deferred with parameter: henry
这里并没有jack合约中打印的log信息啊,log到哪里去了呢?如果我们可以看到节点的log的话,会发现有这样的log:
[(jack,onDeferred)->jack]: CONSOLE OUTPUT BEGIN ===================== hi, it is jack, onDeferred was called with parameter: henry [(jack,onDeferred)->jack]: CONSOLE OUTPUT END =====================
这也说明,这个action后面被执行了。
Transaction.send具有以下特性:- Transaction.send()可以调用jack任意的方法。
- 调用方法时,可以传递任意参数。
- jack中的方法被调用时,具有发起时一样的权限(rose)。
- 交易在不同的transaction中被执行。
总结
通过上面三个方法的执行结果对比,我们可以做一个关于它们的小总结:
方法 调用对方的方法 参数 权限 是否事务性质 Action.requireRecepient 同名方法 相同参数 和发起方权限一致 是的 Action.sendInline 任意方法 任意参数 和发起方权限一致 是的 Transaction.send 任意方法 任意参数 和发起方权限一致 不是 源码
上面我们提供的是代码片断,下面我们附上完整的源码,供大家参考。
rose合约的源码:
import "allocator/arena"; import { Log } from "../../../src/log"; import { Contract } from "../../../src/contract"; import { NAME, RNAME } from "../../../src/account"; import { Action, ActionImpl, SerializableToArray } from "../../../src/action"; import { PermissionLevel } from "../../../src/permission-level"; import { NEX } from "../../../lib/name_ex"; import { Transaction, OnErrorValue } from "../../../src/transaction"; class Parameters implements Serializable { name: string; } class SourceContract extends Contract { @action recepient(name: string): void { Log.s("hi, it is ").s(RNAME(this.receiver)).s(", I will call recepient with parameter: ").s(name).flush(); Action.requireRecipient(NAME("jack")); } @action inline(name: string): void { Log.s("hi, it is ").s(RNAME(this.receiver)).s(", I will call sendInline with parameter: ").s(name).flush(); let pl = new PermissionLevel(this.receiver, NAME("active")); let params = new Parameters(); params.name = "messi"; Action.sendInline([pl], NAME("jack"), NEX("onInline"), params); } @action deferred(name: string): void { Log.s("hi, it is ").s(RNAME(this.receiver)).s(", I will call Tx.send deferred with parameter: ").s(name).flush(); let p = new Parameters(); p.name = name; let act = new ActionImpl(); act.account = NAME("jack"); act.name = NEX("onDeferred"); act.data = SerializableToArray(p); act.authorization.push(new PermissionLevel(this.receiver, NAME("active"))); let tx = new Transaction(0); tx.actions.push(act); tx.header.delay_sec = 5; tx.send(1111, this.receiver, false); } public onError(): void { let error = OnErrorValue.fromCurrentAction(); Log.s("I am ").s(RNAME(this.receiver)).s(", I get a onError calling for id: ").i(error.sender_id).flush(); if (error.sender_id == 1111) { let tx = error.getTransaction(); Log.s("onError action account: ").s(RNAME(tx.actions[0].account)).flush(); // you send deferred tx but something wrong happened. // you can do something to handle this case. } } }
jack的合约源码:
import "allocator/arena"; import { Log } from "../../../src/log"; import { Contract } from "../../../src/contract"; import { RNAME, NAME } from "../../../src/account"; import { Action } from "../../../src/action"; class TargetContract extends Contract { @action recepient(name: string): void { Action.requireAuth(NAME("rose")); Log.s("hi, it is ").s(RNAME(this.receiver)).s(", recepient was called with parameter: ").s(name).flush(); } @action onInline(name: string): void { Action.requireAuth(NAME("rose")); Log.s("hi, it is ").s(RNAME(this.receiver)).s(", onInline was called with parameter: ").s(name).flush(); } @action onDeferred(name: string): void { Action.requireAuth(NAME("rose")); Log.s("hi, it is ").s(RNAME(this.receiver)).s(", onDeferred was called with parameter: ").s(name).flush(); } public filterAction(orginalReceiver: account_name): boolean { return true; // 这里设置本合约可以接受requireRecepient()调用。 } }
-
fanliangqin
在ultrain的智能合约体系结构中,一个帐号要向另个帐号转帐UGAS,一般来说有两种方式可以选择,第一种是发起交易调用utrio.token的transfer方法,第二种是将帐号权限delegate给utrio.code,在合约内完成转帐。
一般来说,在下注的时候使用第一种方式,在发奖的时候使用第二种方式来完成。转帐的通知机制
在utrio.token的transfer方法中,默认会调用
require_recepient(from); require_recepient(to)
来通知from和to帐号(如果它们部署了合约的话)的transfer方法,并且将和转帐相关的参数传递过来。这样一来,在from和to帐号的合约中,就可以检测到转帐交易了。
由于require_recepient方法产生的通知交易是以inline的形式被执行的,所以如果这些交易链条中有一笔失败了,那么就意味着整个交易都失败了。所以,也就达到了我们在合约中控制交易是否允许的目的。
下面我们用一个具体的例子来演示一下怎样下注和实现在合约中开奖。准备工作
1.准备帐号
在开始之前,我们需要准备两个帐号,house和player,并且给这两个帐号购买一些UGAS,以方便后续的测试。其中house帐号是庄家帐号,用来部署游戏合约;player帐号是玩家帐号,它参与到游戏中。
2.编写游戏合约
游戏合中,最需要注意的地方就是接受转帐信息并允许玩家加入游戏。但是这个地方可能产生巨大的漏洞,导致EOS中诸如“假EOS攻击”、“假充值通知”等等主要利用假转帐的攻击方式。这里是需要特别关注点。
示例源码如下:import "allocator/arena"; import { Contract } from "ultrain-ts-lib/src/contract"; import { Log } from "ultrain-ts-lib/src/log"; import { RNAME, NAME } from "ultrain-ts-lib/src/account"; import { Asset } from "ultrain-ts-lib/src/asset"; import { NEX } from "ultrain-ts-lib/lib/name_ex"; const Reward_None: u8 = 0; const Reward_waiting: u8 = 1; const Reward_done: u8 = 2; class Gamer implements Serializable{ account: account_name; reward_status: u8; primaryKey(): u64 { return this.account; } } @database(Gamer, "gamer") class MasterContract extends Contract { db: DBManager<Gamer>; constructor(code: u64) { super(code); this.db = new DBManager<Gamer>(NAME("gamer"), this.receiver, NAME("gamer")); } private startGame(from: account_name): void { let joined = this.db.exists(from); ultrain_assert(!joined, RNAME(from) + " has joined this game yet."); Log.s("欢迎").s(RNAME(from)).s("加入游戏").flush(); let gamer = new Gamer(); gamer.account = from; gamer.reward_status = Reward_None; this.db.emplace(this.receiver, gamer); } // 接收utrio.token的transfer信息,无论自己是作为from还是to。 @action transfer(from: account_name, to: account_name, val: Asset, memo: string): void { // from通过向this.receiver转帐的方式,申请加入了游戏 if (from != this.receiver && to == this.receiver) { Log.s(RNAME(from)).s(" require to join this game.").flush(); // ...... // 其它的一些检查逻辑都没问题,开始游戏逻辑 this.startGame(from); } // 从this.receiver帐号向其它人转帐 else if ( from == this.receiver && to != this.receiver) { let player = new Gamer(); let reward = this.db.get(to, player); // 如果不是向游戏获胜者转帐,将不被允许 // 这意味着没有人可以从游戏帐号中转移资产 ultrain_assert(reward && player.reward_status == Reward_waiting, RNAME(to) + " is not win the game. Why you get reward??"); // 更新发奖状态 player.reward_status = Reward_done; this.db.modify(this.receiver, player); Log.s(RNAME(to)).s(" get rewards. ").flush(); } // 其它情况,一律不允许转帐,不接受转入,也不接受转出 else { ultrain_assert(false, "Do not accept transfer operation for any other account!"); } } // 开始发奖 @action reveal(): void { let cursor = this.db.cursor(); ultrain_assert(cursor.count > 0, "No player joined this game."); // 作为测试,随便挑一个发奖 cursor.first(); let gamer = cursor.get(); // 设置中奖人的状态,在transfer中检查是否允许转帐。 gamer.reward_status = Reward_waiting; this.db.modify(this.receiver, gamer); // 开始转帐给gamer.account,10.0000 UGAS Asset.transfer(this.receiver, gamer.account, new Asset(100000), "Congratulation! you win this game!"); } // 过滤action接收原则,接收utrio.token的transfer事件, 如果不提供这个filter,将不能收到utrio.token的转帐通知 public filterAction(originalReceiver: u64): boolean { // 本合约的transfer方法不接受直接调用,也不接受inline方式的调用。 // 但是接受utrio.token的transfer方法 return (originalReceiver == this.receiver && this.action != NEX("transfer")) || (originalReceiver == NAME("utrio.token") && this.action == NEX("transfer")); // 可以使用系统默认的filter代替上面的判断。 // return Contract.filterAcceptTransferTokenAction(this.receiver, originalReceiver, this.action); } }
上面的合约中,我们提供了详细的注解,而且也相当的简单,不再详述。
部署合约
编译成功之后,我们将合约部署到house帐号上。
测试运行
1.测试player申请加入游戏
部署成功之后,player可以通过转帐的方式加入到游戏中。
clultrain transfer player house "250.0000 UGAS" "player-request to join game" -p player
执行成功的话,会产生以下输出:
executed transaction: 8077d23ff48ea804d3c3555420ce90f6955b439fc36bb0f2d4caa401eef11b7a 168 bytes 1592 us # utrio.token <= utrio.token::transfer {"from":"player","to":"house","quantity":"250.0000 UGAS","memo":"player-request to join game"} # player <= utrio.token::transfer {"from":"player","to":"house","quantity":"250.0000 UGAS","memo":"player-request to join game"} # house <= utrio.token::transfer {"from":"player","to":"house","quantity":"250.0000 UGAS","memo":"player-request to join game"} >> player require to join this game.欢迎player加入游戏
2.测试house向player发奖
已经有玩家加入了游戏,现在可以测试发奖流程了。由于发奖是由一个inline交易发起的(代码中的Assert.transfer()会发起一个inline交易),所以需要将hosuse的权限授权给utrio.token。
clultrain set account permission house active '{"threshold": 1,"keys": [{"key":"PUBLIC_KEY_OF_HOUSE","weight": 1}],"accounts": [{"permission":{"actor":"house","permission":"utrio.code"},"weight":1}]}' owner -p house
接下来,就可以发奖了:
clultrain push action house reveal '[]' -p house
发奖成功的话,会有类似下面的输出:
executed transaction: f11f4378c87a253ef2127ab42798355e72a05270916dfb62d0b4b38c866ffc46 104 bytes 1578 us # house <= house::reveal "" # utrio.token <= utrio.token::transfer {"from":"house","to":"player","quantity":"10.0000 UGAS","memo":"Congratulation! you win this game!"} # house <= utrio.token::transfer {"from":"house","to":"player","quantity":"10.0000 UGAS","memo":"Congratulation! you win this game!"} >> player get rewards. # player <= utrio.token::transfer {"from":"house","to":"player","quantity":"10.0000 UGAS","memo":"Congratulation! you win this game!"}
这样我们就将奖励发给了玩家。
-
fanliangqin
Ultrain的签名和权限管理
多签名体系
多签名是密码学和区块链里经常提及的词汇, 它对于多方签署/同意执行一个操作有着非常重要的意义--避免不怀好意的人单独行动.
在Ultrain的签名体系中, 有几个非常重要的词语: 钱包(wallet), 帐号(account), 公钥(public key), 私钥(private key). 这些元素组成了区块链签名体系.- Wallet
钱包是用来管理帐号私钥的工具, 它使用某个帐号私钥对交易进行签名. 一个钱包可以管理多个帐号的私钥. - Account
帐号由不超过12个字符的可读文字组成, 包括1-5, a-z中的字符. 注册帐号时, 需要关联合法的public key. - Public key
通过非对称加密算法生成的一对公/私钥中的公开部分, 会公布到区块链网络中. - Private key
通过非对称加密算法生成的一对公/私钥中的保密部分, 需要妥善保管, 绝对不可以泄露.
Ultrain中帐号的权限组成
在ultrain的帐号体系中, 一个帐号有两组密钥, 分别称为owner和active, 同时, 每一组密钥同时有阈值(threshold), 权重(weight). 默认情况下, 一个public key的权重为1.
- owner 一般的作用是管理帐号的其它权限(比如active), 较少使用.
- active 一般用来签名交易/申请奖励等, 使用用的频率非常高.
- threshold owner或active权限生效要求的最小值, 默认值为1.
- weight public key的权重, 默认为1. 在多签名的情况下, 如果多个public key的weight累加起来超过threshold, 那么这个交易就可以执行.
实践
准备工作
我们先准备以下帐号:
test1: 合约部署帐号
jack, rose, tony: 三个测试帐号1. 设置test1的active权限为多签名
clultrain set account permission test1 active '{"threshold":2,"keys":[],"accounts":[{"permission":{"actor":"jack","permission":"active"},"weight":1},{"permission":{"actor":"rose","permission":"active"},"weight":1},{"permission":{"actor":"tony","permission":"active"},"weight":1}],"waits":[]}' owner -p test1@owner
以上命令将test1的active权限为多签名, 它的threshold为2, jack, rose, tony三个帐号的weight都是1. 这意味着只要jack, rose, tony中的两个人签名之后, 累加的权重就会超过threshold, 这时也就可以执行test1中的action了.
上述命令行的参数是一个JSON对象, 说明如下(其它参数的意义可以通过命令行的参数说明了解):{ "threshold": 2, #设置权限阈值为2 "keys": [ { "key": "UTRxxxxxxxxxxx", #一个合法的公钥 "weight": 1 #设置这个公钥的权重为1 }, # ..... 其它更多的key对象 ], "accounts": [ { "permission": { "actor": "rose", # 使用rose的active签名 "permission": "active" }, "weight": 1 # 设置rose的权重为1 }, # ...... 其它更多的account对象 ], "waits": [ "wait_sec": 1, "weight": 1 ] }
在上面的参数说明中可以看到, 通过keys和accounts两个对象都可以设置一个签名的权重, 区别在于: 通过keys设置的权限, 是和public key本身产生了关联, 如果后续更新了某个帐号active的public key, 那么要重新设置一遍本命令中的key; 通过account设置的权限, 后续可以随便更新account的public key, 不再需要重设多签名了.
所以, 上述命令的等价的一个设置参数是:{ "threshold": 2, "keys": [ { "key": "<PUB_KEY_OF_JACK", "weight": 1 }, { "key": "<PUB_KEY_OF_ROSE", "weight": 1 }, { "key": "<PUB_KEY_OF_TONY", "weight": 1 } ], "accounts": [ ], "waits": [ ] }
2. 取消/更新多签名设置
要取消/更新多签名设置, 只要重新再设置一遍新的权限就可以了.
3. 执行多签名交易
- 提交一个多签名交易
多签名权限设置好之后, 可以使用utrio.msig合约来执行一个需要多方签名的操作了.
clultrain multisig propose betwin '[{"actor": "jack", "permission": "active"},{"actor": "rose", "permission": "active"}]' '[{"actor": "test1", "permission": "active"}]' utrio.token transfer '{"from":"test1", "to":"tony", "quantity":"25.0000 SYS", "memo":"bet arsenal win."}' -p tony@active
在上面的参数中,
第一个数组 '[{"actor": "jack", "permission": "active"},{"actor": "rose", "permission": "active"}]' 表示希望得到的签名. 其实tony也在可签名的列表中, 但是这个交易是他发起的, 所以避嫌不用自己的签名;
第二个数组 '[{"actor": "test1", "permission": "active"}]' 表示执行Action时使用的签名, 由于test1已经授权给了jack, rose, tony三人中的两人, 所以如果多签名满足了, 那么使用test1的签名是没有问题的.
其它的参数和普通的交易一致.
2. 各签名方reivew交易
现在jack和rose可以在自己的终端上review交易了.clultrain multisig review tony betwin
- 各签名方approve交易
jack和rose认为交易没有问题, 所以他们同意了这个交易
clultrain multisig approve tony betwin '{"actor": "jack", "permission": "active"}' -p jack@active clultrain multisig approve tony betwin '{"actor": "rose", "permission": "active"}' -p rose@active
如果很不幸的, 已经签名过的帐号反悔了, 他可以通过unapprove操作, 取消自己的签名:
clultrain multisig unapprove tony betwin '{"actor": "jack", "permission": "active"}' -p jack@active
- 设置签名帐号的delegate权限
签名帐号需要设置utrio.msig@utrio.code权限, 才能正常的执行这个代理操作. 在这个例子中, jakc和rose都需要设置utrio.msig@utrio.code代理.
clultrain set account permission jack active '{"threshold": 1,"keys": [{"key":"PUB_KEY_OF_JACK","weight": 1}],"accounts": [{"permission":{"actor":"utrio.msig","permission":"utrio.code"},"weight":1}]}' owner -p jack
clultrain set account permission rose active '{"threshold": 1,"keys": [{"key":"PUB_KEY_OF_ROSE","weight": 1}],"accounts": [{"permission":{"actor":"utrio.msig","permission":"utrio.code"},"weight":1}]}' owner -p rose
- 签名数超过threshold, 可以执行交易
clultrain multisig exec tony betwin -p tony@active
- 取消多签名交易
当propose一个多交易之后, 如果要取消的话, 发起人可以使用以下命令来取消:
clultrain multisig cancel tony betwin -p tony@active
更新帐号的签名信息
- 帐号的active的签名可以使用owner的签名来修改.
clultrain set account permission tony active NEW_ACTIVE_PUBLIC_KEY owner -p tony@owner
通过上述命令, tony的active签名已经更新为新的NEW_ACTIVE_PUBLIC_KEY了.
- 设置owner权限为多签名
一般情况下, owner密钥除了管理权限外, 不会用来做普通交易. 如果有需要的话, owner权限也可以设置为多签名的:
clultrain set account permission test1 owner '{"threshold":2,"keys":[],"accounts":[{"permission":{"actor":"jack","permission":"owner"},"weight":1},{"permission":{"actor":"rose","permission":"owner"},"weight":1},{"permission":{"actor":"tony","permission":"owner"},"weight":1}],"waits":[]}' -p test1@owner
设置签名代理
如果想将自己的权限授权给某人来代理(一般写成 accountA@accountB ). 比如将accountA代理给utrio.code, 则可以通过下面的命令来设置:
clultrain set account permission accountA active '{"threshold": 1,"keys": [{"key":"PUB_KEY_OF_ACCOUNT_A","weight": 1}],"accounts": [{"permission":{"actor":"accountA","permission":"utrio.code"},"weight":1}]}' owner -p accountA
取消代理:
clultrain set account permission accountA active '{"threshold": 1,"keys": [{"key":"PUB_KEY_OF_ACCOUNT_A","weight": 1}],"accounts": []}' owner -p accountA
Custome Permission定制权限
1.创建新权限
为了降低自己的帐号密钥被泄漏的风险,可以设置新的权限来避免使用owner,设置命令如下:
clultrain set account permission ACCOUNT NEW_Permission_NAME '{"threshold":1,"keys":[{"key":"YOUR_PUB_KEY","weight":1}]}' "active" -p ACCOUNT@active
假设我们注册了一个帐号jack,现在要给jack@active创建一个新的权限permjack:
clultrain set account permission jack permjack '{"threshold":1,"keys":[{"key":"UTR7V1873J53cVojG3Ptni7FhUigCx9uzeXxw2QkKXjSE3tZPSwmW","weight":1}]}' "active" -p jack@active
设置成功之后,使用
clultrain get account jack
会返回如下值:...... permissions: owner 1: 1 UTR53pD3DqK2DxAMkqWttQYpbyeBVUhKFSGe4f1iwH65QNA23tAh4 active 1: 1 UTR53pD3DqK2DxAMkqWttQYpbyeBVUhKFSGe4f1iwH65QNA23tAh41 jack@utrio.code, permjack 1: 1 UTR7My7dMhKooMBMr6FaaXwWQfuvT8XhPeVPVgXH7PBJcB9PU8tSy ......
2. 将新权限授权给一个特定合约的action
要将新权限授权给一个特定合约,需要使用到下面的命令:
clultrain set action permission ACCOUNT CONTRACT CONTRACT_ACTION NEW_PERMISSION -p ACCOUNT
这样,ACCOUNT的NEW_PERMISSION权限就授权给了CONTRACT的CONTRACT_ACTION命令,在CONTRACT_ACTION中可以通过Action.requireAuth2()来检查新权限了。
例如:clultrain set action permission jack jack check permjack -p jack
jack合约的check方法可以使用permjack权限了。
3. 编写一个合约使用新权限
我们写一个合约,定义check方法,在Action.requireAuth2()方法中检查新的权限。
@action check(): void { Action.requireAuth2(this.receiver, NAME("permjack")); Log.s("Check permission successed.").flush(); }
4. 将合约部署到jack帐号
clultrain set contrack jack contract_path -p jack
5. 使用新权限调用check
clultrain push action jack check '[]' -p jack@permjack
调用成功,产生如下输出:
liangqin@ultrain.local:/Users/liangqin/Public/ultrain-core/build/programs/clultrain> ./clultrain push action jack check '[]' -p jack@permjack 18-12-25 16:59 executed transaction: 36510a921e50806ac5b9b1e5834eb4a34681c1dd672287f915ea27bd4f48d0cd 104 bytes 375 us # jack <= jack::check "" >> Check permission successed.
6. 删除创建的新权限
删除新建的权限,通过两步操作可以完成:
1. 取消合约的授权clultrain set action permission jack jack check NULL -p jack
2. 删除权限
clultrain set account permission jack permjack 'null' "active" -p jack@active
- Wallet
-
fanliangqin
@raymond 原因就在于,typescript是OOP的一种语言,对象在使用前,必须先new一个实例,action在执行的时候,每次都会new新对象(因为节点不会保存虚拟机的context),只是这个new新对象的过程,被ultrainscript编译器封装起来了,实际上是存在这个过程的。
-
fanliangqin
怎样在ultrain的智能合约中持久化存储数据,是很多写过solidity合约的同学都会觉得疑惑的地方,以及为什么我们要这样设计?首先我们需要澄清的一个事实是,每次调用一个合约的action时,共识节点都会启动一个VM实例(事实上并没有新进程产生,但是可以这样理解),来执行目标代码,当一个action执行完成、或者超时、或者异常时,这个VM实例就被回收了,所有VM堆栈上的数据,也都被回收掉。所以,如果数据没有在action完成之前存储到数据库中,那么它们就丢失了。
其次,由于每次action都由不同的VM实例来执行,所以不同action之间不能直接共享状态变量(action是顺序执行的)。可行的方法是在一个action A中先把变量存储到数据库,在action B中再将变量读取出来。
再次,存储到数据库的数据,可以通过rpc接口,从数据库中直接读出来。使用rpc接口读数据库比发起一个action、获得返回值/事件回调的效率高很多倍,在实际编程中,如果只想了解当前合约状态,推荐的方式是在Dapp中直接使用rpc接口读数据库,而不是使用action。
在ultrain中,数据并不会自动的保存、恢复,而是需要在合约代码中使用DBManager(https://developer.ultrain.io/documents)辅助完成(由于typescript的源码开放性,有同学会发现存在更底层的DB api可以使用,但是强烈不建议直接使用底层api)。接下来我们通过一个例子来详细描述如何使用DBManager存取数据。步骤1. 定义一个支持Serializable的class
数据库中的数据是以k-v形式保存的,k通过class的primaryKey()方法确定, v通过serialize()方法将class中的成员序列化获得。定义一个支持
Serializable的class非常简单,如:class Address implements Serializable { street: string; post: string; } class Person implements Serializable { @primaryid id : u64; name: string; age: u32; sex: string; address: Address; @ignore salary: u32; constructor(_id: u64 = 0, _name: string = "unknown") { // do something } }
在这个例子中,我们定义了一个class Person,它支持Serializable。同时,我们在id上加了一个注解@primaryid,表示使用id这个变量的值作为primaryKey。注解@ignore则表示salary这个变量不会被序列化,也不会存储到数据库中,它类似于一个临时变量。
到这里一个简单的支持Serializable的class就定义好了。
很简单,对吧?但是仍然有几下几点需要特别注意:-
1)能够被注解为@primaryid的变量,必须是u64类型。如果没有注解@primaryid或者重写primaryKey()方法,编译器默认使用'0'做为priamry key。
-
2)constructor方法必须支持无参数输入时可调用,即支持let p: Person = new Person();这种语法。(可以通过提供默认参数来实现,如例子中)
-
3)如果Person的成员变量中有除基本数据类型(bool, i8,u8,i16,u16,i32,u32,i64,u64,string等)之外的其它class类型,那这个class也必须支持Serializable接口,如例子中的Address。
通过上面的操作,我们可以使用编译器默认生成的Serializable实现来帮我们简化操作。但是如果我们真有需要定制Serializable方法呢?又应该如何做?其实是有方法的,即重写(override) Serializable中的方法。
Serializable是一个Interface, 它的定义是这样的:
export interface Serializable { deserialize(ds: DataStream): void; serialize(ds : DataStream) : void; primaryKey(): u64; }
接下来我们可以重写Person如下:
class Person implements Serializable { @primaryid id : u64; name: string; age: u32; sex: string; address: Address; @ignore salary: u32; constructor(_id: u64 = 0, _name: string = "unknown") { // do something } primarykey(): u64 { // 使用一种特定的方法产生primary key return this.id + NAME(this.name); } serialize(ds: datastream): void { // 只将id和address保存到数据流 ds.write<u64>(this.id); this.address.serialize(ds); } deserialize(ds: datastream): void { // 从数据流中读出id和address this.id = ds.read<u64>(); this.address.deserialize(ds); } }
重写serializable方法,也有几点是需要注意的:
- 1)可以选择重写primarykey()、serialize()、deserialize()方法中的一个或几个。
- 2)使用了@primaryid、@ignore注解的同时,又重写了serializable的方法,编译器优先使用重写过的方法。如果重写了primarykey()方法,@primaryid注解失效;如果重写了serialize()和deserialize()方法,@ignore注解会失效。
- 3)serialize()和deserialize()中读、写数据的顺序必须严格一致。
步骤2.在合约中声明k-v数据表
在上面的步骤中,我们已经定义一个支持serializable的class person,就可以在合约中使用它了。
const salestable = "tb.sales"; const marketingtable = "tb.marketing"; @database(Person, "tb.sales") @database(Person, "tb.marketing") class HumanResource extends Contract { salesdb: DBManager<Person>; marketingdb: DBManager<Person>; constructor(code: u64) { super(code); this.salesdb = new DBManager<Person>(NAME(salestable), this.receiver, NAME(salestable)); this.marketingdb = new DBManager<Person>(NAME(marketingtable), this.receiver, NAME(marketingtable)); } // ....... }
我们可以使用@database来声明一个k-v数据表。@database有两个参数,第一个参数为Serializable的class,第二个参数是table名字(需符合account name限制)。如果有多个k-v表,可以使用多个@database声明。如果没有使用@database声明,代码并不会出错,但是不能使用rpc方法get_table_records来读取数据库中的数据。
接下来在代码中,我们就可以使用DBManager来关联一个k-v数据表了。DBManager的原型是一个泛型类:DBManager<T>(table_name: u64, code: u64, scope: u64)。table_name是数据表名;code必须使用this.receiver;scope是一个区分数据分类的关键字。步骤3.使用DBManager来操作数据
DBManager提供了一系列的方法,方便操作数据。
DBManager.exists(key: u64): boolean; 判断数据表中是否存在一个primary key的数据。
DBManager.get(key: u64, obj: T): boolean; 从数据表中读取一个primary key,并将数据存入obj中。 如果数据存在,返回true, 否则返回false。
DBManager.emplace(payer: u64, obj: T): void;往数据表中加入一条记录, payer表示谁为存储付费,并且只有payer能够修改数据。
DBManager.modify(payer: u64, obj: T): void; 修改一条记录,payer需要与emplace中使用的payer一致,和obj.primaryKey()一致的数据将会被修改。
DBManager.erase(key: u64): void; 删除key代表的记录。
DBManager.dropAll(): i32; 删除一个k-v数据表中的所有数据。 返回0表示删除成功,-1表示这个表中没有数据。
DBManager.cursor(): Cursor<T>; 获得一个迭代器,迭代数据表中所有数据。步骤4.使用DBManager的api
实干兴邦、空谈误国,下面我们使用一个完整的demo来展示如何使用DB。
import "allocator/arena"; import { Contract } from "ultrain-ts-lib/src/contract"; import { Log } from "ultrain-ts-lib/src/log"; import { NAME } from "ultrain-ts-lib/src/account"; class Address implements Serializable { street: string; post: string; } class Person implements Serializable { @primaryid id : u64; name: string; age: u32; sex: string; address: Address; @ignore salary: u32; constructor(_id: u64 = 0, _name: string = "unknown") { // do something } prints(): void { Log.s("id = ").i(this.id).s(", name = ").s(this.name).flush(); } } const salestable = "tb.sales"; const marketingtable = "tb.marketing"; @database(Person, "tb.sales") @database(Person, "tb.marketing") class HumanResource extends Contract { salesdb: DBManager<Person>; marketingdb: DBManager<Person>; constructor(code: u64) { super(code); this.salesdb = new DBManager<Person>(NAME(salestable), this.receiver, NAME(salestable)); this.marketingdb = new DBManager<Person>(NAME(marketingtable), this.receiver, NAME(marketingtable)); } @action addSales(id: u64, name: string, age: u32, sex: string, street: string, post: string, salary: u32): void { let p = new Person(); p.id = id; p.name = name; p.age = age; p.address.street = street; p.address.post = post; p.salary = salary; let existing = this.salesdb.exists(id); ultrain_assert(!existing, "this person has existed in db yet."); this.salesdb.emplace(this.receiver, p); } @action addMarketing(id: u64, name: string, age: u32, sex: string, street: string, post: string, salary: u32): void { let p = new Person(); p.id = id; p.name = name; p.age = age; p.address.street = street; p.address.post = post; p.salary = salary; let existing = this.marketingdb.exists(id); ultrain_assert(!existing, "this person has existed in db yet."); this.marketingdb.emplace(this.receiver, p); } @action modify(id: u64, name: string, salary: u32): void { let p = new Person(); let existing = this.salesdb.get(id, p); ultrain_assert(existing, "the person does not exist."); p.name = name; p.salary = salary; this.salesdb.modify(this.receiver, p); } @action remove(id: u64): void { Log.s("start to remove: ").i(id).flush(); let existing = this.salesdb.exists(id); ultrain_assert(existing, "this id is not exist."); this.salesdb.erase(id); } @action enumrate(dbname: string): void { let cursor: Cursor<Person>; if (dbname == "sales") { cursor = this.salesdb.cursor(); } else if (dbname == "marketing") { cursor = this.marketingdb.cursor(); } else { ultrain_assert(false, "unknown db name."); } Log.s("cursor.count =").i(cursor.count).flush(); while(cursor.hasNext()) { let p = cursor.get(); p.prints(); cursor.next(); } } @action drop(dbname: string): void { if (dbname == "sales") { this.salesdb.dropAll(); } else if (dbname == "marketing") { this.marketingdb.dropAll(); } else { ultrain_assert(false, "unknown db name."); } } }
-
-
fanliangqin
在solidity语言中,合约中允许使用constructor方法作一些初始化工作,例如
pragma solidity >0.4.99 <0.6.0; contract Coin { // The keyword "public" makes those variables // easily readable from outside. address public minter; constructor() public { minter = msg.sender; } }
Ultrain使用的typescript语言, 也使用了Class Contract作为所有合约的基类, 在合约中也有constructor方法:
class PersonContract extends Contract { db: DBManager<Person>; constructor(code: u64) { super(code); this.db = new DBManager<Person>(NAME(tblname), this.receiver, ''NAME(scope) + 3); } }
在ultrain的合约, constructor也可以做一些初始化的工作.
它们的区别在于:
- solidity的constructor创建合约的时候, 会执行一次; typescript的constructor创建合约的时候不会执行.
- solidity的constructor在整个合约周期只会执行一次; typescript的constructor<strong>每次调用合约方法之前, 都会执行, 它会执行多次.
熟悉solidity编写合约的同学, 需要特别注意这两个区别.