Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Add `perpsAcrossDeposit` and `predictAcrossDeposit` transaction types ([#7806](https://github.com/MetaMask/core/pull/7806))

### Added

- Add optional `requiredAssets` to `TransactionMeta` ([#7820](https://github.com/MetaMask/core/pull/7820))
Expand Down Expand Up @@ -53,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add support for `transactionHistoryLimit` feature flag to configure the maximum number of transactions stored in state ([#7648](https://github.com/MetaMask/core/pull/7648))
- Defaults to 40 if not provided.
- Add optional `callTraceErrors` to `simulationData` ([#7641](https://github.com/MetaMask/core/pull/7641))
- Add `acrossDeposit` transaction type and `MetamaskPayMetadata.executionLatencyMs` for MetaMask Pay tracking ([#7806](https://github.com/MetaMask/core/pull/7806))

### Changed

Expand Down
13 changes: 13 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,16 @@ export enum TransactionType {
*/
relayDeposit = 'relayDeposit',

/**
* Deposit funds for Across quote via Perps.
*/
perpsAcrossDeposit = 'perpsAcrossDeposit',

/**
* Deposit funds for Across quote via Predict.
*/
predictAcrossDeposit = 'predictAcrossDeposit',

/**
* When a transaction is failed it can be retried by
* resubmitting the same transaction with a higher gas fee. This type is also used
Expand Down Expand Up @@ -2096,6 +2106,9 @@ export type MetamaskPayMetadata = {
/** Chain ID of the payment token. */
chainId?: Hex;

/** Total time spent submitting the MetaMask Pay flow, in milliseconds. */
executionLatencyMs?: number;

/** Total network fee in fiat currency, including the original and bridge transactions. */
networkFeeFiat?: string;

Expand Down
21 changes: 18 additions & 3 deletions packages/transaction-pay-controller/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ Quotes are retrieved from the [Relay API](https://docs.relay.link/what-is-relay)

The resulting transaction deposits the necessary funds (on the source network), then a Relayer on the target chain immediately transfers the necessary funds and optionally executes any requested call data.

### Across

The `AcrossStrategy` retrieves quotes from the Across API and submits approvals and deposit transactions via the `TransactionController`.

### Strategy Selection and Fallback

Strategy order and configuration are determined by feature flags:

- `payStrategies.strategyOrder` controls the ordered strategy list (default: `[Relay, Across]`).
- Each strategy can be enabled/disabled via `payStrategies.<strategy>.enabled`.
- Strategies may implement capability gating in `supports(...)` (e.g., Across rejects same-chain swaps).

- The controller selects the **first** strategy in order that returns `supports(...) === true`.
- If the selected strategy fails during quote retrieval or execution, the next compatible strategy is attempted.

## Lifecycle

The high level interaction with the `TransactionPayController` is as follows:
Expand All @@ -54,11 +69,11 @@ The high level interaction with the `TransactionPayController` is as follows:
4. Controller identifies any required tokens and adds them to its state.
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.
- The below steps are also triggered if the transaction `data` is updated.
6. Controller selects an appropriate `PayStrategy` using the `getStrategy` action.
7. Controller requests quotes from the `PayStrategy` and persists them in state, including associated totals.
6. Controller requests an ordered list of strategies via the `getStrategies` action.
7. Controller selects the first compatible strategy and requests quotes, falling back to the next strategy if quote retrieval fails.
8. Resulting fees and totals are presented in the client transaction confirmation.
9. If approved by the user, the target transaction is signed and published.
10. The `TransactionPayPublishHook` is invoked and submits the relevant quotes via the same `PayStrategy`.
10. The `TransactionPayPublishHook` is invoked and submits the relevant quotes via the strategy indicated by the quotes, with fallback on execution errors.
11. The hook waits for any transactions and quotes to complete.
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.
13. Target transaction is finalized and any related controller state is removed.
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Generate required tokens using `requiredAssets` from transaction metadata ([#7820](https://github.com/MetaMask/core/pull/7820))
- Add standalone Across pay strategy with core-level selection and fallback ([#7806](https://github.com/MetaMask/core/pull/7806))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,54 @@ describe('TransactionPayController', () => {
),
).toBe(TransactionPayStrategy.Test);
});

it('returns relay if getStrategies callback returns empty', async () => {
new TransactionPayController({
getDelegationTransaction: jest.fn(),
getStrategies: (): TransactionPayStrategy[] => [],
messenger,
});

expect(
messenger.call(
'TransactionPayController:getStrategy',
TRANSACTION_META_MOCK,
),
).toBe(TransactionPayStrategy.Relay);
});
});

describe('getStrategies Action', () => {
it('returns relay by default', async () => {
createController();

expect(
messenger.call(
'TransactionPayController:getStrategies',
TRANSACTION_META_MOCK,
),
).toStrictEqual([
TransactionPayStrategy.Relay,
TransactionPayStrategy.Across,
]);
});

it('returns callback list if provided', async () => {
new TransactionPayController({
getDelegationTransaction: jest.fn(),
getStrategies: (): TransactionPayStrategy[] => [
TransactionPayStrategy.Test,
],
messenger,
});

expect(
messenger.call(
'TransactionPayController:getStrategies',
TRANSACTION_META_MOCK,
),
).toStrictEqual([TransactionPayStrategy.Test]);
});
});

describe('transaction data update', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
TransactionPayControllerState,
UpdatePaymentTokenRequest,
} from './types';
import { getStrategyOrder } from './utils/feature-flags';
import { updateQuotes } from './utils/quotes';
import { updateSourceAmounts } from './utils/source-amounts';
import { pollTransactionChanges } from './utils/transaction';
Expand Down Expand Up @@ -43,9 +44,14 @@ export class TransactionPayController extends BaseController<
transaction: TransactionMeta,
) => TransactionPayStrategy;

readonly #getStrategies?: (
transaction: TransactionMeta,
) => TransactionPayStrategy[];

constructor({
getDelegationTransaction,
getStrategy,
getStrategies,
messenger,
state,
}: TransactionPayControllerOptions) {
Expand All @@ -58,6 +64,7 @@ export class TransactionPayController extends BaseController<

this.#getDelegationTransaction = getDelegationTransaction;
this.#getStrategy = getStrategy;
this.#getStrategies = getStrategies;

this.#registerActionHandlers();

Expand Down Expand Up @@ -141,15 +148,31 @@ export class TransactionPayController extends BaseController<
}

#registerActionHandlers(): void {
const getStrategies =
this.#getStrategies ??
((transaction: TransactionMeta): TransactionPayStrategy[] => {
if (this.#getStrategy) {
return [this.#getStrategy(transaction)];
}

return getStrategyOrder(this.messenger);
});

this.messenger.registerActionHandler(
'TransactionPayController:getDelegationTransaction',
this.#getDelegationTransaction.bind(this),
);

this.messenger.registerActionHandler(
'TransactionPayController:getStrategy',
this.#getStrategy ??
((): TransactionPayStrategy => TransactionPayStrategy.Relay),
(transaction: TransactionMeta): TransactionPayStrategy =>
getStrategies(transaction)[0] ?? TransactionPayStrategy.Relay,
);

this.messenger.registerActionHandler(
'TransactionPayController:getStrategies',
(transaction: TransactionMeta): TransactionPayStrategy[] =>
getStrategies(transaction),
);

this.messenger.registerActionHandler(
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-pay-controller/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const POLYGON_USDCE_ADDRESS =
'0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex;

export enum TransactionPayStrategy {
Across = 'across',
Bridge = 'bridge',
Relay = 'relay',
Test = 'test',
Expand Down
Loading
Loading