From ce22603c49c55e89bf50581f9d1e9d0724f3f535 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:05:05 +0200 Subject: [PATCH 1/4] feat: Add tracing into the STX controller Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- src/SmartTransactionsController.ts | 71 ++++++++++++++++++------------ 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index 3615978e..8be4a7db 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -9,6 +9,7 @@ import { safelyExecute, ChainId, isSafeDynamicKey, + type TraceCallback, } from '@metamask/controller-utils'; import EthQuery from '@metamask/eth-query'; import type { @@ -206,6 +207,7 @@ type SmartTransactionsControllerOptions = { getMetaMetricsProps: () => Promise; getFeatureFlags: () => FeatureFlags; updateTransaction: (transaction: TransactionMeta, note: string) => void; + trace?: TraceCallback; }; export type SmartTransactionsControllerPollingInput = { @@ -245,6 +247,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo #updateTransaction: SmartTransactionsControllerOptions['updateTransaction']; + #trace: TraceCallback; + /* istanbul ignore next */ async #fetch(request: string, options?: RequestInit) { const fetchOptions = { @@ -272,6 +276,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo getMetaMetricsProps, getFeatureFlags, updateTransaction, + trace, }: SmartTransactionsControllerOptions) { super({ name: controllerName, @@ -295,6 +300,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo this.#getMetaMetricsProps = getMetaMetricsProps; this.#getFeatureFlags = getFeatureFlags; this.#updateTransaction = updateTransaction; + this.#trace = trace ?? (((_request, fn) => fn?.()) as TraceCallback); this.initializeSmartTransactionsForChainId(); @@ -790,10 +796,10 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo APIType.BATCH_STATUS, chainId, )}?${params.toString()}`; - const data = (await this.#fetch(url)) as Record< - string, - SmartTransactionsStatus - >; + const data = (await this.#trace( + { name: 'SmartTransactions:fetchStatus' }, + async () => await this.#fetch(url), + )) as Record; // Process each returned status for (const [uuid, stxStatus] of Object.entries(data)) { @@ -860,7 +866,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo const chainId = this.#getChainId({ networkClientId: selectedNetworkClientId, }); - const transactions = []; + const transactions: UnsignedTransaction[] = []; let unsignedTradeTransactionWithNonce; if (approvalTx) { const unsignedApprovalTransactionWithNonce = @@ -880,14 +886,15 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo ); } transactions.push(unsignedTradeTransactionWithNonce); - const data = await this.#fetch( - getAPIRequestURL(APIType.GET_FEES, chainId), - { - method: 'POST', - body: JSON.stringify({ - txs: transactions, + const data = await this.#trace( + { name: 'SmartTransactions:getFees' }, + async () => + await this.#fetch(getAPIRequestURL(APIType.GET_FEES, chainId), { + method: 'POST', + body: JSON.stringify({ + txs: transactions, + }), }), - }, ); let approvalTxFees: IndividualTxFees | null; let tradeTxFees: IndividualTxFees | null; @@ -943,15 +950,19 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo const ethQuery = this.#getEthQuery({ networkClientId: selectedNetworkClientId, }); - const data = await this.#fetch( - getAPIRequestURL(APIType.SUBMIT_TRANSACTIONS, chainId), - { - method: 'POST', - body: JSON.stringify({ - rawTxs: signedTransactions, - rawCancelTxs: signedCanceledTransactions, - }), - }, + const data = await this.#trace( + { name: 'SmartTransactions:submitTransactions' }, + async () => + await this.#fetch( + getAPIRequestURL(APIType.SUBMIT_TRANSACTIONS, chainId), + { + method: 'POST', + body: JSON.stringify({ + rawTxs: signedTransactions, + rawCancelTxs: signedCanceledTransactions, + }), + }, + ), ); const time = Date.now(); let preTxBalance; @@ -1089,10 +1100,14 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo } = {}, ): Promise { const chainId = this.#getChainId({ networkClientId }); - await this.#fetch(getAPIRequestURL(APIType.CANCEL, chainId), { - method: 'POST', - body: JSON.stringify({ uuid }), - }); + await this.#trace( + { name: 'SmartTransactions:cancelTransaction' }, + async () => + await this.#fetch(getAPIRequestURL(APIType.CANCEL, chainId), { + method: 'POST', + body: JSON.stringify({ uuid }), + }), + ); } async fetchLiveness({ @@ -1103,8 +1118,10 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo const chainId = this.#getChainId({ networkClientId }); let liveness = false; try { - const response = await this.#fetch( - getAPIRequestURL(APIType.LIVENESS, chainId), + const response = await this.#trace( + { name: 'SmartTransactions:fetchLiveness' }, + async () => + await this.#fetch(getAPIRequestURL(APIType.LIVENESS, chainId)), ); liveness = Boolean(response.smartTransactions); } catch (error) { From 1da31abef6850109e1fe924fa84d52002866201a Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:37:09 +0200 Subject: [PATCH 2/4] Add tests for tracing Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- src/SmartTransactionsController.test.ts | 171 ++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/src/SmartTransactionsController.test.ts b/src/SmartTransactionsController.test.ts index 53025fa0..d5992f16 100644 --- a/src/SmartTransactionsController.test.ts +++ b/src/SmartTransactionsController.test.ts @@ -2387,6 +2387,177 @@ describe('SmartTransactionsController', () => { ); }); }); + + describe('Tracing', () => { + const createTraceCallback = () => + jest.fn().mockImplementation(async (_request, fn) => { + return fn?.(); + }); + + it('traces getFees API call with expected name', async () => { + const traceCallback = createTraceCallback(); + + await withController( + { + options: { + trace: traceCallback, + }, + }, + async ({ controller }) => { + const apiUrl = API_BASE_URL; + nock(apiUrl) + .post(`/networks/${ethereumChainIdDec}/getFees`) + .reply(200, createGetFeesApiResponse()); + + const tradeTx = createUnsignedTransaction(ethereumChainIdDec); + await controller.getFees(tradeTx); + + expect(traceCallback).toHaveBeenCalledWith( + { name: 'SmartTransactions:getFees' }, + expect.any(Function), + ); + }, + ); + }); + + it('traces submitSignedTransactions API call with expected name', async () => { + const traceCallback = createTraceCallback(); + + await withController( + { + options: { + trace: traceCallback, + }, + }, + async ({ controller }) => { + const apiUrl = API_BASE_URL; + nock(apiUrl) + .post( + `/networks/${ethereumChainIdDec}/submitTransactions?stxControllerVersion=${packageJson.version}`, + ) + .reply(200, createSubmitTransactionsApiResponse()); + + const signedTx = createSignedTransaction(); + const signedCanceledTx = createSignedCanceledTransaction(); + const txParams = createTxParams(); + + await controller.submitSignedTransactions({ + signedTransactions: [signedTx], + signedCanceledTransactions: [signedCanceledTx], + txParams, + }); + + expect(traceCallback).toHaveBeenCalledWith( + { name: 'SmartTransactions:submitTransactions' }, + expect.any(Function), + ); + }, + ); + }); + + it('traces fetchSmartTransactionsStatus API call with expected name', async () => { + const traceCallback = createTraceCallback(); + + await withController( + { + options: { + trace: traceCallback, + }, + }, + async ({ controller }) => { + nock(API_BASE_URL) + .get(`/networks/${ethereumChainIdDec}/batchStatus?uuids=uuid1`) + .reply(200, createPendingBatchStatusApiResponse()); + + await controller.fetchSmartTransactionsStatus([ + { uuid: 'uuid1', chainId: ChainId.mainnet }, + ]); + + expect(traceCallback).toHaveBeenCalledWith( + { name: 'SmartTransactions:fetchStatus' }, + expect.any(Function), + ); + }, + ); + }); + + it('traces cancelSmartTransaction API call with expected name', async () => { + const traceCallback = createTraceCallback(); + + await withController( + { + options: { + trace: traceCallback, + }, + }, + async ({ controller }) => { + const apiUrl = API_BASE_URL; + nock(apiUrl) + .post(`/networks/${ethereumChainIdDec}/cancel`) + .reply(200, {}); + + await controller.cancelSmartTransaction('uuid1'); + + expect(traceCallback).toHaveBeenCalledWith( + { name: 'SmartTransactions:cancelTransaction' }, + expect.any(Function), + ); + }, + ); + }); + + it('traces fetchLiveness API call with expected name', async () => { + const traceCallback = createTraceCallback(); + + await withController( + { + options: { + trace: traceCallback, + }, + }, + async ({ controller }) => { + nock(SENTINEL_API_BASE_URL_MAP[ethereumChainIdDec]) + .get(`/network`) + .reply(200, createSuccessLivenessApiResponse()); + + await controller.fetchLiveness(); + + expect(traceCallback).toHaveBeenCalledWith( + { name: 'SmartTransactions:fetchLiveness' }, + expect.any(Function), + ); + }, + ); + }); + + it('returns correct result when tracing is enabled', async () => { + const traceCallback = createTraceCallback(); + + await withController( + { + options: { + trace: traceCallback, + }, + }, + async ({ controller }) => { + const apiUrl = API_BASE_URL; + const expectedResponse = createGetFeesApiResponse(); + nock(apiUrl) + .post(`/networks/${ethereumChainIdDec}/getFees`) + .reply(200, expectedResponse); + + const tradeTx = createUnsignedTransaction(ethereumChainIdDec); + const result = await controller.getFees(tradeTx); + + expect(traceCallback).toHaveBeenCalled(); + expect(result).toMatchObject({ + tradeTxFees: expectedResponse.txs[0], + approvalTxFees: null, + }); + }, + ); + }); + }); }); type WithControllerCallback = ({ From 846493fd2c4d128311183f35fc67ef1edd886db9 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:53:18 +0200 Subject: [PATCH 3/4] Remove tracing for SmartTransactions:fetchStatus Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- src/SmartTransactionsController.test.ts | 26 ------------------------- src/SmartTransactionsController.ts | 8 ++++---- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/SmartTransactionsController.test.ts b/src/SmartTransactionsController.test.ts index d5992f16..d8030b88 100644 --- a/src/SmartTransactionsController.test.ts +++ b/src/SmartTransactionsController.test.ts @@ -2455,32 +2455,6 @@ describe('SmartTransactionsController', () => { ); }); - it('traces fetchSmartTransactionsStatus API call with expected name', async () => { - const traceCallback = createTraceCallback(); - - await withController( - { - options: { - trace: traceCallback, - }, - }, - async ({ controller }) => { - nock(API_BASE_URL) - .get(`/networks/${ethereumChainIdDec}/batchStatus?uuids=uuid1`) - .reply(200, createPendingBatchStatusApiResponse()); - - await controller.fetchSmartTransactionsStatus([ - { uuid: 'uuid1', chainId: ChainId.mainnet }, - ]); - - expect(traceCallback).toHaveBeenCalledWith( - { name: 'SmartTransactions:fetchStatus' }, - expect.any(Function), - ); - }, - ); - }); - it('traces cancelSmartTransaction API call with expected name', async () => { const traceCallback = createTraceCallback(); diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index 8be4a7db..fa6654a4 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -796,10 +796,10 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo APIType.BATCH_STATUS, chainId, )}?${params.toString()}`; - const data = (await this.#trace( - { name: 'SmartTransactions:fetchStatus' }, - async () => await this.#fetch(url), - )) as Record; + const data = (await this.#fetch(url)) as Record< + string, + SmartTransactionsStatus + >; // Process each returned status for (const [uuid, stxStatus] of Object.entries(data)) { From 87705ed2f10a0def05275624a939179004f1b8c7 Mon Sep 17 00:00:00 2001 From: dan437 <80175477+dan437@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:34:44 +0200 Subject: [PATCH 4/4] Add SmartTransactionsTraceName enum, use it Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> --- src/SmartTransactionsController.test.ts | 14 +++++++++----- src/SmartTransactionsController.ts | 14 +++++++++----- src/constants.ts | 7 +++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/SmartTransactionsController.test.ts b/src/SmartTransactionsController.test.ts index d8030b88..b0e2bf91 100644 --- a/src/SmartTransactionsController.test.ts +++ b/src/SmartTransactionsController.test.ts @@ -22,7 +22,11 @@ import * as sinon from 'sinon'; import packageJson from '../package.json'; import { advanceTime, flushPromises, getFakeProvider } from '../tests/helpers'; -import { API_BASE_URL, SENTINEL_API_BASE_URL_MAP } from './constants'; +import { + API_BASE_URL, + SENTINEL_API_BASE_URL_MAP, + SmartTransactionsTraceName, +} from './constants'; import SmartTransactionsController, { DEFAULT_INTERVAL, getDefaultSmartTransactionsControllerState, @@ -2413,7 +2417,7 @@ describe('SmartTransactionsController', () => { await controller.getFees(tradeTx); expect(traceCallback).toHaveBeenCalledWith( - { name: 'SmartTransactions:getFees' }, + { name: SmartTransactionsTraceName.GetFees }, expect.any(Function), ); }, @@ -2448,7 +2452,7 @@ describe('SmartTransactionsController', () => { }); expect(traceCallback).toHaveBeenCalledWith( - { name: 'SmartTransactions:submitTransactions' }, + { name: SmartTransactionsTraceName.SubmitTransactions }, expect.any(Function), ); }, @@ -2473,7 +2477,7 @@ describe('SmartTransactionsController', () => { await controller.cancelSmartTransaction('uuid1'); expect(traceCallback).toHaveBeenCalledWith( - { name: 'SmartTransactions:cancelTransaction' }, + { name: SmartTransactionsTraceName.CancelTransaction }, expect.any(Function), ); }, @@ -2497,7 +2501,7 @@ describe('SmartTransactionsController', () => { await controller.fetchLiveness(); expect(traceCallback).toHaveBeenCalledWith( - { name: 'SmartTransactions:fetchLiveness' }, + { name: SmartTransactionsTraceName.FetchLiveness }, expect.any(Function), ); }, diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index fa6654a4..b0645eb6 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -28,7 +28,11 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; import cloneDeep from 'lodash/cloneDeep'; -import { MetaMetricsEventCategory, MetaMetricsEventName } from './constants'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, + SmartTransactionsTraceName, +} from './constants'; import type { Fees, Hex, @@ -887,7 +891,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo } transactions.push(unsignedTradeTransactionWithNonce); const data = await this.#trace( - { name: 'SmartTransactions:getFees' }, + { name: SmartTransactionsTraceName.GetFees }, async () => await this.#fetch(getAPIRequestURL(APIType.GET_FEES, chainId), { method: 'POST', @@ -951,7 +955,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo networkClientId: selectedNetworkClientId, }); const data = await this.#trace( - { name: 'SmartTransactions:submitTransactions' }, + { name: SmartTransactionsTraceName.SubmitTransactions }, async () => await this.#fetch( getAPIRequestURL(APIType.SUBMIT_TRANSACTIONS, chainId), @@ -1101,7 +1105,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo ): Promise { const chainId = this.#getChainId({ networkClientId }); await this.#trace( - { name: 'SmartTransactions:cancelTransaction' }, + { name: SmartTransactionsTraceName.CancelTransaction }, async () => await this.#fetch(getAPIRequestURL(APIType.CANCEL, chainId), { method: 'POST', @@ -1119,7 +1123,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo let liveness = false; try { const response = await this.#trace( - { name: 'SmartTransactions:fetchLiveness' }, + { name: SmartTransactionsTraceName.FetchLiveness }, async () => await this.#fetch(getAPIRequestURL(APIType.LIVENESS, chainId)), ); diff --git a/src/constants.ts b/src/constants.ts index fa25f506..3a606a17 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -23,3 +23,10 @@ export enum MetaMetricsEventCategory { Transactions = 'Transactions', Navigation = 'Navigation', } + +export enum SmartTransactionsTraceName { + GetFees = 'Smart Transactions: Get Fees', + SubmitTransactions = 'Smart Transactions: Submit Transactions', + CancelTransaction = 'Smart Transactions: Cancel Transaction', + FetchLiveness = 'Smart Transactions: Fetch Liveness', +}