第08篇:以太坊钱包开发

第08篇:以太坊钱包开发

上一篇介绍了空投合约的实现,本篇将带大家开发实现以太坊钱包的基础功能。

现在市场上有很多以太坊钱包应用,比如热钱包、冷钱包、网页钱包、App 钱包等等。不论是哪种形式的钱包,其中最主要的两个功能是转入和转出。本质上,转入和转出都可以统一理解成转账形式,只是实现逻辑有差异。

转入

转入是指从其它地址转入本方地址,需要记录每笔 Transaction ,甚至根据业务情况还需对其进行对账处理。

ETH 转入

A 地址向 B 地址转了一定数量的 ETH 后,B 地址 如何知道有一笔入账呢?个人理解有两种方式,一种是通过爬取以太坊区块,分析每个区块中的 Transaction;另一种是通过 web3.eth.getBalance() 方法定时轮循获取 B 地址 的余额。

第一种方式实现起来略复杂,需要爬取钱包发布后的所有以太坊区块,对每个区块中的每笔 Transaction 进行分析对比。第二种方式能定时获取 B 地址的余额,但还要进一步去获取最近尚未做入账处理的 Transaction 记录。

两种方式各有利弊,大家可以根据各自的实际业务情况作出抉择,具体的实现逻辑这里就不做详细介绍,只简单说下个人的观点和思路,有兴趣者可加我微信继续沟通交流。

ERC20 转入

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 并发送到以太坊区块网络上。

ETH 转出

我们看下面这段程序代码:

//本方地址
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 都会被清除。

需要注意的是,当最新的一笔 Transaction 正处于 Pending 状态,此时向节点发送一笔相同 Nonce 值的 Transaction,新的 Transaction 可能会覆盖旧的 Transaction,具体取决于两笔 Transaction 的 Gas 值。当新 Transaction 的 Gas 值小于旧 Transaction 的 Gas 值时,无法覆盖处于 Pending 状态的旧 Transaction。反之,则能覆盖。

Nonce 值的问题需要特别重视,稍有不慎则会导致发送的 Transaction 一直被挂起。我们可以在自己的业务系统当中维护每个账户的 Nonce 值,也可以让以太坊节点客户端来维护。在上述代码中生成签名参数时,并没有传入 Nonce 值,则是利用了以太坊节点客户端会自行维护其值的特点。

ERC20 转出

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 代币的实例化对象,再做后续的处理。

实现了以上这两个功能,钱包的雏形就出来了。当然这只是钱包的基础功能,其他功能的实现,有兴趣者可以自行研究。

上一篇
下一篇
内容互动
写评论
加载更多
评论文章