上一篇文章 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) {
// { "name": "EmptyError", "signature": "EmptyError()", "args": [] }
console.log(e?.revert);
}

有參數

1
2
3
4
5
6
try {
await myContract.callErrorWithArgs();
} catch (e) {
// { "name": "ErrorWithArgs", "signature": "ErrorWithArgs(address)", "args": ["0xdd980c315dfa75682f04381e98ea38bd2a151540"] }
console.log(e?.revert);
}

estimateGas

這個情況拿不到 revert 物件,只有 data

無參數

1
2
3
4
5
6
7
8
9
try {
await myContract.sendEmptyError.estimateGas();
} catch (e) {
// null
console.log(e?.revert);

// 0x4f3d7def
console.log(e.data);
}

有參數

1
2
3
4
5
6
try {
await myContract.sendErrorWithArgs.estimateGas();
} catch (e) {
// 0x034da6bc000000000000000000000000dd980c315dfa75682f04381e98ea38bd2a151540
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
// 沒有設定 gasLimit 的話,他會先執行 estimateGas,會送不出去
const tx = await myContract.sendEmptyError({ gasLimit: 1000000 });

// 送出交易會變成上鏈後才知道有沒有成功,所以不會直接 error, 要等區塊確認後看結果
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) {
// 會得到和 estimateGas 相同的結果
// null
console.log(e?.revert);

// 0x4f3d7def
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 });

// 或是直接用 tx hash
// const tx = await provider.getTransaction(txHash);

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