fix: failed requests reach the page with an error code and the node's data (closes #533)
A request the node refuses now reaches the page with the node's own JSON-RPC code and data: a reverting eth_call, an eth_sendTransaction whose gas estimate reverts, a failed broadcast. Every other refusal from the background carries a code too: 4100 when there is no address, -32603 for a failure inside the wallet, -32602 for a transaction request the wallet refuses as malformed. On the transaction path the page keeps the wallet's own sentence. nodeError(), which finds the node's error inside an ethers error, lives in src/shared/approvalVerify.js beside failureTexts(), which did the same lookup. Not run in a browser. Model: opus-5-5
This commit was merged in pull request #565.
This commit is contained in:
+45
-20
@@ -26,6 +26,7 @@ const {
|
||||
typedDataMessageRefusal,
|
||||
typedDataChainRefusal,
|
||||
failureIsRetryable,
|
||||
nodeError,
|
||||
describeTxFailure,
|
||||
sameAddress,
|
||||
ApprovalMismatchError,
|
||||
@@ -272,6 +273,21 @@ function failureError(err) {
|
||||
return { code: INTERNAL_ERROR_CODE, message: INTERNAL_ERROR_MESSAGE };
|
||||
}
|
||||
|
||||
// The EIP-1193 error for a request that failed, with `message` as the
|
||||
// sentence the page is shown. `rpcError` holds the JSON-RPC code and data the
|
||||
// failure carries, if any: the node's own when the node refused the request,
|
||||
// passed on unchanged so a site can tell a revert from any other failure and
|
||||
// decode its revert data. A failure with no numeric code is the wallet's own,
|
||||
// and is answered with the internal error code above.
|
||||
function requestError(message, rpcError) {
|
||||
if (rpcError && typeof rpcError.code === "number") {
|
||||
const error = { code: rpcError.code, message };
|
||||
if (rpcError.data !== undefined) error.data = rpcError.data;
|
||||
return error;
|
||||
}
|
||||
return { code: INTERNAL_ERROR_CODE, message };
|
||||
}
|
||||
|
||||
// The active address of a profile snapshot. Pure, and taking the snapshot as
|
||||
// an argument rather than reading storage itself: a handler that has already
|
||||
// read state must not answer "which account is this" from a SECOND, later read
|
||||
@@ -317,7 +333,8 @@ function extractHostname(origin) {
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy an RPC call to the Ethereum node
|
||||
// Proxy an RPC call to the Ethereum node. An error the node answers with is
|
||||
// thrown with the node's own JSON-RPC `code` and `data` on it, for the page.
|
||||
async function proxyRpc(method, params) {
|
||||
const rpcUrl = await getRpcUrl();
|
||||
const resp = await debugFetch(rpcUrl, {
|
||||
@@ -332,7 +349,10 @@ async function proxyRpc(method, params) {
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message || "RPC error");
|
||||
const err = new Error(json.error.message || "RPC error");
|
||||
err.code = json.error.code;
|
||||
err.data = json.error.data;
|
||||
throw err;
|
||||
}
|
||||
return json.result;
|
||||
}
|
||||
@@ -728,7 +748,7 @@ async function handleConnectionRequest(origin) {
|
||||
const s = await getState();
|
||||
const activeAddress = activeAddressOf(s);
|
||||
if (!activeAddress) {
|
||||
return { error: { message: "No accounts available" } };
|
||||
return { error: { code: 4100, message: "No accounts available" } };
|
||||
}
|
||||
|
||||
const allowed = s.allowedSites[activeAddress] || [];
|
||||
@@ -992,7 +1012,7 @@ async function handleRpc(method, params, origin) {
|
||||
const s = await getState();
|
||||
const activeAddress = activeAddressOf(s);
|
||||
if (!activeAddress)
|
||||
return { error: { message: "No accounts available" } };
|
||||
return { error: { code: 4100, message: "No accounts available" } };
|
||||
|
||||
const allowed = s.allowedSites[activeAddress] || [];
|
||||
if (
|
||||
@@ -1046,7 +1066,7 @@ async function handleRpc(method, params, origin) {
|
||||
const s = await getState();
|
||||
const activeAddress = activeAddressOf(s);
|
||||
if (!activeAddress)
|
||||
return { error: { message: "No accounts available" } };
|
||||
return { error: { code: 4100, message: "No accounts available" } };
|
||||
|
||||
const allowed = s.allowedSites[activeAddress] || [];
|
||||
if (
|
||||
@@ -1093,12 +1113,13 @@ async function handleRpc(method, params, origin) {
|
||||
const result = await proxyRpc(method, params);
|
||||
return { result };
|
||||
} catch (e) {
|
||||
// A node that answered with an error is reported as itself. The
|
||||
// wallet being unable to read its own profile is not that, and it
|
||||
// must not be flattened into a message with no code: it goes back
|
||||
// to the dispatcher, which has the one answer for it.
|
||||
// A node that answered with an error is reported as itself, with
|
||||
// its own code and data. The wallet being unable to read its own
|
||||
// profile is not that, and it must not be answered as an ordinary
|
||||
// failure: it goes back to the dispatcher, which has the one
|
||||
// answer for it.
|
||||
if (e instanceof StateUnusableError) throw e;
|
||||
return { error: { message: e.message } };
|
||||
return { error: requestError(e.message, e) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1114,7 +1135,9 @@ async function handleRpc(method, params, origin) {
|
||||
async function handleSendTransaction(params, origin) {
|
||||
const s = await getState();
|
||||
const activeAddress = activeAddressOf(s);
|
||||
if (!activeAddress) return { error: { message: "No accounts available" } };
|
||||
if (!activeAddress) {
|
||||
return { error: { code: 4100, message: "No accounts available" } };
|
||||
}
|
||||
|
||||
const allowed = s.allowedSites[activeAddress] || [];
|
||||
if (
|
||||
@@ -1173,7 +1196,7 @@ async function handleSendTransaction(params, origin) {
|
||||
txParams,
|
||||
);
|
||||
} catch (e) {
|
||||
return { error: { message: e.message } };
|
||||
return { error: requestError(e.message, e) };
|
||||
}
|
||||
|
||||
// Population is a network round trip, and the user can switch address
|
||||
@@ -1183,10 +1206,9 @@ async function handleSendTransaction(params, origin) {
|
||||
// while the page is still waiting and nothing has been displayed.
|
||||
if (!sameAddress(await getActiveAddress(), activeAddress)) {
|
||||
return {
|
||||
error: {
|
||||
message:
|
||||
"The active address changed while this transaction was being prepared, so it was not sent.",
|
||||
},
|
||||
error: requestError(
|
||||
"The active address changed while this transaction was being prepared, so it was not sent.",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1679,7 +1701,7 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (outcome.spendApproval) {
|
||||
settleApproval(
|
||||
msg.id,
|
||||
{ error: { message: outcome.error } },
|
||||
{ error: requestError(outcome.error) },
|
||||
{ holdsClaim: true },
|
||||
);
|
||||
} else {
|
||||
@@ -1706,7 +1728,7 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
const outcome = describeTxFailure(TX_STAGE_NONCE, null);
|
||||
settleApproval(
|
||||
msg.id,
|
||||
{ error: { message: outcome.error } },
|
||||
{ error: requestError(outcome.error) },
|
||||
{ holdsClaim: true },
|
||||
);
|
||||
sendResponse({
|
||||
@@ -1739,10 +1761,13 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// that says plainly it did not take the transaction:
|
||||
// describeTxFailure() reclassifies that, and the stage it
|
||||
// returns is the one reported.
|
||||
//
|
||||
// A node that refused the transaction gives the page its own
|
||||
// code and data, whatever sentence the page is shown.
|
||||
const outcome = describeTxFailure(TX_STAGE_BROADCAST, e);
|
||||
settleApproval(
|
||||
msg.id,
|
||||
{ error: { message: outcome.error } },
|
||||
{ error: requestError(outcome.error, nodeError(e)) },
|
||||
{ holdsClaim: true },
|
||||
);
|
||||
sendResponse({
|
||||
@@ -1864,7 +1889,7 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (!retryable) {
|
||||
settleApproval(
|
||||
msg.id,
|
||||
{ error: { message: errMsg } },
|
||||
{ error: requestError(errMsg) },
|
||||
{ holdsClaim: true },
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -41,6 +41,7 @@ const {
|
||||
ALLOWED_TX_TYPES,
|
||||
SERIALIZED_FIELDS,
|
||||
assertWithinCeilings,
|
||||
nodeError,
|
||||
} = require("./approvalVerify");
|
||||
|
||||
// How long the population may take before the request is failed back to the
|
||||
@@ -72,15 +73,27 @@ const REQUEST_FIELDS = [
|
||||
"type",
|
||||
];
|
||||
|
||||
// EIP-1474's code for invalid parameters: what the page is given when the
|
||||
// site asked for a transaction this wallet refuses as it was asked for.
|
||||
const INVALID_PARAMS_CODE = -32602;
|
||||
|
||||
// Why no approval can be raised. The message is the sentence the page is
|
||||
// shown. `code` and `data` are the JSON-RPC ones it is given with it: the
|
||||
// node's own, unchanged, when the node refused the transaction, so a site can
|
||||
// decode a revert; INVALID_PARAMS_CODE when the site's request is refused;
|
||||
// none when the failure is the wallet's own, which the background answers as
|
||||
// an internal error.
|
||||
class ApprovalPrepareError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message, code, data) {
|
||||
super(message);
|
||||
this.name = "ApprovalPrepareError";
|
||||
if (code !== undefined) this.code = code;
|
||||
if (data !== undefined) this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
return new ApprovalPrepareError(message);
|
||||
function fail(message, code, data) {
|
||||
return new ApprovalPrepareError(message, code, data);
|
||||
}
|
||||
|
||||
function present(v) {
|
||||
@@ -134,6 +147,7 @@ function requestFrom(txParams, from) {
|
||||
) {
|
||||
throw fail(
|
||||
"The site asked for two different gas limits for this transaction.",
|
||||
INVALID_PARAMS_CODE,
|
||||
);
|
||||
}
|
||||
request.gasLimit = txParams.gas;
|
||||
@@ -144,6 +158,7 @@ function requestFrom(txParams, from) {
|
||||
) {
|
||||
throw fail(
|
||||
"The site asked for a transaction of a type this wallet does not sign.",
|
||||
INVALID_PARAMS_CODE,
|
||||
);
|
||||
}
|
||||
return request;
|
||||
@@ -228,6 +243,9 @@ async function prepareApprovalTx(provider, from, txParams) {
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ApprovalPrepareError) throw e;
|
||||
// A refusal from the node, such as a reverting gas estimate, keeps
|
||||
// the node's code and revert data.
|
||||
const fromNode = nodeError(e);
|
||||
throw fail(
|
||||
sentence(
|
||||
"The transaction could not be prepared: " +
|
||||
@@ -235,6 +253,8 @@ async function prepareApprovalTx(provider, from, txParams) {
|
||||
e.message ||
|
||||
"the network did not answer"),
|
||||
),
|
||||
fromNode?.code,
|
||||
fromNode?.data,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -746,6 +746,15 @@ function errorText(err) {
|
||||
return "The transaction could not be sent.";
|
||||
}
|
||||
|
||||
// The JSON-RPC error object the node answered with, `{ code, message, data }`,
|
||||
// out of the error ethers threw for that answer. ethers keeps it under
|
||||
// `info.error`, or under `error` when it could not classify it. Null when
|
||||
// there is none, as for a node that could not be reached.
|
||||
function nodeError(err) {
|
||||
if (!err || typeof err !== "object") return null;
|
||||
return err.error || (err.info && err.info.error) || null;
|
||||
}
|
||||
|
||||
// Every string a failure might carry its reason in. ethers reports the node's
|
||||
// own words in `shortMessage`, but a JSON-RPC error it could not classify is
|
||||
// nested under `error` or `info.error` with the node's message intact, and the
|
||||
@@ -757,7 +766,7 @@ function failureTexts(err) {
|
||||
for (const text of [err.shortMessage, err.message, err.reason]) {
|
||||
if (text) texts.push(String(text));
|
||||
}
|
||||
const nested = err.error || (err.info && err.info.error);
|
||||
const nested = nodeError(err);
|
||||
if (nested && nested.message) texts.push(String(nested.message));
|
||||
return texts;
|
||||
}
|
||||
@@ -891,6 +900,7 @@ module.exports = {
|
||||
assertWithinCeilings,
|
||||
sameAddress,
|
||||
failureIsRetryable,
|
||||
nodeError,
|
||||
isNonceCollision,
|
||||
describeTxFailure,
|
||||
describeSigningFailure,
|
||||
|
||||
Reference in New Issue
Block a user