之前的文章 Solidity 智能合約 - Custom Error 中提到如何在智能合約中使用 Custom Error,這篇文章延續這個主題,說明使用 Web3.js 在 call、estimateGas 和交易失敗時,如何解析 Custom Error。這裡使用 Web3.js 4.0.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。
解析
內建 Provider 和瀏覽器的注入的 Provider 行為不同,以下分開說明。
內建 Provider
這個章節是直接使用網址建立的內建 Provider,例如
1
| new Web3('https://xxxx');
|
call
call 的時候內建已經處理好,如果節點有支援錯誤訊息,可以取得 innerError 物件。
無參數
1 2 3 4 5 6
| try { await myContract.methods.callEmptyError({ from }); } catch (e) { console.log(JSON.stringify(e.innerError)); }
|
有參數
1 2 3 4 5 6
| try { await myContract.methods.callErrorWithArgs({ from }); } catch (e) { console.log(JSON.stringify(e.innerError)); }
|
estimateGas
這個情況雖然有 innerError 物件,但只有 data,沒有解析
無參數
1 2 3 4 5 6
| try { await myContract.methods.sendEmptyError().estimateGas({ from }); } catch (e) { console.log(JSON.stringify(e.innerError)); }
|
有參數
1 2 3 4 5 6
| try { await myContract.methods.sendErrorWithArgs().estimateGas({ from }); } catch (e) { console.log(JSON.stringify(e.innerError)); }
|
不知道為什麼內建不處理,我們只能自己進一步解析
1 2 3 4 5 6 7
| import { decodeContractErrorData } from 'web3-eth-abi';
decodeContractErrorData(myContract._errorsInterface, e.innerError);
console.log(JSON.stringify(e.innerError));
|
交易失敗
再來是送出交易後的處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| try { const data = contract.methods.sendEmptyError().encodeABI(); const signedTx = await web3.eth.accounts.signTransaction({ }, privateKey);
await web3.eth.sendSignedTransaction(signedTx.rawTransaction, undefined, { checkRevertBeforeSending: false }); } catch (e: any) { if (e.receipt && !e.receipt.status) { } }
|
這時候我們會發現不管是 tx 物件還是 receipt 都沒有可用的資料,這時候只能用一個比較奇怪的方式達成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if (e.receipt && !e.receipt.status) { try { const tx = await web3.eth.getTransaction(e.receipt.transactionHash); const request: any = { }; ['to', 'from', 'nonce', 'gas', 'gasPrice', 'maxPriorityFeePerGas', 'maxFeePerGas', 'data', 'value', 'chainId', 'type', 'accessList'].forEach((key) => { request[key] = (tx as any)[key]; }); if (request.gasPrice) { delete request.maxPriorityFeePerGas; delete request.maxFeePerGas; } await web3.eth.call(request, tx.blockNumber); } catch (e2) {
console.log(JSON.stringify(e2.innerError));
decodeContractErrorData(contract._errorsInterface, e2.innerError); console.log(JSON.stringify(e2.innerError)); } }
|
要注意的是,如果指定為某個區塊,有一個時間限制,太久以前的區塊是無法執行的。
瀏覽器 Provider
這個章節是使用 Metamask 之類的方式,例如
1
| new Web3(window.ethereum);
|
call
和內建 Provider 不同,沒有 innerError 物件可以使用
無參數
1 2 3 4 5 6
| try { await myContract.methods.callEmptyError({ from }); } catch (e) { console.log(JSON.stringify(e)); }
|
有參數
1 2 3 4 5 6
| try { await myContract.methods.callErrorWithArgs({ from }); } catch (e) { console.log(JSON.stringify(e)); }
|
不過 data 中有我們需要的資料可以使用
1 2 3 4 5 6 7 8 9 10 11
| import { Eip838ExecutionError } from 'web3'; import { decodeContractErrorData } from 'web3-eth-abi';
e.innerError = new Eip838ExecutionError(e.data);
decodeContractErrorData(myContract._errorsInterface, e.innerError);
console.log(JSON.stringify(e.innerError));
|
estimateGas
estimateGas 的行為和處理方式,與上面 call 完全一樣,就不重複贅述。
交易失敗
再來是送出交易後的處理
1 2 3 4 5 6 7 8 9
| try { await contract.methods.sendEmptyError().send({ from, gas: '1000000' }); } catch (e: any) { if (e.receipt && !e.receipt.status) { } }
|
進入 catch 後使用上一個交易失敗章節的方式處理,但在多加上上面產生 innerError 的動作
1 2 3 4 5 6 7 8 9
| try { await web3.eth.call(request, tx.blockNumber); } catch (e2) { e2.innerError = new Eip838ExecutionError(e2.data);
}
|
總結
把上面的程式整理成函式如下
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
| import { Eip838ExecutionError } from 'web3'; import { decodeContractErrorData } from 'web3-eth-abi';
function parseInnerError(e, contract) { if (e?.innerError?.errorSignature) { return e.innerError; }
if (!e.innerError) { if (e?.data?.data.startsWith('0x')) { e.innerError = new Eip838ExecutionError(e.data); } } if (e?.innerError?.data?.startsWith('0x')) { decodeContractErrorData(contract._errorsInterface, e.innerError); } return e.innerError; }
const KEYS = ['to', 'from', 'nonce', 'gas', 'gasPrice', 'maxPriorityFeePerGas', 'maxFeePerGas', 'data', 'value', 'chainId', 'type', 'accessList']; async function parseTxError(txHash, web3, contract) { const tx = await web3.eth.getTransaction(txHash); const request = { }; KEYS.forEach((key) => { request[key] = tx[key]; }); if (request.gasPrice) { delete request.maxPriorityFeePerGas; delete request.maxFeePerGas; } try { await web3.eth.call(request, tx.blockNumber); return null; } catch (e) { return parseInnerError(e, contract); } }
|
然後可以這樣使用
call 或 estimateGas
1 2 3 4 5
| try { await myContract.methods.callEmptyError({ from }); } catch (e) { const innerError = parseInnerError(e, myContract); }
|
交易失敗
1 2 3 4 5 6 7
| try { await contract.methods.sendEmptyError().send({ from, gas: '1000000' }); } catch (e) { if (e.receipt && !e.receipt.status) { const innerError = await parseTxError(e.receipt.transactionHash, web3, contract); } }
|
延伸閱讀
Solidity 智能合約 - Custom Error
使用 Ethers.js 解析 Custom Error