這是關於 Ethers 非常簡短的介紹,但它涵蓋了開發人員通常需要執行的許多操作。
獲取 Ethers#
先安裝 Ethers 包
使用 npm :
npm i ethers
使用 yarn:
yarn add ethers
使用 pnpm:
pnpm i ethers
Ethers 中的所有內容都從其根目錄和 ethers 對象中導出。 package.json 也包含了更細粒度的導入配置。
通常,此文檔假定代碼示例已經導入了所有來自 ethers 的導出對象,但您可以按任意方式導入所需的對象。
在 Node.js 中進行導入:
// 導入所有內容
import { ethers } from "ethers";
// 僅導入部分對象
import { BrowserProvider, parseUnits } from "ethers";
// 在特定模塊中導入對象
import { HDNodeWallet } from "ethers/wallet";
在瀏覽器中導入 ESM:
<script type="module">
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.min.js";
// 在此處編寫您的代碼...
</script>
一些常見術語#
為了開始接觸此領域,理解可用對象類型及其職責是很有用的。
Provider#
Provider 是與區塊鏈建立的只讀連接,允許查詢區塊鏈狀態,例如帳戶、塊或交易詳情,查詢事件日誌或使用 call
評估只讀代碼。
如果您熟悉 Web3.js,則應該習慣 Provider 提供了讀寫訪問權限。但在 Ethers 中,所有寫操作都被進一步抽象化成另一個對象:Signer。
Signer#
Signer 封裝了所有與帳戶交互的操作。帳戶通常具有某個位置上的私鑰,可以用於簽署各種類型的有效載荷。
私鑰可能位於內存中(使用 Wallet),也可以通過某個 IPC 層進行保護,例如 MetaMask,它將網站上的交互代理到瀏覽器插件中,使得私鑰不會被網站獲取,並且僅在請求用戶授權時才允許進行交互。
Transaction#
要對區塊鏈進行任何狀態更改,都需要執行事務,而每次執行事務都需要支付費用。此處的 “費用” 涵蓋了執行事務所需的相關成本(例如讀取磁碟和執行數學運算)以及存儲更新信息的成本。
如果事務回滾,則仍需要支付費用,因為驗證者仍然必須花費資源嘗試運行該事務以確定其是否已回滾,並且其失敗的詳細信息仍將被記錄下來。
交易包括:將以太幣發送給其他用戶、部署合約或對合約進行狀態更改操作。
Contract#
合約是一個已經部署到區塊鏈上的程序,其中包含一些代碼,分配了可以進行讀寫操作的存儲單元。
當與 Provider 連接時,可以從中進行讀取;如果連接到 Signer,則可以調用狀態更改操作。
Receipt#
一旦將 Transaction 提交到區塊鏈中,它將被放置在內存池(mempool)中,直到驗證者決定將其包括進去。
交易的修改只有在其已包含在區塊鏈中並生成了收據時才會生效。此時,會提供一個包括交易詳細信息的收據,例如收據所屬的區塊,實際付費金額、使用燃料總量、所有由交易引發的事件以及它是成功還是回滾等。
連接以太坊#
與以太坊進行交互的第一步是使用 Provider 與其建立連接。
MetaMask(和其他注入程序)#
在以太坊上嘗試和開始開發最快,最簡便的方式是使用 MetaMask,它是一個瀏覽器擴展程序,可以將對象注入到窗口中,提供:
- 對以太坊網絡的只讀訪問權限(Provider)
- 基於私鑰的授權寫入訪問權限(Signer)
在請求授權方法,例如發送交易或甚至請求私鑰地址時,MetaMask 會彈出一個提示用戶授權的窗口。
let signer = null;
let provider;
if (window.ethereum == null) {
// 如果沒有安裝 MetaMask,我們使用默認的 Provider,
// 它由多個第三方服務支持(例如 INFURA),它們沒有安裝私鑰,
// 所以只有只讀訪問權限
console.log("MetaMask not installed; using read-only defaults")
provider = ethers.getDefaultProvider()
} else {
// 連接到 MetaMask 的 EIP-1193 對象,這是一個標準的
// 協議,允許 Ethers 通過 MetaMask 進行所有只讀請求。
provider = new ethers.BrowserProvider(window.ethereum)
// 這也提供了一個機會來請求寫操作的訪問權限,
// 這些操作將由 MetaMask 為用戶管理的私鑰執行。
signer = await provider.getSigner();
}
自定義 RPC 後端#
如果您正在運行自己的以太坊節點(例如 Geth)或使用自定義第三方服務(例如 INFURA),則可以直接採用 JsonRpcProvider 通信,他們使用 link-jsonrpc 協議。
當使用自己的 Ethereum 節點或開發者基於區塊鏈項目(如 Hardhat 或 Ganache)時,可以通過 JsonRpcProvider.getSigner
方法獲取訪問權限。
連接到 JSON-RPC URL
// 如果不提供 url,它將連接到大多數默認節點
// http://localhost:8545。
provider = new ethers.JsonRpcProvider(url)
// 通過獲取 signer 來作為帳戶獲得寫入訪問權限
signer = await provider.getSigner()
用戶交互#
在以太坊中,所有單位都傾向於是整數值,因為使用小數和浮點數會導致執行數學操作時出現不精確和不明顯的結果。
因此,內部使用的單位(例如 wei)適用於機器可讀的目的和數學計算,它們通常非常大且不太適合人類閱讀。
例如,想像一下處理美元和美分;您將顯示像 "$ 2.56" 這樣的值。在區塊鏈世界中,我們會將所有值保留為分(cents),以便在內部表示為 256 分。
因此,在接受用戶鍵入的數據時,必須將其從其十進制字符串表示法(例如 "2.56")轉換為最低單位整數表示法(例如 256)。當向用戶顯示值時則需要相反的操作。
在 Ethereum 中,一個以太幣等於 10 ** 18
個 wei,一個 gwei 等於 10 ** 9
個 wei,因此值會很快變得非常大,所以提供了一些方便的函數來幫助在各種表示之間進行轉換。
// 將以太中用戶提供的字符串轉換為 wei 值
eth = parseEther("1.0")
// 1000000000000000000n
// 將 gwei 中用戶提供的字符串轉換為最大基礎費用對應的 wei 值
feePerGas = parseUnits("4.5", "gwei")
// 4500000000n
// 將 wei 值轉換為顯示在 UI 中的 ether 字符串值
formatEther(eth)
// '1.0'
// 將 wei 值轉換為顯示在 UI 中的 gwei 字符串值
formatUnits(feePerGas, "gwei")
// '4.5'
與區塊鏈進行交互#
查詢狀態#
一旦您擁有了 Provider,您就可以讀取並查詢區塊鏈上的數據。這可以用於查詢當前帳戶狀態、獲取歷史日誌、查找合約代碼等。
// 查看當前塊數
await provider.getBlockNumber()
// 16988474
// 獲取地址或 ENS 不可變域名對應的當前餘額
balance = await provider.getBalance("ethers.eth")
// 182334002436162568n
// 由於餘額是以 wei 為單位的,您可能希望將其顯示
// 為 ether 的形式。
formatEther(balance)
// '0.182334002436162568'
// 獲取發送事務所需的下一個 nonce
await provider.getTransactionCount("ethers.eth")
// 3
發送交易#
要向區塊鏈寫入內容,您需要訪問控制某個帳戶的私鑰。在大多數情況下,這些私鑰不能直接訪問,而是通過 Signer 發出請求,然後分派給一個服務(例如 MetaMask),該服務提供嚴格的門禁訪問,並要求用戶批准或拒絕操作。
// 發送交易時,value 是以 wei 為單位的,因此 parseEther
// 將以太轉換為 wei。
tx = await signer.sendTransaction({
to: "ethers.eth",
value: parseEther("1.0")
});
// 通常您可能希望等待事務被挖掘
receipt = await tx.wait();
合約#
Contract 是一個元類,這意味著它的定義是在運行時根據傳遞給它的 ABI(應用二進制接口)派生出來的,從而確定可用的方法和屬性。
應用二進制接口(Application Binary Interface,ABI)
由於在區塊鏈上發生的所有操作都必須編碼為二進制數據,我們需要一種簡潔的方式來定義如何將通用對象(例如字符串和數字)及其二進制表示之間進行轉換,以及編碼調用和解釋合約的方式。
對於任何您要使用的方法、事件或錯誤,都必須包含一個 Fragment(片段)來告訴 Ethers 它應該如何編碼請求和解碼結果。可以安全地排除不需要的任何方法或事件。
有幾種常見的格式可用於描述 ABI。Solidity 編譯器通常會傾向於轉儲 JSON 表示法,但手動輸入 ABI 時,通常更易於使用(並且易於閱讀)人類可讀的 ABI 表示法,這僅是 Solidity 簽名。
簡化的 ERC-20 ABI 示例:
abi = [
"function decimals() returns (string)",
"function symbol() returns (string)",
"function balanceOf(address addr) returns (uint)"
]
// 創建一個合約
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
只讀方法(即:"view" 和 "pure" 方法)#
只讀方法是不能改變區塊鏈狀態的方法,但通常提供了一個簡單的接口來獲取與合約相關的重要數據。
以下是讀取 DAI ERC-20 合同的示例:
// 合約 ABI(我們關心的片段)
abi = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function balanceOf(address a) view returns (uint)"
]
// 創建一個合約;連接到 Provider,因此它只能訪問只讀方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// 符號名稱
sym = await contract.symbol()
// 'DAI'
// 代幣使用的小數位數
decimals = await contract.decimals()
// 18n
// 讀取帳戶的代幣餘額
balance = await contract.balanceOf("ethers.eth")
// 201469770000000000000000n
// 格式化餘額以供人類閱讀,例如在 UI 中顯示
formatUnits(balance, decimals)
// '201469.77'
狀態改變方法#
修改 ERC-20 合約的狀態
abi = [
"function transfer(address to, uint amount)"
]
// 連接一個 Signer;可以發出更改狀態的事務,
// 這將花費帳戶的以太幣
contract = new Contract("dai.tokens.ethers.eth", abi, signer)
// 發送 1 DAI
amount = parseUnits("1.0", 18);
// 發送交易
tx = await contract.transfer("ethers.eth", amount)
// 目前該交易已發送到 mempool,
// 但尚未包含。 因此我們...
// ...等待交易被包含。
await tx.wait()
強制調用(模擬)一個狀態更改方法
abi = [
"function transfer(address to, uint amount) returns (bool)"
]
// 僅連接到 Provider,因此我們只需要讀取訪問權限
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
amount = parseUnits("1.0", 18)
// 使用靜態調用有許多限制,但通常可以用於預檢查事務。
await contract.transfer.staticCall("ethers.eth", amount)
// true
// 我們還可以模擬另一個帳戶發起的交易
other = new VoidSigner("0x643aA0A61eADCC9Cc202D1915D942d35D005400C")
contractAsOther = contract.connect(other.connect(provider))
await contractAsOther.transfer.staticCall("ethers.eth", amount)
// true
監聽事件#
當添加命名事件的監聽器時,事件參數會被析構。始終有一個額外的參數傳遞給監聽器,它是 EventPayload,其中包括有關事件的更多信息,包括過濾器和用於移除該監聽器的方法。
監聽 ERC-20 事件:
abi = [
"event Transfer(address indexed from, address indexed to, uint amount)"
]
// 創建一個合約;連接到 Provider,因此它只能訪問只讀方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// 開始監聽任何轉帳事件
contract.on("Transfer", (from, to, _amount, event) => {
const amount = formatEther(_amount, 18)
console.log(`${ from } => ${ to }: ${ amount }`);
// The `event.log` has the entire EventLog
// Optionally, convenience method to stop listening
event.removeListener();
});
// 同上
contract.on(contract.filters.Transfer, (from, to, amount, event) => {
// See above
})
// 監聽任何轉帳到 "ethers.eth" 的事件
filter = contract.filters.Transfer("ethers.eth")
contract.on(filter, (from, to, amount, event) => {
// `to` will always be equal to the address of "ethers.eth"
});
// 監聽任何事件,無論是否在 ABI 中,由於未知事件可能被捕獲,因此不會析構參數。
contract.on("*", (event) => {
// The `event.log` has the entire EventLog
});
查詢歷史事件#
在大範圍的區塊內查詢時,某些後端可能會速度過慢,可能會返回錯誤或者不提供結果取決於該後端的伺服器策略。
查詢 ERC-20 歷史事件:
abi = [
"event Transfer(address indexed from, address indexed to, uint amount)"
]
// 創建一個合約;連接到 Provider,因此它只能訪問只讀方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// 查詢最近的 100 個塊中是否有任何轉帳
filter = contract.filters.Transfer
events = await contract.queryFilter(filter, -100)
// 獲取事件數組大小
events.length
// 90
// 第一個匹配的事件
events[0]
// EventLog { ... }
// 查詢所有時間段內的任何轉帳到 "ethers.eth" 的事件
filter = contract.filters.Transfer("ethers.eth")
events = await contract.queryFilter(filter)
// 第一個匹配的事件
events[0]
// EventLog { ... }
簽名消息#
私鑰可以做很多事情,不僅可以簽署交易以授權它,還可以用於簽署其他形式的數據,並且可以驗證這些數據以及達到其他目的。
例如,簽署消息可用於證明對帳戶的所有權,這樣網站就可以用它來驗證用戶並登錄。
// 我們的簽名者;簽署消息不需要 Provider
signer = new Wallet(id("test"))
// Wallet {
// address: '0xC08B5542D177ac6686946920409741463a15dDdB',
// provider: null
// }
message = "sign into ethers.org?"
// 對消息進行簽名
sig = await signer.signMessage(message);
// '0xefc6e1d2f21bb22b1013d05ecf1f06fd73cdcb34388111e4deec58605f3667061783be1297d8e3bee955d5b583bac7b26789b4a4c12042d59799ca75d98d23a51c'
// 驗證消息;請注意地址與簽署者匹配
verifyMessage(message, sig)
// '0xC08B5542D177ac6686946920409741463a15dDdB'
在簽名消息上構建的許多其他高級協議被用於允許一個私鑰授權其他用戶轉移其代幣,從而允許由其他人支付轉移的交易費用。