上一篇介绍了空投合约的实现,本篇将带大家开发实现以太坊钱包的基础功能。
现在市场上有很多以太坊钱包应用,比如热钱包、冷钱包、网页钱包、App 钱包等等。不论是哪种形式的钱包,其中最主要的两个功能是转入和转出。本质上,转入和转出都可以统一理解成转账形式,只是实现逻辑有差异。
转入是指从其它地址转入本方地址,需要记录每笔 Transaction ,甚至根据业务情况还需对其进行对账处理。
A 地址向 B 地址转了一定数量的 ETH 后,B 地址 如何知道有一笔入账呢?个人理解有两种方式,一种是通过爬取以太坊区块,分析每个区块中的 Transaction;另一种是通过 web3.eth.getBalance()
方法定时轮循获取 B 地址 的余额。
第一种方式实现起来略复杂,需要爬取钱包发布后的所有以太坊区块,对每个区块中的每笔 Transaction 进行分析对比。第二种方式能定时获取 B 地址的余额,但还要进一步去获取最近尚未做入账处理的 Transaction 记录。
两种方式各有利弊,大家可以根据各自的实际业务情况作出抉择,具体的实现逻辑这里就不做详细介绍,只简单说下个人的观点和思路,有兴趣者可加我微信继续沟通交流。
ERC20 转入入账可以通过事件监听的方式实现。符合协议标准的 Token 合约都会定义一个 Transfer 事件:
event Transfer(address indexed from, address indexed to, uint256 value);
event 事件是以太坊虚拟机日志功能提供的接口。我们可以在合约外监听事件,当合约内的事件被触发时,EVM 日志机制会回调合约外的事件监听函数。值得一提的是,event 事件可被继承。
细心的读者可能会发现上面代码中 Transfer 事件的参数里,有一个 indexed 的关键字。indexed 可以理解为索引,可以使用 indexed 参数的特定值来进行数据筛选与过滤。
下面贴一段监听 ERC20 转账事件的核心代码:
//引入web3模块
const Web3 = require('web3');
//引入fs模块
const fs = require('fs');
//代币合约ABI
const contractABI = [......];
//合约地址
const contractAddress = "0xea5e7f50bb65518......";
//实例化web3对象,并设置websocket连接
const web3 = new Web3(new Web3.providers.WebsocketProvider('wss://ropsten.infura.io/ws/JECwNr......'));
//实例化代币合约对象
const tokenContract = new web3.eth.Contract(contractABI, contractAddress);
//需要监听的接收地址(即所谓的 B地址)
const toAddress = '0xd24560d8b940ee089a1d83......';
//事件监听
tokenContract.events.Transfer({
filter:{to:toAddress}
}).on('data', function(result){
//将打印转账事件的数据
console.log(result);
}).on('error', function(result){
//将打印异常信息
console.log(result);
});
假设我们发行了一种名为 TEST 的代币,我们往 toAddress 地址转入一定数量的 TEST 代币后,上面的代码会监听到 TEST 代币合约里触发了 Transfer 事件,并接收到这笔 Transaction 的详细数据。我们可以根据这些数据做后续的入账逻辑处理。
转出是指从本方地址转到其它地址,通过程序实施签名后生成一笔 Transaction 并发送到以太坊区块网络上。
我们看下面这段程序代码:
//本方地址
const from = '0xd24560d8b940ee089a1d83......';
//from地址的私钥
const privateKey = '......';
//接收地址
const to = '0xd24560d8b940ee089a1d83......';
//转账金额(单位wei)
const amount = 1;
//转账备注
const data = '0x';
//计算预估gasLimit
const gasLimit = await web3.eth.estimateGas({
to: to,
data: data
});
//获取gas
let gasPrice = await web3.eth.getGasPrice();
//设置gas费用最少为10Gwei,加快打包上链速度
if(Number(gasPrice) < 10000000000){ //10000000000为10Gwei
gasPrice = 10000000000;
}
//获取Nonce值
const nonce = await web3.eth.getTransactionCount(from, 'pending');
//生成transaction签名参数
const txParams = {
// nonce:nonce,
gasPrice: gasPrice,
gas: gasLimit,
to: to,
value: amount,
data:data
};
//对交易进行签名
const signTransaction = await web3.eth.accounts.signTransaction(txParams, privateKey);
//发起交易
const transaction = web3.eth.sendSignedTransaction(signTransaction.rawTransaction);
function getTxHash(){ //包装promise,方便返回txhash
return new Promise((resolve, reject) => {
//监听transactionHash事件
transaction.on('transactionHash', (txhash) => {
resolve(txhash);
}).catch((error)=>{
reject(error);
});;
});
}
//返回transaction hash
const transactionHash = await getTxHash();
代码配合着注释很容易理解,流程也很简单:准备参数、生成签名、发起交易。不过这里涉及到一个 Nonce 值的问题。以太坊中为了防止交易重播攻击,每笔 Transaction 都必须有一个 Nonce 随机数。每个账户从同一个节点发起交易时的 Nonce 值都是从 0 开始,发送一笔 Transaction 后 Nonce 加 1,当 Nonce 为 0 的交易处理完之后,才会处理 Nonce 为 1 的交易,并以此类推。
以下是 Nonce 使用的规则:
当 Nonce 值小于之前 Transaction 的 Nonce 值时,交易会被直接拒绝;
当 Nonce 值过大时,Transaction 会一直处于队列之中,等待执行;
假设账号 C 最后一笔 Transaction 的 Nonce 值为 10,此时发送一笔 Nonce 值为 13 的 Transaction 至节点中。此笔交易会一直处于队列中,不会立即打包上链。需要等待 Nonce 值为 11 和 12 的 Transaction 依次执行完后才会来处理这笔 Transaction。
需要注意的是,当最新的一笔 Transaction 正处于 Pending 状态,此时向节点发送一笔相同 Nonce 值的 Transaction,新的 Transaction 可能会覆盖旧的 Transaction,具体取决于两笔 Transaction 的 Gas 值。当新 Transaction 的 Gas 值小于旧 Transaction 的 Gas 值时,无法覆盖处于 Pending 状态的旧 Transaction。反之,则能覆盖。
Nonce 值的问题需要特别重视,稍有不慎则会导致发送的 Transaction 一直被挂起。我们可以在自己的业务系统当中维护每个账户的 Nonce 值,也可以让以太坊节点客户端来维护。在上述代码中生成签名参数时,并没有传入 Nonce 值,则是利用了以太坊节点客户端会自行维护其值的特点。
ERC20 转出和 ETH 类似,只是处理代码上略有差异:
//本方地址
const from = '0xd24560d8b940ee089a1d83......';
//from地址的私钥
const privateKey = '......';
//接收地址
const to = '0xd24560d8b940ee089a1d83......';
//erc20代币ABI
const contractABI = [......];
//erc20代币合约地址
const contractAddress = '0xd24560d8b940ee089a1d83......';
//转账金额(单位wei)
const amount = 1;
//获取Nonce值
const nonce = await web3.eth.getTransactionCount(from, 'pending');
//获取gas
let gasPrice = await web3.eth.getGasPrice();
//设置gas费用最少为10Gwei,加快打包上链速度
if(Number(gasPrice) < 10000000000){ //10000000000为10Gwei
gasPrice = 10000000000;
}
//实例化erc20代币对象
const tokenContract = new web3.eth.Contract(contractABI, contractAddress);
//编译input data
const data = tokenContract.methods.transfer(to, amount).encodeABI();
//计算预估gasLimit
let gasLimit = await web3.eth.estimateGas({
to: to,
data: data
});
//设置erc20转账时的gasLimit最少为90000
if(Number(gasLimit) < 90000){
gasLimit = 90000;
}
//生成Transaction签名参数
const txParams = {
// nonce:nonce,
gasPrice: gasPrice,
gas: gasLimit,
to: contractAddress, //这里传入的是erc20代币合约地址
data:data,
};
//对交易进行签名
const signTransaction = await web3.eth.accounts.signTransaction(txParams, privateKey);
//发起交易
const transaction = web3.eth.sendSignedTransaction(signTransaction.rawTransaction);
function getTxHash(){ //包装promise,方便返回txhash
return new Promise((resolve, reject) => {
transaction.on('transactionHash', (txhash) => {
resolve(txhash);
}).catch((error)=>{
reject(error);
});
});
}
//返回transaction hash
const transactionHash = await getTxHash();
整体流程和 ETH 转出大同小异,只是必须先获取 ERC20 代币的实例化对象,再做后续的处理。
实现了以上这两个功能,钱包的雏形就出来了。当然这只是钱包的基础功能,其他功能的实现,有兴趣者可以自行研究。