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界面幫你設置好一切。然而弄清實現的基礎還是有必要的,希望這篇文章能給以太坊上的開發者提供一點點幫助。

歡迎大家留言指出錯誤或者提出改進意見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章