diff --git a/apps/frontend/src/components/menus/SettingsMenu/index.tsx b/apps/frontend/src/components/menus/SettingsMenu/index.tsx index 294f03719..7eb07b023 100644 --- a/apps/frontend/src/components/menus/SettingsMenu/index.tsx +++ b/apps/frontend/src/components/menus/SettingsMenu/index.tsx @@ -75,13 +75,13 @@ export const SettingsMenu = () => {
{isAuthenticated && userEmail && ( <> -
-
+
+
- {userEmail} + {userEmail}
-
+
)} diff --git a/apps/frontend/src/hooks/quote/schema.ts b/apps/frontend/src/hooks/quote/schema.ts index 7bd3d8cd6..f7f6a199d 100644 --- a/apps/frontend/src/hooks/quote/schema.ts +++ b/apps/frontend/src/hooks/quote/schema.ts @@ -1,4 +1,4 @@ -import { FiatToken, OnChainToken, RampDirection } from "@vortexfi/shared"; +import { FiatToken, OnChainToken, OnChainTokenSymbol, RampDirection } from "@vortexfi/shared"; import { useTranslation } from "react-i18next"; import * as Yup from "yup"; import { useRampDirection } from "../../stores/rampDirectionStore"; @@ -6,7 +6,7 @@ import { useRampDirection } from "../../stores/rampDirectionStore"; export type QuoteFormValues = { inputAmount: string; outputAmount?: string; - onChainToken: OnChainToken; + onChainToken: OnChainTokenSymbol; fiatToken: FiatToken; slippage?: number; deadline?: number; diff --git a/apps/frontend/src/hooks/quote/useQuoteService.ts b/apps/frontend/src/hooks/quote/useQuoteService.ts index 5f71280d8..f82e5f799 100644 --- a/apps/frontend/src/hooks/quote/useQuoteService.ts +++ b/apps/frontend/src/hooks/quote/useQuoteService.ts @@ -1,4 +1,4 @@ -import { FiatToken, OnChainToken } from "@vortexfi/shared"; +import { FiatToken, OnChainTokenSymbol } from "@vortexfi/shared"; import Big from "big.js"; import { useCallback, useEffect } from "react"; import { useEventsContext } from "../../contexts/events"; @@ -13,7 +13,7 @@ import { useRampDirection } from "../../stores/rampDirectionStore"; // if you don't want to get a new quote - you get outputAmount through useQuoteStore // This is not optimal, and introduce too much cognitive load -export const useQuoteService = (inputAmount: string | undefined, onChainToken: OnChainToken, fiatToken: FiatToken) => { +export const useQuoteService = (inputAmount: string | undefined, onChainToken: OnChainTokenSymbol, fiatToken: FiatToken) => { const { trackEvent } = useEventsContext(); const { selectedNetwork } = useNetwork(); const rampType = useRampDirection(); diff --git a/apps/frontend/src/hooks/useRampUrlParams.ts b/apps/frontend/src/hooks/useRampUrlParams.ts index 8cd96bb1b..a2993d04a 100644 --- a/apps/frontend/src/hooks/useRampUrlParams.ts +++ b/apps/frontend/src/hooks/useRampUrlParams.ts @@ -2,16 +2,22 @@ import { AssetHubToken, DestinationType, EPaymentMethod, + type EvmNetworks, EvmToken, FiatToken, + getEvmTokenConfig, + getEvmTokensLoadedSnapshot, + isNetworkEVM, Networks, OnChainToken, + OnChainTokenSymbol, PaymentMethod, QuoteResponse, - RampDirection + RampDirection, + subscribeEvmTokensLoaded } from "@vortexfi/shared"; import Big from "big.js"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react"; import { getFirstEnabledFiatToken, isFiatTokenEnabled } from "../config/tokenAvailability"; import { useNetwork } from "../contexts/network"; import { useRampActor } from "../contexts/rampState"; @@ -33,7 +39,7 @@ interface RampUrlParams { moneriumCode?: string; fiat?: FiatToken; countryCode?: string; - cryptoLocked?: OnChainToken; + cryptoLocked?: OnChainTokenSymbol; paymentMethod?: PaymentMethod; walletLocked?: string; callbackUrl?: string; @@ -58,7 +64,7 @@ function findFiatToken(fiatToken?: string): FiatToken | undefined { return foundToken; } -function findOnChainToken(tokenStr?: string, networkType?: Networks | string): OnChainToken | undefined { +function findOnChainToken(tokenStr?: string, networkType?: Networks | string): OnChainTokenSymbol | undefined { if (!tokenStr || !networkType) { return undefined; } @@ -76,15 +82,15 @@ function findOnChainToken(tokenStr?: string, networkType?: Networks | string): O const [_, tokenValue] = matchedToken; return tokenValue as unknown as OnChainToken; } else { - const evmTokenEntries = Object.entries(EvmToken); - const matchedToken = evmTokenEntries.find(([_, token]) => token.toUpperCase() === tokenStr); - - if (!matchedToken) { - return EvmToken.USDC; + if (isNetworkEVM(networkType as Networks)) { + const dynamicConfig = getEvmTokenConfig(); + const networkTokens = dynamicConfig[networkType as EvmNetworks]; + if (networkTokens && tokenStr in networkTokens) { + return tokenStr; + } } - const [_, tokenValue] = matchedToken; - return tokenValue as OnChainToken; + return EvmToken.USDC; } } @@ -108,7 +114,7 @@ const mapFiatToDestination = (fiatToken: FiatToken): DestinationType => { interface QuoteParams { inputAmount?: Big; - onChainToken: OnChainToken; + onChainToken: OnChainTokenSymbol; fiatToken: FiatToken; selectedNetwork: DestinationType; rampType: RampDirection; @@ -119,8 +125,8 @@ interface QuotePayload { fromDestination: DestinationType; toDestination: DestinationType; inputAmount: string; - inputCurrency: OnChainToken | FiatToken; - outputCurrency: OnChainToken | FiatToken; + inputCurrency: OnChainTokenSymbol | FiatToken; + outputCurrency: OnChainTokenSymbol | FiatToken; } const createQuotePayload = (params: QuoteParams): QuotePayload => { @@ -171,22 +177,25 @@ export const useRampUrlParams = (): RampUrlParams => { const params = useMemo(() => new URLSearchParams(window.location.search), []); const { selectedNetwork } = useNetwork(); const rampDirectionStore = useRampDirection(); + const evmTokensLoaded = useSyncExternalStore(subscribeEvmTokensLoaded, getEvmTokensLoadedSnapshot); const urlParams = useMemo(() => { const rampDirectionParam = params.get(RampUrlParamsKeys.RAMP_TYPE)?.toUpperCase(); + const fiatParam = params.get(RampUrlParamsKeys.FIAT)?.toUpperCase(); + const cryptoLockedParam = params.get(RampUrlParamsKeys.CRYPTO_LOCKED)?.toUpperCase(); + const countryCodeParam = params.get(RampUrlParamsKeys.COUNTRY_CODE)?.toUpperCase(); + + const moneriumCode = params.get(RampUrlParamsKeys.MONERIUM_CODE)?.toLowerCase(); const networkParam = params.get(RampUrlParamsKeys.NETWORK)?.toLowerCase(); + const providedQuoteId = params.get(RampUrlParamsKeys.PROVIDED_QUOTE_ID)?.toLowerCase(); + const paymentMethodParam = params.get(RampUrlParamsKeys.PAYMENT_METHOD)?.toLowerCase() as PaymentMethod | undefined; + const inputAmountParam = params.get(RampUrlParamsKeys.INPUT_AMOUNT); const partnerIdParam = params.get(RampUrlParamsKeys.PARTNER_ID); const apiKeyParam = params.get(RampUrlParamsKeys.API_KEY); - const moneriumCode = params.get(RampUrlParamsKeys.MONERIUM_CODE)?.toLowerCase(); - const providedQuoteId = params.get(RampUrlParamsKeys.PROVIDED_QUOTE_ID)?.toLowerCase(); - const fiatParam = params.get(RampUrlParamsKeys.FIAT)?.toUpperCase(); - const cryptoLockedParam = params.get(RampUrlParamsKeys.CRYPTO_LOCKED)?.toUpperCase(); - const paymentMethodParam = params.get(RampUrlParamsKeys.PAYMENT_METHOD) as PaymentMethod | undefined; const walletLockedParam = params.get(RampUrlParamsKeys.WALLET_LOCKED); const callbackUrlParam = params.get(RampUrlParamsKeys.CALLBACK_URL); const externalSessionIdParam = params.get(RampUrlParamsKeys.EXTERNAL_SESSION_ID); - const countryCodeParam = params.get(RampUrlParamsKeys.COUNTRY_CODE)?.toUpperCase(); const rampDirection = rampDirectionParam === RampDirection.BUY || rampDirectionParam === RampDirection.SELL @@ -202,6 +211,7 @@ export const useRampUrlParams = (): RampUrlParams => { callbackUrl: callbackUrlParam || undefined, countryCode: countryCodeParam || undefined, cryptoLocked, + evmTokensLoaded, externalSessionId: externalSessionIdParam || undefined, fiat, inputAmount: inputAmountParam || undefined, @@ -213,7 +223,8 @@ export const useRampUrlParams = (): RampUrlParams => { rampDirection, walletLocked: walletLockedParam || undefined }; - }, [params, rampDirectionStore, selectedNetwork]); + // evmTokensLoaded: triggers re-evaluation of cryptoLocked when dynamic tokens (e.g. WETH, WBTC) finish loading from SquidRouter + }, [params, rampDirectionStore, selectedNetwork, evmTokensLoaded]); return urlParams; }; @@ -411,4 +422,10 @@ export const useSetRampUrlParams = () => { handleFiatToken, moneriumCode ]); + + useEffect(() => { + if (cryptoLocked) { + setOnChainToken(cryptoLocked); + } + }, [cryptoLocked, setOnChainToken]); }; diff --git a/apps/frontend/src/sections/business/WhyVortexWidget/index.tsx b/apps/frontend/src/sections/business/WhyVortexWidget/index.tsx index 06afadc6f..4e5d5449d 100644 --- a/apps/frontend/src/sections/business/WhyVortexWidget/index.tsx +++ b/apps/frontend/src/sections/business/WhyVortexWidget/index.tsx @@ -105,12 +105,14 @@ export const WhyVortexWidget = () => {
-
- {t("pages.business.hero.comingSoon")} -
- +
diff --git a/apps/frontend/src/services/api/quote.service.ts b/apps/frontend/src/services/api/quote.service.ts index 4d6e4faf5..56540f595 100644 --- a/apps/frontend/src/services/api/quote.service.ts +++ b/apps/frontend/src/services/api/quote.service.ts @@ -3,9 +3,10 @@ import { DestinationType, FiatToken, getNetworkFromDestination, - OnChainToken, + OnChainTokenSymbol, PaymentMethod, QuoteResponse, + RampCurrency, RampDirection } from "@vortexfi/shared"; import { apiRequest } from "./api-client"; @@ -35,8 +36,8 @@ export class QuoteService { from: DestinationType, to: DestinationType, inputAmount: string, - inputCurrency: OnChainToken | FiatToken, - outputCurrency: OnChainToken | FiatToken, + inputCurrency: OnChainTokenSymbol | FiatToken, + outputCurrency: OnChainTokenSymbol | FiatToken, apiKey?: string, partnerId?: string, paymentMethod?: PaymentMethod, @@ -52,9 +53,9 @@ export class QuoteService { countryCode, from, inputAmount, - inputCurrency, + inputCurrency: inputCurrency as RampCurrency, network, - outputCurrency, + outputCurrency: outputCurrency as RampCurrency, paymentMethod, rampType, to diff --git a/apps/frontend/src/services/signingService.tsx b/apps/frontend/src/services/signingService.tsx index 17aba3b78..ee5975636 100644 --- a/apps/frontend/src/services/signingService.tsx +++ b/apps/frontend/src/services/signingService.tsx @@ -20,9 +20,6 @@ interface SignerServiceSep10Response { masterClientPublic: string; } -type BrlaOfframpState = "BURN" | "MONEY-TRANSFER"; -type OfframpStatus = "QUEUED" | "POSTED" | "SUCCESS" | "FAILED"; - export enum KycStatus { PENDING = "PENDING", REJECTED = "REJECTED", @@ -31,11 +28,6 @@ export enum KycStatus { export type KycStatusType = keyof typeof KycStatus; -interface BrlaOfframpStatus { - type: BrlaOfframpState; - status: OfframpStatus; -} - type TaxIdType = "CPF" | "CNPJ"; export interface RegisterSubaccountPayload { diff --git a/apps/frontend/src/stores/quote/useQuoteFormStore.ts b/apps/frontend/src/stores/quote/useQuoteFormStore.ts index 6df6457f0..607a8774f 100644 --- a/apps/frontend/src/stores/quote/useQuoteFormStore.ts +++ b/apps/frontend/src/stores/quote/useQuoteFormStore.ts @@ -5,6 +5,7 @@ import { getOnChainTokenDetails, Networks, OnChainToken, + OnChainTokenSymbol, RampDirection } from "@vortexfi/shared"; import { create } from "zustand"; @@ -42,7 +43,7 @@ const defaultOnChainToken = interface RampFormState { inputAmount: string; - onChainToken: OnChainToken; + onChainToken: OnChainTokenSymbol; fiatToken: FiatToken; lastConstraintDirection: RampDirection; taxId?: string; @@ -52,7 +53,7 @@ interface RampFormState { interface RampFormActions { actions: { setInputAmount: (amount?: string) => void; - setOnChainToken: (token: OnChainToken) => void; + setOnChainToken: (token: OnChainTokenSymbol) => void; setFiatToken: (token: FiatToken) => void; setConstraintDirection: (direction: RampDirection) => void; handleNetworkChange: (network: Networks) => void; @@ -93,7 +94,7 @@ export const useQuoteFormStore = create()( setConstraintDirection: (direction: RampDirection) => set({ lastConstraintDirection: direction }), setFiatToken: (token: FiatToken) => set({ fiatToken: token }), setInputAmount: (amount?: string) => set({ inputAmount: amount }), - setOnChainToken: (token: OnChainToken) => set({ onChainToken: token }), + setOnChainToken: (token: OnChainTokenSymbol) => set({ onChainToken: token }), setPixId: (pixId: string) => set({ pixId }), setTaxId: (taxId: string) => set({ taxId }) } diff --git a/apps/frontend/src/stores/quote/useQuoteStore.ts b/apps/frontend/src/stores/quote/useQuoteStore.ts index 9cf217466..24f25958f 100644 --- a/apps/frontend/src/stores/quote/useQuoteStore.ts +++ b/apps/frontend/src/stores/quote/useQuoteStore.ts @@ -3,6 +3,7 @@ import { EPaymentMethod, FiatToken, OnChainToken, + OnChainTokenSymbol, QuoteError, QuoteResponse, RampDirection @@ -14,7 +15,7 @@ import { QuoteService } from "../../services/api"; interface QuoteParams { inputAmount?: Big; - onChainToken: OnChainToken; + onChainToken: OnChainTokenSymbol; fiatToken: FiatToken; selectedNetwork: DestinationType; rampType: RampDirection; @@ -27,8 +28,8 @@ interface QuotePayload { fromDestination: DestinationType; toDestination: DestinationType; inputAmount: string; - inputCurrency: OnChainToken | FiatToken; - outputCurrency: OnChainToken | FiatToken; + inputCurrency: OnChainTokenSymbol | FiatToken; + outputCurrency: OnChainTokenSymbol | FiatToken; } interface QuoteActions { diff --git a/apps/frontend/src/types/phases.ts b/apps/frontend/src/types/phases.ts index ab8a3d221..0a0535f62 100644 --- a/apps/frontend/src/types/phases.ts +++ b/apps/frontend/src/types/phases.ts @@ -2,7 +2,7 @@ import { EphemeralAccount, FiatToken, Networks, - OnChainToken, + OnChainTokenSymbol, PaymentData, PresignedTx, QuoteResponse, @@ -24,7 +24,7 @@ export interface RampState { export interface RampExecutionInput { quote: QuoteResponse; - onChainToken: OnChainToken; + onChainToken: OnChainTokenSymbol; fiatToken: FiatToken; sourceOrDestinationAddress: string; // The source address for offramps, destination address for onramps moneriumWalletAddress?: string; // Only needed for Monerium offramps to non-EVM chains (e.g. Monerium -> Assethub) diff --git a/apps/frontend/src/types/searchParams.ts b/apps/frontend/src/types/searchParams.ts index 15fc59cb5..9f8a2cae7 100644 --- a/apps/frontend/src/types/searchParams.ts +++ b/apps/frontend/src/types/searchParams.ts @@ -1,4 +1,3 @@ -import { EPaymentMethod, RampDirection } from "@vortexfi/shared"; import { z } from "zod"; /** @@ -22,9 +21,9 @@ export const rampSearchSchema = z.object({ inputAmount: stringOrNumberParam, network: z.string().optional(), partnerId: z.string().optional(), - paymentMethod: z.nativeEnum(EPaymentMethod).optional().catch(undefined), + paymentMethod: z.string().optional(), quoteId: z.string().optional(), - rampType: z.nativeEnum(RampDirection).optional().catch(undefined), + rampType: z.string().optional(), walletAddressLocked: z.string().optional() }); diff --git a/packages/shared/src/tokens/evm/dynamicEvmTokens.ts b/packages/shared/src/tokens/evm/dynamicEvmTokens.ts index d2a6640e3..a220c7401 100644 --- a/packages/shared/src/tokens/evm/dynamicEvmTokens.ts +++ b/packages/shared/src/tokens/evm/dynamicEvmTokens.ts @@ -45,6 +45,19 @@ const state: DynamicEvmTokensState = { tokensByNetwork: {} as Record>> }; +const evmTokenListeners = new Set<() => void>(); + +export function subscribeEvmTokensLoaded(onStoreChange: () => void): () => void { + evmTokenListeners.add(onStoreChange); + return () => { + evmTokenListeners.delete(onStoreChange); + }; +} + +export function getEvmTokensLoadedSnapshot(): boolean { + return state.isLoaded; +} + /** * Iterates over all EVM networks and calls the callback for each. */ @@ -283,13 +296,26 @@ export async function initializeEvmTokens(): Promise { state.tokensByNetwork = mergeWithStaticConfig(groupedTokens); state.priceBySymbol = buildPriceLookup(state.tokensByNetwork); state.isLoaded = true; + for (const listener of evmTokenListeners) { + try { + listener(); + } catch (listenerErr) { + console.error("[DynamicEvmTokens] Error in EVM token listener", listenerErr); + } + } } catch (err) { console.error("[DynamicEvmTokens] Failed to fetch tokens from SquidRouter, using fallback:", err); - state.tokensByNetwork = buildFallbackFromStaticConfig(); state.priceBySymbol = buildPriceLookup(state.tokensByNetwork); state.isLoaded = true; } + for (const listener of evmTokenListeners) { + try { + listener(); + } catch (listenerErr) { + console.error("[DynamicEvmTokens] Error in EVM token listener", listenerErr); + } + } } /** diff --git a/packages/shared/src/tokens/types/base.ts b/packages/shared/src/tokens/types/base.ts index 6d2c0b1fc..19250c380 100644 --- a/packages/shared/src/tokens/types/base.ts +++ b/packages/shared/src/tokens/types/base.ts @@ -20,6 +20,8 @@ export enum AssetHubToken { } export type OnChainToken = EvmToken | AssetHubToken; +/** Includes dynamic tokens (e.g. WETH, WBTC) loaded at runtime from SquidRouter */ +export type OnChainTokenSymbol = OnChainToken | (string & {}); export type NablaToken = OnChainToken; // Combines fiat currencies with tokens in one type diff --git a/packages/shared/src/tokens/utils/helpers.ts b/packages/shared/src/tokens/utils/helpers.ts index 6fd5d4a0c..b6fa5bac4 100644 --- a/packages/shared/src/tokens/utils/helpers.ts +++ b/packages/shared/src/tokens/utils/helpers.ts @@ -9,7 +9,7 @@ import { evmTokenConfig } from "../evm/config"; import { getEvmTokenConfig } from "../evm/dynamicEvmTokens"; import { moonbeamTokenConfig } from "../moonbeam/config"; import { stellarTokenConfig } from "../stellar/config"; -import { AssetHubToken, FiatToken, OnChainToken, RampCurrency } from "../types/base"; +import { AssetHubToken, FiatToken, OnChainToken, OnChainTokenSymbol, RampCurrency } from "../types/base"; import { EvmToken, EvmTokenDetails } from "../types/evm"; import { MoonbeamTokenDetails } from "../types/moonbeam"; import { PendulumTokenDetails } from "../types/pendulum"; @@ -22,7 +22,7 @@ import { FiatTokenDetails, OnChainTokenDetails } from "./typeGuards"; */ export function getOnChainTokenDetails( network: Networks, - onChainToken: OnChainToken, + onChainToken: OnChainTokenSymbol, dynamicEvmTokenConfig?: Record>> ): OnChainTokenDetails | undefined { const normalizedOnChainToken = normalizeTokenSymbol(onChainToken); @@ -51,7 +51,7 @@ export function getOnChainTokenDetails( */ export function getOnChainTokenDetailsOrDefault( network: Networks, - onChainToken: OnChainToken, + onChainToken: OnChainTokenSymbol, dynamicEvmTokenConfig?: Record>> ): OnChainTokenDetails { // AXLUSDC doesn't exist Ethereum