前言

最近在写一个自动转账的脚本,经过了一番搜寻查阅及实战编写,总结了一些心得和见解,写出来希望能帮助到更多有需要的人。

以太币及合约币转账概念

一个以太坊账号包含三个部分,助记词(Mnemonic Phrase)私钥(Private Key) 以及 地址 ,其中助记词与私钥是可以相互转换的。

由于私钥64位,长得太难看,没有可读性,而私钥的备份在电脑上复制起来容易,手抄下来就比较麻烦,但私钥保存在联网的电脑上毕竟不安全,有被其他人看到的风险,于是有了助记词工具,利用某种算法可以将64位私钥转换成十多个常见的英文单词,这些单词都来源于一个固定词库,根据一定算法得来。私钥与助记词之间的转换是互通的,助记词只是你的私钥的另一种外貌体现。

以太坊是一个分布式的智能合约平台,有一套 ERC-20 标准,通过此标准可以发行自己的合约币(Token),最早叫代币,后来统一翻译为通证。以太币是以太坊发行的币,主要是用来结算合约币(Token)交易费用。

以太坊有一套核心 EVM(以太坊虚拟机) 运行在分布式的智能合约平台,这套核心在区块链上每个参与的节点上运行,开发者可以在其上开发各种应用。与比特币的脚本引擎不同,以太坊的 EVM 功能非常强大,号称“图灵完备”。运行在 EVM 上的脚本称作 DApp(Decentralized Application),当我们发送一条交易时,这条交易会广播一条消息,收到这条消息的账户会运行消息相应的一系列指令,而运行指令的过程会消耗 gas,当整个交易完成后会根据总共运行的指令量计算出 gasUsedgas 的单位为 Gwei,运行不同的指令会干不同的或活,干不同的活所消耗的资源也不尽相同,这个表格 列举了以太坊的指令所对应消耗的 gas 量。

gas 这个名字起的非常贴切,翻译过来就是 汽油 的意思。如果把以太坊比做一台汽车,运行需要汽油驱动。汽油的价格称作 gasPrice,车跑的过程所消耗的油量称作 gasUsed,可以通过 gasLimit 来限制 gasUsed 的最大值,当超过这个值时就会终止,在终止之前所消耗的 gasUsed 依然会被扣除。如果直到交易完成也没触发或者恰好等于 gasLimit,那么这个交易就会成功,交易完成后只扣除 gasUsed。这里的 gasPrice 不像现实的汽油一样可以由自己控制,这是因为当你设置 gasPrice 油价越高时,你的交易就会被提前处理,举个不恰当的例子:相当于出高价可以买 98 的油,出低价只能买 92 的油一样。

gas 常用单位为 Gwei,还有比它小的 Weigas 在交易完成后会转换为 Ether 进行结算,具体转换如下:

1
2
3
4
5
1 Gwei  = 1,000,000,000 wei
1 Ether = 1,000,000,000 Gwei

交易费:
fee = gasUsed * gasPrice

每笔交易所消耗的 gas 不能提前计算获得,只能交易完成后才能确定。虽然不能提前知道,但是可以根据最近转账所使用的 gasUsed 大概预估出来。

通过以上的学习我们可以做个小练习,在以太坊浏览器 https://etherscan.io 随便找个交易然后计算它的 gas 消耗,比如拿这个交易进行测试 0x20b727…866df6,只需要关注以下四个字段中的三个就能套用以上教程进行推导计算

keyvalue
Transaction Fee0.000393081 Ether ($0.06)
Gas Limit30,237
Gas Used by Transaction30,237 (100%)
Gas Price0.000000013 Ether (13 Gwei)
1
2
3
4
5
6
7
const gasPrice = 13; // Gwei
const gasLimit = gasUsed = 30237; // 这里 Gas Used by Transaction 为 100%,说明刚好达到限制额度

// 由以上的 fee = gasUsed * gasPrice 公式可的
const fee = gasUsed * gasPrice; // 393081 Gwei
// 由 1 ether = 1,000,000,000 Gwei 可得
const ether = gasUsed * gasPrice / 1e9; // 0.000393081 Ether

最终结果:0.000393081 Ether,与 Transaction Fee 字段结果一样,说明我们的算法是没毛病的。

第三方接口及主要依赖库

  1. https://infura.io/docs
  2. https://github.com/ethereum/web3.js

转账业务逻辑编写

安装依赖包

1
2
3
$ npm install bip39 eth-json-rpc-infura ethereumjs-wallet web3
$ # 如果出错使用带版本号方式安装
$ npm install bip39@2.5.0 eth-json-rpc-infura@4.0.0 ethereumjs-wallet@0.6.3 web3@1.0.0-beta.51

创建 ./provider.js,让 web3 支持 infura,支持后可以链接到同步以太坊 mainnet 主网络

1
2
3
4
5
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider');

const provider = {send: createInfuraProvider().sendAsync};

module.exports = provider;

创建 ./client.js,初始化 web3 对象

1
2
3
4
5
6
7
8
const Web3 = require('web3');
const Transaction = require('ethereumjs-tx');

const provider = require('./provider');
// ERC20 ABI:https://github.com/ethereum/wiki/wiki/Contract-ERC20-ABI
const contractABI = require('./erc20-abi.json');

const web3 = new Web3(provider);

转账需要密钥,如果你使用的是助记词(Mnemonic Phrase),需要先转换成密钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const bip39 = require('bip39');
const hdkey = require('ethereumjs-wallet/hdkey');


function generateAddressesFromSeed(seed, count = 2) {
const hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(seed));
const wallet_hdpath = "m/44'/60'/0'/0/";

const accounts = [];
for (let i = 0; i < count; i++) {
let wallet = hdwallet.derivePath(wallet_hdpath + i).getWallet();
let address = '0x' + wallet.getAddress().toString("hex");
let privateKey = wallet.getPrivateKey().toString("hex");
accounts.push({ address: address, privateKey: privateKey });
}

return accounts;
}


const accounts = generateAddressesFromSeed('epoch research about divide instrument with postdoctoral optional science someone epoch can');
const myAddress = accounts[0].address;
const privateKey = accounts[0].privateKey; // 转换后的密钥

以太币转账逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const privateKey = Buffer.from('YOUR_PRIVATE_KEY', 'hex'); // 私钥

const myAddress = 'ADDRESS_THAT_SENDS_TRANSACTION'; // 你的地址
const toAddress = '0x114fdf4ebd30ae646fac486b4e796616c95ca244'; // 接收者的地址

// 转账数量,单位 Wei,web3.utils.toWei 可将 1 个 Ether 转换为 Wei
const amount = web3.utils.toHex(web3.utils.toWei('1', 'ether'));

web3.eth.getTransactionCount(accounts[0].address)
.then(async count => {
//creating raw tranaction
const txParams = {
from: myAddress,
gasPrice: web3.utils.toHex(1 * 1e9),
gasLimit: web3.utils.toHex(210000),
to: toAddress,
value: amount,
nonce: web3.utils.toHex(count)
};
//creating tranaction via ethereumjs-tx
const tx = new Transaction(txParams);
//signing transaction with private key
tx.sign(privateKey);
//sending transacton via web3 module
web3.eth.sendSignedTransaction('0x' + tx.serialize().toString('hex'))
.on('transactionHash', console.log);
})
.catch(err => {
console.log(err);
});

使用合约币(Token)转账,注意这里的 amount 合约币的转账数量需要乘上合约币发行时设置的精度(decimals)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 合约币地址
const contractAddress = '0x3c76ef53be46ed2e9be224e8f0b92e8acbc24ea0';

// 创建合约币对象
const contract = new web3.eth.Contract(contractABI, contractAddress);

// 设置 amount 前需要先获取合约币精度
const decimals = await contract.methods.decimals().call();

// amount = 转账数量 × decimals,例如这里转一个 XXX 合约币
const amount = web3.utils.toHex(1 * decimals);


web3.eth.getTransactionCount(accounts[0].address)
.then(async count => {
//creating raw tranaction
const txParams = {
from: myAddress,
gasPrice: web3.utils.toHex(1 * 1e9),
gasLimit: web3.utils.toHex(210000),
to: contractAddress,
value: web3.utils.toHex(0),
data: contract.methods.transfer(toAddress, amount).encodeABI(),
nonce: web3.utils.toHex(count)
};
//creating tranaction via ethereumjs-tx
const tx = new Transaction(txParams);
//signing transaction with private key
tx.sign(privateKey);
//sending transacton via web3 module
web3.eth.sendSignedTransaction('0x' + tx.serialize().toString('hex'))
.on('transactionHash', console.log);
})
.catch(err => {
console.log(err);
});

txParams 参数说明:

keydescription
nonce发送者发送交易数的计数,按当前账号下的交易进行递增(从 0 开始),使用相同的 nonce 则可覆盖 pending 状态的交易,为了保险起见调用 web3.eth.getTransactionCount 方法获取当前账号交易计数
gasPrice发送者愿意支付执行交易所需的每个gas的Wei数量
gasLimit发送者愿意为执行交易支付gas数量的最大值。这个数量被设置之后在任何计算完成之前就会被提前扣掉
to接收者的地址。在合约创建交易中,合约账户的地址还没有存在,所以值先空着
value从发送者转移到接收者的Wei数量。在合约创建交易中,value作为新建合约账户的开始余额
init用来初始化新合约账户的EVM代码片段(只有在合约创建交易中存在)。init值会执行一次,然后就会被丢弃。当init第一次执行的时候,它返回一个账户代码体,也就是永久与合约账户关联的一段代码。
data消息通话中的输入数据,也就是参数(可选域,只有在消息通信中存在)例如,如果智能合约就是一个域名注册服务,那么调用合约可能就会期待输入域例如域名和IP地址
v,r,s用于产生标识交易发生着的签名

txParams.nonce 说明:

  • 当nonce太小,交易会被直接拒绝。
  • 当nonce太大,交易会一直处于队列之中,这也就是导致我们上面描述的问题的原因。
  • 当发送一个比较大的nonce值,然后补齐开始nonce到那个值之间的nonce,那么交易依旧可以被执行。
  • 当交易处于queue中时停止geth客户端,那么交易queue中的交易会被清除掉。

使用合约币转账注意事项:

  • txParams.to 地址要写合约币地址
  • txParams.data 真正接收者的地址转换后写到这里

预估当前的转账价格

通过以下三种方式获取预估 gasPrice

  1. 使用第三方 API:https://ethgasstation.info/json/ethgasAPI.json
  2. 使用官方的 API,单位为 Gwei:https://www.etherchain.org/api/gasPriceOracle
  3. 调用的 web3.eth.getGasPrice() 方法获取,单位为 Wei,除于 1e9 可以转换为 Gwei。这种方法只能获取的相当于以上第一种的 average 字段与第二种的 standard 字段,转账快,费用高。

这里讲下第一种方式,因为这种获取到的结果比较丰富一些,方便应对五花八门的需求,学会了这种另外两种自然也就会了,请求接口:https://ethgasstation.info/json/ethgasAPI.json,返回 JSON 中有以下字段:

字段参考值单位说明
fastest200除 10 后为 Gwei转账速度很快,价格相对较贵
fast100同上转账速度一般,价格相对一般
safeLow30同上转账速度很慢,价格相对较低
fastestWait14.2分钟快速转账预计花费时间
fastWait2.2同上一般转账预计花费时间
safeLowWait2.2同上较慢转账预计花费时间

比如这里使用 safeLow 较慢转账的费用计算:

1
2
3
4
5
6
7
8
9
const params = {from: myAddress, to: toAddress};

const estimateGasPrice = 30; // safeLow
const gwei = estimateGasPrice / 10;
const estimategasUsed = web3.eth.estimateGas(params); // 21000
const gas = gwei * estimategasUsed;
const ether = gas / 1e9; // 1e9 为 Gwei 转 Ether 的汇率
const usd = ether * 171.645; // 假设 Ether(以太币) 转 美元的汇率为 171.645
// output: 0.010813635 ($)

如果是合约币需换成以下 params

1
2
3
4
5
const parmas = {
from: myAddress,
to: contractAddress,
data: contract.methods.transfer(toAddress, amount).encodeABI()
};

或者直接使用上边初始化过的合约对象 contract 来获取会更简单一些:

1
const estimategasUsed = contract.methods.transfer(toAddress, amount).estimateGas({from: myAddress});

合约币要注意的是 amount 不能大于账户实际的余额,要不然 estimateGas 请求会得到一个 code 为 -32000 的错误提示。

Ether(以太币)转法币的实时汇率可以通过这个接口获取:https://api.infura.io/v1/ticker/symbols,接口会返回一个所支持对法币的列表:

1
2
3
4
5
6
7
8
9
{
"symbols": [
"ethusd", // 以太币对美元
"ethhkd", // 以太币对港元
"etheur", // 以太币对欧元
"ethgbp" // 以太币对英镑
...
]
}

然后找个支持的 symbol 来获取具体的汇率,例如使用以太币对美元的 symbol 为 ethusd,拼接后的 API 为:https://api.infura.io/v1/ticker/ethusd,返回示例:

1
2
3
4
5
6
7
8
9
10
11
{
"base": "ETH",
"quote": "USD",
"bid": 171.645,
"ask": 171.691,
"volume": 970347.3443,
"exchange": "hitbtc",
"total_volume": 2751280.28735102,
"num_exchanges": 10,
"timestamp": 1554259623
}

因为数字货币的波动很大,这里有一个最新撮合的 bid (买单价格)、 ask (卖单价格),我们提取取 bid 作为汇率即可。

小结

  1. 如果 gasPrice 设置的小或者网络繁忙的话,一般超过 10 小时就会 drop 掉,如果需要保证转账成功率的话需要检测区块状态,dropped 状态的要重新提交转账申请。
  2. 比特币为 10 分钟出一个块,一个块大概有 1M 左右;以太坊为 15 秒出一个块,块大小无限制。

至此结束,感谢阅读。