上一篇文章 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