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
13 changes: 11 additions & 2 deletions app/components/UI/Ramp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ import { NativeRampsSdk } from '@consensys/native-ramps-sdk';
import useDetectGeolocation from './hooks/useDetectGeolocation';
import useHydrateRampsController from './hooks/useHydrateRampsController';
import useRampsSmartRouting from './hooks/useRampsSmartRouting';
import useRampsUnifiedV2Enabled from './hooks/useRampsUnifiedV2Enabled';

const POLLING_FREQUENCY = AppConstants.FIAT_ORDERS.POLLING_FREQUENCY;

export interface ProcessorOptions {
forced?: boolean;
sdk?: NativeRampsSdk;
/** When true, use the unified V2 order processor instead of SDK-specific processors. */
useUnifiedProcessor?: boolean;
}

export async function processFiatOrder(
Expand Down Expand Up @@ -122,6 +125,7 @@ function FiatOrders() {
useFetchRampNetworks();
useDetectGeolocation();
useRampsSmartRouting();
const isUnifiedV2Enabled = useRampsUnifiedV2Enabled();
const dispatch = useDispatch();
const dispatchThunk = useThunkDispatch();
const navigation = useNavigation();
Expand Down Expand Up @@ -171,7 +175,9 @@ function FiatOrders() {
async () => {
await Promise.all(
pendingOrders.map((order) =>
processFiatOrder(order, dispatchUpdateFiatOrder, dispatchThunk),
processFiatOrder(order, dispatchUpdateFiatOrder, dispatchThunk, {
useUnifiedProcessor: isUnifiedV2Enabled,
}),
),
);
},
Expand Down Expand Up @@ -201,7 +207,10 @@ function FiatOrders() {
async () => {
await Promise.all(
forceUpdateOrders.map((order) =>
processFiatOrder(order, dispatchUpdateFiatOrder, dispatchThunk),
processFiatOrder(order, dispatchUpdateFiatOrder, dispatchThunk, {
forced: true,
useUnifiedProcessor: isUnifiedV2Enabled,
}),
),
);
},
Expand Down
7 changes: 7 additions & 0 deletions app/components/UI/Ramp/orderProcessor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FiatOrder } from '../../../../reducers/fiatOrders';
import Logger from '../../../../util/Logger';
import { processAggregatorOrder } from '../Aggregator/orderProcessor/aggregator';
import { processDepositOrder } from '../Deposit/orderProcessor';
import { processUnifiedOrder } from './unifiedOrderProcessor';

function processOrder(
order: FiatOrder,
Expand All @@ -16,9 +17,15 @@ function processOrder(
return order;
}
case FIAT_ORDER_PROVIDERS.AGGREGATOR: {
if (options?.useUnifiedProcessor) {
return processUnifiedOrder(order, options);
}
return processAggregatorOrder(order, options);
}
case FIAT_ORDER_PROVIDERS.DEPOSIT: {
if (options?.useUnifiedProcessor) {
return processUnifiedOrder(order, options);
}
return processDepositOrder(order, options);
}
default: {
Expand Down
220 changes: 220 additions & 0 deletions app/components/UI/Ramp/orderProcessor/unifiedOrderProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import type { RampsOrder } from '@metamask/ramps-controller';

Check failure on line 1 in app/components/UI/Ramp/orderProcessor/unifiedOrderProcessor.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Module '"@metamask/ramps-controller"' has no exported member 'RampsOrder'.
import { FIAT_ORDER_STATES } from '../../../../constants/on-ramp';
import Logger from '../../../../util/Logger';
import { FiatOrder } from '../../../../reducers/fiatOrders';
import AppConstants from '../../../../core/AppConstants';
import Engine from '../../../../core/Engine';
import type { ProcessorOptions } from '../index';

export const POLLING_FREQUENCY = AppConstants.FIAT_ORDERS.POLLING_FREQUENCY;
export const POLLING_FREQUENCY_IN_SECONDS = POLLING_FREQUENCY / 1000;

/**
* Maps a V2 unified order status string to a FiatOrder state.
*/
const orderStatusToFiatOrderState = (status: string): FIAT_ORDER_STATES => {
switch (status) {
case 'COMPLETED':
return FIAT_ORDER_STATES.COMPLETED;
case 'FAILED':
return FIAT_ORDER_STATES.FAILED;
case 'CANCELLED':
return FIAT_ORDER_STATES.CANCELLED;
case 'CREATED':
return FIAT_ORDER_STATES.CREATED;
case 'PENDING':
case 'UNKNOWN':
default:
return FIAT_ORDER_STATES.PENDING;
}
};

/**
* Extracts the crypto currency symbol from a RampsOrder.
* The V2 API may return cryptoCurrency as a string or an object.
*/
function getCryptoSymbol(cryptoCurrency: RampsOrder['cryptoCurrency']): string {
if (!cryptoCurrency) {
return '';
}
if (typeof cryptoCurrency === 'string') {
return cryptoCurrency;
}
return cryptoCurrency.symbol || '';
}

/**
* Extracts the network chainId from a RampsOrder.
* The V2 API may return network as a string or an object with chainId.
*/
function getNetworkChainId(network: RampsOrder['network']): string {
if (!network) {
return '';
}
if (typeof network === 'string') {
return network;
}
return network.chainId || '';
}

/**
* Extracts the provider code and order code from a FiatOrder.
*
* Deposit orders have IDs in the format "/providers/{providerCode}/orders/{orderCode}".
* Aggregator orders store the provider in order.data.provider (as an object or string).
*/
function extractProviderAndOrderCode(order: FiatOrder): {
providerCode: string;
orderCode: string;
} {
// Deposit-style IDs: /providers/transak-native/orders/abc123
if (order.id.startsWith('/providers/')) {
const parts = order.id.split('/');
// parts = ['', 'providers', 'transak-native', 'orders', 'abc123']
const providerCode = parts[2] || '';
const orderCode = parts[4] || '';
return { providerCode, orderCode };
}

// Aggregator orders: provider is in the data
const data = order.data as Record<string, unknown> | undefined;
let providerCode = '';
if (data?.provider) {
if (typeof data.provider === 'string') {
providerCode = data.provider;
} else if (
typeof data.provider === 'object' &&
data.provider !== null &&
'id' in data.provider
) {
providerCode = String(
(data.provider as Record<string, unknown>).id || '',
);
}
}

// Extract just the provider code from paths like "/providers/moonpay"
if (providerCode.startsWith('/providers/')) {
providerCode = providerCode.split('/')[2] || providerCode;
}

return { providerCode, orderCode: order.id };
}

/**
* Converts a V2 RampsOrder to a FiatOrder for Redux storage.
* Uses the original order's provider type to maintain routing consistency.
*/
function rampsOrderToFiatOrder(
rampsOrder: RampsOrder,
originalOrder: FiatOrder,
): FiatOrder {
return {
id: originalOrder.id,
provider: originalOrder.provider,
createdAt: rampsOrder.createdAt,
amount: rampsOrder.fiatAmount,
fee: rampsOrder.totalFeesFiat,
cryptoAmount: rampsOrder.cryptoAmount || 0,
cryptoFee: rampsOrder.totalFeesFiat || 0,
currency: rampsOrder.fiatCurrency || originalOrder.currency,
currencySymbol: originalOrder.currencySymbol || '',
cryptocurrency: getCryptoSymbol(rampsOrder.cryptoCurrency),
network: getNetworkChainId(rampsOrder.network) || originalOrder.network,
state: orderStatusToFiatOrderState(rampsOrder.status),
account: rampsOrder.walletAddress || originalOrder.account,
txHash: rampsOrder.txHash,
excludeFromPurchases: rampsOrder.excludeFromPurchases,
orderType: rampsOrder.orderType as FiatOrder['orderType'],
errorCount: 0,
lastTimeFetched: Date.now(),
data: rampsOrder as unknown as FiatOrder['data'],
};
}

/**
* Unified order processor that uses the V2 API via RampsController.getOrder.
* Replaces both processAggregatorOrder and processDepositOrder.
*
* Includes exponential backoff on errors and pollingSecondsMinimum support.
*/
export async function processUnifiedOrder(
order: FiatOrder,
options?: ProcessorOptions,
): Promise<FiatOrder> {
const now = Date.now();

// Exponential backoff on errors
if (
options?.forced !== true &&
order.errorCount &&
order.lastTimeFetched &&
order.errorCount > 0 &&
order.lastTimeFetched +
Math.pow(POLLING_FREQUENCY_IN_SECONDS, order.errorCount + 1) * 1000 >
now
) {
return order;
}

// Respect pollingSecondsMinimum from order data
const pollingMinimum = (order.data as Record<string, unknown>)
?.pollingSecondsMinimum;
if (
options?.forced !== true &&
typeof pollingMinimum === 'number' &&
pollingMinimum > 0 &&
order.lastTimeFetched &&
order.lastTimeFetched + pollingMinimum * 1000 > now
) {
return order;
}

try {
const { providerCode, orderCode } = extractProviderAndOrderCode(order);

if (!providerCode || !orderCode) {
throw new Error(
`Cannot extract provider/order code from order ${order.id}`,
);
}

const updatedOrder = await Engine.context.RampsController.getOrder(

Check failure on line 182 in app/components/UI/Ramp/orderProcessor/unifiedOrderProcessor.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Property 'getOrder' does not exist on type 'RampsController'.
providerCode,
orderCode,
order.account,
);

if (!updatedOrder) {
throw new Error('Order not found');
}

// Handle unknown status: increment error count
if (options?.forced !== true && updatedOrder.status === 'UNKNOWN') {
return {
...order,
lastTimeFetched: Date.now(),
errorCount: (order.errorCount || 0) + 1,
};
}

const transformedOrder = rampsOrderToFiatOrder(updatedOrder, order);

return {
...order,
...transformedOrder,
id: order.id,
network: transformedOrder.network || order.network,
account: order.account || transformedOrder.account,
lastTimeFetched: now,
errorCount: 0,
forceUpdate: false,
};
} catch (error) {
Logger.error(error as Error, {
message: 'FiatOrders::UnifiedProcessor error while processing order',
orderId: order.id,
});
return order;
}
}
Loading