diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index a7a5fb27ac0..12d297fd78d 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Defaults to 100 if not provided. - Add support for `transactionHistoryLimit` feature flag to configure the maximum number of transactions stored in state ([#7648](https://github.com/MetaMask/core/pull/7648)) - Defaults to 40 if not provided. +- Add callTrace errors in the simulation data ([#7641](https://github.com/MetaMask/core/pull/7641)) ### Changed 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.test.ts b/packages/transaction-controller/src/utils/balance-changes.test.ts index 135ded3a8dc..3d0e9db098b 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,128 @@ 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 +1066,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: undefined, tokenBalanceChanges: [], error: { code: ERROR_CODE_MOCK, @@ -950,6 +1086,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: undefined, tokenBalanceChanges: [], error: { code: ERROR_CODE_MOCK, @@ -973,6 +1110,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], tokenBalanceChanges: [], error: { code: SimulationErrorCode.InvalidResponse, @@ -1001,6 +1139,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], tokenBalanceChanges: [], error: { code: SimulationErrorCode.Reverted, @@ -1029,6 +1168,7 @@ describe('Balance Change Utils', () => { expect(result).toStrictEqual({ simulationData: { + callTraceErrors: [], tokenBalanceChanges: [], error: { code: undefined, @@ -1049,6 +1189,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 0820e97c369..47e865838bc 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, nativeBalanceChange, tokenBalanceChanges, }; @@ -167,6 +172,7 @@ export async function getBalanceChanges( return { simulationData: { + callTraceErrors, tokenBalanceChanges: [], error: { code, @@ -632,6 +638,26 @@ 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 ?? []; + const nestedErrors = nestedCalls.flatMap((nestedCall) => + extractCallTraceErrors(nestedCall), + ); + + return [...errors, ...nestedErrors]; +} + /** * Generate balance change data from previous and new balances. *