上一篇文章 Solidity 智能合約 - Custom Error 中提到如何在智能合約中使用 Custom Error,這篇文章延續這個主題,說明使用 Ethers.js 在 call、estimateGas 和交易失敗時,如何解析 Custom Error。這裡使用 Ethers.js 6.5.1 版本,在 Arbitrum Goerli 進行測試。
範例合約
首先我們先準備一個合約如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| error EmptyError(); error ErrorWithArgs(address user);
contract MyContract { function sendEmptyError() external { revert EmptyError(); }
function callEmptyError() external pure { revert EmptyError(); }
function sendErrorWithArgs() external { revert ErrorWithArgs(msg.sender); }
function callErrorWithArgs() external view { revert ErrorWithArgs(msg.sender); } }
|
其中包含 send 和 call 的函式,也分別包含有參數和無參數的 Custom Error。
解析
call
call 的時候內建已經處理好,如果節點有支援錯誤訊息,可以取得 revert 物件。
無參數
1 2 3 4 5 6
| try { await myContract.callEmptyError(); } catch (e) { console.log(e?.revert); }
|
有參數
1 2 3 4 5 6
| try { await myContract.callErrorWithArgs(); } catch (e) { console.log(e?.revert); }
|
estimateGas
這個情況拿不到 revert 物件,只有 data
無參數
1 2 3 4 5 6 7 8 9
| try { await myContract.sendEmptyError.estimateGas(); } catch (e) { console.log(e?.revert);
console.log(e.data); }
|
有參數
1 2 3 4 5 6
| try { await myContract.sendErrorWithArgs.estimateGas(); } catch (e) { console.log(e.data); }
|
不知道為什麼內建不處理,我們只能自己進一步解析
1 2 3 4 5 6 7 8 9
| const selector = e.data.substring(0, 10); const fragment = myContract.interface.fragments.find((fragment) => fragment.selector === selector); if (fragment) { const revert = { name: fragment.name, signature: fragment.format(), args: myContract.interface.decodeErrorResult(fragment, e.data) }; }
|
交易失敗
再來是送出交易後的處理
1 2 3 4 5 6 7 8 9 10
| const tx = await myContract.sendEmptyError({ gasLimit: 1000000 });
const receipt = await tx.wait();
if (!receipt.status) { }
|
這時候我們會發現不管是 tx 物件還是 receipt 都沒有可用的資料,這時候只能用一個比較奇怪的方式達成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| if (!receipt.status) { try { const request: any = { blockTag: receipt.blockNumber }; ['to', 'from', 'nonce', 'gasLimit', 'gasPrice', 'maxPriorityFeePerGas', 'maxFeePerGas', 'data', 'value', 'chainId', 'type', 'accessList'].forEach((key) => { request[key] = tx[key]; }); await provider.call(request); } catch (e) { console.log(e?.revert);
console.log(e.data);
} }
|
要注意的是,如果指定 blockTag 為某個區塊,有一個時間限制,太久以前的區塊是無法執行的。
總結
把上面的程式整理成函式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| function parseCustomError(error, contract) { if (error?.revert) { return error.revert; } const data = error.data; if (typeof data !== 'string' || !data.startsWith('0x')) { return null; } const selector = data.substring(0, 10); const fragment = contract.interface.fragments.find((fragment) => fragment.selector === selector); if (!fragment) { return null; } return { name: fragment.name, signature: fragment.format(), args: contract.interface.decodeErrorResult(fragment, data) }; }
const KEYS = ['to', 'from', 'nonce', 'gasLimit', 'gasPrice', 'maxPriorityFeePerGas', 'maxFeePerGas', 'data', 'value', 'chainId', 'type', 'accessList']; async function parseTxCustomError(tx, provider, contract) { const request = { blockTag: tx.blockNumber }; KEYS.forEach((key) => { request[key] = tx[key]; }); if (request.gasPrice) { delete request.maxPriorityFeePerGas; delete request.maxFeePerGas; } try { await provider.call(request); return null; } catch (e) { return parseCustomError(e, contract); } }
|
然後可以這樣使用
call 或 estimateGas
1 2 3 4 5
| try { await myContract.callEmptyError(); } catch (e) { const customError = parseCustomError(e, myContract); }
|
交易失敗
1 2 3 4 5 6 7 8 9 10
| const tx = await myContract.sendEmptyError({ gasLimit: 1000000 });
const receipt = await tx.wait(); if (!receipt.status) { tx.blockNumber = receipt.blockNumber; const customError = await parseTxCustomError(tx, provider, myContract); }
|
延伸閱讀
Solidity 智能合約 - Custom Error
使用 Web3.js 解析 Custom Error