Skip to content

Commit 0c5dbd4

Browse files
authored
feat: authenticate calls to transaction api (#570)
* feat: authenticate calls to transaction api * chore: lint fix * feat: authenticate sentinel /network calls
1 parent fb59b2c commit 0c5dbd4

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

src/SmartTransactionsController.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,6 +2194,48 @@ describe('SmartTransactionsController', () => {
21942194
expect(apiCall.isDone()).toBe(true);
21952195
});
21962196
});
2197+
2198+
it('sends Authorization header when getBearerToken is provided', async () => {
2199+
const bearerToken = 'test-bearer-token-123';
2200+
await withController(
2201+
{
2202+
options: {
2203+
getBearerToken: async () => Promise.resolve(bearerToken),
2204+
},
2205+
},
2206+
async ({ controller }) => {
2207+
const apiCall = nock(API_BASE_URL)
2208+
.post(`/networks/${ethereumChainIdDec}/cancel`)
2209+
.matchHeader('Authorization', `Bearer ${bearerToken}`)
2210+
.reply(200, { message: 'successful' });
2211+
2212+
await controller.cancelSmartTransaction('uuid1');
2213+
2214+
expect(apiCall.isDone()).toBe(true);
2215+
},
2216+
);
2217+
});
2218+
2219+
it('sends Authorization header to Sentinel /network when getBearerToken is provided', async () => {
2220+
const bearerToken = 'test-bearer-token-456';
2221+
await withController(
2222+
{
2223+
options: {
2224+
getBearerToken: async () => Promise.resolve(bearerToken),
2225+
},
2226+
},
2227+
async ({ controller }) => {
2228+
const apiCall = nock(SENTINEL_API_BASE_URL_MAP[ethereumChainIdDec])
2229+
.get(`/network`)
2230+
.matchHeader('Authorization', `Bearer ${bearerToken}`)
2231+
.reply(200, createSuccessLivenessApiResponse());
2232+
2233+
await controller.fetchLiveness();
2234+
2235+
expect(apiCall.isDone()).toBe(true);
2236+
},
2237+
);
2238+
});
21972239
});
21982240

21992241
describe('getTransactions', () => {

src/SmartTransactionsController.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ import { BigNumber } from 'bignumber.js';
3737
import cloneDeep from 'lodash/cloneDeep';
3838

3939
import {
40+
API_BASE_URL,
4041
DEFAULT_DISABLED_SMART_TRANSACTIONS_FEATURE_FLAGS,
4142
MetaMetricsEventCategory,
4243
MetaMetricsEventName,
44+
SENTINEL_API_BASE_URL_MAP,
4345
SmartTransactionsTraceName,
4446
} from './constants';
4547
import {
@@ -230,6 +232,14 @@ type SmartTransactionsControllerOptions = {
230232
* removed in a future version.
231233
*/
232234
getFeatureFlags?: () => FeatureFlags;
235+
/**
236+
* Optional callback to obtain a bearer token for authenticating requests to
237+
* the Transaction API. When provided, the token is sent in the
238+
* Authorization header for all Transaction API calls. Can be used with
239+
* the authentication flow from @metamask/core-backend (e.g. from
240+
* AuthenticationController.getBearerToken).
241+
*/
242+
getBearerToken?: () => Promise<string | undefined> | string | undefined;
233243
trace?: TraceCallback;
234244
};
235245

@@ -258,6 +268,11 @@ export class SmartTransactionsController extends StaticIntervalPollingController
258268

259269
readonly #getMetaMetricsProps: () => Promise<MetaMetricsProps>;
260270

271+
readonly #getBearerToken?: () =>
272+
| Promise<string | undefined>
273+
| string
274+
| undefined;
275+
261276
#trace: TraceCallback;
262277

263278
/**
@@ -292,11 +307,28 @@ export class SmartTransactionsController extends StaticIntervalPollingController
292307

293308
/* istanbul ignore next */
294309
async #fetch(request: string, options?: RequestInit) {
310+
const headers: Record<string, string> = {
311+
'Content-Type': 'application/json',
312+
...(this.#clientId && { 'X-Client-Id': this.#clientId }),
313+
};
314+
315+
const urlMatches =
316+
request.startsWith(API_BASE_URL) ||
317+
Object.values(SENTINEL_API_BASE_URL_MAP).some((baseUrl) =>
318+
request.startsWith(baseUrl),
319+
);
320+
if (this.#getBearerToken && urlMatches) {
321+
const token = await Promise.resolve(this.#getBearerToken());
322+
if (token) {
323+
headers.Authorization = `Bearer ${token}`;
324+
}
325+
}
326+
295327
const fetchOptions = {
296328
...options,
297329
headers: {
298-
'Content-Type': 'application/json',
299-
...(this.#clientId && { 'X-Client-Id': this.#clientId }),
330+
...headers,
331+
...options?.headers,
300332
},
301333
};
302334

@@ -312,6 +344,7 @@ export class SmartTransactionsController extends StaticIntervalPollingController
312344
state = {},
313345
messenger,
314346
getMetaMetricsProps,
347+
getBearerToken,
315348
trace,
316349
}: SmartTransactionsControllerOptions) {
317350
super({
@@ -323,6 +356,7 @@ export class SmartTransactionsController extends StaticIntervalPollingController
323356
...state,
324357
},
325358
});
359+
326360
this.#interval = interval;
327361
this.#clientId = clientId;
328362
this.#chainId = InitialChainId;
@@ -331,6 +365,7 @@ export class SmartTransactionsController extends StaticIntervalPollingController
331365
this.#ethQuery = undefined;
332366
this.#trackMetaMetricsEvent = trackMetaMetricsEvent;
333367
this.#getMetaMetricsProps = getMetaMetricsProps;
368+
this.#getBearerToken = getBearerToken;
334369
this.#trace = trace ?? (((_request, fn) => fn?.()) as TraceCallback);
335370

336371
this.initializeSmartTransactionsForChainId();

0 commit comments

Comments
 (0)