Skip to content

Commit 26c090a

Browse files
refactor transaction-pay strategy fallback and feature-flag order parsing
1 parent 9e780e0 commit 26c090a

File tree

8 files changed

+181
-64
lines changed

8 files changed

+181
-64
lines changed

packages/transaction-pay-controller/src/TransactionPayController.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,27 @@ describe('TransactionPayController', () => {
164164
).toBe(TransactionPayStrategy.Test);
165165
});
166166

167+
it('does not query feature flag strategy order when getStrategies callback returns values', async () => {
168+
new TransactionPayController({
169+
getDelegationTransaction: jest.fn(),
170+
getStrategies: (): TransactionPayStrategy[] => [
171+
TransactionPayStrategy.Test,
172+
],
173+
messenger,
174+
});
175+
176+
getStrategyOrderMock.mockClear();
177+
178+
expect(
179+
messenger.call(
180+
'TransactionPayController:getStrategy',
181+
TRANSACTION_META_MOCK,
182+
),
183+
).toBe(TransactionPayStrategy.Test);
184+
185+
expect(getStrategyOrderMock).not.toHaveBeenCalled();
186+
});
187+
167188
it('returns relay if getStrategies callback returns empty', async () => {
168189
getStrategyOrderMock.mockReturnValue([TransactionPayStrategy.Test]);
169190

packages/transaction-pay-controller/src/TransactionPayController.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type { Draft } from 'immer';
55
import { noop } from 'lodash';
66

77
import { updatePaymentToken } from './actions/update-payment-token';
8-
import { CONTROLLER_NAME, TransactionPayStrategy } from './constants';
8+
import {
9+
CONTROLLER_NAME,
10+
isTransactionPayStrategy,
11+
TransactionPayStrategy,
12+
} from './constants';
913
import { QuoteRefresher } from './helpers/QuoteRefresher';
1014
import type {
1115
GetDelegationTransactionCallback,
@@ -176,12 +180,8 @@ export class TransactionPayController extends BaseController<
176180

177181
this.messenger.registerActionHandler(
178182
'TransactionPayController:getStrategy',
179-
(transaction: TransactionMeta): TransactionPayStrategy => {
180-
const fallbackStrategy = getStrategyOrder(this.messenger)[0];
181-
return (
182-
this.#getStrategiesWithFallback(transaction)[0] ?? fallbackStrategy
183-
);
184-
},
183+
(transaction: TransactionMeta): TransactionPayStrategy =>
184+
this.#getStrategiesWithFallback(transaction)[0],
185185
);
186186

187187
this.messenger.registerActionHandler(
@@ -197,18 +197,21 @@ export class TransactionPayController extends BaseController<
197197

198198
#getStrategiesWithFallback(
199199
transaction: TransactionMeta,
200-
): TransactionPayStrategy[] {
201-
const fallbackStrategies = getStrategyOrder(this.messenger);
202-
let strategies: TransactionPayStrategy[] = [];
203-
204-
if (this.#getStrategies) {
205-
strategies = this.#getStrategies(transaction);
206-
} else if (this.#getStrategy) {
207-
strategies = [this.#getStrategy(transaction)];
208-
} else {
209-
strategies = fallbackStrategies;
210-
}
200+
): [TransactionPayStrategy, ...TransactionPayStrategy[]] {
201+
const strategyCandidates: unknown[] =
202+
this.#getStrategies?.(transaction) ??
203+
(this.#getStrategy ? [this.#getStrategy(transaction)] : []);
204+
205+
const validStrategies = strategyCandidates.filter(
206+
(strategy): strategy is TransactionPayStrategy =>
207+
isTransactionPayStrategy(strategy),
208+
);
211209

212-
return strategies.length ? strategies : fallbackStrategies;
210+
return validStrategies.length
211+
? (validStrategies as [
212+
TransactionPayStrategy,
213+
...TransactionPayStrategy[],
214+
])
215+
: getStrategyOrder(this.messenger);
213216
}
214217
}

packages/transaction-pay-controller/src/constants.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,17 @@ export enum TransactionPayStrategy {
3737
Relay = 'relay',
3838
Test = 'test',
3939
}
40+
41+
const VALID_STRATEGIES = new Set(Object.values(TransactionPayStrategy));
42+
43+
/**
44+
* Checks if a value is a valid transaction pay strategy.
45+
*
46+
* @param strategy - Candidate strategy value.
47+
* @returns True if the value is a valid strategy.
48+
*/
49+
export function isTransactionPayStrategy(
50+
strategy: unknown,
51+
): strategy is TransactionPayStrategy {
52+
return VALID_STRATEGIES.has(strategy as TransactionPayStrategy);
53+
}

packages/transaction-pay-controller/src/utils/feature-flags.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import type { Hex } from '@metamask/utils';
22
import { createModuleLogger } from '@metamask/utils';
3+
import { uniq } from 'lodash';
34

45
import type { TransactionPayControllerMessenger } from '..';
5-
import { TransactionPayStrategy } from '../constants';
6+
import { isTransactionPayStrategy, TransactionPayStrategy } from '../constants';
67
import { projectLogger } from '../logger';
78
import { RELAY_URL_BASE } from '../strategy/relay/constants';
89

910
const log = createModuleLogger(projectLogger, 'feature-flags');
1011

12+
type StrategyOrder = [TransactionPayStrategy, ...TransactionPayStrategy[]];
13+
1114
export const DEFAULT_GAS_BUFFER = 1.0;
1215
export const DEFAULT_RELAY_FALLBACK_GAS_ESTIMATE = 900000;
1316
export const DEFAULT_RELAY_FALLBACK_GAS_MAX = 1500000;
1417
export const DEFAULT_RELAY_QUOTE_URL = `${RELAY_URL_BASE}/quote`;
1518
export const DEFAULT_SLIPPAGE = 0.005;
16-
export const DEFAULT_STRATEGY_ORDER: [
17-
TransactionPayStrategy,
18-
...TransactionPayStrategy[],
19-
] = [TransactionPayStrategy.Relay];
20-
21-
const VALID_STRATEGIES = new Set(Object.values(TransactionPayStrategy));
19+
export const DEFAULT_STRATEGY_ORDER: StrategyOrder = [
20+
TransactionPayStrategy.Relay,
21+
];
2222

2323
type FeatureFlagsRaw = {
2424
gasBuffer?: {
@@ -37,7 +37,7 @@ type FeatureFlagsRaw = {
3737
max?: number;
3838
};
3939
relayQuoteUrl?: string;
40-
strategyOrder?: unknown[];
40+
strategyOrder?: string[];
4141
slippage?: number;
4242
slippageTokens?: Record<Hex, Record<Hex, number>>;
4343
};
@@ -60,29 +60,24 @@ export type FeatureFlags = {
6060
*/
6161
export function getStrategyOrder(
6262
messenger: TransactionPayControllerMessenger,
63-
): [TransactionPayStrategy, ...TransactionPayStrategy[]] {
64-
const { strategyOrder } = getFeatureFlagsRaw(messenger);
63+
): StrategyOrder {
64+
const { strategyOrder: strategyPriority } = getFeatureFlagsRaw(messenger);
6565

66-
if (!Array.isArray(strategyOrder)) {
66+
if (!Array.isArray(strategyPriority)) {
6767
return [...DEFAULT_STRATEGY_ORDER];
6868
}
6969

70-
const validStrategyOrder = strategyOrder
71-
.filter((strategy): strategy is TransactionPayStrategy =>
72-
VALID_STRATEGIES.has(strategy as TransactionPayStrategy),
73-
)
74-
.filter(
75-
(strategy, index, strategies) => strategies.indexOf(strategy) === index,
76-
);
70+
const validStrategyPriority = uniq(
71+
strategyPriority.filter((strategy): strategy is TransactionPayStrategy =>
72+
isTransactionPayStrategy(strategy),
73+
),
74+
);
7775

78-
if (!validStrategyOrder.length) {
76+
if (!validStrategyPriority.length) {
7977
return [...DEFAULT_STRATEGY_ORDER];
8078
}
8179

82-
return validStrategyOrder as [
83-
TransactionPayStrategy,
84-
...TransactionPayStrategy[],
85-
];
80+
return validStrategyPriority as StrategyOrder;
8681
}
8782

8883
/**

packages/transaction-pay-controller/src/utils/quotes.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash';
66

77
import type { UpdateQuotesRequest } from './quotes';
88
import { refreshQuotes, updateQuotes } from './quotes';
9-
import { getStrategyByName } from './strategy';
9+
import { getStrategiesByName, getStrategyByName } from './strategy';
1010
import { calculateTotals } from './totals';
1111
import { getTransaction, updateTransaction } from './transaction';
1212
import { TransactionPayStrategy } from '../constants';
@@ -93,6 +93,7 @@ describe('Quotes Utils', () => {
9393
const { messenger, getControllerStateMock } = getMessengerMock();
9494
const updateTransactionDataMock = jest.fn();
9595
const getStrategyByNameMock = jest.mocked(getStrategyByName);
96+
const getStrategiesByNameMock = jest.mocked(getStrategiesByName);
9697
const getTransactionMock = jest.mocked(getTransaction);
9798
const updateTransactionMock = jest.mocked(updateTransaction);
9899
const calculateTotalsMock = jest.mocked(calculateTotals);
@@ -128,6 +129,22 @@ describe('Quotes Utils', () => {
128129
getQuotes: getQuotesMock,
129130
getBatchTransactions: getBatchTransactionsMock,
130131
});
132+
getStrategiesByNameMock.mockImplementation(
133+
(strategyNames, onUnknownStrategy) =>
134+
strategyNames.flatMap((strategyName) => {
135+
try {
136+
return [
137+
{
138+
name: strategyName,
139+
strategy: getStrategyByNameMock(strategyName),
140+
},
141+
];
142+
} catch {
143+
onUnknownStrategy?.(strategyName);
144+
return [];
145+
}
146+
}),
147+
);
131148

132149
getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK);
133150
getQuotesMock.mockResolvedValue([QUOTE_MOCK]);
@@ -468,6 +485,15 @@ describe('Quotes Utils', () => {
468485
});
469486
});
470487

488+
it('resolves strategies via getStrategiesByName', async () => {
489+
await run();
490+
491+
expect(getStrategiesByNameMock).toHaveBeenCalledWith(
492+
[TransactionPayStrategy.Test],
493+
expect.any(Function),
494+
);
495+
});
496+
471497
it('gets quotes with no minimum if allowUnderMinimum is true', async () => {
472498
await run({
473499
transactionData: {

packages/transaction-pay-controller/src/utils/quotes.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller';
44
import type { Hex, Json } from '@metamask/utils';
55
import { createModuleLogger } from '@metamask/utils';
66

7-
import { getStrategyByName } from './strategy';
7+
import { getStrategiesByName, getStrategyByName } from './strategy';
88
import { calculateTotals } from './totals';
99
import { getTransaction, updateTransaction } from './transaction';
1010
import { TransactionPayStrategy } from '../constants';
@@ -383,25 +383,15 @@ async function getQuotes(
383383
quotes: TransactionPayQuote<Json>[];
384384
}> {
385385
const { id: transactionId } = transaction;
386-
const strategies = getStrategies(transaction)
387-
.map((strategyName) => {
388-
try {
389-
return {
390-
name: strategyName,
391-
strategy: getStrategyByName(strategyName),
392-
};
393-
} catch {
394-
log('Skipping unknown strategy', {
395-
strategy: strategyName,
396-
transactionId,
397-
});
398-
return undefined;
399-
}
400-
})
401-
.filter(Boolean) as {
402-
name: TransactionPayStrategy;
403-
strategy: ReturnType<typeof getStrategyByName>;
404-
}[];
386+
const strategies = getStrategiesByName(
387+
getStrategies(transaction),
388+
(strategyName) => {
389+
log('Skipping unknown strategy', {
390+
strategy: strategyName,
391+
transactionId,
392+
});
393+
},
394+
);
405395

406396
if (!requests?.length) {
407397
return {

packages/transaction-pay-controller/src/utils/strategy.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getStrategyByName } from './strategy';
1+
import { getStrategiesByName, getStrategyByName } from './strategy';
22
import { TransactionPayStrategy } from '../constants';
33
import { BridgeStrategy } from '../strategy/bridge/BridgeStrategy';
44
import { RelayStrategy } from '../strategy/relay/RelayStrategy';
@@ -27,4 +27,41 @@ describe('Strategy Utils', () => {
2727
);
2828
});
2929
});
30+
31+
describe('getStrategiesByName', () => {
32+
it('returns strategies in input order', () => {
33+
const strategies = getStrategiesByName([
34+
TransactionPayStrategy.Test,
35+
TransactionPayStrategy.Bridge,
36+
TransactionPayStrategy.Relay,
37+
]);
38+
39+
expect(strategies).toHaveLength(3);
40+
expect(strategies[0].name).toBe(TransactionPayStrategy.Test);
41+
expect(strategies[1].name).toBe(TransactionPayStrategy.Bridge);
42+
expect(strategies[2].name).toBe(TransactionPayStrategy.Relay);
43+
expect(strategies[0].strategy).toBeInstanceOf(TestStrategy);
44+
expect(strategies[1].strategy).toBeInstanceOf(BridgeStrategy);
45+
expect(strategies[2].strategy).toBeInstanceOf(RelayStrategy);
46+
});
47+
48+
it('skips unknown strategies and calls callback', () => {
49+
const onUnknownStrategy = jest.fn();
50+
51+
const strategies = getStrategiesByName(
52+
[
53+
TransactionPayStrategy.Test,
54+
'UnknownStrategy' as TransactionPayStrategy,
55+
TransactionPayStrategy.Relay,
56+
],
57+
onUnknownStrategy,
58+
);
59+
60+
expect(strategies.map(({ name }) => name)).toStrictEqual([
61+
TransactionPayStrategy.Test,
62+
TransactionPayStrategy.Relay,
63+
]);
64+
expect(onUnknownStrategy).toHaveBeenCalledWith('UnknownStrategy');
65+
});
66+
});
3067
});

packages/transaction-pay-controller/src/utils/strategy.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { RelayStrategy } from '../strategy/relay/RelayStrategy';
44
import { TestStrategy } from '../strategy/test/TestStrategy';
55
import type { PayStrategy } from '../types';
66

7+
export type NamedStrategy = {
8+
name: TransactionPayStrategy;
9+
strategy: PayStrategy<unknown>;
10+
};
11+
712
/**
813
* Get strategy instance by name.
914
*
@@ -27,3 +32,29 @@ export function getStrategyByName(
2732
throw new Error(`Unknown strategy: ${strategyName as string}`);
2833
}
2934
}
35+
36+
/**
37+
* Resolve strategy names into strategy instances, skipping unknown entries.
38+
*
39+
* @param strategyNames - Ordered strategy names.
40+
* @param onUnknownStrategy - Callback invoked for unknown strategies.
41+
* @returns Ordered valid strategies with names.
42+
*/
43+
export function getStrategiesByName(
44+
strategyNames: TransactionPayStrategy[],
45+
onUnknownStrategy?: (strategyName: TransactionPayStrategy) => void,
46+
): NamedStrategy[] {
47+
return strategyNames.flatMap((strategyName) => {
48+
try {
49+
return [
50+
{
51+
name: strategyName,
52+
strategy: getStrategyByName(strategyName),
53+
},
54+
];
55+
} catch {
56+
onUnknownStrategy?.(strategyName);
57+
return [];
58+
}
59+
});
60+
}

0 commit comments

Comments
 (0)