diff --git a/packages/eip-5792-middleware/CHANGELOG.md b/packages/eip-5792-middleware/CHANGELOG.md index 5cee382d72d..621c28073c1 100644 --- a/packages/eip-5792-middleware/CHANGELOG.md +++ b/packages/eip-5792-middleware/CHANGELOG.md @@ -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)) diff --git a/packages/eip-5792-middleware/src/hooks/getCallsStatus.test.ts b/packages/eip-5792-middleware/src/hooks/getCallsStatus.test.ts index 0626016e078..c82dea6a441 100644 --- a/packages/eip-5792-middleware/src/hooks/getCallsStatus.test.ts +++ b/packages/eip-5792-middleware/src/hooks/getCallsStatus.test.ts @@ -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, + }); + }); }); }); diff --git a/packages/eip-5792-middleware/src/hooks/getCallsStatus.ts b/packages/eip-5792-middleware/src/hooks/getCallsStatus.ts index f3af9bb27eb..e75c5f700af 100644 --- a/packages/eip-5792-middleware/src/hooks/getCallsStatus.ts +++ b/packages/eip-5792-middleware/src/hooks/getCallsStatus.ts @@ -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'; @@ -54,6 +54,12 @@ export function getCallsStatus( }, ]; + // Extract error information when status is REVERTED (500) + const errorInfo = + status === GetCallsStatusCode.REVERTED + ? extractErrorInfo(transaction, txReceipt) + : undefined; + return { version: VERSION, id, @@ -61,9 +67,62 @@ export function getCallsStatus( 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 | 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. * diff --git a/packages/eip-5792-middleware/src/types.ts b/packages/eip-5792-middleware/src/types.ts index 819a009014a..6d18247816d 100644 --- a/packages/eip-5792-middleware/src/types.ts +++ b/packages/eip-5792-middleware/src/types.ts @@ -56,6 +56,12 @@ export type GetCallsStatusResult = { transactionHash: Hex; }[]; capabilities?: Record; + error?: { + message: string; + code?: string; + name?: string; + rpc?: Json; + }; }; export type GetCallsStatusHook = (