快速學習-以太坊編寫合約的編譯腳本

編寫合約的編譯腳本

之前的課程中,我們已經熟悉了智能合約的編譯。編譯是對合約進行部署和測試的前置步驟,編譯步驟的目標是把源代碼轉成 ABI 和 Bytecode,並且能夠處理編譯時拋出的錯誤,確保不會在包含錯誤的源代碼上進行編譯。

開始我們的編譯方式是用 solc 工具做命令行編譯,這個過程中牽涉到大段內容的複製粘貼,很容易出錯;之後在項目中引入 solc 模塊,可以在 node 命令行中自動編譯並讀取結果內容。於是我們自然會想到,能不能將這個過程寫成腳本,自動完成這些過程呢?這節課我們就來完成這個任務。

目錄結構

首先新建一個項目目錄,可以叫做 contract_workflow。

mkdir contract_workflow
cd contract_workflow

爲了存放不同目的不同類型的文件,我們先在項目根目錄下新建 4 個子目錄:

mkdir contracts
mkdir scripts
mkdir compiled
mkdir tests

其中 contracts 目錄存放合約源代碼,scripts 目錄存放編譯腳本,complied 目錄存放編譯結果,tests 目錄存放測試文件。

準備合約源碼

爲了簡化工作,我們可以直接複製以前的 solidity 代碼,也可以自己寫一個簡單的合約。比如,這裏用到了我們最初寫的簡單合約 Car.sol:

pragma solidity ^ 0.4 .22;
contract Car {
	string public brand;
	constructor(string initialBrand) public {
		brand = initialBrand;
	}

	function setBrand(string newBrand) public {
		brand = newBrand;
	}
}

將它放到 contracts 目錄下。

準備編譯工具

我們用 solc 作爲編譯的基礎工具。用 npm 將 solc 安裝到本地目錄中:npm install solc

開發編譯腳本

我們已經熟悉了命令行編譯的流程,現在我們試圖將它腳本中。在 scripts 目錄下新建文件 compile.js

const fs = require('fs'); 
const path = require('path'); 
const solc = require('solc'); 
const contractPath = path.resolve(__dirname, '../contracts', 
'Car.sol'); 
const contractSource = fs.readFileSync(contractPath, 'utf8'); 
const result = solc.compile(contractSource, 1);
console.log(result);

我們把合約源碼從文件中讀出來,然後傳給 solc 編譯器,等待同步編譯完成之後,把編譯結果輸出到控制檯。

其中 solc.compile() 的第二個參數給 1,表示啓用 solc 的編譯優化器。編譯結果是一個嵌套的 js 對象,其中可以看到 contracts 屬性包含了所有找到的合約(當然,我們的源碼中只有一個 Car)。每個合約下面包含了 assembly、bytecode、interface、metadata、opcodes 等字段,我們最關心的當然是這兩個:

  • bytecode:字節碼,部署合約到以太坊區塊鏈上時需要使用;
  • interface: 二進制應用接口(ABI),使用 web3 初始化智能合約交互實例的時候需要使用。

其中 interface 是被 JSON.stringify 過的字符串,我們用 JSON.parse 反解出來並格式化,就可以拿到合約的 abi 對象。

保存編譯結果

讓我們繼續課程,現在將合約部署到區塊鏈上。爲此,你必須先通過傳入 abi 定義來創建一個合約對象 VotingContract。然後用這個對象在鏈上部署並初始化合約。爲了方便後續的部署和測試過程直接使用編譯結果,需要把編譯結果保存到文件系統中,在做改動之前,我們引入一個非常好用的小工具 fs-extra,在腳本中使用 fs-extra 直接替換到 fs,然後在腳本中加入以下代碼:

Object.keys(result.contracts).forEach(name => {
	const contractName = name.replace(/^:/, '');
	const filePath = path.resolve(__dirname, '../compiled',
		`${contractName}.json`);
	fs.outputJsonSync(filePath, result.contracts[name]);
	console.log(`save compiled contract ${contractName} to 
${filePath}`);
});

然後重新運行編譯腳本,確保 complied 目錄下包含了新生成的 Car.json。

類似於前端構建流程中的編譯步驟,我們編譯前通常需要把之前的結果清空,然後把最新的編譯結果保存下來,這對保障一致性非常重要。所以繼續對編譯腳本做如下改動:

在腳本執行的開始加入清除編譯結果的代碼:

// cleanup
const compiledDir = path.resolve(__dirname, '../compiled');
fs.removeSync(compiledDir);
fs.ensureDirSync(compiledDir);

這裏專門定義了 compiledDir,所以後面的 filePath 也可以改爲:

const filePath = 
path.resolve(compiledDir, `${contractName}.json`);

新增的 cleanup 代碼段的作用就是準備全新的目錄,修改完之後,需要重新運行編譯腳本,確保一切正常。

處理編譯錯誤

現在的編譯腳本只處理了最常見的情況,即 Solidity 源代碼沒問題,這個假設其實是不成立的。如果源代碼有問題,我們在編譯階段就應該報出來,而不應該把錯誤的結果寫入到文件系統,因爲這樣會導致後續步驟失敗。 爲了搞清楚編譯器 solc 遇到錯誤時的行爲,我們人爲在源代碼中引入錯誤(例如把function 關鍵字寫成 functio),看看腳本的表現如何。

重新運行編譯腳本,發現它並沒有報錯,而是把錯誤作爲輸出內容打印出來,其中錯誤的可讀性比較差。

所以我們對編譯腳本稍作改動,在編譯完成之後就檢查 error,讓它能夠在出錯時直接拋出錯誤:

// check errors
if (Array.isArray(result.errors) && result.errors.length) {
	throw new Error(result.errors[0]);
}

重新運行編譯腳本,可以看到我們得到了可讀性更好的錯誤提示。
在這裏插入圖片描述
最終版編譯腳本

編譯腳本的最終版如下:

const fs = require('fs-extra'); 
const path = require('path'); 
const solc = require('solc'); 
// cleanup 
const compiledDir = path.resolve(__dirname, '../compiled');
fs.removeSync(compiledDir); 
fs.ensureDirSync(compiledDir);
// compile const contractPath = path.resolve(__dirname, 
'../contracts', 'Car.sol'); 
const contractSource = fs.readFileSync(contractPath, 'utf8'); 
const result = solc.compile(contractSource, 1); 
// check errors 
if (Array.isArray(result.errors) && result.errors.length) {
throw new Error(result.errors[0]); 
}
// save to disk 
Object.keys(result.contracts).forEach(name => { 
const contractName = name.replace(/^:/, ''); 
const filePath = path.resolve(compiledDir, 
`${contractName}.json`); 
fs.outputJsonSync(filePath, result.contracts[name]); 
console.log(`save compiled contract ${contractName} to 
${filePath}`); });
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章