Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/eip-5792-middleware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `error` field to `wallet_getCallsStatus` response when statusCode is 500 (REVERTED) ([#7568](https://github.com/MetaMask/core/pull/7568))

### Changed

- Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511))
Expand Down
128 changes: 128 additions & 0 deletions packages/eip-5792-middleware/src/hooks/getCallsStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,133 @@ describe('EIP-5792', () => {
`No matching bundle found`,
);
});

it('includes error information when transaction status is reverted with error', () => {
const errorMock = {
name: 'TransactionError',
message: 'Transaction reverted: insufficient funds',
code: 'INSUFFICIENT_FUNDS',
rpc: { data: '0x08c379a0' },
};

getTransactionControllerStateMock.mockReturnValueOnce({
transactions: [
{
...TRANSACTION_META_MOCK,
status: TransactionStatus.failed,
hash: '0x123',
error: errorMock,
txReceipt: {
...TRANSACTION_META_MOCK.txReceipt,
status: '0x0',
},
},
],
} as unknown as TransactionControllerState);

const result = getCallsStatus(messenger, BATCH_ID_MOCK);

expect(result?.status).toStrictEqual(GetCallsStatusCode.REVERTED);
expect(result?.error).toStrictEqual({
message: errorMock.message,
code: errorMock.code,
name: errorMock.name,
rpc: errorMock.rpc,
});
});

it('includes error information when transaction status is dropped with error', () => {
const errorMock = {
name: 'TransactionError',
message: 'Transaction dropped',
code: 'DROPPED',
};

getTransactionControllerStateMock.mockReturnValueOnce({
transactions: [
{
...TRANSACTION_META_MOCK,
status: TransactionStatus.dropped,
error: errorMock,
},
],
} as unknown as TransactionControllerState);

const result = getCallsStatus(messenger, BATCH_ID_MOCK);

expect(result?.status).toStrictEqual(GetCallsStatusCode.REVERTED);
expect(result?.error).toStrictEqual({
message: errorMock.message,
code: errorMock.code,
name: errorMock.name,
});
});

it('includes error information when receipt status is 0x0 even without transaction error', () => {
getTransactionControllerStateMock.mockReturnValueOnce({
transactions: [
{
...TRANSACTION_META_MOCK,
status: TransactionStatus.failed,
hash: '0x123',
txReceipt: {
...TRANSACTION_META_MOCK.txReceipt,
status: '0x0',
},
},
],
} as unknown as TransactionControllerState);

const result = getCallsStatus(messenger, BATCH_ID_MOCK);

expect(result?.status).toStrictEqual(GetCallsStatusCode.REVERTED);
expect(result?.error).toStrictEqual({
message: 'Transaction reverted',
});
});

it('does not include error information when status is not REVERTED', () => {
getTransactionControllerStateMock.mockReturnValueOnce({
transactions: [
{
...TRANSACTION_META_MOCK,
status: TransactionStatus.confirmed,
error: {
name: 'SomeError',
message: 'Some error message',
},
},
],
} as unknown as TransactionControllerState);

const result = getCallsStatus(messenger, BATCH_ID_MOCK);

expect(result?.status).toStrictEqual(GetCallsStatusCode.CONFIRMED);
expect(result?.error).toBeUndefined();
});

it('includes only message when error has minimal information', () => {
const errorMock = {
message: 'Transaction failed',
};

getTransactionControllerStateMock.mockReturnValueOnce({
transactions: [
{
...TRANSACTION_META_MOCK,
status: TransactionStatus.failed,
hash: '0x123',
error: errorMock,
},
],
} as unknown as TransactionControllerState);

const result = getCallsStatus(messenger, BATCH_ID_MOCK);

expect(result?.status).toStrictEqual(GetCallsStatusCode.REVERTED);
expect(result?.error).toStrictEqual({
message: errorMock.message,
});
});
});
});
61 changes: 60 additions & 1 deletion packages/eip-5792-middleware/src/hooks/getCallsStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
TransactionReceipt,
} from '@metamask/transaction-controller';
import { TransactionStatus } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';
import type { Hex, Json } from '@metamask/utils';

import { EIP5792ErrorCode, GetCallsStatusCode, VERSION } from '../constants';
import type { EIP5792Messenger, GetCallsStatusResult } from '../types';
Expand Down Expand Up @@ -54,16 +54,75 @@ export function getCallsStatus(
},
];

// Extract error information when status is REVERTED (500)
const errorInfo =
status === GetCallsStatusCode.REVERTED
? extractErrorInfo(transaction, txReceipt)
: undefined;

return {
version: VERSION,
id,
chainId,
atomic: true, // Always atomic as we currently only support EIP-7702 batches
status,
receipts,
...(errorInfo && { error: errorInfo }),
};
}

/**
* Extracts error information from a transaction when it has reverted.
*
* @param transactionMeta - The transaction metadata containing error information.
* @param txReceipt - The transaction receipt, if available.
* @returns Error information object with at least a message.
*/
function extractErrorInfo(
transactionMeta: TransactionMeta,
txReceipt: Required<TransactionReceipt> | undefined,
): GetCallsStatusResult['error'] {
const { error } = transactionMeta;

// Determine the error message
let message: string;
if (error?.message) {
message = error.message;
} else if (txReceipt?.status === '0x0') {
message = 'Transaction reverted';
} else {
// Default message for dropped transactions or other revert scenarios
message = 'Transaction reverted';
}

// Build error info with at least the message
const errorInfo: GetCallsStatusResult['error'] = {
message,
};

// Add optional error fields if available
if (error?.code) {
errorInfo.code = error.code;
}

if (error?.name) {
errorInfo.name = error.name;
}

// Include RPC error details if available and is JSON-compatible
if (error?.rpc) {
try {
// Ensure rpc is JSON-serializable
JSON.stringify(error.rpc);
errorInfo.rpc = error.rpc as Json;
} catch {
// If rpc is not JSON-serializable, skip it
}
}

return errorInfo;
}

/**
* Maps transaction status to EIP-5792 call status codes.
*
Expand Down
6 changes: 6 additions & 0 deletions packages/eip-5792-middleware/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ export type GetCallsStatusResult = {
transactionHash: Hex;
}[];
capabilities?: Record<string, Json>;
error?: {
message: string;
code?: string;
name?: string;
rpc?: Json;
};
};

export type GetCallsStatusHook = (
Expand Down
Loading