如果您已經在以太坊上構建了daap,那麼很可能您使用web3.js構建了javascript前端。Ethers.js是一個輕量級的JavaScript庫,可以替代Web3.js。在這篇文章/教程中,我將展示如何使用Ethers.js來構建一個簡單的dApp。
與web3.js相比,ethers.js有許多優勢。我想集中討論ethers.js中的一個主要特性,即狀態和金鑰管理。web3假設有一個本地節點連線到應用程式。假設該節點儲存金鑰、簽署事務、與以太坊區塊鏈互動和讀取。實際上情況並非如此,大多數使用者都沒有在本地執行geth。Metamask透過瀏覽器應用程式有效地模擬該環境,因此大多數Web3應用程式需要Metamask來儲存金鑰、簽署事務,並與以太坊主網進行互動。
Ethers.js採用了不同的方法,為開發人員提供了更大的靈活性。 Ethers.js將“節點”分為兩個單獨的角色:
持有鑰匙並簽署交易的“錢包”。
一個“provider”用作與以太坊網路的匿名連線,檢查狀態併傳送交易。
編譯和部署智慧合約
在我們的教程中,我們將與ERC20智慧合約進行互動。 您需要在裝置中安裝nodejs和npm。
1、建立一個名為ethers-template的資料夾,並在ethers-template內建立另一個名為contract的資料夾。
mkdir ethers-template && cd ethers-template && mkdir contracts
2、執行npm init命令建立一個package.json檔案,該檔案將儲存專案依賴項:
`npm init -y`
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” (main network)
“rinkeby”
“ropsten”
“kovan”
“goerli”
ERC20:“ 0x0b0Ce2d3f67b4482BD830Ac24ECa5E8d022Fd32f”行是可選的。 如果要與已部署的智慧合約進行互動,則可以在此處寫地址。名稱,符號,小數與ERC20引數有關。
4、使用npm安裝ethers.js
npm install --save ethers
5、要編寫合同,請安裝fs-extra和solc
npm install [email protected] [email protected] --save
6、在合約資料夾中將以下程式碼另存為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;
}
}
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');
}
node compile.js
上面的程式碼將讀取Contracts目錄中的所有智慧合約,並將abi和bytecode儲存為json檔案。 所有的json檔案將儲存在構建目錄中。 編譯後,我們得到以下結構。
+ethers-template
+compile.js
+contracts
-erc20.sol
+build
-ERC.json
-Context.json
-IERC20.sjon
-SafeMath.json
-package.json
8、建立一個名為deploy.json的新檔案並貼上以下程式碼
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"
}
);
})();
執行部署檔案時,應將合同名稱作為命令列引數。
注意:
1.上面程式碼中的預設網路是kovan測試網路。
2.您需要具有該網路的以太幣才能支付部署的交易費用。
3.合同將從config.json中指定的私鑰進行部署。
部署命令為:
node deploy.js <contract_name>
在我們的例子中,命令將是
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中.
與智慧合約互動
1、在本教程中,我們使用ES6編寫程式碼並將ES6轉換為ES5,我們將使用帶babel loader的webpack。
安裝開發人員依賴項:
npm i webpack webpack-cli @babel/core @babel/plugin-proposal-object-rest-spread @babel/preset-env babel-loader babel-polyfill -D
2、建立一個新檔案app.js並貼上下面的程式碼。此檔案的已轉換ES5程式碼將儲存在dist/bundle.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;
};
首先,我們必須指定我們將要工作的provider/network。
const provider = ethers.getDefaultProvider(config['network']);
為了與智慧合約進行互動,您需要做兩件事:
1.智慧合約地址。
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;
3、在建立合同例項之前,我們必須先建立一個錢包例項,以便每當呼叫setter函式(或必須進行交易)時,都必須有一個私鑰來簽署這些交易。 在ethers.js中,您只需要建立wallet(簽名者),所有setter函式(交易)都將由此wallet簽名。
const wallet = new ethers.Wallet(config['private_key'] , provider);
您也可以在金鑰庫和助記符的幫助下製作錢包。 如果您要與此錢包進行智慧合約互動,則應透過provider。 如果您只想使用私鑰對郵件簽名,則不需要provider。
erc20 = new ethers.Contract( address , abi , wallet );
上面的程式碼建立了一個合約例項,您可以像這樣呼叫合約功能
erc20.functions.function_name_in_smart_contract(parameters);
例如:在ERC20中,我們有一個名為transfer的函式,該函式將地址和金額作為引數。 下面的程式碼呼叫傳遞函式。 電子錢包將簽署此交易並將其釋出到指定的網路。
erc20.functions.transfer(address, "1000000000000000000");
注意:無論何時進行交易,都應該在錢包裡放以太幣,以支付gas。
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"
}
}
現在每當你對智慧合同做任何修改時,你就必須編譯和修改這些更改。編譯和部署都可以在一個命令中完成,即
npm run deploy
這將自動更改配置和構建檔案中的地址和ABI。在互動過程中,您無需更改任何地址或abi,它將自動更新。
如果您對app.js進行了任何更改,則必須透過以下方式對其進行編譯
npm run build
這將生成(或更新)dist / bundle.js檔案。
5、新建一個名為index.html的檔案。 該頁面僅包含一個文字輸入,該文字輸入需要地址並向輸入的地址傳送1個令牌。 交易雜湊值附加在文字輸入下方,並且當前交易的狀態也可以在狀態部分中看到。
<!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">
<h2>Ethers Template</h2>
<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>
現在,您的資料夾結構應如下所示。
執行後建立構建資料夾
node complie.js
執行後建立dist資料夾
npm run build
總結
整個專案的原始碼可以在這裡找到。您可以將此儲存庫用作模板。複製原始碼並做必要的改變。
git clone https://github.com/SauravKanchan/ethers-template.git
cd ethers-template
npm i
npm run deploy
npm run build