使用Node.js部署智能合約(Smart Contract)
從智能合約原始檔、編譯、部署,一氣呵成
我想大部分的人應該都是為了寫智能合約(Smart Contract)而選擇使用Ethereum,在開發應用程式(Dapp)時,若能透過程式碼自動部署智能合約,就像truffle一樣,即可以省下可觀的測試時間。
本教學文適合有Node.js與Solidity基礎的讀者。
本文預設的系統架構如下圖,共有兩個角色,首先有作為測試使用的PoA chain,包含至少兩個Authority node node0, node1
。接著是以node.js開發的app,將會使用web3.js module (這是本篇的主角),透過JSON RPC API與 node0
溝通。全部的流程只有下列三個步驟:
- 連結至
node0
- 編譯智能合約
- 透過
node0
部署至PoA chain
1. 架設測試用的chain
可以依據自己的習慣使用testrpc, ethereum testnet或是參考下列文章建立一個測試用的PoA chain。
在後續的操作過程中,chain都要保持開啟的狀態,不再特別提醒。
2. 建立Node.js專案
$ mkdir deploy_contract
$ cd deploy_contract
$ npm init #一路enter到底
此時檔案結構如下:
deploy-contract
└── package.json
3. 連接至PoA chain
為了讓其他開發者能夠方便地與Ethereum node溝通,Ethereum定義了JSON RPC API介面,使用JSON-RPC 2.0標準。基本上這是一個無關傳輸協定(transport agnostic)的標準,只定義資料交換格式,所以不管是透過HTTP或是socket通訊,都應該要得到相同的結果。
因為我們使用node.js開發app,所以選擇使用web3.js模組,它完整實作了JSON RPC API,可以很方便地呼叫 node0
幫我們做事。
Step 1. 安裝web3.js模組
$ npm install web3 --save #在deploy_contract目錄裡安裝web3.js模組
Step 2. 新增index.js檔案,填入下列code
- ethereumUri設定連接至
node0
的位址與port,一般來說JSON RPC的port是8545,但此範例是依照PoA chain的設定使用http://localhost:8450
。 - 設定web3 Provider連接至
node0
,接著呼叫isConnected()確認連接是否成功 - 最後取些基本資料,coinbase, getBalance, accounts,測試是否能夠順利溝通。輸出資料如下(若有按照PoA chain來設定輸出應該會一致,但balance可能會有些微差異)
coinbase 就是node0
的Authority account位址
accounts 列表會有兩個,一個是 user account,另一個是Authority account。
$ node index.js //執行
此時檔案結構如下:
deploy-contract
├── index.js
├── node_modules
└── package.json
4. 編譯智能合約sol檔案
這邊要使用solc.js模組來編譯智能合約sol檔案,最後取得部署合約需要的資訊:
- ABI (Application Binary Interface)
- bytecode
Step 1. 安裝solc.js
$ npm install solc --save
Step 2. 建立contracts資料夾
$ mkdir contracts
$ cd contracts
Step 3. 在contracts資料夾裡面新增一個BasicToken.sol檔案
填入下列code (此範例由openZeppelin專案修改而來)
此時檔案結構如下:
deploy-contract
├── contracts //新增
│ └── BasicToken.sol //新增
├── index.js
├── node_modules
└── package.json
Step 4. 開啟index.js,載入fs與solc模組,並加入編譯合約的程式碼
- 透過fs.readFileSync讀取BasicToken.sol內容
- 使用solc.compile編譯合約,得到一個JSON物件
- 取出我們需要的bytecode與ABI
- 最後印出ABI看看是否跟BasicToken.sol定義的function相同
$ node index.js //執行
5. 部署智能合約
部署合約就跟一般的交易一樣,需要發送一個交易至 node0
,因此要付交易手續費(gas),所以需要一個有足夠ETH的帳戶,這邊使用 user
這個User account,但使用之前要透過web3.personal.unlockAccount解除鎖定。
Step 1. unlockAccount
開啟index.js,修改connect部分的程式碼,如下:
- 宣告address為user account
const address = '0x004ec07d2329997267Ec62b4166639513386F32E'; // user
- 呼叫web3.personal.unlockAccount(addr, passwd)解鎖user account
addr填入欲解鎖的帳戶,即上面的address
passwd填入帳戶的密碼,如PoA chain裡的'user'
if (web3.personal.unlockAccount(address, 'user')) {
console.log(`${address} is unlocaked`);
}else{
console.log(`unlock failed, ${address}`);
}
執行
$ node index.js //執行
Step 2. 加入部署合約的code
開啟index.js,code加在最後面,如下:
- 先預估會消耗的gas,
web3.eth.estimateGas({data: bytecode})
這邊只需要提供交易的data資訊即可。
但是,若只填入bytecode,執行時會發生下列Invalid params錯誤,這是因為Parity確實依照JSON RPC 2.0規範來實作,數值都是以
0x
起頭,而solc.js產生的bytecode並沒有0x
因此會丟出錯誤,所以正確的參數為:
web3.eth.estimateGas({data: '0x' + bytecode})
若是連接到geth node而非parity node,應該就不會遇到這問題。
- 使用abi建立一個contract instance
let MyContract = web3.eth.contract(abi);
- 部署合約使用 web3.js封裝過的MyContract.new(),就會自動幫我們發送transaction至
node0
new()參數如下圖,總共有四個,其中 param1, param2
是optional,就我們的例子不需要填寫,只要給 transaction object與callback即可。
transaction object需要提供下列資訊:
{
from: address, // user account
data: '0x'+ bytecode, // 記得加 '0x'
gas: gasEstimate + 50000 // 多加一些gas,不然gas會不足
}
- 為了等待callback,以取得transaction id與部署之後的contract address,再加入下列wait function
(function wait () {
setTimeout(wait, 1000);
})();
執行程式
$ node index.js
完整的index.js內容應該要長這樣:
6. 測試智能合約功能
我們有contract address與abi就可以使用Parity UI加入已經部署成功的合約,並且使用合約上面的功能。
礙於篇幅長度,這邊使用Parity UI來測試,下次再寫篇透過web3.js呼叫智能合約的介紹文。
Step 1. 到SETTINGS頁面開啟Contracts功能
Step 2. 設定已經部署成功的智能合約
至CONTRACTS頁面,選擇WATCH CONTRACT
填入contract address, abi, 以及名稱
- contract address,請填入node.js app輸出的address
0xaec4f25d8eb795b14f665ceb88b6fd9114c34bce
- abi
[{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"type":"function"},{"inputs":[],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]
點進去之後就可以使用合約功能
這樣就完成了,完整的程式碼都在這裡。