From d1812a4310c130ae11b814ac8cb3dba3b6d98db4 Mon Sep 17 00:00:00 2001 From: Kevin Le Jeune Date: Fri, 6 Mar 2026 11:33:16 +0100 Subject: [PATCH 1/3] feat: authenticate calls to transaction api --- src/SmartTransactionsController.test.ts | 21 +++++++++++++++++ src/SmartTransactionsController.ts | 31 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/SmartTransactionsController.test.ts b/src/SmartTransactionsController.test.ts index 2d77a73..d713177 100644 --- a/src/SmartTransactionsController.test.ts +++ b/src/SmartTransactionsController.test.ts @@ -2194,6 +2194,27 @@ describe('SmartTransactionsController', () => { expect(apiCall.isDone()).toBe(true); }); }); + + it('sends Authorization header when getBearerToken is provided', async () => { + const bearerToken = 'test-bearer-token-123'; + await withController( + { + options: { + getBearerToken: () => Promise.resolve(bearerToken), + }, + }, + async ({ controller }) => { + const apiCall = nock(API_BASE_URL) + .post(`/networks/${ethereumChainIdDec}/cancel`) + .matchHeader('Authorization', `Bearer ${bearerToken}`) + .reply(200, { message: 'successful' }); + + await controller.cancelSmartTransaction('uuid1'); + + expect(apiCall.isDone()).toBe(true); + }, + ); + }); }); describe('getTransactions', () => { diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index ee5dc6f..d9a6811 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -37,6 +37,7 @@ import { BigNumber } from 'bignumber.js'; import cloneDeep from 'lodash/cloneDeep'; import { + API_BASE_URL, DEFAULT_DISABLED_SMART_TRANSACTIONS_FEATURE_FLAGS, MetaMetricsEventCategory, MetaMetricsEventName, @@ -230,6 +231,14 @@ type SmartTransactionsControllerOptions = { * removed in a future version. */ getFeatureFlags?: () => FeatureFlags; + /** + * Optional callback to obtain a bearer token for authenticating requests to + * the Transaction API. When provided, the token is sent in the + * Authorization header for all Transaction API calls. Can be used with + * the authentication flow from @metamask/core-backend (e.g. from + * AuthenticationController.getBearerToken). + */ + getBearerToken?: () => Promise | string | undefined; trace?: TraceCallback; }; @@ -258,6 +267,8 @@ export class SmartTransactionsController extends StaticIntervalPollingController readonly #getMetaMetricsProps: () => Promise; + readonly #getBearerToken?: () => Promise | string | undefined; + #trace: TraceCallback; /** @@ -292,11 +303,24 @@ export class SmartTransactionsController extends StaticIntervalPollingController /* istanbul ignore next */ async #fetch(request: string, options?: RequestInit) { + const headers: Record = { + 'Content-Type': 'application/json', + ...(this.#clientId && { 'X-Client-Id': this.#clientId }), + }; + + const urlMatches = request.startsWith(API_BASE_URL); + if (this.#getBearerToken && urlMatches) { + const token = await Promise.resolve(this.#getBearerToken()); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + } + const fetchOptions = { ...options, headers: { - 'Content-Type': 'application/json', - ...(this.#clientId && { 'X-Client-Id': this.#clientId }), + ...headers, + ...options?.headers, }, }; @@ -312,6 +336,7 @@ export class SmartTransactionsController extends StaticIntervalPollingController state = {}, messenger, getMetaMetricsProps, + getBearerToken, trace, }: SmartTransactionsControllerOptions) { super({ @@ -323,6 +348,7 @@ export class SmartTransactionsController extends StaticIntervalPollingController ...state, }, }); + this.#interval = interval; this.#clientId = clientId; this.#chainId = InitialChainId; @@ -331,6 +357,7 @@ export class SmartTransactionsController extends StaticIntervalPollingController this.#ethQuery = undefined; this.#trackMetaMetricsEvent = trackMetaMetricsEvent; this.#getMetaMetricsProps = getMetaMetricsProps; + this.#getBearerToken = getBearerToken; this.#trace = trace ?? (((_request, fn) => fn?.()) as TraceCallback); this.initializeSmartTransactionsForChainId(); From d309dafa139b2b496da673137438394066254d51 Mon Sep 17 00:00:00 2001 From: Kevin Le Jeune Date: Fri, 6 Mar 2026 11:45:13 +0100 Subject: [PATCH 2/3] chore: lint fix --- src/SmartTransactionsController.test.ts | 2 +- src/SmartTransactionsController.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/SmartTransactionsController.test.ts b/src/SmartTransactionsController.test.ts index d713177..a1d4db7 100644 --- a/src/SmartTransactionsController.test.ts +++ b/src/SmartTransactionsController.test.ts @@ -2200,7 +2200,7 @@ describe('SmartTransactionsController', () => { await withController( { options: { - getBearerToken: () => Promise.resolve(bearerToken), + getBearerToken: async () => Promise.resolve(bearerToken), }, }, async ({ controller }) => { diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index d9a6811..550b805 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -267,7 +267,10 @@ export class SmartTransactionsController extends StaticIntervalPollingController readonly #getMetaMetricsProps: () => Promise; - readonly #getBearerToken?: () => Promise | string | undefined; + readonly #getBearerToken?: () => + | Promise + | string + | undefined; #trace: TraceCallback; @@ -307,12 +310,12 @@ export class SmartTransactionsController extends StaticIntervalPollingController 'Content-Type': 'application/json', ...(this.#clientId && { 'X-Client-Id': this.#clientId }), }; - + const urlMatches = request.startsWith(API_BASE_URL); if (this.#getBearerToken && urlMatches) { const token = await Promise.resolve(this.#getBearerToken()); if (token) { - headers['Authorization'] = `Bearer ${token}`; + headers.Authorization = `Bearer ${token}`; } } From 0bd2986aeec48c08a83df1bd0d91a60b5a8322f8 Mon Sep 17 00:00:00 2001 From: Kevin Le Jeune Date: Fri, 6 Mar 2026 16:03:46 +0100 Subject: [PATCH 3/3] feat: authenticate sentinel /network calls --- src/SmartTransactionsController.test.ts | 21 +++++++++++++++++++++ src/SmartTransactionsController.ts | 7 ++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/SmartTransactionsController.test.ts b/src/SmartTransactionsController.test.ts index a1d4db7..0f303bc 100644 --- a/src/SmartTransactionsController.test.ts +++ b/src/SmartTransactionsController.test.ts @@ -2215,6 +2215,27 @@ describe('SmartTransactionsController', () => { }, ); }); + + it('sends Authorization header to Sentinel /network when getBearerToken is provided', async () => { + const bearerToken = 'test-bearer-token-456'; + await withController( + { + options: { + getBearerToken: async () => Promise.resolve(bearerToken), + }, + }, + async ({ controller }) => { + const apiCall = nock(SENTINEL_API_BASE_URL_MAP[ethereumChainIdDec]) + .get(`/network`) + .matchHeader('Authorization', `Bearer ${bearerToken}`) + .reply(200, createSuccessLivenessApiResponse()); + + await controller.fetchLiveness(); + + expect(apiCall.isDone()).toBe(true); + }, + ); + }); }); describe('getTransactions', () => { diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index 550b805..d537a22 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -41,6 +41,7 @@ import { DEFAULT_DISABLED_SMART_TRANSACTIONS_FEATURE_FLAGS, MetaMetricsEventCategory, MetaMetricsEventName, + SENTINEL_API_BASE_URL_MAP, SmartTransactionsTraceName, } from './constants'; import { @@ -311,7 +312,11 @@ export class SmartTransactionsController extends StaticIntervalPollingController ...(this.#clientId && { 'X-Client-Id': this.#clientId }), }; - const urlMatches = request.startsWith(API_BASE_URL); + const urlMatches = + request.startsWith(API_BASE_URL) || + Object.values(SENTINEL_API_BASE_URL_MAP).some((baseUrl) => + request.startsWith(baseUrl), + ); if (this.#getBearerToken && urlMatches) { const token = await Promise.resolve(this.#getBearerToken()); if (token) {