From 5918b3aa6cea400b9c5918fdb93138d828560f01 Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Thu, 15 Jan 2026 15:33:21 +0100 Subject: [PATCH 1/5] feat: add callTrace errors in the simulation data response --- .../src/api/simulation-api.ts | 7 ++-- packages/transaction-controller/src/types.ts | 3 ++ .../src/utils/balance-changes.ts | 32 +++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/api/simulation-api.ts b/packages/transaction-controller/src/api/simulation-api.ts index 96a5280dbb2..5e77dab80e4 100644 --- a/packages/transaction-controller/src/api/simulation-api.ts +++ b/packages/transaction-controller/src/api/simulation-api.ts @@ -135,10 +135,13 @@ export type SimulationResponseLog = { /** Call trace of a single simulated transaction. */ export type SimulationResponseCallTrace = { /** Nested calls. */ - calls: SimulationResponseCallTrace[]; + calls?: SimulationResponseCallTrace[] | null; + + /** Error message for the call, if any. */ + error?: string; /** Raw event logs created by the call. */ - logs: SimulationResponseLog[]; + logs?: SimulationResponseLog[] | null; }; /** diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 2d6aaf3d565..acfdcdd5735 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1551,6 +1551,9 @@ export type SimulationData = { /** Whether the simulation response changed after a security check triggered a re-simulation. */ isUpdatedAfterSecurityCheck?: boolean; + /** Error messages extracted from call traces, if any. */ + callTraceErrors?: string[]; + /** Data concerning a change to the user's native balance. */ nativeBalanceChange?: SimulationBalanceChange; diff --git a/packages/transaction-controller/src/utils/balance-changes.ts b/packages/transaction-controller/src/utils/balance-changes.ts index 0820e97c369..9b9780820de 100644 --- a/packages/transaction-controller/src/utils/balance-changes.ts +++ b/packages/transaction-controller/src/utils/balance-changes.ts @@ -121,6 +121,8 @@ export async function getBalanceChanges( ): Promise<{ simulationData: SimulationData; gasUsed?: Hex }> { log('Request', request); + let callTraceErrors: string[] | undefined; + try { const response = await baseRequest({ request, @@ -130,7 +132,9 @@ export async function getBalanceChanges( }, }); - const transactionError = response.transactions?.[0]?.error; + const transactionResponse = response.transactions?.[0]; + callTraceErrors = extractCallTraceErrors(transactionResponse?.callTrace); + const transactionError = transactionResponse?.error; if (transactionError) { throw new SimulationError(transactionError); @@ -143,8 +147,9 @@ export async function getBalanceChanges( const tokenBalanceChanges = await getTokenBalanceChanges(request, events); - const gasUsed = response.transactions?.[0]?.gasUsed; + const gasUsed = transactionResponse?.gasUsed; const simulationData = { + ...(callTraceErrors?.length ? { callTraceErrors } : {}), nativeBalanceChange, tokenBalanceChanges, }; @@ -167,6 +172,7 @@ export async function getBalanceChanges( return { simulationData: { + ...(callTraceErrors?.length ? { callTraceErrors } : {}), tokenBalanceChanges: [], error: { code, @@ -632,6 +638,28 @@ function extractLogs( ]; } +/** + * Extract all error messages from a call trace tree. + * + * @param call - The root call trace. + * @returns An array of error messages. + */ +function extractCallTraceErrors(call?: SimulationResponseCallTrace): string[] { + if (!call) { + return []; + } + + const errors = call.error ? [call.error] : []; + const nestedCalls = call.calls ?? []; + + return [ + ...errors, + ...nestedCalls + .map((nestedCall) => extractCallTraceErrors(nestedCall)) + .flat(), + ]; +} + /** * Generate balance change data from previous and new balances. * From 486e3912e6a8285f1a2a5ac73bb6e22873865d33 Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Thu, 15 Jan 2026 16:11:21 +0100 Subject: [PATCH 2/5] chore: update changelog --- packages/transaction-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 7ff51c30758..8c207ac80a4 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Exclude transactions where `isTransfer` is defined when marking nonce duplicates as dropped ([#7637](https://github.com/MetaMask/core/pull/7637)) +### Added + +- Add callTrace errors in the simulation data ([#7641](https://github.com/MetaMask/core/pull/7641)) + ## [62.9.1] ### Changed From dba45287df0752121ed2ebbc8daeeb7923b8219c Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Thu, 15 Jan 2026 16:25:52 +0100 Subject: [PATCH 3/5] chore: fix changelog lint --- packages/transaction-controller/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 8c207ac80a4..4522099dbcb 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,14 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- Exclude transactions where `isTransfer` is defined when marking nonce duplicates as dropped ([#7637](https://github.com/MetaMask/core/pull/7637)) - ### Added - Add callTrace errors in the simulation data ([#7641](https://github.com/MetaMask/core/pull/7641)) +### Fixed + +- Exclude transactions where `isTransfer` is defined when marking nonce duplicates as dropped ([#7637](https://github.com/MetaMask/core/pull/7637)) + ## [62.9.1] ### Changed From c762189109838a79409cf36a1dcc7a06becd7970 Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Fri, 23 Jan 2026 15:34:38 +0100 Subject: [PATCH 4/5] chore: addresses multiple comments --- .../src/utils/balance-changes.test.ts | 137 ++++++++++++++++++ .../src/utils/balance-changes.ts | 14 +- 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/packages/transaction-controller/src/utils/balance-changes.test.ts b/packages/transaction-controller/src/utils/balance-changes.test.ts index 135ded3a8dc..c1f23df6a3d 100644 --- a/packages/transaction-controller/src/utils/balance-changes.test.ts +++ b/packages/transaction-controller/src/utils/balance-changes.test.ts @@ -306,6 +306,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: { difference: DIFFERENCE_MOCK, isDecrease, @@ -328,6 +329,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [], }, @@ -344,6 +346,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: { difference: '0x7', isDecrease: false, @@ -465,6 +468,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -515,6 +519,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -574,6 +579,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -621,6 +627,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -732,6 +739,7 @@ describe('Balance Change Utils', () => { ); expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -773,6 +781,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [], }, @@ -800,6 +809,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [], }, @@ -824,6 +834,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [], }, @@ -844,6 +855,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -902,6 +914,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], nativeBalanceChange: undefined, tokenBalanceChanges: [ { @@ -920,6 +933,124 @@ describe('Balance Change Utils', () => { }); }); + describe('returns call trace errors', () => { + it('from root call trace', async () => { + simulateTransactionsMock.mockResolvedValueOnce({ + transactions: [ + { + ...defaultResponseTx, + callTrace: { + calls: [], + logs: [], + error: 'Root error', + }, + }, + ], + sponsorship: { + isSponsored: false, + error: null, + }, + }); + + const result = await getBalanceChanges(REQUEST_MOCK); + + expect(result).toStrictEqual({ + simulationData: { + callTraceErrors: ['Root error'], + nativeBalanceChange: undefined, + tokenBalanceChanges: [], + }, + gasUsed: undefined, + }); + }); + + it('from nested call traces', async () => { + simulateTransactionsMock.mockResolvedValueOnce({ + transactions: [ + { + ...defaultResponseTx, + callTrace: { + calls: [ + { + calls: [ + { + calls: [], + logs: [], + error: 'Deeply nested error', + }, + ], + logs: [], + error: 'Nested error', + }, + ], + logs: [], + error: 'Root error', + }, + }, + ], + sponsorship: { + isSponsored: false, + error: null, + }, + }); + + const result = await getBalanceChanges(REQUEST_MOCK); + + expect(result).toStrictEqual({ + simulationData: { + callTraceErrors: ['Root error', 'Nested error', 'Deeply nested error'], + nativeBalanceChange: undefined, + tokenBalanceChanges: [], + }, + gasUsed: undefined, + }); + }); + + it('as empty array when no errors in call trace', async () => { + simulateTransactionsMock.mockResolvedValueOnce( + createNativeBalanceResponse(BALANCE_1_MOCK, BALANCE_2_MOCK), + ); + + const result = await getBalanceChanges(REQUEST_MOCK); + + expect(result.simulationData.callTraceErrors).toStrictEqual([]); + }); + + it('in error response when call trace errors exist', async () => { + simulateTransactionsMock.mockResolvedValueOnce({ + transactions: [ + { + ...defaultResponseTx, + error: 'Transaction failed', + callTrace: { + calls: [], + logs: [], + error: 'Call trace error', + }, + }, + ], + sponsorship: { + isSponsored: false, + error: null, + }, + }); + + const result = await getBalanceChanges(REQUEST_MOCK); + + expect(result).toStrictEqual({ + simulationData: { + callTraceErrors: ['Call trace error'], + tokenBalanceChanges: [], + error: { + code: undefined, + message: 'Transaction failed', + }, + }, + gasUsed: undefined, + }); + }); + }); + describe('returns error', () => { it('if API request throws', async () => { simulateTransactionsMock.mockRejectedValueOnce({ @@ -931,6 +1062,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: undefined, tokenBalanceChanges: [], error: { code: ERROR_CODE_MOCK, @@ -950,6 +1082,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: undefined, tokenBalanceChanges: [], error: { code: ERROR_CODE_MOCK, @@ -973,6 +1106,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], tokenBalanceChanges: [], error: { code: SimulationErrorCode.InvalidResponse, @@ -1001,6 +1135,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], tokenBalanceChanges: [], error: { code: SimulationErrorCode.Reverted, @@ -1029,6 +1164,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], tokenBalanceChanges: [], error: { code: undefined, @@ -1049,6 +1185,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: undefined, tokenBalanceChanges: [], error: { code: SimulationErrorCode.Reverted, diff --git a/packages/transaction-controller/src/utils/balance-changes.ts b/packages/transaction-controller/src/utils/balance-changes.ts index 9b9780820de..47e865838bc 100644 --- a/packages/transaction-controller/src/utils/balance-changes.ts +++ b/packages/transaction-controller/src/utils/balance-changes.ts @@ -149,7 +149,7 @@ export async function getBalanceChanges( const gasUsed = transactionResponse?.gasUsed; const simulationData = { - ...(callTraceErrors?.length ? { callTraceErrors } : {}), + callTraceErrors, nativeBalanceChange, tokenBalanceChanges, }; @@ -172,7 +172,7 @@ export async function getBalanceChanges( return { simulationData: { - ...(callTraceErrors?.length ? { callTraceErrors } : {}), + callTraceErrors, tokenBalanceChanges: [], error: { code, @@ -651,13 +651,11 @@ function extractCallTraceErrors(call?: SimulationResponseCallTrace): string[] { const errors = call.error ? [call.error] : []; const nestedCalls = call.calls ?? []; + const nestedErrors = nestedCalls.flatMap((nestedCall) => + extractCallTraceErrors(nestedCall), + ); - return [ - ...errors, - ...nestedCalls - .map((nestedCall) => extractCallTraceErrors(nestedCall)) - .flat(), - ]; + return [...errors, ...nestedErrors]; } /** From 3b92fdf3aa21a4261b0bcfcb286cbfd933820756 Mon Sep 17 00:00:00 2001 From: Julien Fontanel Date: Fri, 23 Jan 2026 15:47:33 +0100 Subject: [PATCH 5/5] chore: lint fix --- .../src/utils/balance-changes.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/utils/balance-changes.test.ts b/packages/transaction-controller/src/utils/balance-changes.test.ts index c1f23df6a3d..3d0e9db098b 100644 --- a/packages/transaction-controller/src/utils/balance-changes.test.ts +++ b/packages/transaction-controller/src/utils/balance-changes.test.ts @@ -998,7 +998,11 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { - callTraceErrors: ['Root error', 'Nested error', 'Deeply nested error'], + callTraceErrors: [ + 'Root error', + 'Nested error', + 'Deeply nested error', + ], nativeBalanceChange: undefined, tokenBalanceChanges: [], },