如果你已經在以太坊上開發過DApp,那你在前端JavaScript中可能用過web3.js。Ethers.js則是一個輕量級的web3.js替代品,在本文中,我們将學習如何使用Ether.js建構一個簡單的DApp。
與Web3.js相比,Ethers.js有很多優點,其中我最喜歡的一個特性是Ethers.js提供的狀态和密鑰管理。Web3的設計場景是DApp應該連接配接到一個本地節點,由這個節點負責儲存密鑰、簽名交易并與以太坊區塊鍊互動。現實并不是這樣的,絕大多數使用者不會在本地運作一個geth節點。Metamask在浏覽器
應用中有效地模拟了這種節點環境,是以絕大多數web3應用需要使用Metamask來儲存密鑰、簽名交易并完成與以太坊的互動。
Ethers.js采取了不同的設計思路,它提供給開發者更多的靈活性。Ethers.js将“節點”拆分為兩個不同的角色:
- 錢包:負責密鑰儲存和交易簽名
- 提供器:負責以太坊網絡的匿名連接配接、狀态檢查和交易發送
要快速掌握以太坊區塊鍊應用開發,推薦彙智網的線上互動教程:
1、編譯、部署智能合約
在這個教程中我們将與一個ERC20智能合約互動,你需要在機器裡先安裝nodejs和npm。
1.1 建立項目檔案夾
首先建立一個檔案夾ethers-template,然後再這個檔案夾裡再建立另一個contracts檔案夾:
~$ mkdir -p ethers-template/contracts
1.2 初始化npm配置
然後進入ethers-template目錄初始化npm配置:
~$ cd ethers-template
~/ethers-template$ npm init -y
1.3 建立項目配置檔案
接下來建立一個config.json檔案儲存你的項目配置:
{
"private_key": "24C4FE6063E62710EAD956611B71825B778B041B18ED53118CE5DA5F02E494BA",
"network": "kovan",
"ERC20": "0x0DEd9F7D82a24099F09AF7831CaB61B31Df10487",
"name": "Kanchan Coin",
"symbol": "SNK",
"total_supply": "1000000000000000000000000",
"decimals": 18
}
說明如下:
- private_key:賬戶私鑰,将使用這個私鑰對應的賬戶在指定網絡上部署智能合約。
- network:要接入的以太坊網絡,ethers.js支援以下網絡:
- homestead:主網
- rinkeby
- ropsten
- kovan
- goerli
- ERC20:聲明要互動的已部署合約,可選
- name/symbol/decimals:ERC20合約的參數
1.4 安裝ethers.js
現在可以安裝ethers.js:
~/ethers-template$ npm install --save ethers
1.5 安裝編譯合約需要的npm包
為了編譯合約,我們還需要安裝solc和fs-extra:
~/ethers-template$ npm install [email protected] [email protected] --save
1.6 建立ERC20合約代碼
在contracts目錄下建立檔案erc20.sol:
~/ethers-template$ touch contracts/erc20.sol
并按如下内容修改:
pragma solidity ^0.5.0;
contract ERC20 {
using SafeMath for uint256;
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
event Transfer(address indexed from, address indexed to, uint tokens);
mapping(address => uint256) balances;
mapping(address => mapping (address => uint256)) allowed;
string public symbol;
uint8 public decimals;
string public name;
uint256 private _totalSupply;
constructor(uint8 _decimals, string memory _symbol, string memory _name, uint256 _total_supply) public{
decimals = _decimals;
symbol = _symbol;
name = _name;
_totalSupply = _total_supply;
balances[msg.sender] = _totalSupply;
}
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
function balanceOf(address tokenOwner) public view returns (uint) {
return balances[tokenOwner];
}
function transfer(address receiver, uint numTokens) public returns (bool) {
require(numTokens <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender].sub(numTokens);
balances[receiver] = balances[receiver].add(numTokens);
emit Transfer(msg.sender, receiver, numTokens);
return true;
}
function approve(address delegate, uint numTokens) public returns (bool) {
allowed[msg.sender][delegate] = numTokens;
emit Approval(msg.sender, delegate, numTokens);
return true;
}
function allowance(address owner, address delegate) public view returns (uint) {
return allowed[owner][delegate];
}
function transferFrom(address owner, address buyer, uint numTokens) public returns (bool) {
require(numTokens <= balances[owner]);
require(numTokens <= allowed[owner][msg.sender]);
balances[owner] = balances[owner].sub(numTokens);
allowed[owner][msg.sender] = allowed[owner][msg.sender].sub(numTokens);
balances[buyer] = balances[buyer].add(numTokens);
emit Transfer(owner, buyer, numTokens);
return true;
}
}
library SafeMath {
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
1.7 編寫合約編譯腳本
下面的代碼使用solc編譯合約檔案,将其儲存為compile.js:
const path = require('path');
const fs = require('fs-extra');
const solc = require('solc');
const config = require('./config.json');
const sourceFolderPath = path.resolve(__dirname, 'contracts');
const buildFolderPath = path.resolve(__dirname, 'build');
const getContractSource = contractFileName => {
const contractPath = path.resolve(__dirname, 'contracts', contractFileName);
const source = fs.readFileSync(contractPath, 'utf8');
return source;
};
let sources = {};
fs.readdirSync(sourceFolderPath).forEach(contractFileName => {
sources = {
...sources,
[contractFileName]: {
content: getContractSource(contractFileName)
}
}
});
const input = {
language: 'Solidity',
sources,
settings: {
outputSelection: {
'*': {
'*': [ '*' ]
}
}
}
}
console.log('\nCompiling contracts...');
const output = JSON.parse(solc.compile(JSON.stringify(input)));
console.log('Done');
let shouldBuild = true;
if (output.errors) {
console.error(output.errors);
// throw '\nError in compilation please check the contract\n';
for(error of output.errors) {
if(error.severity === 'error') {
shouldBuild = false;
throw 'Error found';
break;
}
}
}
if(shouldBuild) {
console.log('\nBuilding please wait...');
fs.removeSync(buildFolderPath);
fs.ensureDirSync(buildFolderPath);
for (let contractFile in output.contracts) {
for(let key in output.contracts[contractFile]) {
fs.outputJsonSync(
path.resolve(buildFolderPath, `${key}.json`),
{
abi: output.contracts[contractFile][key]["abi"],
bytecode: output.contracts[contractFile][key]["evm"]["bytecode"]["object"]
},
{
spaces:2,
EOL: "\n"
}
);
}
}
console.log('Build finished successfully!\n');
} else {
console.log('\nBuild failed\n');
}
上面的代碼将讀入并編譯contracts目錄中的所有合約檔案,然後将編譯得到的abi和位元組碼儲存為json檔案。
1.8 編譯合約
現在我們使用compile.js來編譯erc20.sol合約:
~/ethers-template$ node compile.js
編譯結束後,我們得到如下的目錄結構;
+ethers-template
+compile.js
+contracts
-erc20.sol
+build
-ERC.json
-Context.json
-IERC20.sjon
-SafeMath.json
-package.json
1.9 編寫合約部署腳本
建立檔案deploy.js:
~/ethers-template$ touch deploy.js
然後按如下内容修改:
const startTimestamp = Date.now();
const ethers = require('ethers');
const config = require('./config.json');
const fs = require('fs-extra');
const provider = ethers.getDefaultProvider(config["network"]);
const wallet = new ethers.Wallet(config["private_key"], provider);
console.log(`Loaded wallet ${wallet.address}`);
let compiled = require(`./build/${process.argv[2]}.json`);
(async() => {
console.log(`\nDeploying ${process.argv[2]} in ${config["network"]}...`);
let contract = new ethers.ContractFactory(
compiled.abi,
compiled.bytecode,
wallet
);
let instance = await contract.deploy(config["decimals"], config["symbol"], config["name"], config["total_supply"]);
console.log(`deployed at ${instance.address}`)
config[`${process.argv[2]}`] = instance.address
console.log("Waiting for the contract to get mined...")
await instance.deployed()
console.log("Contract deployed")
fs.outputJsonSync(
'config.json',
config,
{
spaces:2,
EOL: "\n"
}
);
})();
注意:
- 上面代碼中的預設網絡是kovan測試網
- 在這個測試網中,你的賬号需要一些以太币來支付部署交易的手續費
- 将使用config.json中的private_key來部署合約
1.10 部署合約
運作deploy.js腳本時,需要在指令行傳入要部署的合約名稱ERC20:
~/ethers-template$ node deploy.js ERC20
輸出結果如下:
Loaded wallet 0xC8e1F3B9a0CdFceF9fFd2343B943989A22517b26
Deploying ERC20 in kovan...
deployed at 0x77Bb3546f5ee356E4026BaA96b7DDf22141bd77B
Waiting for the contract to get mined...
Contract deployed
在與合約互動時需要合約部署位址,上面的代碼會自動将合約部署位址儲存到config.json檔案中。
2、與智能合約互動
在這個教程中,我們使用ES6來編寫合約互動代碼,然後使用webpack和babel将ES6代碼轉換為ES5代碼。
2.1 安裝ES6建構工具
首先安裝這些依賴項:
~/ethers-template$ npm i webpack webpack-cli @babel/core \
@babel/plugin-proposal-object-rest-spread \
@babel/preset-env babel-loader \
babel-polyfill -D
2.2 編寫前端腳本
建立一個檔案app.js:
~/ethers-template$ touch app.js
然後按照如下代碼修改:
const ethers = require('ethers');
const config = require('./config.json');
// Import the json file from build to get the abi
const erc_json = require('./build/ERC20.json'); //import the json of the contract which you want to interact
// You can use any standard network name
// - "homestead"
// - "rinkeby"
// - "ropsten"
// - "kovan"
// - "goerli"
const provider = ethers.getDefaultProvider(config['network']);
// Make a wallet instance using private key and provider
const wallet = new ethers.Wallet(config['private_key'] , provider);
const address = config["ERC20"];
const abi = erc_json.abi;
erc20 = new ethers.Contract( address , abi , wallet );
document.getElementById("send").onsubmit = async function(e) {
e.preventDefault();
let address = document.getElementById("address").value;
document.getElementById("status").innerText = "Waiting for transaction to get published...";
let tx = await erc20.functions.transfer(address, "1000000000000000000");
let tx_hash = tx.hash;
let node = document.createElement("LI");
let link = document.createElement("A");
link.target = "_blank";
link.href = `https://${config["network"]}.etherscan.io/tx/` + tx_hash;
let textnode = document.createTextNode(tx_hash);
link.appendChild(textnode);
node.appendChild(link);
document.getElementById("transactions").appendChild(node);
document.getElementById("status").innerText = "Waiting for transaction to be mined...";
await tx.wait();
document.getElementById("status").innerText = "Transaction confirmed";
return false;
};
首先我們需要指定要使用的網絡/提供器:
const provider = ethers.getDefaultProvider(config['network']);
為了與合約互動,我們需要2個東西:
- 合約部署位址
- 合約ABI接口
在上面app.js中,我們從配置檔案引入了合約位址,從合約編譯結果目錄中引入了合約ABI:
//import the json of the contract which you want to interact
const erc_json = require('./build/ERC20.json');
const config = require('./config.json');
const address = config["ERC20"];
const abi = erc_json.abi;
2.3 建立錢包執行個體
為了建立合約執行個體,我們需要先建立一個錢包執行個體,這樣不管什麼時候調用setter方法,都需要一個私鑰來簽名交易。在ethers.js中,你隻需要建立錢包,所有的setter方法就會由這個錢包簽名。
const wallet = new ethers.Wallet(config['private_key'] , provider);
你也可以使用keystore和助記詞來建立一個錢包。如果你希望用這個錢包與智能合約互動,你還需要傳入提供器。如果你隻是想用私鑰簽名消息,那麼就不需要提供器了。
erc20 = new ethers.Contract( address , abi , wallet );
上面的代碼建立了一個合約執行個體,然後你就可以像這樣調用合約函數:
erc20.functions.function_name_in_smart_contract(parameters);
小白備注:在ERC20中定義了一個函數transfer,它的參數是轉賬位址和代币數量。下面的代碼調用了合約的transfer函數,錢包将簽名這個交易,然後釋出到指定的網絡中:
erc20.functions.transfer(address, "1000000000000000000");
注意:無論任何時候你要建立一個交易,錢包裡都需要以太币來支付交易手續費。
2.4 修改啟動腳本
在package.json中添加如下内容:
"deploy": "node compile.js && node deploy.js ERC20",
"build": "webpack — mode production',
修改後的package.json看起來是這樣:
{
"name": "ethers-template",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"deploy": "node compile.js && node deploy.js ERC20",
"build": "webpack --mode production",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ethers": "^4.0.37",
"fs-extra": "^8.1.0",
"solc": "^0.5.11"
},
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/preset-env": "^7.6.0",
"babel-loader": "^8.0.6",
"babel-polyfill": "^6.26.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
}
}
如果你後續修改了智能合約,就需要重新編譯和部署。可以用一個指令來完成合約的編譯和部署:
~/ethers-template$ npm run deploy
這個指令會自動修改配置檔案和合約構件檔案。這樣在與合約互動時,你就不需要修改合約位址或者ABI接口了。
如果你修改了app.js,也需要重新建構前端代碼:
~/ethers-template$ npm run build
這會生成新的釋出版本:dist/bundle.js。
2.5 建立宿主html檔案
建立一個新的檔案index.html:
~/ethers-template$ touch index.html
按如下内容修改:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Ethers Template</title>
<style>
body {
padding-top: 75px;
}
.search-container {
width: 490px;
display: block;
margin: 0 auto;
}
input#address {
margin: 0 auto;
width: 100%;
height: 45px;
padding: 0 20px;
font-size: 1rem;
border: 1px solid #D0CFCE;
outline: none;
}
.center {
text-align: center;
}
ol {
counter-reset: list;
list-style: none;
}
li {
counter-increment: list;
margin-bottom: 10px;
}
li::before {
content: counter(list, decimal-leading-zero);
background: #2b4353;
font-family: Arial, sans-serif;
color: #fff;
font-size: 13px;
text-align: center;
border-radius: 50%;
width: 2.2em;
height: 2.2em;
line-height: 2.3em;
display: inline-block;
margin-right: 1em;
}
.button {
background-color: #4CAF50;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 19px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="center">
<h1>Ethers Template</h1>
<form class="search-container" id="send">
<input type="text" id="address" placeholder="Enter you address to get kanchan Coin">
<button type="submit" class="button">Send</button>
</form>
<h2>Status:</h2>
<p id="status"></p>
<h2>Transactions</h2>
<ol id="transactions">
</ol>
</div>
<script src="dist/bundle.js"></script>
</body>
</html>
現在你的檔案夾看起來就是這樣了:

其中build檔案夾将在運作compile.js之後自動建立,而dist
檔案夾是在
npm run build
執行後自動建立。
用浏覽器通路看起來是這樣:
原文連結:
Ethers.js簡明教程 - 彙智網