Skip to content
Merged
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 @@ -28,12 +28,14 @@ import { MAX_FINAL_SETTLEMENT_SUBSIDY_USD } from "../../../../constants/constant
import QuoteTicket from "../../../../models/quoteTicket.model";
import RampState from "../../../../models/rampState.model";
import { priceFeedService } from "../../priceFeed.service";
import { isBrlToBrlaBaseDirect, isEurToEurcBaseDirect } from "../../quote/utils";
import { isFiatToOwnStablecoinBaseDirect } from "../../quote/utils";
import { BasePhaseHandler } from "../base-phase-handler";
import { getEvmFundingAccount } from "../evm-funding";

const BALANCE_POLLING_TIME_MS = 5000;
const EVM_BALANCE_CHECK_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
// Wait for >=90% of expected bridge delivery to absorb slippage while still waiting for actual bridge arrival.
const MIN_BRIDGE_DELIVERY_RATIO = 0.9;

const NATIVE_TOKENS: Record<EvmNetworks, { symbol: string; decimals: number }> = {
[Networks.Ethereum]: { decimals: 18, symbol: "ETH" },
Expand Down Expand Up @@ -80,10 +82,7 @@ export class FinalSettlementSubsidyHandler extends BasePhaseHandler {
`FinalSettlementSubsidyHandler: Quote found. inputCurrency=${quote.inputCurrency}, outputCurrency=${quote.outputCurrency}, network=${quote.network}`
);

if (
isEurToEurcBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network) ||
isBrlToBrlaBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network)
) {
if (isFiatToOwnStablecoinBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network)) {
logger.info(`FinalSettlementSubsidyHandler: Skipping subsidy for Base direct-transfer route (ramp ${state.id})`);
return this.transitionToNextPhase(state, "destinationTransfer");
}
Expand Down Expand Up @@ -153,7 +152,7 @@ export class FinalSettlementSubsidyHandler extends BasePhaseHandler {
`FinalSettlementSubsidyHandler: Polling ephemeral balance for ${ephemeralAddress} on ${destinationNetwork} (timeout=${EVM_BALANCE_CHECK_TIMEOUT_MS}ms, interval=${BALANCE_POLLING_TIME_MS}ms)`
);
const actualBalance = await checkEvmBalanceForToken({
amountDesiredRaw: expectedAmountRaw.mul(0.9).toFixed(0, 0), // Wait for >=90% of expected bridge delivery to absorb slippage while still waiting for actual bridge arrival.
amountDesiredRaw: expectedAmountRaw.mul(MIN_BRIDGE_DELIVERY_RATIO).toFixed(0, 0),
chain: destinationNetwork,
intervalMs: BALANCE_POLLING_TIME_MS,
ownerAddress: ephemeralAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import RampState from "../../../../models/rampState.model";
import { UnrecoverablePhaseError } from "../../../errors/phase-error";
import { multiplyByPowerOfTen } from "../../pendulum/helpers";
import { fundEphemeralAccount } from "../../pendulum/pendulum.service";
import { isBrlToBrlaBaseDirect, isEurToEurcBaseDirect } from "../../quote/utils";
import { isFiatToOwnStablecoinBaseDirect } from "../../quote/utils";
import { BasePhaseHandler } from "../base-phase-handler";
import { getEvmFundingAccount } from "../evm-funding";
import { verifyUserSubmittedTxByHash } from "../helpers/user-tx-verifier";
Expand Down Expand Up @@ -243,9 +243,7 @@ export class FundEphemeralPhaseHandler extends BasePhaseHandler {
protected nextPhaseSelector(state: RampState, quote: QuoteTicket): RampPhase {
if (
state.state.isDirectTransfer === true ||
(isOnramp(state) &&
(isEurToEurcBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network) ||
isBrlToBrlaBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network)))
(isOnramp(state) && isFiatToOwnStablecoinBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network))
) {
return "destinationTransfer";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { PublicClient } from "viem";
import logger from "../../../../config/logger";
import QuoteTicket from "../../../../models/quoteTicket.model";
import RampState from "../../../../models/rampState.model";
import { isBrlToBrlaBaseDirect, isEurToEurcBaseDirect } from "../../quote/utils";
import { isFiatToOwnStablecoinBaseDirect } from "../../quote/utils";
import { BasePhaseHandler } from "../base-phase-handler";

/**
Expand Down Expand Up @@ -54,10 +54,7 @@ export class SquidRouterPhaseHandler extends BasePhaseHandler {
throw new Error("Quote not found for the given state");
}

if (
isEurToEurcBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network) ||
isBrlToBrlaBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network)
) {
if (isFiatToOwnStablecoinBaseDirect(quote.inputCurrency, quote.outputCurrency, quote.network)) {
logger.info(`SquidRouterPhaseHandler: Skipping squidRouter for Base direct-transfer route (ramp ${state.id})`);
return this.transitionToNextPhase(state, "destinationTransfer");
}
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/api/services/quote/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,12 @@ export function isEurToEurcBaseDirect(inputCurrency: string, outputCurrency: str
export function isBrlToBrlaBaseDirect(inputCurrency: string, outputCurrency: string, toNetwork: string): boolean {
return inputCurrency === FiatToken.BRL && outputCurrency === EvmToken.BRLA && toNetwork === Networks.Base;
}

// Fiat -> own-stablecoin passthrough on Base (EUR->EURC, BRL->BRLA): the anchor already minted the
// requested stablecoin, so the swap/bridge/subsidy steps are skipped and funds transfer directly.
export function isFiatToOwnStablecoinBaseDirect(inputCurrency: string, outputCurrency: string, toNetwork: string): boolean {
return (
isEurToEurcBaseDirect(inputCurrency, outputCurrency, toNetwork) ||
isBrlToBrlaBaseDirect(inputCurrency, outputCurrency, toNetwork)
);
}
2 changes: 1 addition & 1 deletion apps/api/src/api/services/ramp/base.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import QuoteTicket from "../../../models/quoteTicket.model";
import {BaseRampService} from "./base.service";

const transaction = { id: "cleanup-test-transaction" };
const expectedDeleteBatchSize = 500;
const expectedDeleteBatchSize = 5000;

type QuoteCleanupWhere = {
expiresAt?: {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api/services/ramp/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import QuoteTicket from "../../../models/quoteTicket.model";
import RampState, { RampStateAttributes } from "../../../models/rampState.model";
import { StateMetadata } from "../phases/meta-state-types";

const EXPIRED_QUOTE_DELETE_BATCH_SIZE = 1000;
const EXPIRED_QUOTE_DELETE_BATCH_SIZE = 5000;
const QUOTE_CLEANUP_ADVISORY_LOCK_NAMESPACE = 918521;
const QUOTE_CLEANUP_ADVISORY_LOCK_KEY = 1;

Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/database/migrations/031-add-quote-created-at-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { QueryInterface } from "sequelize";

export async function up(queryInterface: QueryInterface): Promise<void> {
await queryInterface.sequelize.query(`
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_quote_tickets_created_at
ON quote_tickets (created_at DESC);
`);
}

export async function down(queryInterface: QueryInterface): Promise<void> {
await queryInterface.sequelize.query(`
DROP INDEX CONCURRENTLY IF EXISTS idx_quote_tickets_created_at;
`);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArrowDownIcon } from "@heroicons/react/20/solid";
import { useQuery } from "@tanstack/react-query";
import { skipToken, useQuery } from "@tanstack/react-query";
import {
BaseFiatTokenDetails,
FiatToken,
Expand Down Expand Up @@ -69,8 +69,7 @@ export const TransactionTokensDisplay: FC<TransactionTokensDisplayProps> = ({ ex
const isEurOfframp = !isOnramp && executionInput.fiatToken === FiatToken.EURC;

const { data: mykoboProfile } = useQuery({
enabled: isEurOfframp && !!userEmail,
queryFn: () => MykoboService.getProfile(userEmail as string),
queryFn: isEurOfframp && userEmail ? () => MykoboService.getProfile(userEmail) : skipToken,
queryKey: ["mykoboProfile", userEmail]
});

Expand Down
20 changes: 18 additions & 2 deletions apps/frontend/src/contexts/rampState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,29 @@ import { RampExecutionInput } from "../types/phases";

const RAMP_STATE_STORAGE_KEY = "rampState";
const RAMP_EPHEMERALS_STORAGE_KEY = "rampEphemerals";
const MAX_RAMP_EPHEMERALS = 50;

type RampEphemeralsMap = Record<string, { substrateEphemeral: EphemeralAccount; evmEphemeral: EphemeralAccount }>;
type RampEphemeralEntry = {
substrateEphemeral: EphemeralAccount;
evmEphemeral: EphemeralAccount;
timestamp?: number;
};
Comment on lines +21 to +25
type RampEphemeralsMap = Record<string, RampEphemeralEntry>;

export function updateRampEphemeral(rampId: string, ephemerals: RampExecutionInput["ephemerals"]): void {
try {
const existing = readRampEphemerals();
existing[rampId] = ephemerals;
existing[rampId] = { ...ephemerals, timestamp: Date.now() };

const keys = Object.keys(existing);
if (keys.length > MAX_RAMP_EPHEMERALS) {
const sorted = keys.sort((a, b) => (existing[a]?.timestamp ?? 0) - (existing[b]?.timestamp ?? 0));
const toRemove = sorted.slice(0, sorted.length - MAX_RAMP_EPHEMERALS);
for (const key of toRemove) {
delete existing[key];
}
}

localStorage.setItem(RAMP_EPHEMERALS_STORAGE_KEY, JSON.stringify(existing));
} catch {
// localStorage may be full or unavailable — non-critical backup
Expand Down
Loading