⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { updatePaymentToken } from './actions/update-payment-token';
import { CONTROLLER_NAME, TransactionPayStrategy } from './constants';
import { QuoteRefresher } from './helpers/QuoteRefresher';
import type {
FiatPaymentData,
GetDelegationTransactionCallback,
TransactionData,
TransactionPayControllerMessenger,
Expand Down Expand Up @@ -41,6 +42,7 @@ export class TransactionPayController extends BaseController<

readonly #getStrategy?: (
transaction: TransactionMeta,
transactionData?: TransactionData,
) => TransactionPayStrategy;

constructor({
Expand Down Expand Up @@ -80,6 +82,26 @@ export class TransactionPayController extends BaseController<
});
}

setFiatPayment(
transactionId: string,
fiatPayment: FiatPaymentData | undefined,
): void {
this.#updateTransactionData(transactionId, (transactionData) => {
transactionData.fiatPayment = fiatPayment;

// When setting fiat payment, clear token-based fields
if (fiatPayment !== undefined) {
transactionData.paymentToken = undefined;
transactionData.sourceAmounts = undefined;
transactionData.quotes = undefined;
transactionData.totals = undefined;
transactionData.isMaxAmount = undefined;
transactionData.isLoading = false;
transactionData.quotesLastUpdated = undefined;
}
});
}

updatePaymentToken(request: UpdatePaymentTokenRequest): void {
updatePaymentToken(request, {
messenger: this.messenger,
Expand Down Expand Up @@ -148,15 +170,25 @@ export class TransactionPayController extends BaseController<

this.messenger.registerActionHandler(
'TransactionPayController:getStrategy',
this.#getStrategy ??
((): TransactionPayStrategy => TransactionPayStrategy.Relay),
(transaction: TransactionMeta): TransactionPayStrategy => {
const transactionData = this.state.transactionData?.[transaction.id];
return (
this.#getStrategy?.(transaction, transactionData) ??
TransactionPayStrategy.Relay
);
},
);

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

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

this.messenger.registerActionHandler(
'TransactionPayController:updatePaymentToken',
this.updatePaymentToken.bind(this),
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 @@ -17,4 +17,5 @@ export enum TransactionPayStrategy {
Bridge = 'bridge',
Relay = 'relay',
Test = 'test',
Fiat = 'fiat',
}
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
export type {
FiatPaymentData,
FiatPaymentQuote,
TransactionData,
TransactionPayControllerActions,
TransactionPayControllerEvents,
TransactionPayControllerGetDelegationTransactionAction,
TransactionPayControllerGetStateAction,
TransactionPayControllerGetStrategyAction,
TransactionPayControllerMessenger,
TransactionPayControllerOptions,
TransactionPayControllerSetFiatPaymentAction,
TransactionPayControllerSetIsMaxAmountAction,
TransactionPayControllerState,
TransactionPayControllerStateChangeEvent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Hex } from '@metamask/utils';
import { createModuleLogger } from '@metamask/utils';

import { TransactionPayStrategy } from '../..';
import { projectLogger } from '../../logger';
import type {
PayStrategy,
PayStrategyExecuteRequest,
PayStrategyGetQuotesRequest,
TransactionPayQuote,
} from '../../types';

const log = createModuleLogger(projectLogger, 'fiat-strategy');

const MOCK_TRANSACTION_HASH =
'0x0000000000000000000000000000000000000000000000000000000000000001' as Hex;

const POC_DELAY_MS = 5000; // 5 seconds

export class FiatStrategy implements PayStrategy<void> {
async getQuotes(
request: PayStrategyGetQuotesRequest,
): Promise<TransactionPayQuote<void>[]> {
const { messenger, requests, transaction } = request;

log('Getting fiat quotes (PoC mock)', { transactionId: transaction.id });

// Get required tokens from controller state to compute fee
const state = messenger.call('TransactionPayController:getState');
const transactionData = state.transactionData?.[transaction.id];
const tokens = transactionData?.tokens ?? [];

// Sum up the USD amounts of required tokens (excluding skipIfBalance)
const requiredTokens = tokens.filter((token) => !token.skipIfBalance);
const baseAmountUsd = requiredTokens.reduce(
(sum, token) => sum + parseFloat(token.amountUsd || '0'),
0,
);

// Compute 2% provider fee
const providerFeeUsd = baseAmountUsd * 0.02;

// Use the dummy request passed in (from buildFiatQuoteRequests)
const dummyRequest = requests[0];

// PoC: Return a single mocked quote with computed fee
return [
{
dust: { fiat: '0', usd: '0' },
estimatedDuration: POC_DELAY_MS / 1000,
fees: {
provider: {
fiat: providerFeeUsd.toFixed(2),
usd: providerFeeUsd.toFixed(2),
},
sourceNetwork: {
estimate: {
human: '0',
fiat: '0',
usd: '0',
raw: '0',
},
max: {
human: '0',
fiat: '0',
usd: '0',
raw: '0',
},
},
targetNetwork: {
fiat: '0',
usd: '0',
},
},
original: undefined,
request: dummyRequest,
sourceAmount: {
human: baseAmountUsd.toFixed(2),
fiat: baseAmountUsd.toFixed(2),
raw: baseAmountUsd.toFixed(2),
usd: baseAmountUsd.toFixed(2),
},
targetAmount: {
human: baseAmountUsd.toFixed(2),
fiat: baseAmountUsd.toFixed(2),
raw: baseAmountUsd.toFixed(2),
usd: baseAmountUsd.toFixed(2),
},
strategy: TransactionPayStrategy.Fiat,
},
];
}

async execute(
request: PayStrategyExecuteRequest<void>,
): ReturnType<PayStrategy<void>['execute']> {
const { quotes } = request;

log('Executing fiat strategy (PoC mock)', quotes);
log(`Waiting ${POC_DELAY_MS / 1000} seconds...`);

await this.#timeout(POC_DELAY_MS);

log('Returning mock transaction hash:', MOCK_TRANSACTION_HASH);

return { transactionHash: MOCK_TRANSACTION_HASH };
}

#timeout(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
51 changes: 49 additions & 2 deletions packages/transaction-pay-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ import type { Draft } from 'immer';

import type { CONTROLLER_NAME, TransactionPayStrategy } from './constants';

/** Quote data for a fiat payment. */
export type FiatPaymentQuote = {
/** Provider fee in fiat currency (e.g. "2.50"). */
providerFeeFiat: string;
/** Provider fee in USD (e.g. "2.50"). */
providerFeeUsd: string;
/** Total amount in fiat currency (amountFiat + providerFeeFiat). */
totalFiat: string;
/** Total amount in USD. */
totalUsd: string;
/** Estimated duration in seconds. */
estimatedDurationSeconds: number;
};

/** Data for a fiat payment selection. */
export type FiatPaymentData = {
/** Provider ID (PoC: 'transak'). */
providerId: string;
/** Provider method to use for payment. (PoC: 'credit_debit_card' | 'apple_pay') */
method: string;
/** Provider method name to use for payment. (PoC: 'Credit Debit Card' | 'Apple Pay') */
methodName: string;
/** User-entered amount in fiat (e.g. "50"). */
amountFiat?: string;
/** Fiat currency (PoC: 'usd'). */
fiatCurrency?: 'usd';
};

export type AllowedActions =
| AccountTrackerControllerGetStateAction
| BridgeControllerActions
Expand Down Expand Up @@ -76,7 +104,10 @@ export type TransactionPayControllerGetDelegationTransactionAction = {
/** Action to get the pay strategy type used for a transaction. */
export type TransactionPayControllerGetStrategyAction = {
type: `${typeof CONTROLLER_NAME}:getStrategy`;
handler: (transaction: TransactionMeta) => TransactionPayStrategy;
handler: (
transaction: TransactionMeta,
transactionData?: TransactionData,
) => TransactionPayStrategy;
};

/** Action to update the payment token for a transaction. */
Expand All @@ -91,6 +122,15 @@ export type TransactionPayControllerSetIsMaxAmountAction = {
handler: (transactionId: string, isMaxAmount: boolean) => void;
};

/** Action to set the fiat payment data for a transaction. */
export type TransactionPayControllerSetFiatPaymentAction = {
type: `${typeof CONTROLLER_NAME}:setFiatPayment`;
handler: (
transactionId: string,
fiatPayment: FiatPaymentData | undefined,
) => void;
};

export type TransactionPayControllerStateChangeEvent =
ControllerStateChangeEvent<
typeof CONTROLLER_NAME,
Expand All @@ -101,6 +141,7 @@ export type TransactionPayControllerActions =
| TransactionPayControllerGetDelegationTransactionAction
| TransactionPayControllerGetStateAction
| TransactionPayControllerGetStrategyAction
| TransactionPayControllerSetFiatPaymentAction
| TransactionPayControllerSetIsMaxAmountAction
| TransactionPayControllerUpdatePaymentTokenAction;

Expand All @@ -119,7 +160,10 @@ export type TransactionPayControllerOptions = {
getDelegationTransaction: GetDelegationTransactionCallback;

/** Callback to select the PayStrategy for a transaction. */
getStrategy?: (transaction: TransactionMeta) => TransactionPayStrategy;
getStrategy?: (
transaction: TransactionMeta,
transactionData?: TransactionData,
) => TransactionPayStrategy;

/** Controller messenger. */
messenger: TransactionPayControllerMessenger;
Expand Down Expand Up @@ -159,6 +203,9 @@ export type TransactionData = {

/** Calculated totals for the transaction. */
totals?: TransactionPayTotals;

/** Fiat payment data if user selected fiat payment method. */
fiatPayment?: FiatPaymentData;
};

/** A token required by a transaction. */
Expand Down
61 changes: 46 additions & 15 deletions packages/transaction-pay-controller/src/utils/quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,24 @@ export async function updateQuotes(
return false;
}

const isFiatSelected = Boolean(transactionData.fiatPayment);

log('Updating quotes', { transactionId });

const { isMaxAmount, paymentToken, sourceAmounts, tokens } = transactionData;

const requests = buildQuoteRequests({
from: transaction.txParams.from as Hex,
isMaxAmount: isMaxAmount ?? false,
paymentToken,
sourceAmounts,
tokens,
transactionId,
});
const requests = isFiatSelected
? buildFiatQuoteRequests({
tokens,
})
: buildQuoteRequests({
from: transaction.txParams.from as Hex,
isMaxAmount: isMaxAmount ?? false,
paymentToken,
sourceAmounts,
tokens,
transactionId,
});

updateTransactionData(transactionId, (data) => {
data.isLoading = true;
Expand All @@ -77,13 +83,15 @@ export async function updateQuotes(
messenger,
);

const totals = calculateTotals({
isMaxAmount,
messenger,
quotes: quotes as TransactionPayQuote<unknown>[],
tokens,
transaction,
});
const totals = isFiatSelected
? (undefined as unknown as TransactionPayTotals)
: calculateTotals({
isMaxAmount,
messenger,
quotes: quotes as TransactionPayQuote<unknown>[],
tokens,
transaction,
});

log('Calculated totals', { transactionId, totals });

Expand Down Expand Up @@ -263,6 +271,29 @@ function buildQuoteRequests({
return requests;
}

function buildFiatQuoteRequests({
tokens,
}: {
tokens: TransactionPayRequiredToken[];
// TODO: fix type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): any[] {
const primary = tokens[0];
if (!primary) {
return [];
}

return [
{
sourceChainId: primary.chainId,
sourceTokenAddress: primary.address,
targetAmountMinimum: primary.allowUnderMinimum ? '0' : primary.amountRaw,
targetChainId: primary.chainId,
targetTokenAddress: primary.address,
},
];
}

/**
* Retrieve quotes for a transaction.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function updateSourceAmounts(
transactionData: TransactionData | undefined,
messenger: TransactionPayControllerMessenger,
): void {
if (!transactionData) {
if (!transactionData || transactionData.fiatPayment) {
return;
}

Expand Down
Loading
Loading