我有一个非常简单的 React 应用程序,它使用 Web3.js (
4.3.2
) 以及来自表单的一些数据来调用 Solidity 智能合约的方法。我试图在调用合约方法或发送交易时捕获错误,例如用户关闭钱包模式,以便显示错误消息。
这是设置代码(简化):
window.ethereum.request({ method: "eth_requestAccounts" });
const web3 = new Web3(window.ethereum);
const factoryCampaignContract = new web3.eth.Contract(CAMPAIGN_ABI, CAMPAIGN_ADDRESS);
在表单的提交处理程序中,我尝试了 3 种不同的选项来捕获错误。
try-catch
try {
const receipt = await factoryCampaignContract.methods
.createCampaign(minimumContribution)
.send({ from: accounts[0] });
} catch(err) {
console.log(err);
}
.then()
+ .catch()
factoryCampaignContract.methods
.createCampaign(minimumContribution)
.send({ from: accounts[0] })
.then(receipt => console.log(receipt))
.catch(err => console.log(err));
send()
的PromiEvents
factoryCampaignContract.methods
.createCampaign(minimumContribution)
.send({ from: accounts[0] })
.on('transactionHash', (hash) => console.log(hash))
.on('confirmation', (confirmationNumber) => console.log(confirmationNumber))
.on('receipt', (receipt) => console.log(receipt))
.on('error', (error) => console.log(error));
一旦关闭钱包模式,我就会在控制台中看到以下错误,但实际上没有调用上面应该捕获错误的块:
Uncaught (in promise) Error: User rejected the request.
at ap.<anonymous> (<anonymous>:7:4040)
at Generator.next (<anonymous>)
at u (<anonymous>:1:1048)
没关系,谜团已解:
简短回答:
Phantom 的钱包中存在一个错误,导致 dApp 无法捕获错误。使用 MetaMask 再次尝试后,一切都按预期运行:
问题出在钱包方面,尤其是我正在使用的钱包,
VM35:1 MetaMask - RPC Error: MetaMask Tx Signature: User denied transaction
signature. {code: 4001, message: 'MetaMask Tx Signature: User denied transaction
signature.'}
try-catch
会成功捕获该问题,使我能够正确处理它,并向用户显示错误消息。
如果您遇到同样的问题,并且想知道为什么您在网上找到的解决方案甚至官方 Web3.js 文档中的示例都不起作用,只需尝试使用不同的钱包即可。
长答案:
深入挖掘以了解有关 Phantom 错误的更多信息,您会在他们的contentScript.js
中找到这样的块:
switch (i = a ? "USE_METAMASK" : i, i) {
case "ALWAYS_ASK":
X_('\nObject.defineProperty(window, "ethereum", {\n value: undefined,\n writable: true,\n});\ndelete window.ethereum;\n '),
X_('<VERY LONG STRING>');
break;
case "USE_PHANTOM":
X_('\nObject.defineProperty(window, "ethereum", {\n value: undefined,\n writable: true,\n});\ndelete window.ethereum;\n '),
X_('<VERY LONG STRING>');
break;
case "USE_METAMASK":
X_('\nObject.defineProperty(window, "ethereum", {\n value: undefined,\n writable: true,\n});\ndelete window.ethereum;\n '),
X_('<VERY LONG STRING>');
break;
}
我在使用 Phantom 时在控制台中看到但无法捕获的错误来自这些 eval-ed 表达式,这就是为什么控制台会说它来自一个名为 VM1813
this.request = r => ct(this, null, function* () {
var n, u;
let o;
try {
let { method: h } = r
, E = "params" in r ? (n = r.params) != null ? n : [] : []
, x = vn[h];
if (!x) throw new Error("MethodNotFound");
let g = x.request.safeParse({ ... });
if (!g.success) { ... }
let P = g.data;
if (o = x.response.parse(yield lt(this, Mn).send(P)), "error" in o) {
// ⚠️ Here's the error we see on the console:
throw new gr(o.error);
}
try {
...
} catch (M) {
console.error("event emitter error", M)
}
return o.result
} catch (h) {
// ⚠️ Which is caught and re-thrown here, as `h instanceof gr` is true:
throw h instanceof gr ? h : h instanceof gt ? new gr({
code: -32e3,
message: "Missing or invalid parameters."
}, {
method: r.method
}) : h instanceof Error && h.message === "MethodNotFound" ? new gr({
code: -32601,
message: "The method does not exist / is not available."
}, {
method: r.method
}) : new gr({
code: -32603,
message: "Internal JSON-RPC error."
}, {
method: r.method
})
}
});
请注意,它们大量使用生成器函数并将它们包装在另一个函数(ct
)中以返回
Promise
。你闻到了吗?最有可能的是,在隐藏在代码中的某个地方,他们正在解决
Promise
并随后抛出错误。
就像这个问题一样,但由于他们的代码更复杂,主要是由于使用了生成器函数和转换为Promise
,所以在哪里不是那么明显,尤其是在查看缩小的代码时。让我们尝试在某种程度上重现该错误:
/**
* Straight out of their bundle, just renamed or removed some params to make it a bit
* easier to understand.
*/
function ct(generatorFn) {
return new Promise((resolve, reject)=>{
var u = x=>{
try {
E(generatorFn.next(x))
} catch (err) {
reject(err)
}
}
, h = x=>{
try {
E(generatorFn.throw(x))
} catch (err) {
reject(err)
}
}
, E = x=>x.done ? resolve(x.value) : Promise.resolve(x.value).then(u, h);
E((generatorFn = generatorFn()).next())
})
}
/**
* This is trying to simulate `lt(this, Mn).send(P)`, which opens the wallet modal and waits
* for the user to confirm/reject the transaction. Note that while this allows us to reproduce
* the error I was experiencing, it's not a perfect mock of what's happening in Phantom's send()
* function, as explained below.
*/
function ltDotSend() {
return ct(function*() {
// This mimics (more or less, as this is blocking code) opening the wallet and
// clicking Confirm (number >= 50) or Close (number < 50):
const n = parseInt(prompt('Enter a number')) || 0;
if (n < 50) {
// Somewhere in their minified code for the send() function, they are throwing
// an error AFTER the return for this function has already been called.
setTimeout(() => {
throw new Error('Inner error');
});
}
return yield n;
});
}
let request = () => ct(function*() {
try {
const n = yield ltDotSend();
console.log('n =', n);
if (n === 'ERROR') {
throw new Error('Outter error');
}
return n;
} catch (err) {
console.log('request catch', err.message);
throw err;
}
});
async function submitHandler() {
try {
const requestResult = await request();
console.log('requestResult =', requestResult);
} catch (err) {
// This will never be invoked:
console.log('submitHandler catch', err.message);
}
}
document.getElementById('requestBtn').addEventListener('click', submitHandler);
<button id="requestBtn">request()</button>
但是,这种模拟我所经历的相同行为的尝试与 Phantom 的实现之间存在一些差异:
Promise
返回的
request()
也会解析,但在原始代码中则不然。我尝试尝试使用
ltDotSend()
的实现,但错误实际上是在
submitHandler
的
try-catch
中捕获的,这又不是原始代码中的情况。
request
函数包装在
async function
中即可解决问题:
const originalRequest = window.ethereum.request;
window.ethereum.request = async (...args) => {
return originalRequest(...args)
}
send()
实现比我写的假的要复杂得多,因为它实际上是与扩展后台脚本通信,所以错误可能更深入。