MetaMask / MetaMask/metamask-extension

[Bug]: User rejection of wallet_sendCalls surfaces a different error code depending on batch length

Open
#46,120 0 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

external-contributor needs-triage regression-prod-13.44.0 team-core-platform team-extension-platform type-bug
Dominant language
TypeScript
Stars
13.2k
Forks
5.6k
Avg merge
2d 5h
Merged PRs (30d)
451

Description

### Describe the bug

When `wallet_sendCalls` is called with a **single** call, MetaMask resolves the request with a batch `id` *before* the user has acted on the confirmation. If the user then rejects, the dApp never receives `4001 User Rejected Request`. Instead it receives a successful `wallet_sendCalls` response, and every subsequent `wallet_getCallsStatus` on that very same `id` fails with `5730 Unknown bundle id — "No matching bundle found"`.

With **two or more** calls the same user action produces the correct `4001`. So the error surface for an identical dApp code path, identical account and identical user action depends on the number of calls in the batch — which is an implementation detail the dApp cannot control or observe.

Practical impact: dApps cannot distinguish "the user cancelled" from "the wallet lost the bundle". A plain cancellation is presented to users as a transaction failure. This is the same class of issue as #32956 (wrong error code for user upgrade rejections).

Reproduced with an EIP-7702 smart account in the MetaMask browser extension.

### Expected behavior

`wallet_sendCalls` should reject with `4001 User Rejected Request` when the user rejects the confirmation, regardless of how many calls the batch contains.

Two points from [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) support this:

1. The error table assigns `4001` to `wallet_sendCalls`:

| 4001 | User Rejected Request | The user rejected submitting the batch of calls | `wallet_sendCalls` |

That error is unreachable if the wallet has already resolved `wallet_sendCalls` with an `id` before the user decides. The single-call path therefore cannot implement this part of the contract at all.

2. > Within 24 hours from the corresponding `wallet_sendCalls`, wallets SHOULD return a call-batch status when `wallet_getCallsStatus` is called with the same `id`.

Here MetaMask issues an `id` and then, seconds later, answers `5730` for it — an error whose spec definition is "This bundle id is unknown / **has not been submitted**". The wallet denies the existence of an identifier it just issued.

### Screenshots/Recordings

_No response_

### Steps to reproduce

1. Connect the MetaMask extension to a dApp with an account upgraded to a smart account (EIP-7702).
2. Call `wallet_sendCalls` with exactly **one** call, `atomicRequired: false`, no capabilities. For example, via viem:

```ts
const { id } = await walletClient.sendCalls({
account,
calls: [{ to: '0x...', value: parseEther('0.001') }],
})
// resolves BEFORE the user confirms

await walletClient.waitForCallsStatus({ id })
```
3. Reject the transaction in the MetaMask confirmation UI.
4. Observe: step 2's `sendCalls` has already resolved successfully with an `id`; `waitForCallsStatus` / `wallet_getCallsStatus` then fails with `5730`.
5. Repeat with **two** calls in the batch (e.g. ERC-20 `approve` + a call that consumes the allowance) and reject again. Now `wallet_sendCalls` correctly rejects with `4001`.

### Error messages or log output

```shell
UnknownBundleIdError: This bundle id is unknown / has not been submitted

Details: No matching bundle found
Version: viem@2.55.11

{
"details": "No matching bundle found",
"shortMessage": "This bundle id is unknown / has not been submitted",
"name": "UnknownBundleIdError",
"code": 5730
}

Expected instead (this is what the 2-call batch produces):

TransactionExecutionError: User rejected the request.

Details: MetaMask Tx Signature: User denied transaction signature.

{
"cause": {
"details": "MetaMask Tx Signature: User denied transaction signature.",
"shortMessage": "User rejected the request.",
"name": "UserRejectedRequestError",
"code": 4001
},
"name": "TransactionExecutionError"
}
```

### Where was this bug found?

Live version (from official store)

### Version

13.44.0

### Build type

None

### Browser

Chrome

### Operating system

MacOS

### Hardware wallet

_No response_

### Additional context

The divergence looks like it originates in `@metamask/eip-5792-middleware`, which branches on batch length (links pinned to `MetaMask/core@be1dd5f`, `@metamask/eip-5792-middleware@3.0.5`, `@metamask/transaction-controller@69.8.1`):

[`processSendCalls.ts#L113-L148`](https://github.com/MetaMask/core/blob/be1dd5f239389e2f1c121dd6040e42dbfdfa4b99/packages/eip-5792-middleware/src/hooks/processSendCalls.ts#L113-L148)

```ts
let batchId: Hex;
if (Object.keys(transactions).length === 1) {
batchId = await processSingleTransaction({ ... });
} else {
batchId = await processMultipleTransaction({ ... });
}
return { id: batchId };
```

**Single-call path** — [`processSendCalls.ts#L225-L233`](https://github.com/MetaMask/core/blob/be1dd5f239389e2f1c121dd6040e42dbfdfa4b99/packages/eip-5792-middleware/src/hooks/processSendCalls.ts#L225-L233) awaits only the *adding* of the transaction and discards the `result` promise, so `batchId` is returned before the user confirms:

```ts
await addTransaction(txParams, {
batchId,
...
});
return batchId;
```

**Multi-call path** — [`transaction-controller/src/utils/batch.ts#L524-L542`](https://github.com/MetaMask/core/blob/be1dd5f239389e2f1c121dd6040e42dbfdfa4b99/packages/transaction-controller/src/utils/batch.ts#L524-L542) does await `result`, which is what propagates the rejection out of `wallet_sendCalls`:

```ts
const { result } = await addTransaction(txParams, { ... });
const transactionHash = await result;
```

On rejection, `TransactionController.#rejectTransaction` calls `#deleteTransaction(transactionId)`, removing the record from `state.transactions`. [`getCallsStatus.ts#L25-L34`](https://github.com/MetaMask/core/blob/be1dd5f239389e2f1c121dd6040e42dbfdfa4b99/packages/eip-5792-middleware/src/hooks/getCallsStatus.ts#L25-L34) then finds nothing for that `batchId` and throws:

```ts
const transactions = messenger
.call('TransactionController:getState')
.transactions.filter((tx) => tx.batchId === id);

if (!transactions?.length) {
throw new JsonRpcError(EIP5792ErrorCode.UnknownBundleId, `No matching bundle found`);
}
```

A possible fix would be for `processSingleTransaction` to await the `result` promise the way `addTransactionBatchWith7702` does, so that a rejection propagates out of `wallet_sendCalls` as `4001` on both paths.

Secondary note: a rejected or dropped bundle is currently indistinguishable from an unknown one, since the record is deleted. Even with the fix above, a `5730` for a bundle MetaMask itself issued within the last 24 hours conflicts with the spec's "SHOULD return a call-batch status" requirement.

### Severity

Cancelling a transaction is shown to users as a failed transaction, and dApps have no reliable way to detect a plain user rejection for single-call batches.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/eip-5792-middleware/src/hooks/processSendCalls.ts, comparing processSingleTransaction with the multi-call path in packages/transaction-controller/src/utils/batch.ts. Trace status lookup in packages/eip-5792-middleware/src/hooks/getCallsStatus.ts, then reproduce rejection with one and two calls. Done means both paths surface 4001 User Rejected Request instead of returning an id that later produces 5730.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, blockchain
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.