Js中构建以太坊交易对象详解

       本文以ethers库为底层实现,讲述了在Javascript中构建的以太坊交易对象的详细属性,本文假定读者掌握一定的以太坊基础知识。

一、什么是ethers

       下面是它的文档的一个原文介绍:

       The ethers.js library aims to be a complete and compact library for interacting with the Ethereum Blockchain and its ecosystem. It was originally designed for use with ethers.io and has since expanded into a much more general-purpose library.

       大致意思就是ethers.js是应用于以太坊和它的生态系统的一个完全而又紧凑的库。它最初被设计在ethers.io上使用,慢慢地扩展成了一个多功能库。

       ethers库有如下几个特点:

  • 私钥保存在客户端,安全无风险
  • 支持导入和导出json格式的钱包(可用于Geth或者Parity)
  • 支持导入和导出助记词及硬件钱包,助记词支持多语言
  • 支持多种ABI格式 ,包括ABIv2和人类可读的ABI
  • 支持通过多种方式连接以太坊节点,比如JSON-RPC、 INFURA、 Etherscan或者 MetaMask。
  • ENS names作为第一类要素,得到了全面支持
  • 库很小(未压缩版本284kb,压缩版本88kb)
  • 功能完备,能满足你对以太坊的一切需求
  • 文档详尽
  • 增加了很多测试用例
  • TypeScript可读
  • MIT 证书(包括它的依赖),完全开源

二、构建一个交易对象

       在ethers.js中,一个以太坊交易对象就是一个普通的对象{},它包含以下几个可选属性:

  • to
  • gasLimit
  • gasPrice
  • nonce
  • data
  • value
  • chainId

       上面的属性都是可选的,意味着它们是可以省略的,但是不能全部省略,至少要有一个属性。我们通过如下方式来创建一个交易对象:

// All properties are optional
let transaction = {
    
    
    nonce: 0,
    gasLimit: 21000,
    gasPrice: utils.bigNumberify("20000000000"),

    to: "0x88a5C2d9919e46F883EB62F7b8Dd9d0CC45bc290",
    // ... or supports ENS names
    // to: "ricmoo.firefly.eth",

    value: utils.parseEther("1.0"),
    data: "0x",

    // This ensures the transaction cannot be replayed on different networks
    chainId: ethers.utils.getNetwork('homestead').chainId
}

       下面我们通过在Kovan测试网上的实际交易来讲解这几个属性。

三、交易对象属性详解

       有如下的代码片断,我们以后的交易都是在这个片断上进行修改或者增加。这是一个创建合约的交易:

let data='0x60....'
let provider = ethers.getDefaultProvider('kovan')
let wallet_new = wallet.connect(provider)
let trans = {
    
    
    data:inputData
}
wallet_new.sendTransaction(trans).then( tx => {
    
    
    console.log(tx)
}).catch( err => {
    
    
    console.log(err)
})

可以看到,我们的交易对象只有一个属性data,它的值是创建合约的字节码。注意:创建合约时的字节码并不是被创建合约编译后的字节码,而是通过运行能够得到被创建合约字节码的字节码。

       我们看一下打印出来的交易响应(Transaction Response):
在这里插入图片描述
       可以看到,在交易响应对象里,除了to属性,其它属性都是存在的。所以上面提到的属性可以省略是指构建交易对象时可以省略。如果省略,底层的ethers库会自动帮你设置好。让我们从这个最简单的交易对象开始,一步一步增加并讲解它的属性。

3.1 to

       既然to属性为null,我们就从to属性开始讲起。to代表交易中被调用者的地址。

       以太坊上的交易必须有一个发起者(外部账号,非合约账号),通常为from。因为我们使用的ethers库通过钱包签名交易,所以谁签名谁就是from。交易通常还有一个接收者(外部账号与合约账号均可),也就是to。为什么讲通常呢?因为像我们刚才这个例子,创建合约时是没有接收者的。虽然合约创建后,它的地址会做为to的属性来返回,但是在创建时,这个to地址是空的。让我们来看一下etherscan上的截图来加深这个印象:
在这里插入图片描述
       可以看到,交易执行完毕后,这个to属性就是新合约的地址。这里补充一下,合约的地址是根据调用者的地址和调用者已完成的交易数量(nonce)来计算得到的。所以一个合约在实际部署前,地址就设置好了,是可以获取的。

       归纳一下,to属性就是交易中被调用者的地址。具体的讲:如果是向外部账号转ETH,就是ETH接收地址;如果是调用合约(向合约账号转ETH也是属于调用合约),就是合约地址;如果是创建合约,因为此时没有被调用者,就缺省它。

3.2 data

       接下来我们讲上面的代码中使用了的属性:data。在交易时,我们可以随交易发送交易数据。交易数据可以是对合约的方法调用,也可以为一些无意义的数据,这些数据有时也叫payload。在上例中,data属性的值就是我们创建合约的字节码。让我们将上面的交易对象增加一个to属性并修改data属性的值:

let trans = {
    
    
    data:"0x496c6f7665457468657265756d",
    to:"0xDD55634e1027d706a235374e01D69c2D121E1CCb"
}

       这里的to是一个外部账号地址,data是" I love Ethereum"转换成16进制值时的字符串(data必须以0x开头)。交易发送后的响应如下:
在这里插入图片描述
       让我们来看etherscan上的结果:
在这里插入图片描述
       从代码片断中看以看出,我们直接向某个账号发送了一条消息(字符串)。在最下方的InputData那里,它默认显示原生的数据。选择 View Input As UTF-8,就会显示 IloveEthereum了。这里没有显示空格是因为我使用的工具没有将空格编码。这个向某个账号发送字符串的功能像不像向手机号码发送短消息?你甚至可以发送一篇文章(不过要出不少手续费),以太坊是不是很有趣?

       如果发送的数据为合约方法调用时的数据,通常它有固定的格式,不能是任意数据。举例如下:

data:0x07391dd6000000000000000000000000000000000000000000000000000000000000000a

       这里第一个32字节的前8位07391dd6是函数选择器,32字节以后就是对应类型的数据。有兴趣的读者可以自行看一下以太坊编码方面的有关文章。

       好了,归纳一下:data属性就是随调用发送的数据。如果被调用对象是一个合约,通常为合约调用方法的编码;如果为创建合约,则为创建的字节码;如果被调用对象是一个外部账号,这个数据的内容就是随意了(外部账号没有代码,并不会执行发送的数据)。

3.3 value

       value属性代表随这次交易发送的以太币数量。不管交易类型是直接ETH转账(包括向合约转和向外部账号转),还是创建合约(这时ETH会作为被创建合约的初始ETH),还是合约调用(合约方法为payable),它都忠实的记录了你在交易中发送的ETH数量(不包含手续费,手续费是额外的消耗)。让我们将刚才的交易对象增加一个value属性,注意它的值是以wei为单位的。而平常我们一般提及以太币时都是以ether为单位的,使用时需要作一个转换。

let trans = {
    
    
    data:"0x496c6f7665457468657265756d",
    value:ethers.utils.parseEther('0.1'),
    to:"0xDD55634e1027d706a235374e01D69c2D121E1CCb"
}

代码中value的值为0.1个ETH。让我们发送这个交易:
在这里插入图片描述
       在JS中,如果数字比较大,会超过js十进制表示的上限(大约10 ** 15),所以和以太坊交互一般使用BigNumber。可以看到这个发送的WEI的数量转成了一个BigNumber。我们再看一下etherscan的结果:
在这里插入图片描述
       这里没有显示data是因为我没有点击 Click to see More进行展开。可以看到,我们的确是随交易发送了0.1ETH。

3.4 gasLimitgasPrice

       接下来我们来介绍两个和gas相关的属性:gasLimitgasPrice。这其中gasLimit是指该次交易最大消耗gas,gasPrice是指你愿意为实际消耗的gas出多少价格。具体消耗的gas数量再乘于gasPrice就是你愿意付给矿工的手续费。交易执行完成后,未消耗的gas会返还给你(这里不讨论交易出错情况,在这种情况下有时不会退还未消耗的gas)。

       gasLimit 通常用于限定某个交易不能消耗太多资源。举一个使用场景:我们经常使用MetaMask直接向外部账号转账,在MetaMask里gasLimit默认就是23000,最低不能低于21000。
在这里插入图片描述

       让我们查看etherscan上的一个具体的转账交易:
在这里插入图片描述
       从上图中可以看到,在我们没有随交易发送任何数据的情况下(data属性为空,如果不为空则会额外消耗gas),向一个外部账号转账会消耗21000的gas,这个消耗基本是固定的。所以本次交易的gasLimit上限也设置成了21000,使用率为100%

       上图中我们的gasPrice5 Gwei。你给的价格越高,交易的越快,当然你的手续费越多。通常讲到gasPrice时,我们都使用Gwei作为单位,但是使用时还是要转换成wei。这个5 Gwei乘于消耗的gas21000,刚好就是上图中显示的Transaction Fee0.000105ETH。笔者写到这里时ETH价格为$205上下,所以发送一次的手续费大概为0.15RMB

       让我们在交易对象中加上这两个属性,看多余的gas是否消耗掉。我们的gasLimit设定为100000gasPrice设置为3 Gwei,让我们重新发送交易:

let trans = {
    
    
    data:"0x496c6f7665457468657265756d",
    value:ethers.utils.parseEther('0.1'),
    gasLimit:100000,
    gasPrice:ethers.utils.parseUnits("3",'gwei'),
    to:"0xDD55634e1027d706a235374e01D69c2D121E1CCb"
}

       这里因为我们的gasLimit不可能超过JS的十进制上限,所以直接使用了十进制的100000。交易响应为:
在这里插入图片描述
       我们直接看etherscan上的交易结果:
在这里插入图片描述
       因为我们随交易发送了I love Ethereum这个字符串,所以我们消耗的gas多了208。根据我们扣除的手续费可以得和,未使用的gas是没有计入费用的。

       对于gasLimit来讲,一般在使用ethers库时不需要设置,让它缺省就行。如果要手动设置的话,可以先用进行一下估算,然后再适当的向上扩大一点,比如下面的代码片断:

let args = [_address,amount]
let gasLimit = await contract.estimate.transfer(...args)
let step = ethers.utils.bigNumberify(1000)
gasLimit = gasLimit.add(step)

       对于gasPrice来讲,一般正常情况下设置为5 Gwei或者6 Gwei就行。gas消耗很多或者网络很轻闲的情况下可以设置成1.5 Gwei或者2 Gwei。不过这样交易时间会延长,甚至有可能失败。如果想快速交易,设置成10 Gwei或者20 Gwei甚至更高,不过这样会出更多的手续费。钱多就会快,钱少就会慢,道理就这么简单。并且要注意:过低的手续费可能会导致没有矿工打包这笔交易,从而交易失败。当然如果在测试网,你可以设置高一些,因为你不必真的花钱。

3.5 nonce

       在交易对象中,nonce代表该地址已经完成的交易数量,它从0开始,是一个自动增长的整数,通常我们不用设置。但是在某种特殊情况与可以手动设置。有一种场景就是在覆盖交易时。你可以手动设置nonce为一个已经发送但还未完成的交易的nonce值来覆盖这笔交易。通常这样做的目的是为了加速交易(增加gasPrice)或者完全使用一个新的交易。这个也好理解,比如我第122号交易是向A发送一个ETH,但是在这个交易未发送或者未完成之前,我来了个紧急修改,将这个122号交易改成向B发送一个ETH。此时我只需要将新交易的nonce设置成122就行了。

       如果你想在通常使用的交易对象中进行设置,需要查询到你已经完成的交易数量,这个数量就是你应该使用的nonce值。使用如下代码:

let address = "0x02F024e0882B310c6734703AB9066EdD3a10C6e0";

provider.getTransactionCount(address).then((transactionCount) => {
    
    
    console.log("Total Transactions Ever Sent: " + transactionCount);
});

       值得注意的是:nonce有一个特殊的用法,你可以指定一个未来的值。打比方来讲,你当前已经完成的交易数量为2096,那么下一次交易时nonce值就应该为2097。此时你也可以跳过2097,设置成2098,那么会发生什么事情呢?此时编号为2098的交易相当于一个延时交易,会被发送出去,但是不会被执行,你在etherscan上也查询不到。然后我们再进行一个正常的将nonce值设置成为2097的交易,此时交易会被发送并执行。重点来了:在下一个block里,nonce为2098的交易也会被执行(因为2097已经执行了,轮到它了)。

3.6 chainId

       chainId代表你想发起交易的网络ID。以主网和三大测试网为例,主网(mainnet,但是在ethers中还是叫homestead家园)为1,Ropsten测试网为3,Rinkeby测试网为4,Kovan测试网为42。自定义网络可以自己设置等。

       通常来讲,使用钱包时不需要设定这个chainId。因为钱包登录里会绑定一个网络,它就是你交易对象的网络。但是你也可以手动设置为一个具体的值来防止在错误的网络上交易。你可以直接使用上面的十进制数字值,也可以使用ethers中的示例代码:

chainId: ethers.utils.getNetwork('homestead').chainId

       如果我们是在Kovan测试网上进行交易,方法里的参数就要改成kovan。让我们将chainIdnonce一起加到交易里去。并且将value改成0.01ETH以做区分。

let count = await provider.getTransactionCount(wallet_new.address)
let trans = {
    
    
    data:"0x496c6f7665457468657265756d",
    value:ethers.utils.parseEther('0.01'),
    gasLimit:100000,
    nonce: count,
    chainId:ethers.utils.getNetwork('kovan').chainId,
    gasPrice:ethers.utils.parseUnits("3",'gwei'),
    to:"0xDD55634e1027d706a235374e01D69c2D121E1CCb"
}

下面是交易响应:
在这里插入图片描述
       因为编号2097,2098在使用未来nonce值时消耗了,所以现在编号是2099。我们来看一下etherscan上的结果:
在这里插入图片描述
       可以看到发送的ETH数量为0.01ETH,而nonce为2099。也许有人问为什么etherscan上不显示chainId啊,因为etherscan根据主网和测试网分成了好几个站点,每个站点只显示它自己网络的交易。比如我访问的etherscan实际网址为:

https://kovan.etherscan.io/tx/0x4db8e6b4096d6c27be341b73af99a8d0477e19ba483248c1fdb6fb431fbb3646

       该站点显示的所有交易的chainId都为42。

四、总结

       本文中,我们对手动创建的以太坊交易对象的具体属性进行了详细介绍。这些属性都是可省略的,然而不能全省略(因为全省略了没有意义)。我们平常用的最多的就是tovaluedata属性。注意:这只是代码中手动创建交易对象时需要设置的属性;如果你直接使用通用的钱包(比如MetaMask或者Trust钱包),钱包会有UI界面帮你设置好一切。然而弄清实现的基础还是有必要的,希望这篇文章能给以太坊上的开发者提供一点点帮助。

欢迎大家留言指出错误或者提出改进意见。

猜你喜欢

转载自blog.csdn.net/weixin_39430411/article/details/104188489