基於星雲鏈的智慧合約與Dapp(四)——編寫並執行智慧合約
一般智慧合約需要以下幾個步驟:
1.編寫智慧合約
2.部署智慧合約
3.呼叫智慧合約,驗證合約執行結果
編寫智慧合約
Nebulas實現了NVM虛擬機器來執行智慧合約,NVM的實現使用了JavaScript V8引擎,所以我們可以使用JavaScript、TypeScript來編寫智慧合約。
編寫智慧合約的簡要規範:
1.智慧合約程式碼必須是一個Prototype的物件;
2.智慧合約程式碼必須有一個init()的方法,這個方法只會在部署的時候被執行一次;
3.智慧合約裡面的私有方法是以_開頭的方法,私有方法不能被外部直接呼叫;
我們使用JavaScript來編寫第一個智慧合約:銀行保險櫃。 這個智慧合約需要實現以下功能:
1.使用者可以向這個銀行保險櫃存錢。
2.使用者可以從這個銀行保險櫃取錢。
3.使用者可以查詢銀行保險櫃中的餘額。
智慧合約示例:
'use strict';
var DepositeContent = function (text) {
if (text) {
var o = JSON.parse(text);
this.balance = new BigNumber(o.balance);
this.expiryHeight = new BigNumber(o.expiryHeight);
} else {
this.balance = new BigNumber(0);
this.expiryHeight = new BigNumber(0);
}
};
DepositeContent.prototype = {
toString: function () {
return JSON.stringify(this);
}
};
var BankVaultContract = function () {
LocalContractStorage.defineMapProperty(this, "bankVault", {
parse: function (text) {
return new DepositeContent(text);
},
stringify: function (o) {
return o.toString();
}
});
};
// save value to contract, only after height of block, users can takeout
BankVaultContract.prototype = {
init: function () {
//TODO:
},
save: function (height) {
var from = Blockchain.transaction.from;
var value = Blockchain.transaction.value;
var bk_height = new BigNumber(Blockchain.block.height);
var orig_deposit = this.bankVault.get(from);
if (orig_deposit) {
value = value.plus(orig_deposit.balance);
}
var deposit = new DepositeContent();
deposit.balance = value;
deposit.expiryHeight = bk_height.plus(height);
this.bankVault.put(from, deposit);
},
takeout: function (value) {
var from = Blockchain.transaction.from;
var bk_height = new BigNumber(Blockchain.block.height);
var amount = new BigNumber(value);
var deposit = this.bankVault.get(from);
if (!deposit) {
throw new Error("No deposit before.");
}
if (bk_height.lt(deposit.expiryHeight)) {
throw new Error("Can not takeout before expiryHeight.");
}
if (amount.gt(deposit.balance)) {
throw new Error("Insufficient balance.");
}
var result = Blockchain.transfer(from, amount);
if (!result) {
throw new Error("transfer failed.");
}
Event.Trigger("BankVault", {
Transfer: {
from: Blockchain.transaction.to,
to: from,
value: amount.toString()
}
});
deposit.balance = deposit.balance.sub(amount);
this.bankVault.put(from, deposit);
},
balanceOf: function () {
var from = Blockchain.transaction.from;
return this.bankVault.get(from);
},
verifyAddress: function (address) {
// 1-valid, 0-invalid
var result = Blockchain.verifyAddress(address);
return {
valid: result == 0 ? false : true
};
}
};
module.exports = BankVaultContract;
上面智慧合約的示例可以看到,BankVaultContract是一個prototype
物件,這個物件有一個init()
方法,滿足了我們說的編寫智慧合約最基礎的規範。
BankVaultContract實現了另外兩個方法:
- save(): 使用者可以通過呼叫save()方法向銀行保險櫃存錢;
- takeout(): 使用者可以通過呼叫takeout()方法向銀行保險櫃取錢;
- balanceOf(): 使用者可以通過呼叫balanceOf()方法向銀行保險櫃查詢餘額;
上面的合約程式碼用到了內建的Blockchain物件和內建的BigNumber()
方法,下面我們來逐行拆解分析合約程式碼:
save():
// Deposit the amount into the safe
save: function (height) {
var from = Blockchain.transaction.from;
var value = Blockchain.transaction.value;
var bk_height = new BigNumber(Blockchain.block.height);
var orig_deposit = this.bankVault.get(from);
if (orig_deposit) {
value = value.plus(orig_deposit.balance);
}
var deposit = new DepositeContent();
deposit.balance = value;
deposit.expiryHeight = bk_height.plus(height);
this.bankVault.put(from, deposit);
},
takeout():
takeout: function (value) {
var from = Blockchain.transaction.from;
var bk_height = new BigNumber(Blockchain.block.height);
var amount = new BigNumber(value);
var deposit = this.bankVault.get(from);
if (!deposit) {
throw new Error("No deposit before.");
}
if (bk_height.lt(deposit.expiryHeight)) {
throw new Error("Can not takeout before expiryHeight.");
}
if (amount.gt(deposit.balance)) {
throw new Error("Insufficient balance.");
}
var result = Blockchain.transfer(from, amount);
if (!result) {
throw new Error("transfer failed.");
}
Event.Trigger("BankVault", {
Transfer: {
from: Blockchain.transaction.to,
to: from,
value: amount.toString()
}
});
deposit.balance = deposit.balance.sub(amount);
this.bankVault.put(from, deposit);
},
部署智慧合約
上面介紹了在Nebulas中怎麼去編寫一個智慧合約,現在我們需要把智慧合約部署到鏈上。在Nebulas中部署一個智慧合約其實也是傳送一個transaction來實現,前面有介紹過使用者如何在Nebulas中進行轉賬交易,由於是本地測試,我們直接使用解鎖 & 傳送的方式來發送交易。
首先,我們在conf/default/genesis.conf
中預分配過代幣的賬戶裡,選擇賬戶n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so
作為傳送者賬號,並檢查該賬戶的狀態。
curl -i -H Accept:application/json -X POST http://localhost:8685/v1/user/accountstate -d '{"address":"n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 13:49:39 GMT
Content-Length: 72
{
"result": {
"balance": "4999999999999977984000000",
"nonce": "1",
"type": 87
}
}
該賬戶有足夠的錢來支付手續費,接下來,我們解鎖傳送者賬戶n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so
,解鎖12小時。
curl -i -H 'Content-Type: application/json' -X POST http://localhost:8685/v1/admin/account/unlock -d '{"address":"n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so","passphrase":"passphrase","duration":"43200000000000"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 13:52:02 GMT
Content-Length: 26
{
"result": {
"result": true
}
}
然後,我們傳送部署BankVault合約的交易。
curl -i -H 'Accept: application/json' -X POST http://localhost:8685/v1/admin/transactionWithPassphrase -H 'Content-Type: application/json' -d '{"transaction": {"from":"n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so","to":"n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so", "value":"0","nonce":2,"gasPrice":"1000000","gasLimit":"2000000","contract":{"source":"\"use strict\";var DepositeContent=function(text){if(text){var o=JSON.parse(text);this.balance=new BigNumber(o.balance);this.expiryHeight=new BigNumber(o.expiryHeight);}else{this.balance=new BigNumber(0);this.expiryHeight=new BigNumber(0);}};DepositeContent.prototype={toString:function(){return JSON.stringify(this);}};var BankVaultContract=function(){LocalContractStorage.defineMapProperty(this,\"bankVault\",{parse:function(text){return new DepositeContent(text);},stringify:function(o){return o.toString();}});};BankVaultContract.prototype={init:function(){},save:function(height){var from=Blockchain.transaction.from;var value=Blockchain.transaction.value;var bk_height=new BigNumber(Blockchain.block.height);var orig_deposit=this.bankVault.get(from);if(orig_deposit){value=value.plus(orig_deposit.balance);} var deposit=new DepositeContent();deposit.balance=value;deposit.expiryHeight=bk_height.plus(height);this.bankVault.put(from,deposit);},takeout:function(value){var from=Blockchain.transaction.from;var bk_height=new BigNumber(Blockchain.block.height);var amount=new BigNumber(value);var deposit=this.bankVault.get(from);if(!deposit){throw new Error(\"No deposit before.\");} if(bk_height.lt(deposit.expiryHeight)){throw new Error(\"Can not takeout before expiryHeight.\");} if(amount.gt(deposit.balance)){throw new Error(\"Insufficient balance.\");} var result=Blockchain.transfer(from,amount);if(!result){throw new Error(\"transfer failed.\");} Event.Trigger(\"BankVault\",{Transfer:{from:Blockchain.transaction.to,to:from,value:amount.toString()}});deposit.balance=deposit.balance.sub(amount);this.bankVault.put(from,deposit);},balanceOf:function(){var from=Blockchain.transaction.from;return this.bankVault.get(from);},verifyAddress:function(address){var result=Blockchain.verifyAddress(address);return{valid:result==0?false:true};}};module.exports=BankVaultContract;","sourceType":"js", "args":""}}, "passphrase": "passphrase"}'
HTTP/1.1 100 Continue
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 13:54:46 GMT
Content-Length: 145
{
"result": {
"txhash": "b6d3f13146ee3533f55536230e08c3108bb7a074cc102a08c871dee6aa37ff58",
"contract_address": "n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k"
}
}
- from: 合約建立者賬戶地址
- to: 和from一致,同為合約建立者地址
- value:部署合約時為
"0"
- nonce: 比建立者當前的
nonce
多1,可以通過GetAccountState獲取建立前當前nonce
- gasPrice:部署智慧合約用到的gasPrice,可以通過GetGasPrice獲取,或者使用預設值:
"1000000"
; - gasLimit: 部署合約的gasLimit,通過EstimateGas可以獲取部署合約的gas消耗,不能使用預設值,也可以設定一個較大值,執行時以實際使用計算。
- contract: 合約資訊,部署合約時傳入的引數
source
: 合約程式碼sourceType
: 合約程式碼型別,支援js
和ts
(對應javaScript和typeScript程式碼)args
: 合約初始化方法引數,無引數為空字串,有引數時為JSON陣列
部署智慧合約的返回值是transaction的hash地址txhash和合約的部署地址contract_address。 得到返回值並不能保證合約已經部署成功,因為傳送交易是一個非同步的過程,需要經過礦工打包,正如之前的轉賬交易一樣,轉賬並不能實時到賬,依賴礦工打包的速度,所以需要等待一段時間,然後可以通過查詢合約地址的資訊或者呼叫智慧合約來驗證合約是否部署成功。
驗證合約是否部署成功
在部署智慧合約的時候得到了transaction的hash地址txhash,我們可以很方便的使用console控制檯查詢transaction的hash資訊來驗證合約是否部署成功。
curl -i -H 'Content-Type: application/json' -X POST http://localhost:8685/v1/user/getTransactionReceipt -d '{"hash":"b6d3f13146ee3533f55536230e08c3108bb7a074cc102a08c871dee6aa37ff58"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 14:02:39 GMT
Transfer-Encoding: chunked
{
"result": {
"hash": "b6d3f13146ee3533f55536230e08c3108bb7a074cc102a08c871dee6aa37ff58",
"chainId": 100,
"from": "n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so",
"to": "n1H4MYms9F55ehcvygwWE71J8tJC4CRr2so",
"value": "0",
"nonce": "2",
"timestamp": "1530885286",
"type": "deploy",
"data": "eyJTb3VyY2VUeXBlIjoianMiLCJTb3VyY2UiOiJcInVzZSBzdHJpY3RcIjt2YXIgRGVwb3NpdGVDb250ZW50PWZ1bmN0aW9uKHRleHQpe2lmKHRleHQpe3ZhciBvPUpTT04ucGFyc2UodGV4dCk7dGhpcy5iYWxhbmNlPW5ldyBCaWdOdW1iZXIoby5iYWxhbmNlKTt0aGlzLmV4cGlyeUhlaWdodD1uZXcgQmlnTnVtYmVyKG8uZXhwaXJ5SGVpZ2h0KTt9ZWxzZXt0aGlzLmJhbGFuY2U9bmV3IEJpZ051bWJlcigwKTt0aGlzLmV4cGlyeUhlaWdodD1uZXcgQmlnTnVtYmVyKDApO319O0RlcG9zaXRlQ29udGVudC5wcm90b3R5cGU9e3RvU3RyaW5nOmZ1bmN0aW9uKCl7cmV0dXJuIEpTT04uc3RyaW5naWZ5KHRoaXMpO319O3ZhciBCYW5rVmF1bHRDb250cmFjdD1mdW5jdGlvbigpe0xvY2FsQ29udHJhY3RTdG9yYWdlLmRlZmluZU1hcFByb3BlcnR5KHRoaXMsXCJiYW5rVmF1bHRcIix7cGFyc2U6ZnVuY3Rpb24odGV4dCl7cmV0dXJuIG5ldyBEZXBvc2l0ZUNvbnRlbnQodGV4dCk7fSxzdHJpbmdpZnk6ZnVuY3Rpb24obyl7cmV0dXJuIG8udG9TdHJpbmcoKTt9fSk7fTtCYW5rVmF1bHRDb250cmFjdC5wcm90b3R5cGU9e2luaXQ6ZnVuY3Rpb24oKXt9LHNhdmU6ZnVuY3Rpb24oaGVpZ2h0KXt2YXIgZnJvbT1CbG9ja2NoYWluLnRyYW5zYWN0aW9uLmZyb207dmFyIHZhbHVlPUJsb2NrY2hhaW4udHJhbnNhY3Rpb24udmFsdWU7dmFyIGJrX2hlaWdodD1uZXcgQmlnTnVtYmVyKEJsb2NrY2hhaW4uYmxvY2suaGVpZ2h0KTt2YXIgb3JpZ19kZXBvc2l0PXRoaXMuYmFua1ZhdWx0LmdldChmcm9tKTtpZihvcmlnX2RlcG9zaXQpe3ZhbHVlPXZhbHVlLnBsdXMob3JpZ19kZXBvc2l0LmJhbGFuY2UpO30gdmFyIGRlcG9zaXQ9bmV3IERlcG9zaXRlQ29udGVudCgpO2RlcG9zaXQuYmFsYW5jZT12YWx1ZTtkZXBvc2l0LmV4cGlyeUhlaWdodD1ia19oZWlnaHQucGx1cyhoZWlnaHQpO3RoaXMuYmFua1ZhdWx0LnB1dChmcm9tLGRlcG9zaXQpO30sdGFrZW91dDpmdW5jdGlvbih2YWx1ZSl7dmFyIGZyb209QmxvY2tjaGFpbi50cmFuc2FjdGlvbi5mcm9tO3ZhciBia19oZWlnaHQ9bmV3IEJpZ051bWJlcihCbG9ja2NoYWluLmJsb2NrLmhlaWdodCk7dmFyIGFtb3VudD1uZXcgQmlnTnVtYmVyKHZhbHVlKTt2YXIgZGVwb3NpdD10aGlzLmJhbmtWYXVsdC5nZXQoZnJvbSk7aWYoIWRlcG9zaXQpe3Rocm93IG5ldyBFcnJvcihcIk5vIGRlcG9zaXQgYmVmb3JlLlwiKTt9IGlmKGJrX2hlaWdodC5sdChkZXBvc2l0LmV4cGlyeUhlaWdodCkpe3Rocm93IG5ldyBFcnJvcihcIkNhbiBub3QgdGFrZW91dCBiZWZvcmUgZXhwaXJ5SGVpZ2h0LlwiKTt9IGlmKGFtb3VudC5ndChkZXBvc2l0LmJhbGFuY2UpKXt0aHJvdyBuZXcgRXJyb3IoXCJJbnN1ZmZpY2llbnQgYmFsYW5jZS5cIik7fSB2YXIgcmVzdWx0PUJsb2NrY2hhaW4udHJhbnNmZXIoZnJvbSxhbW91bnQpO2lmKCFyZXN1bHQpe3Rocm93IG5ldyBFcnJvcihcInRyYW5zZmVyIGZhaWxlZC5cIik7fSBFdmVudC5UcmlnZ2VyKFwiQmFua1ZhdWx0XCIse1RyYW5zZmVyOntmcm9tOkJsb2NrY2hhaW4udHJhbnNhY3Rpb24udG8sdG86ZnJvbSx2YWx1ZTphbW91bnQudG9TdHJpbmcoKX19KTtkZXBvc2l0LmJhbGFuY2U9ZGVwb3NpdC5iYWxhbmNlLnN1YihhbW91bnQpO3RoaXMuYmFua1ZhdWx0LnB1dChmcm9tLGRlcG9zaXQpO30sYmFsYW5jZU9mOmZ1bmN0aW9uKCl7dmFyIGZyb209QmxvY2tjaGFpbi50cmFuc2FjdGlvbi5mcm9tO3JldHVybiB0aGlzLmJhbmtWYXVsdC5nZXQoZnJvbSk7fSx2ZXJpZnlBZGRyZXNzOmZ1bmN0aW9uKGFkZHJlc3Mpe3ZhciByZXN1bHQ9QmxvY2tjaGFpbi52ZXJpZnlBZGRyZXNzKGFkZHJlc3MpO3JldHVybnt2YWxpZDpyZXN1bHQ9PTA/ZmFsc2U6dHJ1ZX07fX07bW9kdWxlLmV4cG9ydHM9QmFua1ZhdWx0Q29udHJhY3Q7IiwiQXJncyI6IiJ9",
"gas_price": "1000000",
"gas_limit": "2000000",
"contract_address": "n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k",
"status": 1,
"gas_used": "22016",
"execute_error": "",
"execute_result": "\"\""
}
}
如上所示,部署合約的交易的狀態status
變成了1,表示合約部署成功了。
執行智慧合約方法
在Nebulas中呼叫智慧合約的方式也很簡單,同樣是通過傳送交易來呼叫智慧合約。
呼叫智慧合約的save
方法
curl -i -H 'Accept: application/json' -X POST http://localhost:8685/v1/admin/trction":{"from":"n1LkDi2gGMqPrjYcczUiweyP4RxTB6Go1qS","to":"n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k", "value":"100","nonce":8,"gasPrice":"1000000","gasLimit":"2000000","contract":{"function":"save","args":"[0]"}}, "passphrase": "passphrase"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 14:08:55 GMT
Content-Length: 110
{
"result": {
"txhash": "48b7c9f388c149295ee27b0d1a51a72f2c6979245ecd58c0bd70aa4fb9ff4095",
"contract_address": ""
}
}
- from: 使用者的賬戶地址
- to: 智慧合約地址
- value: 呼叫save()方法時想要存入智慧合約代幣數量
- nonce: 比建立者當前的
nonce
多1,可以通過GetAccountState獲取建立前當前nonce
- gasPrice:部署智慧合約用到的gasPrice,可以通過GetGasPrice獲取,或者使用預設值:
"1000000"
; - gasLimit: 部署合約的gasLimit,通過EstimateGas可以獲取部署合約的gas消耗,不能使用預設值,也可以設定一個較大值,執行時以實際使用計算。
- contract: 合約資訊,部署合約時傳入的引數
function
: 呼叫合約方法args
: 合約方法引數,無引數為空字串,有引數時為JSON陣列
驗證智慧合約的方法save
是否執行成功
由於執行合約方法本質是提交一個交易,所以我們可以通過驗證交易是否提交成功來判斷合約方法是否執行成功。
curl -i -H 'Content-Type: application/json' -X POST http://localhost:8685/v1/user/getTransactionReceipt -d '{"hash":"48b7c9f388c149295ee27b0d1a51a72f2c6979245ecd58c0bd70aa4fb9ff4095"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 14:12:03 GMT
Content-Length: 446
{
"result": {
"hash": "48b7c9f388c149295ee27b0d1a51a72f2c6979245ecd58c0bd70aa4fb9ff4095",
"chainId": 100,
"from": "n1LkDi2gGMqPrjYcczUiweyP4RxTB6Go1qS",
"to": "n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k",
"value": "100",
"nonce": "8",
"timestamp": "1530886135",
"type": "call",
"data": "eyJGdW5jdGlvbiI6InNhdmUiLCJBcmdzIjoiWzBdIn0=",
"gas_price": "1000000",
"gas_limit": "2000000",
"contract_address": "",
"status": 1,
"gas_used": "20363",
"execute_error": "",
"execute_result": "\"\""
}
}
如上所示,交易狀態status
變為了1,表示執行save方法成功了。
呼叫智慧合約的takeout
方法
curl -i -H 'Accept: application/json' -X POST http://localhost:8685/v1/admin/transactionWithPassphrase -H 'Content-Type: application/json' -d '{"transaction":{"from":"n1LkDi2gGMqPrjYcczUiweyP4RxTB6Go1qS","to":"n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k", "value":"0","nonce":9,"gasPrice":"1000000","gasLimit":"2000000","contract":{"function":"takeout","args":"[50]"}}, "passphrase": "passphrase"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 14:16:27 GMT
Content-Length: 110
{
"result": {
"txhash": "d5a99ee3a8cf1cc18cb1f8852b61b6372b93843bfea5a51095dc8b69c5678baa",
"contract_address": ""
}
}
驗證智慧合約的方法takeout是否執行成功
在上面save方法的執行中,我們在合約n1rVLTRxQEXscTgThmbTnn2NqdWFEKwpYUM
中存了100 wei(10^-18 NAS)。此時,我們執行takeout
函式,從中取出50 wei。合約裡應該還有50 wei。我們檢測下合約賬戶的餘額來驗證takeout
方法執行是否成功。
curl -i -H 'Content-Type: application/json' -X POST http://localhost:8685/v1/user/accountstate -d '{"address":"n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 14:19:47 GMT
Content-Length: 49
{
"result": {
"balance": "50",
"nonce": "0",
"type": 88
}
}
結果和預期相符,當然由於曠工的原因我們需要等待一定的時間才能查詢到正確的資料。
查詢智慧合約資料
在智慧合約中,我們有一些方法並不更改合約的狀態,這些方法被設計來幫助我們獲取合約資料,它們是隻讀的。在星雲鏈上,我們在API Module中為使用者提供了Call介面來幫助使用者來執行這些只讀的方法,使用Call
介面不會發送交易,也就無需支付上鍊手續費。
我們呼叫合約n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k
中的balanceOf
方法來查詢該合約的餘額。
curl -i -H 'Accept: application/json' -X POST http://localhost:8685/v1/user/call -H 'Content-Type: application/json' -d '{"from":"n1LkDi2gGMqPrjYcczUiweyP4RxTB6Go1qS","to":"n1joQPGKfM1PVRSZwrFqYzwx1Sc4xgaBM8k","value":"0","nonce":10,"gasPrice":"1000000","gasLimit":"2000000","contract":{"function":"balanceOf","args":""}}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Fri, 06 Jul 2018 14:24:06 GMT
Content-Length: 110
{
"result": {
"result": "{\"balance\":\"50\",\"expiryHeight\":\"3261\"}",
"execute_err": "",
"estimate_gas": "20209"
}
}