Skip to content

Commit 5158351

Browse files
feat(transaction-pay): add ordered strategy fallback orchestration
1 parent d482581 commit 5158351

13 files changed

Lines changed: 684 additions & 78 deletions

packages/transaction-pay-controller/ARCHITECTURE.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ The mechanism by which the tokens are provided on the target chain is abstracted
2828

2929
Each `PayStrategy` dictates how the `quotes` are retrieved, which detail the associated fees and strategy specific data, and how those quotes are actioned or "submitted".
3030

31+
`TransactionPayController` provides an ordered strategy list via `getStrategies`.
32+
The quote flow iterates strategies in order, applies `supports(...)` compatibility checks when present, and falls back to the next compatible strategy if quote retrieval fails or returns no quotes.
33+
The publish hook uses the same ordered fallback approach if execution fails for the primary quote strategy.
34+
3135
### Bridge
3236

3337
The `BridgeStrategy` bridges tokens from the payment our source token to the target chain.
@@ -54,14 +58,15 @@ The high level interaction with the `TransactionPayController` is as follows:
5458
4. Controller identifies any required tokens and adds them to its state.
5559
5. If a client confirmation is using `MetaMask Pay`, the user selects a payment token (or it is done automatically) which invokes the `updatePaymentToken` action.
5660
- The below steps are also triggered if the transaction `data` is updated.
57-
6. Controller selects an appropriate `PayStrategy` using the `getStrategy` action.
58-
7. Controller requests quotes from the `PayStrategy` and persists them in state, including associated totals.
61+
6. Controller resolves an ordered set of `PayStrategy` implementations using the `getStrategies` action.
62+
7. Controller requests quotes from each compatible strategy in order until one returns quotes, then persists those quotes and associated totals.
5963
8. Resulting fees and totals are presented in the client transaction confirmation.
6064
9. If approved by the user, the target transaction is signed and published.
61-
10. The `TransactionPayPublishHook` is invoked and submits the relevant quotes via the same `PayStrategy`.
62-
11. The hook waits for any transactions and quotes to complete.
63-
12. Depending on the pay strategy and required tokens, the original target transaction is also published as the required funds are now in place on the user's account on the target chain.
64-
13. Target transaction is finalized and any related controller state is removed.
65+
10. The `TransactionPayPublishHook` is invoked and submits the relevant quotes via the strategy encoded in the quote.
66+
11. If primary execution fails, the hook rebuilds quote requests and retries using the next compatible strategy in order.
67+
12. The hook waits for any transactions and quotes to complete.
68+
13. Depending on the pay strategy and required tokens, the original target transaction is also published as the required funds are now in place on the user's account on the target chain.
69+
14. Target transaction is finalized and any related controller state is removed.
6570

6671
## State
6772

packages/transaction-pay-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add ordered strategy fallback mechanism for quote retrieval and publish-time execution ([#7868](https://github.com/MetaMask/core/pull/7868))
13+
1014
### Changed
1115

1216
- Bump `@metamask/bridge-controller` from `^65.3.0` to `^66.0.0` ([#7862](https://github.com/MetaMask/core/pull/7862))

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,51 @@ describe('TransactionPayController', () => {
112112
),
113113
).toBe(TransactionPayStrategy.Test);
114114
});
115+
116+
it('returns relay if getStrategies callback returns empty', async () => {
117+
new TransactionPayController({
118+
getDelegationTransaction: jest.fn(),
119+
getStrategies: (): TransactionPayStrategy[] => [],
120+
messenger,
121+
});
122+
123+
expect(
124+
messenger.call(
125+
'TransactionPayController:getStrategy',
126+
TRANSACTION_META_MOCK,
127+
),
128+
).toBe(TransactionPayStrategy.Relay);
129+
});
130+
});
131+
132+
describe('getStrategies Action', () => {
133+
it('returns relay by default', async () => {
134+
createController();
135+
136+
expect(
137+
messenger.call(
138+
'TransactionPayController:getStrategies',
139+
TRANSACTION_META_MOCK,
140+
),
141+
).toStrictEqual([TransactionPayStrategy.Relay]);
142+
});
143+
144+
it('returns callback list if provided', async () => {
145+
new TransactionPayController({
146+
getDelegationTransaction: jest.fn(),
147+
getStrategies: (): TransactionPayStrategy[] => [
148+
TransactionPayStrategy.Test,
149+
],
150+
messenger,
151+
});
152+
153+
expect(
154+
messenger.call(
155+
'TransactionPayController:getStrategies',
156+
TRANSACTION_META_MOCK,
157+
),
158+
).toStrictEqual([TransactionPayStrategy.Test]);
159+
});
115160
});
116161

117162
describe('transaction data update', () => {

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,14 @@ export class TransactionPayController extends BaseController<
4343
transaction: TransactionMeta,
4444
) => TransactionPayStrategy;
4545

46+
readonly #getStrategies?: (
47+
transaction: TransactionMeta,
48+
) => TransactionPayStrategy[];
49+
4650
constructor({
4751
getDelegationTransaction,
4852
getStrategy,
53+
getStrategies,
4954
messenger,
5055
state,
5156
}: TransactionPayControllerOptions) {
@@ -58,6 +63,7 @@ export class TransactionPayController extends BaseController<
5863

5964
this.#getDelegationTransaction = getDelegationTransaction;
6065
this.#getStrategy = getStrategy;
66+
this.#getStrategies = getStrategies;
6167

6268
this.#registerActionHandlers();
6369

@@ -141,15 +147,23 @@ export class TransactionPayController extends BaseController<
141147
}
142148

143149
#registerActionHandlers(): void {
150+
const getStrategies = this.#getStrategiesWithFallback.bind(this);
151+
144152
this.messenger.registerActionHandler(
145153
'TransactionPayController:getDelegationTransaction',
146154
this.#getDelegationTransaction.bind(this),
147155
);
148156

149157
this.messenger.registerActionHandler(
150158
'TransactionPayController:getStrategy',
151-
this.#getStrategy ??
152-
((): TransactionPayStrategy => TransactionPayStrategy.Relay),
159+
(transaction: TransactionMeta): TransactionPayStrategy =>
160+
getStrategies(transaction)[0] ?? TransactionPayStrategy.Relay,
161+
);
162+
163+
this.messenger.registerActionHandler(
164+
'TransactionPayController:getStrategies',
165+
(transaction: TransactionMeta): TransactionPayStrategy[] =>
166+
getStrategies(transaction),
153167
);
154168

155169
this.messenger.registerActionHandler(
@@ -162,4 +176,18 @@ export class TransactionPayController extends BaseController<
162176
this.updatePaymentToken.bind(this),
163177
);
164178
}
179+
180+
#getStrategiesWithFallback(
181+
transaction: TransactionMeta,
182+
): TransactionPayStrategy[] {
183+
if (this.#getStrategies) {
184+
return this.#getStrategies(transaction);
185+
}
186+
187+
if (this.#getStrategy) {
188+
return [this.#getStrategy(transaction)];
189+
}
190+
191+
return [TransactionPayStrategy.Relay];
192+
}
165193
}

0 commit comments

Comments
 (0)