本文以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 gasLimit
和gasPrice
接下來我們來介紹兩個和gas相關的屬性:gasLimit
和gasPrice
。這其中gasLimit
是指該次交易最大消耗gas,gasPrice
是指你願意爲實際消耗的gas出多少價格。具體消耗的gas數量再乘於gasPrice
就是你願意付給礦工的手續費。交易執行完成後,未消耗的gas會返還給你(這裏不討論交易出錯情況,在這種情況下有時不會退還未消耗的gas)。
gasLimit
通常用於限定某個交易不能消耗太多資源。舉一個使用場景:我們經常使用MetaMask直接向外部賬號轉賬,在MetaMask裏gasLimit
默認就是23000,最低不能低於21000。
讓我們查看etherscan上的一個具體的轉賬交易:
從上圖中可以看到,在我們沒有隨交易發送任何數據的情況下(data
屬性爲空,如果不爲空則會額外消耗gas),向一個外部賬號轉賬會消耗21000的gas,這個消耗基本是固定的。所以本次交易的gasLimit
上限也設置成了21000
,使用率爲100%
。
上圖中我們的gasPrice
爲5 Gwei
。你給的價格越高,交易的越快,當然你的手續費越多。通常講到gasPrice
時,我們都使用Gwei
作爲單位,但是使用時還是要轉換成wei
。這個5 Gwei
乘於消耗的gas21000
,剛好就是上圖中顯示的Transaction Fee
:0.000105
ETH。筆者寫到這裏時ETH價格爲$205上下,所以發送一次的手續費大概爲0.15RMB
。
讓我們在交易對象中加上這兩個屬性,看多餘的gas是否消耗掉。我們的gasLimit設定爲100000
,gasPrice
設置爲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
。讓我們將chainId
和nonce
一起加到交易裏去。並且將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。
四、總結
本文中,我們對手動創建的以太坊交易對象的具體屬性進行了詳細介紹。這些屬性都是可省略的,然而不能全省略(因爲全省略了沒有意義)。我們平常用的最多的就是to
、value
和data
屬性。注意:這只是代碼中手動創建交易對象時需要設置的屬性;如果你直接使用通用的錢包(比如MetaMask或者Trust錢包),錢包會有UI界面幫你設置好一切。然而弄清實現的基礎還是有必要的,希望這篇文章能給以太坊上的開發者提供一點點幫助。
歡迎大家留言指出錯誤或者提出改進意見。