4

以太坊概念与使用 wep3/infura 实现以太币及合约币交易流程

 2 years ago
source link: https://sanonz.github.io/2019/eth-transaction-for-wep3-and-infura/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

以太币及合约币转账概念

一个以太坊账号包含三个部分,助记词(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 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)
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 字段结果一样,说明我们的算法是没毛病的。

第三方接口及主要依赖库

转账业务逻辑编写

安装依赖包

$ npm install bip39 eth-json-rpc-infura ethereumjs-wallet web3
$ # 如果出错使用带版本号方式安装
$ npm install [email protected] [email protected] [email protected] [email protected]

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

const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider');

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

module.exports = provider;

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

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),需要先转换成密钥:

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; // 转换后的密钥

以太币转账逻辑

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)

// 合约币地址
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 较慢转账的费用计算:

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

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

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

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

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

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

{
"symbols": [
"ethusd", // 以太币对美元
"ethhkd", // 以太币对港元
"etheur", // 以太币对欧元
"ethgbp" // 以太币对英镑
...
]
}

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

{
"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 秒出一个块,块大小无限制。

至此结束,感谢阅读。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK