diff --git a/apps/api/src/api/controllers/quote.controller.ts b/apps/api/src/api/controllers/quote.controller.ts index f14377f57..10cb4d2f9 100644 --- a/apps/api/src/api/controllers/quote.controller.ts +++ b/apps/api/src/api/controllers/quote.controller.ts @@ -53,7 +53,7 @@ export const createQuote = async ( res.status(httpStatus.CREATED).json(quote); } catch (error) { - logger.error("Error creating quote:", error); + logger.error(`Error creating quote: ${error instanceof Error ? error.message : String(error)}`); next(error); } }; diff --git a/apps/api/src/api/controllers/ramp.controller.ts b/apps/api/src/api/controllers/ramp.controller.ts index 737d2ed57..01fe74d18 100644 --- a/apps/api/src/api/controllers/ramp.controller.ts +++ b/apps/api/src/api/controllers/ramp.controller.ts @@ -52,13 +52,12 @@ export const registerRamp = async (req: Request, res: Response, nex * @public */ export const updateRamp = async ( - req: Request<{ rampId: string }, unknown, Omit>, + req: Request, res: Response, next: NextFunction ): Promise => { try { - const { rampId } = req.params; - const { presignedTxs, additionalData } = req.body; + const { rampId, presignedTxs, additionalData } = req.body; // Validate required fields if (!rampId || !presignedTxs) { diff --git a/apps/api/src/api/routes/v1/ramp.route.ts b/apps/api/src/api/routes/v1/ramp.route.ts index 2a5b1b4ff..fba50b422 100644 --- a/apps/api/src/api/routes/v1/ramp.route.ts +++ b/apps/api/src/api/routes/v1/ramp.route.ts @@ -86,7 +86,7 @@ router.get("/quotes/:id", quoteController.getQuote); router.post("/register", rampController.registerRamp); /** - * @api {post} v1/ramp/:rampId/update Update ramping process + * @api {post} v1/ramp/update Update ramping process * @apiDescription Update a ramping process with presigned transactions and additional data * @apiVersion 1.0.0 * @apiName UpdateRamp @@ -110,7 +110,7 @@ router.post("/register", rampController.registerRamp); * @apiError (Not Found 404) NotFound Ramp does not exist * @apiError (Conflict 409) ConflictError Ramp is not in a state that allows updates */ -router.post("/:rampId/update", rampController.updateRamp); +router.post("/update", rampController.updateRamp); /** * @api {post} v1/ramp/start Start ramping process diff --git a/apps/api/src/api/services/brla/brlaTeleportService.ts b/apps/api/src/api/services/brla/brlaTeleportService.ts index 23a8955bb..032219760 100644 --- a/apps/api/src/api/services/brla/brlaTeleportService.ts +++ b/apps/api/src/api/services/brla/brlaTeleportService.ts @@ -84,7 +84,7 @@ export class BrlaTeleportService { status: "claimed", subaccountId }; - logger.info(`Requesting teleport ${compositeKey}: ${teleport}`); + logger.info(`Requesting teleport ${compositeKey}: ${JSON.stringify(teleport)}`); this.teleports.set(compositeKey, teleport); this.maybeStartPeriodicChecks(); } diff --git a/apps/api/src/api/services/nablaReads/outAmount.ts b/apps/api/src/api/services/nablaReads/outAmount.ts index 0533ff4a7..6e67eb87c 100644 --- a/apps/api/src/api/services/nablaReads/outAmount.ts +++ b/apps/api/src/api/services/nablaReads/outAmount.ts @@ -1,4 +1,4 @@ -import { NABLA_ROUTER, PendulumDetails } from "@packages/shared"; +import { NABLA_ROUTER, PendulumTokenDetails } from "@packages/shared"; import { ApiPromise } from "@polkadot/api"; import Big from "big.js"; import BigNumber from "big.js"; @@ -21,11 +21,11 @@ export interface TokenOutData { export async function getTokenOutAmount(params: { api: ApiPromise; fromAmountString: string; - inputTokenDetails: PendulumDetails; - outputTokenDetails: PendulumDetails; + inputTokenPendulumDetails: PendulumTokenDetails; + outputTokenPendulumDetails: PendulumTokenDetails; maximumFromAmount?: BigNumber; }): Promise { - const { api, fromAmountString, inputTokenDetails, outputTokenDetails, maximumFromAmount } = params; + const { api, fromAmountString, inputTokenPendulumDetails, outputTokenPendulumDetails, maximumFromAmount } = params; let amountBig: Big; try { @@ -34,7 +34,7 @@ export async function getTokenOutAmount(params: { throw new Error("Invalid amount string provided"); } - const fromTokenDecimals = inputTokenDetails.pendulumDecimals; + const fromTokenDecimals = inputTokenPendulumDetails.decimals; if (fromTokenDecimals === undefined) { throw new Error("Input token decimals not defined"); } @@ -48,7 +48,7 @@ export async function getTokenOutAmount(params: { abi: routerAbi, address: NABLA_ROUTER, api, - args: [amountIn, [inputTokenDetails.pendulumErc20WrapperAddress, outputTokenDetails.pendulumErc20WrapperAddress]], + args: [amountIn, [inputTokenPendulumDetails.erc20WrapperAddress, outputTokenPendulumDetails.erc20WrapperAddress]], method: "getAmountOut", noWalletAddressRequired: true, parseError: error => { @@ -66,8 +66,8 @@ export async function getTokenOutAmount(params: { } }, parseSuccessOutput: (data: bigint[]) => { - const preciseQuotedAmountOut = parseContractBalanceResponse(outputTokenDetails.pendulumDecimals, data[0]); - const swapFee = parseContractBalanceResponse(outputTokenDetails.pendulumDecimals, data[1]); + const preciseQuotedAmountOut = parseContractBalanceResponse(outputTokenPendulumDetails.decimals, data[0]); + const swapFee = parseContractBalanceResponse(outputTokenPendulumDetails.decimals, data[1]); return { effectiveExchangeRate: stringifyBigWithSignificantDecimals(preciseQuotedAmountOut.preciseBigDecimal.div(amountBig), 4), preciseQuotedAmountOut, diff --git a/apps/api/src/api/services/phases/handlers/initial-phase-handler.ts b/apps/api/src/api/services/phases/handlers/initial-phase-handler.ts index 3bf4ecd3e..37d5ec822 100644 --- a/apps/api/src/api/services/phases/handlers/initial-phase-handler.ts +++ b/apps/api/src/api/services/phases/handlers/initial-phase-handler.ts @@ -29,7 +29,7 @@ export class InitialPhaseHandler extends BasePhaseHandler { throw new Error("InitialPhaseHandler: No signed transactions found. Cannot proceed."); } else if (state.from === "assethub" && !state.state.assetHubToPendulumHash) { throw new Error("InitialPhaseHandler: Missing required additional data for offramps. Cannot proceed."); - } else if (state.from !== "assethub" && (!state.state.squidRouterApproveHash || !state.state.squidRouterSwapHash)) { + } else if (state.from !== "assethub" && !state.state.squidRouterSwapHash) { throw new Error("InitialPhaseHandler: Missing required additional data for offramps. Cannot proceed."); } } diff --git a/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts b/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts index 463af9985..c43d315bf 100644 --- a/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts +++ b/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts @@ -51,7 +51,7 @@ export class MoonbeamToPendulumPhaseHandler extends BasePhaseHandler { const didInputTokenArrivedOnPendulum = async () => { const balanceResponse = await pendulumNode.api.query.tokens.accounts( pendulumEphemeralAddress, - inputTokenPendulumDetails.pendulumCurrencyId + inputTokenPendulumDetails.currencyId ); // @ts-ignore diff --git a/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts b/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts index 2c9fc2e65..ca19227ce 100644 --- a/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts +++ b/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-xcm-handler.ts @@ -25,10 +25,10 @@ export class MoonbeamToPendulumXcmPhaseHandler extends BasePhaseHandler { throw new Error("MoonbeamToPendulumXcmPhaseHandler: State metadata corrupted. This is a bug."); } - const didInputTokenArrivedOnPendulum = async () => { + const didInputTokenArriveOnPendulum = async () => { const balanceResponse = await pendulumNode.api.query.tokens.accounts( pendulumEphemeralAddress, - inputTokenPendulumDetails.pendulumCurrencyId + inputTokenPendulumDetails.currencyId ); // @ts-ignore @@ -37,7 +37,7 @@ export class MoonbeamToPendulumXcmPhaseHandler extends BasePhaseHandler { }; try { - if (!(await didInputTokenArrivedOnPendulum())) { + if (!(await didInputTokenArriveOnPendulum())) { const { txData: moonbeamToPendulumXcmTransaction } = this.getPresignedTransaction(state, "moonbeamToPendulumXcm"); const xcmTransaction = decodeSubmittableExtrinsic(moonbeamToPendulumXcmTransaction as string, moonbeamNode.api); @@ -61,7 +61,7 @@ export class MoonbeamToPendulumXcmPhaseHandler extends BasePhaseHandler { try { logger.info("waiting for token to arrive on pendulum..."); - await waitUntilTrue(didInputTokenArrivedOnPendulum, 5000); + await waitUntilTrue(didInputTokenArriveOnPendulum, 5000); } catch (e) { console.error("Error while waiting for transaction receipt:", e); throw new Error("MoonbeamToPendulumXcmPhaseHandler: Failed to wait for tokens to arrive on Pendulum."); diff --git a/apps/api/src/api/services/phases/handlers/nabla-approve-handler.ts b/apps/api/src/api/services/phases/handlers/nabla-approve-handler.ts index 27ebd0bc6..9c53a18eb 100644 --- a/apps/api/src/api/services/phases/handlers/nabla-approve-handler.ts +++ b/apps/api/src/api/services/phases/handlers/nabla-approve-handler.ts @@ -21,7 +21,7 @@ export class NablaApprovePhaseHandler extends BasePhaseHandler { // Pre check: check if the approve has already been performed. try { const approval = await pendulumNode.api.query.tokenAllowance.approvals( - state.state.inputTokenPendulumDetails.pendulumCurrencyId, + state.state.inputTokenPendulumDetails.currencyId, state.state.pendulumEphemeralAddress, NABLA_ROUTER ); diff --git a/apps/api/src/api/services/phases/handlers/nabla-swap-handler.ts b/apps/api/src/api/services/phases/handlers/nabla-swap-handler.ts index a49239d13..7fbf46392 100644 --- a/apps/api/src/api/services/phases/handlers/nabla-swap-handler.ts +++ b/apps/api/src/api/services/phases/handlers/nabla-swap-handler.ts @@ -76,7 +76,7 @@ export class NablaSwapPhaseHandler extends BasePhaseHandler { limits: defaultReadLimits, messageArguments: [ inputAmountBeforeSwapRaw, - [inputTokenPendulumDetails.pendulumErc20WrapperAddress, outputTokenPendulumDetails.pendulumErc20WrapperAddress] + [inputTokenPendulumDetails.erc20WrapperAddress, outputTokenPendulumDetails.erc20WrapperAddress] ], messageName: "getAmountOut" }); diff --git a/apps/api/src/api/services/phases/handlers/pendulum-moonbeam-phase-handler.ts b/apps/api/src/api/services/phases/handlers/pendulum-moonbeam-phase-handler.ts index 13c788bdb..ffe63c8c9 100644 --- a/apps/api/src/api/services/phases/handlers/pendulum-moonbeam-phase-handler.ts +++ b/apps/api/src/api/services/phases/handlers/pendulum-moonbeam-phase-handler.ts @@ -4,10 +4,12 @@ import { FiatToken, getAddressForFormat, getAnyFiatTokenDetailsMoonbeam, + PENDULUM_USDC_AXL, RampPhase } from "@packages/shared"; import Big from "big.js"; import { moonbeam } from "viem/chains"; +import logger from "../../../../config/logger"; import RampState from "../../../../models/rampState.model"; import { getEvmTokenBalance } from "../../moonbeam/balance"; import { ApiManager } from "../../pendulum/apiManager"; @@ -24,8 +26,7 @@ export class PendulumToMoonbeamXCMPhaseHandler extends BasePhaseHandler { const apiManager = ApiManager.getInstance(); const pendulumNode = await apiManager.getApi("pendulum"); - const { pendulumEphemeralAddress, moonbeamEphemeralAddress, brlaEvmAddress, outputAmountBeforeFinalStep } = - state.state as StateMetadata; + const { pendulumEphemeralAddress, moonbeamEphemeralAddress, brlaEvmAddress, outputAmountBeforeFinalStep } = state.state; if (!pendulumEphemeralAddress) { throw new Error("Ephemeral address not defined in the state. This is a bug."); @@ -37,7 +38,20 @@ export class PendulumToMoonbeamXCMPhaseHandler extends BasePhaseHandler { ); } - const didInputTokenArrivedOnMoonbeam = async () => { + const didTokensLeavePendulum = async () => { + // Token is always either axlUSDC or BRL. + const currencyId = + state.type === "off" + ? getAnyFiatTokenDetailsMoonbeam(FiatToken.BRL).pendulumRepresentative.currencyId + : PENDULUM_USDC_AXL.currencyId; + const balanceResponse = await pendulumNode.api.query.tokens.accounts(pendulumEphemeralAddress, currencyId); + + // @ts-ignore + const currentBalance = Big(balanceResponse?.free?.toString() ?? "0"); + return currentBalance.lt(outputAmountBeforeFinalStep.raw); + }; + + const didTokensArriveOnMoonbeam = async () => { // Token is always either axlUSDC or BRL. const tokenAddress = state.type === "off" ? getAnyFiatTokenDetailsMoonbeam(FiatToken.BRL).moonbeamErc20Address : AXL_USDC_MOONBEAM; @@ -54,7 +68,12 @@ export class PendulumToMoonbeamXCMPhaseHandler extends BasePhaseHandler { }; try { - if (await didInputTokenArrivedOnMoonbeam()) { + // We have to check if the input token already arrived on Moonbeam and if it left Pendulum. + // If we'd only check if it arrived on Moonbeam, we might miss transferring them if the target account already has some tokens. + if ((await didTokensLeavePendulum()) && (await didTokensArriveOnMoonbeam())) { + logger.info( + `PendulumToMoonbeamPhaseHandler: Input token already arrived on Moonbeam, skipping XCM transfer for ramp ${state.id}.` + ); return this.transitionToNextPhase(state, this.nextPhaseSelector(state)); } @@ -65,11 +84,17 @@ export class PendulumToMoonbeamXCMPhaseHandler extends BasePhaseHandler { } const xcmExtrinsic = decodeSubmittableExtrinsic(pendulumToMoonbeamTransaction, pendulumNode.api); + logger.info(`PendulumToMoonbeamPhaseHandler: Submitting XCM transfer to Moonbeam for ramp ${state.id}`); const { hash } = await submitXTokens( getAddressForFormat(pendulumEphemeralAddress, pendulumNode.ss58Format), xcmExtrinsic ); + logger.info( + `PendulumToMoonbeamPhaseHandler: XCM transfer submitted with hash ${hash} for ramp ${state.id}. Waiting for the token to arrive on Moonbeam...` + ); + await didTokensArriveOnMoonbeam(); + state.state = { ...state.state, pendulumToMoonbeamXcmHash: hash diff --git a/apps/api/src/api/services/phases/handlers/squid-router-pay-phase-handler.ts b/apps/api/src/api/services/phases/handlers/squid-router-pay-phase-handler.ts index dcfce24f4..f513c7c8e 100644 --- a/apps/api/src/api/services/phases/handlers/squid-router-pay-phase-handler.ts +++ b/apps/api/src/api/services/phases/handlers/squid-router-pay-phase-handler.ts @@ -10,7 +10,7 @@ import RampState from "../../../../models/rampState.model"; import { PhaseError } from "../../../errors/phase-error"; import { createMoonbeamClientsAndConfig } from "../../moonbeam/createServices"; import { getTokenDetailsForEvmDestination } from "../../ramp/quote.service/gross-output"; -import { createOnrampRouteParams, getRoute } from "../../transactions/squidrouter/route"; +import { createOnrampRouteParams, getRoute, getStatus } from "../../transactions/squidrouter/route"; import { BasePhaseHandler } from "../base-phase-handler"; interface AxelarScanStatusResponse { @@ -95,7 +95,8 @@ export class SquidRouterPayPhaseHandler extends BasePhaseHandler { */ private async checkStatus(state: RampState, swapHash: string): Promise { try { - // const _ = await getStatus(swapHash); // Found to be unreliable. Returned "not found" for valid transactions. + // Found to be unreliable. We call it anyway so that squidrouter can log the status. + getStatus(swapHash).catch(error => logger.error(`Couldn't fetch status for ${swapHash} from squidrouter`, error.message)); let isExecuted = false; let payTxHash: string | undefined = state.state.squidRouterPayTxHash; // in case of recovery, we may have already paid. @@ -152,6 +153,9 @@ export class SquidRouterPayPhaseHandler extends BasePhaseHandler { functionName: "addNativeGas" }); const { maxFeePerGas, maxPriorityFeePerGas } = await this.publicClient.estimateFeesPerGas(); + logger.info( + `SquidRouterPayPhaseHandler: Funding Axelar gas service for swap hash ${swapHash} with GLMR: ${gmlrValueRaw}` + ); const gasPaymentHash = await this.walletClient.sendTransaction({ data: transactionData, maxFeePerGas, @@ -187,7 +191,10 @@ export class SquidRouterPayPhaseHandler extends BasePhaseHandler { return (responseData as { data: unknown[] }).data[0] as AxelarScanStatusResponse; } catch (error) { if ((error as { response: unknown }).response) { - console.error("API error:", (error as { response: unknown }).response); + logger.error( + `SquidRouterPayPhaseHandler: Couldn't get status for ${swapHash} from AxelarScan:`, + (error as { response: unknown }).response + ); } throw error; } @@ -210,7 +217,7 @@ export class SquidRouterPayPhaseHandler extends BasePhaseHandler { const { route } = routeResult.data; const feeValue = route.transactionRequest.value; - console.log(`SquidRouterPayPhaseHandler: Fresh route value fetched: ${feeValue}`); + logger.info(`SquidRouterPayPhaseHandler: Fresh route value fetched: ${feeValue}`); return feeValue; } catch (error) { logger.error("SquidRouterPayPhaseHandler: Error fetching fresh route:", error); diff --git a/apps/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts b/apps/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts index 9f9e388ec..79958e322 100644 --- a/apps/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts +++ b/apps/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts @@ -27,7 +27,7 @@ export class SubsidizePostSwapPhaseHandler extends BasePhaseHandler { try { const balanceResponse = await pendulumNode.api.query.tokens.accounts( pendulumEphemeralAddress, - outputTokenPendulumDetails.pendulumCurrencyId + outputTokenPendulumDetails.currencyId ); // @ts-ignore @@ -44,7 +44,7 @@ export class SubsidizePostSwapPhaseHandler extends BasePhaseHandler { ); const fundingAccountKeypair = getFundingAccount(); await pendulumNode.api.tx.tokens - .transfer(pendulumEphemeralAddress, outputTokenPendulumDetails.pendulumCurrencyId, requiredAmount.toFixed(0, 0)) + .transfer(pendulumEphemeralAddress, outputTokenPendulumDetails.currencyId, requiredAmount.toFixed(0, 0)) .signAndSend(fundingAccountKeypair); } diff --git a/apps/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts b/apps/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts index be40b0128..5800c9f48 100644 --- a/apps/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts +++ b/apps/api/src/api/services/phases/handlers/subsidize-pre-swap-handler.ts @@ -27,7 +27,7 @@ export class SubsidizePreSwapPhaseHandler extends BasePhaseHandler { try { const balanceResponse = await pendulumNode.api.query.tokens.accounts( pendulumEphemeralAddress, - inputTokenPendulumDetails.pendulumCurrencyId + inputTokenPendulumDetails.currencyId ); // @ts-ignore @@ -45,7 +45,7 @@ export class SubsidizePreSwapPhaseHandler extends BasePhaseHandler { const fundingAccountKeypair = getFundingAccount(); // TODO this and other calls, add to executeApiCall to avoid low priority errors. await pendulumNode.api.tx.tokens - .transfer(pendulumEphemeralAddress, inputTokenPendulumDetails.pendulumCurrencyId, requiredAmount.toFixed(0, 0)) + .transfer(pendulumEphemeralAddress, inputTokenPendulumDetails.currencyId, requiredAmount.toFixed(0, 0)) .signAndSend(fundingAccountKeypair); } diff --git a/apps/api/src/api/services/phases/meta-state-types.ts b/apps/api/src/api/services/phases/meta-state-types.ts index 7199d9da8..29d909ce8 100644 --- a/apps/api/src/api/services/phases/meta-state-types.ts +++ b/apps/api/src/api/services/phases/meta-state-types.ts @@ -1,4 +1,4 @@ -import { PendulumDetails, RampCurrency, StellarTokenDetails } from "@packages/shared"; +import { PendulumTokenDetails, RampCurrency, StellarTokenDetails } from "@packages/shared"; import { ExtrinsicOptions } from "../transactions/nabla"; export interface StateMetadata { @@ -8,8 +8,8 @@ export interface StateMetadata { outputCurrency: RampCurrency; nablaSoftMinimumOutputRaw: string; pendulumEphemeralAddress: string; - inputTokenPendulumDetails: PendulumDetails; - outputTokenPendulumDetails: PendulumDetails; + inputTokenPendulumDetails: PendulumTokenDetails; + outputTokenPendulumDetails: PendulumTokenDetails; outputTokenType: RampCurrency; inputAmountBeforeSwapRaw: string; // The final step for onramp is the squidRouterSwap or XCM transfer, for offramps it's the anchor payout diff --git a/apps/api/src/api/services/priceFeed.service.ts b/apps/api/src/api/services/priceFeed.service.ts index 06d6d626c..e276756fa 100644 --- a/apps/api/src/api/services/priceFeed.service.ts +++ b/apps/api/src/api/services/priceFeed.service.ts @@ -208,8 +208,8 @@ export class PriceFeedService { const amountOut = await getTokenOutAmount({ api: apiInstance.api, fromAmountString: inputAmount, - inputTokenDetails: inputTokenPendulumDetails, - outputTokenDetails: outputTokenPendulumDetails + inputTokenPendulumDetails, + outputTokenPendulumDetails }); const exchangeRate = parseFloat(amountOut.effectiveExchangeRate); @@ -251,7 +251,7 @@ export class PriceFeedService { MATIC: "matic-network" }; - return tokenIdMap[currency as string] || null; + return tokenIdMap[currency.toUpperCase()] || null; } // Helper method to satisfy eslint for 'this' usage diff --git a/apps/api/src/api/services/ramp/quote.service/gross-output.ts b/apps/api/src/api/services/ramp/quote.service/gross-output.ts index 10c293bfe..a9f6759ef 100644 --- a/apps/api/src/api/services/ramp/quote.service/gross-output.ts +++ b/apps/api/src/api/services/ramp/quote.service/gross-output.ts @@ -5,9 +5,8 @@ import { getOnChainTokenDetails, getPendulumDetails, isEvmTokenDetails, - Networks, OnChainToken, - PendulumDetails, + PendulumTokenDetails, RampCurrency } from "@packages/shared"; import { ApiPromise } from "@polkadot/api"; @@ -20,7 +19,13 @@ import { getTokenOutAmount, TokenOutData } from "../../nablaReads/outAmount"; import { ApiManager } from "../../pendulum/apiManager"; import { multiplyByPowerOfTen } from "../../pendulum/helpers"; import { priceFeedService } from "../../priceFeed.service"; -import { createOnrampRouteParams, getRoute, RouteParams, SquidrouterRoute } from "../../transactions/squidrouter/route"; +import { + createOfframpRouteParams, + createOnrampRouteParams, + getRoute, + RouteParams, + SquidrouterRoute +} from "../../transactions/squidrouter/route"; export interface NablaSwapRequest { inputAmountForSwap: string; @@ -39,10 +44,17 @@ export interface NablaSwapResult { export interface EvmBridgeRequest { intermediateAmountRaw: string; // Raw output from Nabla swap (e.g. axlUSDC on Moonbeam) - intermediateCurrencyOnEvm: OnChainToken; // e.g. EvmToken.axlUSDC finalOutputCurrency: OnChainToken; // Target token on final EVM chain finalEvmDestination: DestinationType; // Target EVM chain originalInputAmountForRateCalc: string; // The inputAmountForSwap that went into Nabla, for final rate calculation + rampType: "on" | "off"; // Whether this is an onramp or offramp +} + +export interface EvmBridgeQuoteRequest { + rampType: "on" | "off"; // Whether this is an onramp or offramp + amountDecimal: string; // Raw amount + inputOrOutputCurrency: OnChainToken; // The currency being swapped (input for offramp, output for onramp) + sourceOrDestination: DestinationType; // The source or destination EVM chain based on rampType } export interface EvmBridgeResult { @@ -54,14 +66,14 @@ export interface EvmBridgeResult { async function getNablaSwapOutAmount( apiInstance: { api: ApiPromise }, fromAmountString: string, - inputTokenDetails: PendulumDetails, - outputTokenDetails: PendulumDetails + inputTokenPendulumDetails: PendulumTokenDetails, + outputTokenPendulumDetails: PendulumTokenDetails ): Promise { return await getTokenOutAmount({ api: apiInstance.api, fromAmountString, - inputTokenDetails, - outputTokenDetails + inputTokenPendulumDetails, + outputTokenPendulumDetails }); } @@ -96,11 +108,15 @@ export function getTokenDetailsForEvmDestination( * Helper to prepare route parameters for Squidrouter */ function prepareSquidrouterRouteParams( - intermediateAmountRaw: string, + rampType: "on" | "off", + amountRaw: string, tokenDetails: EvmTokenDetails, - finalEvmDestination: DestinationType + sourceOrDestination: DestinationType ): RouteParams { - const network = getNetworkFromDestination(finalEvmDestination); + const placeholderAddress = "0x30a300612ab372cc73e53ffe87fb73d62ed68da3"; + const placeholderHash = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + const network = getNetworkFromDestination(sourceOrDestination); if (!network) { throw new APIError({ message: "Invalid EVM destination network", @@ -108,29 +124,12 @@ function prepareSquidrouterRouteParams( }); } - return createOnrampRouteParams( - "0x30a300612ab372cc73e53ffe87fb73d62ed68da3", // Placeholder address - intermediateAmountRaw, - tokenDetails, - network, - "0x30a300612ab372cc73e53ffe87fb73d62ed68da3" // Placeholder address - ); -} - -/** - * Helper to execute Squidrouter route and validate response - */ -async function getSquidrouterRouteData(routeParams: RouteParams): Promise { - const routeResult = await getRoute(routeParams); + const routeParams = + rampType === "on" + ? createOnrampRouteParams(placeholderAddress, amountRaw, tokenDetails, network, placeholderAddress) + : createOfframpRouteParams(placeholderAddress, amountRaw, tokenDetails, network, placeholderAddress, placeholderHash); - if (!routeResult?.data?.route?.estimate) { - throw new APIError({ - message: "Invalid Squidrouter response", - status: httpStatus.SERVICE_UNAVAILABLE - }); - } - - return routeResult.data; + return routeParams; } /** @@ -233,27 +232,54 @@ export async function calculateNablaSwapOutput(request: NablaSwapRequest): Promi } } +function buildRouteRequest(request: EvmBridgeQuoteRequest) { + const token = getTokenDetailsForEvmDestination(request.inputOrOutputCurrency, request.sourceOrDestination); + const amountRaw = multiplyByPowerOfTen(request.amountDecimal, token.decimals).toFixed(0, 0); + return prepareSquidrouterRouteParams(request.rampType, amountRaw, token, request.sourceOrDestination); +} + +async function getSquidrouterRouteData(routeParams: RouteParams) { + const routeResult = await getRoute(routeParams); + + if (!routeResult?.data?.route?.estimate) { + throw new APIError({ + message: "Invalid Squidrouter response", + status: httpStatus.SERVICE_UNAVAILABLE + }); + } + + const routeData = routeResult.data; + const outputTokenDecimals = routeData.route.estimate.toToken.decimals; + const outputAmountRaw = routeData.route.estimate.toAmount; + const outputAmountDecimal = parseContractBalanceResponse(outputTokenDecimals, BigInt(outputAmountRaw)).preciseBigDecimal; + const networkFeeUSD = await calculateSquidrouterNetworkFee(routeData); + + return { + networkFeeUSD, + outputAmountDecimal, + routeData + }; +} + /** * Handles EVM bridging/swapping via Squidrouter and calculates its specific network fee */ export async function calculateEvmBridgeAndNetworkFee(request: EvmBridgeRequest): Promise { - const { intermediateAmountRaw, finalOutputCurrency, finalEvmDestination, originalInputAmountForRateCalc } = request; + const { intermediateAmountRaw, finalOutputCurrency, finalEvmDestination, originalInputAmountForRateCalc, rampType } = request; try { // Get token details for final output currency const tokenDetails = getTokenDetailsForEvmDestination(finalOutputCurrency, finalEvmDestination); // Prepare route parameters for Squidrouter - const routeParams = prepareSquidrouterRouteParams(intermediateAmountRaw, tokenDetails, finalEvmDestination); + const routeParams = prepareSquidrouterRouteParams(rampType, intermediateAmountRaw, tokenDetails, finalEvmDestination); // Execute Squidrouter route and validate response - const routeResult = await getSquidrouterRouteData(routeParams); + const { networkFeeUSD, routeData } = await getSquidrouterRouteData(routeParams); // Calculate network fee (Squidrouter fee) - const networkFeeUSD = await calculateSquidrouterNetworkFee(routeResult); - // Parse final gross output amount - const finalGrossOutputAmount = routeResult.route.estimate.toAmountMin; + const finalGrossOutputAmount = routeData.route.estimate.toAmountMin; const finalGrossOutputAmountDecimal = parseContractBalanceResponse( tokenDetails.decimals, BigInt(finalGrossOutputAmount) @@ -272,10 +298,16 @@ export async function calculateEvmBridgeAndNetworkFee(request: EvmBridgeRequest) networkFeeUSD }; } catch (error) { - logger.error("Error calculating EVM bridge and network fee:", error); + logger.error(`Error calculating EVM bridge and network fee: ${error instanceof Error ? error.message : String(error)}`); + // We assume that the error is due to a low input amount throw new APIError({ - message: "Failed to calculate the quote. Please try a higher amount.", + message: "Input amount too low. Please try a larger amount.", status: httpStatus.INTERNAL_SERVER_ERROR }); } } + +export async function getEvmBridgeQuote(request: EvmBridgeQuoteRequest) { + const routeParams = buildRouteRequest(request); + return getSquidrouterRouteData(routeParams); +} diff --git a/apps/api/src/api/services/ramp/quote.service/index.ts b/apps/api/src/api/services/ramp/quote.service/index.ts index 67e1b185d..8a0094e6f 100644 --- a/apps/api/src/api/services/ramp/quote.service/index.ts +++ b/apps/api/src/api/services/ramp/quote.service/index.ts @@ -1,10 +1,7 @@ import { CreateQuoteRequest, - DestinationType, EvmToken, FiatToken, - getOnChainTokenDetailsOrDefault, - Networks, OnChainToken, QuoteFeeStructure, QuoteResponse, @@ -20,10 +17,29 @@ import { APIError } from "../../../errors/api-error"; import { multiplyByPowerOfTen } from "../../pendulum/helpers"; import { priceFeedService } from "../../priceFeed.service"; import { BaseRampService } from "../base.service"; -import { calculateEvmBridgeAndNetworkFee, calculateNablaSwapOutput } from "./gross-output"; +import { calculateEvmBridgeAndNetworkFee, calculateNablaSwapOutput, getEvmBridgeQuote } from "./gross-output"; import { getTargetFiatCurrency, trimTrailingZeros, validateChainSupport } from "./helpers"; import { calculateFeeComponents, calculatePreNablaDeductibleFees } from "./quote-fees"; +async function calculateInputAmountForNablaSwap( + request: CreateQuoteRequest, + preNablaDeductibleFeeInInputCurrency: Big.BigSource, + preNablaDeductibleFeeAmount: Big.BigSource +) { + if (request.rampType === "off" && request.from !== "assethub") { + // Check squidrouter rate and adjust the input amount accordingly + const bridgeQuote = await getEvmBridgeQuote({ + amountDecimal: request.inputAmount, + inputOrOutputCurrency: request.inputCurrency as OnChainToken, + rampType: request.rampType, + sourceOrDestination: request.from + }); + return new Big(bridgeQuote.outputAmountDecimal).minus(preNablaDeductibleFeeAmount); + } else { + return new Big(request.inputAmount).minus(preNablaDeductibleFeeInInputCurrency); + } +} + export class QuoteService extends BaseRampService { public async createQuote(request: CreateQuoteRequest): Promise { // a. Initial Setup @@ -67,12 +83,16 @@ export class QuoteService extends BaseRampService { request.inputCurrency ); - const inputAmountForNablaSwap = new Big(request.inputAmount).minus(preNablaDeductibleFeeInInputCurrency); + const inputAmountForNablaSwap = await calculateInputAmountForNablaSwap( + request, + preNablaDeductibleFeeInInputCurrency, + preNablaDeductibleFeeAmount + ); // Ensure inputAmountForNablaSwap is not negative if (inputAmountForNablaSwap.lte(0)) { throw new APIError({ - message: "Input amount too low to cover pre-Nabla deductible fees", + message: "Input amount too low to cover fees.", status: httpStatus.BAD_REQUEST }); } @@ -80,16 +100,13 @@ export class QuoteService extends BaseRampService { // d. Perform Nabla Swap // Determine nablaOutputCurrency based on ramp type and destination let nablaOutputCurrency: RampCurrency; - let toPolkadotDestination: DestinationType; if (request.rampType === "on") { // On-Ramp: intermediate currency on Pendulum/Moonbeam if (request.to === "assethub") { nablaOutputCurrency = request.outputCurrency; // Direct to target OnChainToken - toPolkadotDestination = Networks.AssetHub; } else { nablaOutputCurrency = EvmToken.USDC; // Use USDC as intermediate for EVM destinations - toPolkadotDestination = Networks.Moonbeam; } } else { // Off-Ramp: fiat-representative token on Pendulum @@ -105,17 +122,15 @@ export class QuoteService extends BaseRampService { status: httpStatus.BAD_REQUEST }); } - toPolkadotDestination = Networks.Pendulum; } const nablaSwapResult = await calculateNablaSwapOutput({ - fromPolkadotDestination: - request.rampType === "on" ? request.from : request.from === "assethub" ? Networks.AssetHub : Networks.Moonbeam, + fromPolkadotDestination: request.from, inputAmountForSwap: inputAmountForNablaSwap.toString(), inputCurrency: request.inputCurrency, nablaOutputCurrency, rampType: request.rampType, - toPolkadotDestination + toPolkadotDestination: request.to }); // e. Calculate Full Fee Breakdown @@ -174,8 +189,8 @@ export class QuoteService extends BaseRampService { finalEvmDestination: request.to, finalOutputCurrency: request.outputCurrency as OnChainToken, intermediateAmountRaw: nablaSwapResult.nablaOutputAmountRaw, - intermediateCurrencyOnEvm: EvmToken.USDC as OnChainToken, - originalInputAmountForRateCalc: inputAmountForNablaSwap.toString() + originalInputAmountForRateCalc: inputAmountForNablaSwap.toString(), + rampType: request.rampType }); squidRouterNetworkFeeUSD = preliminaryResult.networkFeeUSD; @@ -184,18 +199,16 @@ export class QuoteService extends BaseRampService { .minus(vortexFeeUsd) .minus(partnerMarkupFeeUsd) .minus(squidRouterNetworkFeeUSD); - outputAmountMoonbeamRaw = multiplyByPowerOfTen( - outputAmountMoonbeamDecimal, - getOnChainTokenDetailsOrDefault(Networks.Moonbeam, usdCurrency).pendulumDecimals - ).toString(); + // axlUSDC on Moonbeam is 6 decimals + outputAmountMoonbeamRaw = multiplyByPowerOfTen(outputAmountMoonbeamDecimal, 6).toString(); // Do a second call with all fees deducted to get the final gross output amount const evmBridgeResult = await calculateEvmBridgeAndNetworkFee({ finalEvmDestination: request.to, finalOutputCurrency: request.outputCurrency as OnChainToken, intermediateAmountRaw: outputAmountMoonbeamRaw, - intermediateCurrencyOnEvm: EvmToken.USDC as OnChainToken, - originalInputAmountForRateCalc: inputAmountForNablaSwap.toString() + originalInputAmountForRateCalc: inputAmountForNablaSwap.toString(), + rampType: request.rampType }); finalGrossOutputAmountDecimal = new Big(evmBridgeResult.finalGrossOutputAmountDecimal); @@ -255,7 +268,7 @@ export class QuoteService extends BaseRampService { // Validate final output amount if (finalNetOutputAmount.lte(0)) { throw new APIError({ - message: "Input amount too low to cover calculated fees", + message: "Input amount too low to cover calculated fees.", status: httpStatus.BAD_REQUEST }); } @@ -298,6 +311,7 @@ export class QuoteService extends BaseRampService { inputAmount: request.inputAmount, inputCurrency: request.inputCurrency, metadata: { + inputAmountForNablaSwapDecimal: inputAmountForNablaSwap.toFixed(undefined, 0), offrampAmountBeforeAnchorFees, onrampOutputAmountMoonbeamRaw, usdFeeStructure diff --git a/apps/api/src/api/services/ramp/ramp.service.ts b/apps/api/src/api/services/ramp/ramp.service.ts index 48a1a0b18..575d19f07 100644 --- a/apps/api/src/api/services/ramp/ramp.service.ts +++ b/apps/api/src/api/services/ramp/ramp.service.ts @@ -84,9 +84,14 @@ export class RampService extends BaseRampService { userAddress: additionalData.walletAddress }); - const brCode = await this.validateBrlaOnrampRequest(additionalData.taxId, quote, quote.inputAmount); + await this.validateBrlaOfframpRequest( + additionalData.taxId, + additionalData.pixDestination, + additionalData.receiverTaxId, + quote.inputAmount + ); - return { brCode, stateMeta, unsignedTxs }; + return { stateMeta, unsignedTxs }; } private async prepareOfframpNonBrlTransactions( diff --git a/apps/api/src/api/services/transactions/nabla/approve.ts b/apps/api/src/api/services/transactions/nabla/approve.ts index 890ad797f..5b5750ba5 100644 --- a/apps/api/src/api/services/transactions/nabla/approve.ts +++ b/apps/api/src/api/services/transactions/nabla/approve.ts @@ -1,4 +1,4 @@ -import { NABLA_ROUTER, PendulumDetails } from "@packages/shared"; +import { NABLA_ROUTER, PendulumTokenDetails } from "@packages/shared"; import { createExecuteMessageExtrinsic, Extrinsic, ReadMessageResult, readMessage } from "@pendulum-chain/api-solang"; import { ApiPromise } from "@polkadot/api"; import { Abi } from "@polkadot/api-contract"; @@ -15,7 +15,7 @@ import { API } from "../../pendulum/apiManager"; import { ExtrinsicOptions } from "./index"; export interface PrepareNablaApproveParams { - inputTokenDetails: PendulumDetails; + inputTokenPendulumDetails: PendulumTokenDetails; amountRaw: string; pendulumEphemeralAddress: string; pendulumNode: API; @@ -63,7 +63,7 @@ async function createApproveExtrinsic({ } export async function prepareNablaApproveTransaction({ - inputTokenDetails, + inputTokenPendulumDetails, amountRaw, pendulumEphemeralAddress, pendulumNode @@ -80,7 +80,7 @@ export async function prepareNablaApproveTransaction({ abi: erc20ContractAbi, api, callerAddress: pendulumEphemeralAddress, - contractDeploymentAddress: inputTokenDetails.pendulumErc20WrapperAddress, + contractDeploymentAddress: inputTokenPendulumDetails.erc20WrapperAddress, limits: defaultReadLimits, messageArguments: [pendulumEphemeralAddress, NABLA_ROUTER], messageName: "allowance" @@ -92,19 +92,19 @@ export async function prepareNablaApproveTransaction({ throw new Error(message); } - const currentAllowance = parseContractBalanceResponse(inputTokenDetails.pendulumDecimals, response.value); + const currentAllowance = parseContractBalanceResponse(inputTokenPendulumDetails.decimals, response.value); // maybe do allowance if (currentAllowance === undefined || currentAllowance.rawBalance.lt(Big(amountRaw))) { try { - logger.info(`Preparing transaction to approve tokens: ${amountRaw} ${inputTokenDetails.pendulumAssetSymbol}`); + logger.info(`Preparing transaction to approve tokens: ${amountRaw} ${inputTokenPendulumDetails.assetSymbol}`); return createApproveExtrinsic({ amount: amountRaw, api, callerAddress: pendulumEphemeralAddress, contractAbi: erc20ContractAbi, spender: NABLA_ROUTER, - token: inputTokenDetails.pendulumErc20WrapperAddress + token: inputTokenPendulumDetails.erc20WrapperAddress }); } catch (e) { logger.info(`Could not approve token: ${e}`); diff --git a/apps/api/src/api/services/transactions/nabla/index.ts b/apps/api/src/api/services/transactions/nabla/index.ts index d74e907d3..c5cec05e2 100644 --- a/apps/api/src/api/services/transactions/nabla/index.ts +++ b/apps/api/src/api/services/transactions/nabla/index.ts @@ -1,4 +1,4 @@ -import { AccountMeta, encodeSubmittableExtrinsic, Networks, PendulumDetails } from "@packages/shared"; +import { AccountMeta, encodeSubmittableExtrinsic, Networks, PendulumTokenDetails } from "@packages/shared"; import { CreateExecuteMessageExtrinsicOptions } from "@pendulum-chain/api-solang"; import { ApiManager } from "../../pendulum/apiManager"; import { prepareNablaApproveTransaction } from "./approve"; @@ -9,8 +9,8 @@ export type ExtrinsicOptions = Omit min ${nablaHardMinimumOutputRaw} ${outputTokenDetails.pendulumAssetSymbol}` + `Preparing transaction to swap tokens: ${amountRaw} ${inputTokenPendulumDetails.assetSymbol} -> min ${nablaHardMinimumOutputRaw} ${outputTokenPendulumDetails.assetSymbol}` ); return createSwapExtrinsic({ amount: amountRaw, @@ -90,8 +90,8 @@ export async function prepareNablaSwapTransaction({ api, callerAddress: pendulumEphemeralAddress, contractAbi: routerAbiObject, - tokenIn: inputTokenDetails.pendulumErc20WrapperAddress, - tokenOut: outputTokenDetails.pendulumErc20WrapperAddress + tokenIn: inputTokenPendulumDetails.erc20WrapperAddress, + tokenOut: outputTokenPendulumDetails.erc20WrapperAddress }); } catch (e) { logger.error(`Error creating swap extrinsic: ${e}`); diff --git a/apps/api/src/api/services/transactions/offrampTransactions.ts b/apps/api/src/api/services/transactions/offrampTransactions.ts index 9e17ebd1c..1ae5526f3 100644 --- a/apps/api/src/api/services/transactions/offrampTransactions.ts +++ b/apps/api/src/api/services/transactions/offrampTransactions.ts @@ -20,7 +20,6 @@ import { PaymentData, PENDULUM_USDC_ASSETHUB, PENDULUM_USDC_AXL, - PendulumDetails, PendulumTokenDetails, StellarTokenDetails, UnsignedTx @@ -33,7 +32,6 @@ import { QuoteTicketAttributes, QuoteTicketMetadata } from "../../../models/quot import { ApiManager } from "../pendulum/apiManager"; import { multiplyByPowerOfTen } from "../pendulum/helpers"; import { StateMetadata } from "../phases/meta-state-types"; -import { priceFeedService } from "../priceFeed.service"; import { encodeEvmTransactionData } from "./index"; import { createNablaTransactionsForOfframp } from "./nabla"; import { preparePendulumCleanupTransaction } from "./pendulum/cleanup"; @@ -93,8 +91,8 @@ async function createFeeDistributionTransaction(quote: QuoteTicketAttributes): P // Select stablecoin based on source network const isAssetHubSource = fromNetwork === Networks.AssetHub; const stablecoinDetails = isAssetHubSource ? PENDULUM_USDC_ASSETHUB : PENDULUM_USDC_AXL; - const stablecoinCurrencyId = stablecoinDetails.pendulumCurrencyId; - const stablecoinDecimals = stablecoinDetails.pendulumDecimals; + const stablecoinCurrencyId = stablecoinDetails.currencyId; + const stablecoinDecimals = stablecoinDetails.decimals; // Convert USD fees to stablecoin raw units const networkFeeStablecoinRaw = multiplyByPowerOfTen(networkFeeUSD, stablecoinDecimals).toFixed(0, 0); @@ -227,40 +225,26 @@ async function createNablaSwapTransactions( ): Promise<{ nextNonce: number; stateMeta: Partial }> { const { quote, account, inputTokenPendulumDetails, outputTokenPendulumDetails } = params; - // For offramps, all fees except for the anchor fee are paid out (-> deducted) before the swap. - // Thus, we need to adjust the input amount to account for all deducted fees. - const anchorFeeInInputCurrency = await priceFeedService.convertCurrency( - quote.fee.anchor, - quote.outputCurrency, - quote.inputCurrency - ); - const totalFeeInInputCurrency = await priceFeedService.convertCurrency( - quote.fee.total, - quote.outputCurrency, - quote.inputCurrency - ); - const inputAmountBeforeSwapRaw = multiplyByPowerOfTen( - new Big(quote.inputAmount).minus(totalFeeInInputCurrency).plus(anchorFeeInInputCurrency), - inputTokenPendulumDetails.pendulumDecimals + // The input amount for the Nabla swap was already calculated in the quote + const inputAmountForNablaSwapRaw = multiplyByPowerOfTen( + new Big(quote.metadata.inputAmountForNablaSwapDecimal), + inputTokenPendulumDetails.decimals ).toFixed(0, 0); // For these minimums, we use the output amount after all fees have been deducted except for the anchor fee. const anchorFeeInOutputCurrency = quote.fee.anchor; // No conversion needed, already in output currency const outputBeforeAnchorFee = new Big(quote.outputAmount).minus(anchorFeeInOutputCurrency); const nablaSoftMinimumOutput = outputBeforeAnchorFee.mul(1 - AMM_MINIMUM_OUTPUT_SOFT_MARGIN); - const nablaSoftMinimumOutputRaw = multiplyByPowerOfTen( - nablaSoftMinimumOutput, - outputTokenPendulumDetails.pendulumDecimals - ).toFixed(); + const nablaSoftMinimumOutputRaw = multiplyByPowerOfTen(nablaSoftMinimumOutput, outputTokenPendulumDetails.decimals).toFixed(); const nablaHardMinimumOutput = outputBeforeAnchorFee.mul(1 - AMM_MINIMUM_OUTPUT_HARD_MARGIN).toFixed(0, 0); const nablaHardMinimumOutputRaw = multiplyByPowerOfTen( new Big(nablaHardMinimumOutput), - outputTokenPendulumDetails.pendulumDecimals + outputTokenPendulumDetails.decimals ).toFixed(0, 0); const { approve, swap } = await createNablaTransactionsForOfframp( - inputAmountBeforeSwapRaw, + inputAmountForNablaSwapRaw, account, inputTokenPendulumDetails, outputTokenPendulumDetails, @@ -290,7 +274,7 @@ async function createNablaSwapTransactions( return { nextNonce, stateMeta: { - inputAmountBeforeSwapRaw, + inputAmountBeforeSwapRaw: inputAmountForNablaSwapRaw, nabla: { approveExtrinsicOptions: approve.extrinsicOptions, swapExtrinsicOptions: swap.extrinsicOptions @@ -343,7 +327,7 @@ async function createBRLTransactions( params: { brlaEvmAddress: string; outputAmountRaw: string; - outputTokenDetails: PendulumDetails; + outputTokenPendulumDetails: PendulumTokenDetails; account: AccountMeta; taxId: string; pixDestination: string; @@ -353,12 +337,12 @@ async function createBRLTransactions( pendulumCleanupTx: Omit, nextNonce: number ): Promise<{ nextNonce: number; stateMeta: Partial }> { - const { brlaEvmAddress, outputAmountRaw, outputTokenDetails, account, taxId, pixDestination, receiverTaxId } = params; + const { brlaEvmAddress, outputAmountRaw, outputTokenPendulumDetails, account, taxId, pixDestination, receiverTaxId } = params; const pendulumToMoonbeamTransaction = await createPendulumToMoonbeamTransfer( brlaEvmAddress, outputAmountRaw, - outputTokenDetails.pendulumCurrencyId + outputTokenPendulumDetails.currencyId ); unsignedTxs.push({ @@ -695,8 +679,8 @@ export async function prepareOfframpTransactions({ // Prepare cleanup transaction to be added later with the correct nonce const pendulumCleanupTransaction = await preparePendulumCleanupTransaction( - inputTokenPendulumDetails.pendulumCurrencyId, - outputTokenPendulumDetails.pendulumCurrencyId + inputTokenPendulumDetails.currencyId, + outputTokenPendulumDetails.currencyId ); const pendulumCleanupTx: Omit = { @@ -719,7 +703,7 @@ export async function prepareOfframpTransactions({ account, brlaEvmAddress, outputAmountRaw: offrampAmountBeforeAnchorFeesRaw, - outputTokenDetails, + outputTokenPendulumDetails: outputTokenDetails.pendulumRepresentative, pixDestination, receiverTaxId, taxId diff --git a/apps/api/src/api/services/transactions/onrampTransactions.ts b/apps/api/src/api/services/transactions/onrampTransactions.ts index 3ea234fd2..cebcaa30a 100644 --- a/apps/api/src/api/services/transactions/onrampTransactions.ts +++ b/apps/api/src/api/services/transactions/onrampTransactions.ts @@ -91,8 +91,8 @@ async function createFeeDistributionTransaction(quote: QuoteTicketAttributes): P // Select stablecoin based on destination network const isAssetHubDestination = toNetwork === Networks.AssetHub; const stablecoinDetails = isAssetHubDestination ? PENDULUM_USDC_ASSETHUB : PENDULUM_USDC_AXL; - const stablecoinCurrencyId = stablecoinDetails.pendulumCurrencyId; - const stablecoinDecimals = stablecoinDetails.pendulumDecimals; + const stablecoinCurrencyId = stablecoinDetails.currencyId; + const stablecoinDecimals = stablecoinDetails.decimals; // Convert USD fees to stablecoin raw units const networkFeeStablecoinRaw = multiplyByPowerOfTen(networkFeeUSD, stablecoinDecimals).toFixed(0, 0); @@ -261,41 +261,42 @@ async function createNablaSwapTransactions( ): Promise<{ nextNonce: number; stateMeta: Partial }> { const { quote, account, inputTokenPendulumDetails, outputTokenPendulumDetails } = params; - // The input amount before the swap is the input amount minus the anchor fee - const anchorFeeInInputCurrency = quote.fee.anchor; // Already denoted in the input currency - const inputAmountBeforeSwapRaw = multiplyByPowerOfTen( - new Big(quote.inputAmount).minus(anchorFeeInInputCurrency), - inputTokenPendulumDetails.pendulumDecimals + // The input amount for the swap was already calculated in the quote. + const inputAmountForNablaSwapRaw = multiplyByPowerOfTen( + new Big(quote.metadata.inputAmountForNablaSwapDecimal), + inputTokenPendulumDetails.decimals ).toFixed(0, 0); // For these minimums, we use the output amount after anchor fee deduction but before the other fees are deducted. // This is because for onramps, the anchor fee is deducted before the nabla swap. - const anchorFeeInOutputCurrency = await priceFeedService.convertCurrency( + const anchorFeeInSwapOutputCurrency = await priceFeedService.convertCurrency( quote.fee.anchor, quote.inputCurrency, - quote.outputCurrency + outputTokenPendulumDetails.currency // Use the currency of the output token's pendulum representative ); - const totalFeeInOutputCurrency = await priceFeedService.convertCurrency( + const totalFeeInSwapOutputCurrency = await priceFeedService.convertCurrency( quote.fee.total, quote.inputCurrency, - quote.outputCurrency + outputTokenPendulumDetails.currency // Use the currency of the output token's pendulum representative ); - const outputAfterAnchorFee = new Big(quote.outputAmount).plus(totalFeeInOutputCurrency).minus(anchorFeeInOutputCurrency); + const outputAfterAnchorFee = new Big(quote.outputAmount) + .plus(totalFeeInSwapOutputCurrency) + .minus(anchorFeeInSwapOutputCurrency); const nablaSoftMinimumOutput = outputAfterAnchorFee.mul(1 - AMM_MINIMUM_OUTPUT_SOFT_MARGIN); - const nablaSoftMinimumOutputRaw = multiplyByPowerOfTen( - nablaSoftMinimumOutput, - outputTokenPendulumDetails.pendulumDecimals - ).toFixed(0, 0); + const nablaSoftMinimumOutputRaw = multiplyByPowerOfTen(nablaSoftMinimumOutput, outputTokenPendulumDetails.decimals).toFixed( + 0, + 0 + ); const nablaHardMinimumOutput = outputAfterAnchorFee.mul(1 - AMM_MINIMUM_OUTPUT_HARD_MARGIN); - const nablaHardMinimumOutputRaw = multiplyByPowerOfTen( - nablaHardMinimumOutput, - outputTokenPendulumDetails.pendulumDecimals - ).toFixed(0, 0); + const nablaHardMinimumOutputRaw = multiplyByPowerOfTen(nablaHardMinimumOutput, outputTokenPendulumDetails.decimals).toFixed( + 0, + 0 + ); const { approve, swap } = await createNablaTransactionsForOnramp( - inputAmountBeforeSwapRaw, + inputAmountForNablaSwapRaw, account, inputTokenPendulumDetails, outputTokenPendulumDetails, @@ -327,7 +328,7 @@ async function createNablaSwapTransactions( return { nextNonce, stateMeta: { - inputAmountBeforeSwapRaw, + inputAmountBeforeSwapRaw: inputAmountForNablaSwapRaw, nabla: { approveExtrinsicOptions: approve.extrinsicOptions, swapExtrinsicOptions: swap.extrinsicOptions @@ -384,8 +385,8 @@ async function createPendulumCleanupTx(params: { const { inputTokenPendulumDetails, outputTokenPendulumDetails, account } = params; const pendulumCleanupTransaction = await preparePendulumCleanupTransaction( - inputTokenPendulumDetails.pendulumCurrencyId, - outputTokenPendulumDetails.pendulumCurrencyId + inputTokenPendulumDetails.currencyId, + outputTokenPendulumDetails.currencyId ); return { @@ -419,14 +420,14 @@ async function createAssetHubDestinationTransactions( const { destinationAddress, outputTokenDetails, quote, account } = params; // Use the final output amount (net of all fees) for the final transfer - const finalOutputAmountRaw = multiplyByPowerOfTen(new Big(quote.outputAmount), outputTokenDetails.pendulumDecimals).toFixed( - 0, - 0 - ); + const finalOutputAmountRaw = multiplyByPowerOfTen( + new Big(quote.outputAmount), + outputTokenDetails.pendulumRepresentative.decimals + ).toFixed(0, 0); const pendulumToAssethubXcmTransaction = await createPendulumToAssethubTransfer( destinationAddress, - outputTokenDetails.pendulumCurrencyId, + outputTokenDetails.pendulumRepresentative.currencyId, finalOutputAmountRaw ); @@ -474,7 +475,7 @@ async function createEvmDestinationTransactions( const pendulumToMoonbeamXcmTransaction = await createPendulumToMoonbeamTransfer( moonbeamEphemeralAddress, quote.metadata.onrampOutputAmountMoonbeamRaw, - outputTokenDetails.pendulumCurrencyId + outputTokenDetails.pendulumRepresentative.currencyId ); unsignedTxs.push({ diff --git a/apps/api/src/api/services/transactions/squidrouter/route.ts b/apps/api/src/api/services/transactions/squidrouter/route.ts index afab31b81..1aae4a0f2 100644 --- a/apps/api/src/api/services/transactions/squidrouter/route.ts +++ b/apps/api/src/api/services/transactions/squidrouter/route.ts @@ -59,6 +59,8 @@ export function createOnrampRouteParams( export interface SquidrouterRoute { route: { estimate: { + toToken: { decimals: number }; + toAmount: string; toAmountMin: string; }; transactionRequest: { @@ -92,10 +94,12 @@ export async function getRoute(params: RouteParams): Promise { }); }; -const mapEvmTokenToDetails = (network: Networks, token: EvmToken): SupportedCryptocurrencyDetails => { +const mapEvmTokenToDetails = (network: EvmNetworks, token: EvmToken): SupportedCryptocurrencyDetails => { const details = evmTokenConfig[network][token]; + if (!details) { + throw new APIError({ + message: `Token '${token}' is not supported on network '${network}'.` + }); + } + return { assetContractAddress: details.erc20AddressSourceChain, assetDecimals: details.decimals, @@ -41,10 +48,11 @@ const mapAssetHubTokenToDetails = (token: AssetHubToken): SupportedCryptocurrenc }; const getEvmNetworkTokens = (network: Networks): SupportedCryptocurrencyDetails[] => { - if (!isNetworkEVM(network)) { - throwInvalidNetworkError(network); + if (isNetworkEVM(network)) { + return Object.values(EvmToken).map(token => mapEvmTokenToDetails(network, token)); + } else { + return throwInvalidNetworkError(network); } - return Object.values(EvmToken).map(token => mapEvmTokenToDetails(network, token)); }; const getAssetHubTokens = (): SupportedCryptocurrencyDetails[] => { diff --git a/apps/api/src/config/logger.ts b/apps/api/src/config/logger.ts index c2b9f7d45..3bfdfe6d7 100644 --- a/apps/api/src/config/logger.ts +++ b/apps/api/src/config/logger.ts @@ -2,7 +2,7 @@ import { StreamOptions } from "morgan"; import winston, { format } from "winston"; const customFormat = winston.format.printf( - ({ timestamp, level, message, label = "" }) => `[${timestamp}] ${level}\t ${label} ${message} }` + ({ timestamp, level, message, label = "" }) => `[${timestamp}] ${level}\t ${label} ${message}` ); const logger = winston.createLogger({ diff --git a/apps/api/src/models/quoteTicket.model.ts b/apps/api/src/models/quoteTicket.model.ts index 35d6bada3..6ed734f01 100644 --- a/apps/api/src/models/quoteTicket.model.ts +++ b/apps/api/src/models/quoteTicket.model.ts @@ -22,6 +22,8 @@ export interface QuoteTicketAttributes { } export interface QuoteTicketMetadata { + // The input amount to be used for the nabla swap transaction. + inputAmountForNablaSwapDecimal: string; onrampOutputAmountMoonbeamRaw: string; offrampAmountBeforeAnchorFees?: string; // We have the fee structure in the metadata for easy access when creating the transactions to distribute fees in USD-like diff --git a/apps/frontend/src/assets/coins/ETH_ARBITRUM.svg b/apps/frontend/src/assets/coins/ETH_ARBITRUM.svg new file mode 100644 index 000000000..c0c41fcc9 --- /dev/null +++ b/apps/frontend/src/assets/coins/ETH_ARBITRUM.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/coins/ETH_BASE.svg b/apps/frontend/src/assets/coins/ETH_BASE.svg new file mode 100644 index 000000000..6467bb05f --- /dev/null +++ b/apps/frontend/src/assets/coins/ETH_BASE.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/coins/ETH_BSC.svg b/apps/frontend/src/assets/coins/ETH_BSC.svg new file mode 100644 index 000000000..30865e490 --- /dev/null +++ b/apps/frontend/src/assets/coins/ETH_BSC.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/coins/ETH_ETHEREUM.svg b/apps/frontend/src/assets/coins/ETH_ETHEREUM.svg new file mode 100644 index 000000000..736a106cd --- /dev/null +++ b/apps/frontend/src/assets/coins/ETH_ETHEREUM.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/components/AssetNumericInput/index.tsx b/apps/frontend/src/components/AssetNumericInput/index.tsx index 500606881..62c3c400f 100644 --- a/apps/frontend/src/components/AssetNumericInput/index.tsx +++ b/apps/frontend/src/components/AssetNumericInput/index.tsx @@ -5,6 +5,12 @@ import type { RampFormValues } from "../../hooks/ramp/schema"; import { AssetButton } from "../buttons/AssetButton"; import { NumericInput } from "../NumericInput"; +// A helper function to determine the number of decimals based on the token symbol. +// For now, it assumes that ETH-based tokens have 4 decimals and others have 2. +function getMaxDecimalsForToken(tokenSymbol: string): number { + return tokenSymbol.toLowerCase().includes("eth") ? 4 : 2; +} + interface AssetNumericInputProps { assetIcon: string; tokenSymbol: string; @@ -41,6 +47,7 @@ export const AssetNumericInput: FC = ({ diff --git a/apps/frontend/src/components/InputKeys/SelectionModal.tsx b/apps/frontend/src/components/InputKeys/SelectionModal.tsx index 5108ed547..5e7b475b2 100644 --- a/apps/frontend/src/components/InputKeys/SelectionModal.tsx +++ b/apps/frontend/src/components/InputKeys/SelectionModal.tsx @@ -1,9 +1,11 @@ import { assetHubTokenConfig, + EvmNetworks, evmTokenConfig, FiatToken, FiatTokenDetails, getEnumKeyByStringValue, + isNetworkEVM, moonbeamTokenConfig, Networks, OnChainToken, @@ -166,14 +168,14 @@ function getOnChainTokensDefinitionsForNetwork(selectedNetwork: Networks) { details: value as OnChainTokenDetails, type: key as OnChainToken })); - } - - return Object.entries(evmTokenConfig[selectedNetwork]).map(([key, value]) => ({ - assetIcon: value.networkAssetIcon, - assetSymbol: value.assetSymbol, - details: value as OnChainTokenDetails, - type: key as OnChainToken - })); + } else if (isNetworkEVM(selectedNetwork)) { + return Object.entries(evmTokenConfig[selectedNetwork]).map(([key, value]) => ({ + assetIcon: value.networkAssetIcon, + assetSymbol: value.assetSymbol, + details: value as OnChainTokenDetails, + type: key as OnChainToken + })); + } else throw new Error(`Network ${selectedNetwork} is not a valid origin network`); } const getTokenDefinitionsForNetwork = ( diff --git a/apps/frontend/src/components/NumericInput/index.tsx b/apps/frontend/src/components/NumericInput/index.tsx index c730610e5..0df4ca76b 100644 --- a/apps/frontend/src/components/NumericInput/index.tsx +++ b/apps/frontend/src/components/NumericInput/index.tsx @@ -1,7 +1,7 @@ -import { ChangeEvent, ClipboardEvent } from "react"; -import { UseFormRegisterReturn } from "react-hook-form"; +import { ChangeEvent, ClipboardEvent, useEffect, useRef } from "react"; +import { UseFormRegisterReturn, useFormContext, useWatch } from "react-hook-form"; import { cn } from "../../helpers/cn"; -import { handleOnChangeNumericInput, handleOnPasteNumericInput } from "./helpers"; +import { handleOnChangeNumericInput, handleOnPasteNumericInput, trimToMaxDecimals } from "./helpers"; interface NumericInputProps { register: UseFormRegisterReturn; @@ -20,22 +20,38 @@ export const NumericInput = ({ readOnly = false, additionalStyle, maxDecimals = 2, - defaultValue, autoFocus, onChange, loading = false, disabled = false }: NumericInputProps) => { - function handleOnChange(e: ChangeEvent): void { + const { setValue } = useFormContext(); + const fieldName = register.name; + const inputValue = useWatch({ name: fieldName }); + const prevMaxDecimals = useRef(maxDecimals); + + function handleOnChange(e: ChangeEvent): void { handleOnChangeNumericInput(e, maxDecimals); + const value = e.target.value; + setValue(fieldName, value, { shouldDirty: true, shouldValidate: true }); if (onChange) onChange(e); register.onChange(e); } - function handleOnPaste(e: ClipboardEvent): void { - handleOnPasteNumericInput(e, maxDecimals); - register.onChange(e); - } + // Watch for maxDecimals changes and trim value if needed + useEffect(() => { + if (prevMaxDecimals.current > maxDecimals) { + const trimmed = trimToMaxDecimals(inputValue, maxDecimals); + if (trimmed !== inputValue) { + setValue(fieldName, trimmed, { shouldDirty: true, shouldValidate: true }); + // Create a synthetic event for register.onChange + const syntheticEvent = { target: { value: trimmed } } as ChangeEvent; + register.onChange(syntheticEvent); + } + } + prevMaxDecimals.current = maxDecimals; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [maxDecimals, inputValue, setValue, fieldName, register]); return (
@@ -54,14 +70,14 @@ export const NumericInput = ({ inputMode="decimal" minLength={1} onChange={handleOnChange} - onPaste={handleOnPaste} + onPaste={event => handleOnPasteNumericInput(event, maxDecimals)} pattern="^[0-9]*[.,]?[0-9]*$" placeholder="0.0" readOnly={readOnly} spellCheck={false} step="any" type="text" - value={defaultValue} + value={inputValue ?? ""} /> {loading && ( diff --git a/apps/frontend/src/components/RampFeeCollapse/index.tsx b/apps/frontend/src/components/RampFeeCollapse/index.tsx index b5ccdd730..c5fc38d53 100644 --- a/apps/frontend/src/components/RampFeeCollapse/index.tsx +++ b/apps/frontend/src/components/RampFeeCollapse/index.tsx @@ -1,5 +1,5 @@ import { InformationCircleIcon } from "@heroicons/react/20/solid"; -import { QuoteFeeStructure } from "@packages/shared"; +import { QuoteFeeStructure, roundDownToSignificantDecimals } from "@packages/shared"; import Big from "big.js"; import { useTranslation } from "react-i18next"; import { useQuote } from "../../stores/ramp/useQuoteStore"; @@ -45,7 +45,9 @@ function calculateNetExchangeRate(inputAmountString: Big.BigSource, outputAmount // Helper function to format exchange rate strings function formatExchangeRateString(rate: number, input: string, output: string) { - return `1 ${input} ≈ ${rate.toFixed(4)} ${output}`; + // Check the rate to determine how many decimal places to show + // Always show at least 3 significant decimal places + return `1 ${input} ≈ ${roundDownToSignificantDecimals(rate, 3)} ${output}`; } export function RampFeeCollapse() { diff --git a/apps/frontend/src/components/RampSummaryDialog/index.tsx b/apps/frontend/src/components/RampSummaryDialog/index.tsx index f3f98a8c4..09f5cd4b2 100644 --- a/apps/frontend/src/components/RampSummaryDialog/index.tsx +++ b/apps/frontend/src/components/RampSummaryDialog/index.tsx @@ -38,7 +38,7 @@ export const RampSummaryDialog: FC = () => { fiatToken, inputAmount: Big(quote?.inputAmount || "0"), onChainToken, - partnerId: partnerId === null ? undefined : partnerId, // Handle null case + partnerId: partnerId === null ? undefined : partnerId, // Handle null case, rampType: isOnramp ? "on" : "off", selectedNetwork }); diff --git a/apps/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts b/apps/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts index f781b505a..d458959f1 100644 --- a/apps/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts +++ b/apps/frontend/src/hooks/offramp/useRampService/useRegisterRamp.ts @@ -1,4 +1,11 @@ -import { AccountMeta, FiatToken, getAddressForFormat, Networks, signUnsignedTransactions } from "@packages/shared"; +import { + AccountMeta, + FiatToken, + getAddressForFormat, + getOnChainTokenDetails, + Networks, + signUnsignedTransactions +} from "@packages/shared"; import { useCallback, useEffect } from "react"; import { useAssetHubNode, useMoonbeamNode, usePendulumNode } from "../../../contexts/polkadotNode"; import { usePolkadotWalletState } from "../../../contexts/polkadotWallet"; @@ -197,9 +204,7 @@ export const useRegisterRamp = () => { walletAddress: address }; - console.log("Registering ramp with additional data:", additionalData); const rampProcess = await RampService.registerRamp(quoteId, signingAccounts, additionalData); - console.log("Ramp process registered:", rampProcess); const ephemeralTxs = rampProcess.unsignedTxs.filter(tx => { if (!address) { @@ -264,9 +269,7 @@ export const useRegisterRamp = () => { useEffect(() => { // Determine if conditions are met before filtering transactions const requiredMetaIsEmpty = - !rampState?.userSigningMeta?.squidRouterApproveHash && - !rampState?.userSigningMeta?.squidRouterSwapHash && - !rampState?.userSigningMeta?.assetHubToPendulumHash; + !rampState?.userSigningMeta?.squidRouterSwapHash && !rampState?.userSigningMeta?.assetHubToPendulumHash; const shouldRequestSignatures = Boolean(rampState?.ramp) && // Ramp process data exists @@ -324,8 +327,19 @@ export const useRegisterRamp = () => { throw new Error("Missing sorted transactions"); } + const isNativeTokenTransfer = Boolean( + executionInput?.onChainToken && getOnChainTokenDetails(executionInput.network, executionInput.onChainToken)?.isNative + ); + for (const tx of sortedTxs) { + // Approve is not necessary when transferring the native token if (tx.phase === "squidRouterApprove") { + if (isNativeTokenTransfer) { + // We don't care about the approve transaction when transferring native tokens + // We set the signing phase to "login" as a hacky workaround to make sure that 1/1 is shown in the UI + setRampSigningPhase("login"); + continue; + } setRampSigningPhase("started"); squidRouterApproveHash = await signAndSubmitEvmTransaction(tx); setRampSigningPhase("signed"); @@ -405,7 +419,9 @@ export const useRegisterRamp = () => { showToast, signingRejected, ToastMessage.SIGNING_REJECTED, - setSigningRejected + setSigningRejected, + executionInput?.network, + executionInput?.onChainToken ]); return { diff --git a/apps/frontend/src/hooks/offramp/useRampService/useStartRamp.ts b/apps/frontend/src/hooks/offramp/useRampService/useStartRamp.ts index 7059fdf93..a111f4ce6 100644 --- a/apps/frontend/src/hooks/offramp/useRampService/useStartRamp.ts +++ b/apps/frontend/src/hooks/offramp/useRampService/useStartRamp.ts @@ -36,9 +36,9 @@ export const useStartRamp = () => { return; } } else { - // Here we assume we are in any EVM network and need squidrouter - if (!rampState.userSigningMeta.squidRouterApproveHash || !rampState.userSigningMeta.squidRouterSwapHash) { - console.error("Squid router hash is missing. Cannot start ramp."); + // Native token transfers don't require an approval so we only check the swap hash + if (!rampState.userSigningMeta.squidRouterSwapHash) { + console.error("Squid router swap hash is missing. Cannot start ramp."); return; } } diff --git a/apps/frontend/src/hooks/ramp/useRampValidation.ts b/apps/frontend/src/hooks/ramp/useRampValidation.ts index f50307e50..c88773277 100644 --- a/apps/frontend/src/hooks/ramp/useRampValidation.ts +++ b/apps/frontend/src/hooks/ramp/useRampValidation.ts @@ -84,6 +84,7 @@ function validateOfframp( } ): string | null { if (typeof userInputTokenBalance === "string") { + const isNativeToken = fromToken.isNative; if (Big(userInputTokenBalance).lt(inputAmount ?? 0)) { trackEvent({ error_message: "insufficient_balance", @@ -94,6 +95,9 @@ function validateOfframp( assetSymbol: fromToken?.assetSymbol, userInputTokenBalance }); + // If the user chose the max amount, show a warning for native tokens due to gas fees + } else if (isNativeToken && Big(userInputTokenBalance).eq(inputAmount)) { + return t("pages.swap.error.gasWarning"); } } diff --git a/apps/frontend/src/hooks/useGetAssetIcon.tsx b/apps/frontend/src/hooks/useGetAssetIcon.tsx index 621c64cc7..5b736c1f6 100644 --- a/apps/frontend/src/hooks/useGetAssetIcon.tsx +++ b/apps/frontend/src/hooks/useGetAssetIcon.tsx @@ -1,5 +1,9 @@ import ARS from "../assets/coins/ARS.png"; import BRL from "../assets/coins/BRL.png"; +import ETH_ARBITRUM from "../assets/coins/ETH_ARBITRUM.svg"; +import ETH_BASE from "../assets/coins/ETH_BASE.svg"; +import ETH_BSC from "../assets/coins/ETH_BSC.svg"; +import ETH_ETHEREUM from "../assets/coins/ETH_ETHEREUM.svg"; import EUR from "../assets/coins/EUR.svg"; import EURC from "../assets/coins/EURC.png"; import USDC from "../assets/coins/USDC.png"; @@ -19,17 +23,21 @@ import USDT_ETHEREUM from "../assets/coins/USDT_ETHEREUM.svg"; import USDT_POLYGON from "../assets/coins/USDT_POLYGON.svg"; const ICONS = { + arbitrumETH: ETH_ARBITRUM, arbitrumUSDC: USDC_ARBITRUM, arbitrumUSDT: USDT_ARBITRUM, ars: ARS, assethubUSDC: USDC_ASSETHUB, avalancheUSDC: USDC_AVALANCHE, avalancheUSDT: USDT_AVALANCHE, + baseETH: ETH_BASE, baseUSDC: USDC_BASE, baseUSDT: USDT_BASE, brl: BRL, + bscETH: ETH_BSC, bscUSDC: USDC_BSC, bscUSDT: USDT_BSC, + ethereumETH: ETH_ETHEREUM, ethereumUSDC: USDC_ETHEREUM, ethereumUSDT: USDT_ETHEREUM, eur: EUR, diff --git a/apps/frontend/src/hooks/useOnchainTokenBalance.ts b/apps/frontend/src/hooks/useOnchainTokenBalance.ts index 5a15cb76b..519e9b93b 100644 --- a/apps/frontend/src/hooks/useOnchainTokenBalance.ts +++ b/apps/frontend/src/hooks/useOnchainTokenBalance.ts @@ -5,6 +5,7 @@ import { useOnchainTokenBalances } from "./useOnchainTokenBalances"; export const useOnchainTokenBalance = ({ token }: { token: OnChainTokenDetails }): OnChainTokenDetailsWithBalance => { const tokens = useMemo(() => [token], [token]); const balances = useOnchainTokenBalances(tokens); + return balances[0]; }; diff --git a/apps/frontend/src/hooks/useOnchainTokenBalances.ts b/apps/frontend/src/hooks/useOnchainTokenBalances.ts index e6f76e1e6..217d1d3ca 100644 --- a/apps/frontend/src/hooks/useOnchainTokenBalances.ts +++ b/apps/frontend/src/hooks/useOnchainTokenBalances.ts @@ -1,12 +1,16 @@ import { AssetHubTokenDetails, AssetHubTokenDetailsWithBalance, + assetHubTokenConfig, EvmTokenDetails, EvmTokenDetailsWithBalance, + evmTokenConfig, FiatTokenDetails, getNetworkId, isAssetHubTokenDetails, isEvmTokenDetails, + isNetworkEVM, + Networks, nativeToDecimal, OnChainTokenDetails, OnChainTokenDetailsWithBalance @@ -14,7 +18,7 @@ import { import Big from "big.js"; import { useEffect, useMemo, useState } from "react"; import { Abi } from "viem"; -import { useReadContracts } from "wagmi"; +import { useBalance, useReadContracts } from "wagmi"; import { useNetwork } from "../contexts/network"; import { useAssetHubNode } from "../contexts/polkadotNode"; @@ -23,6 +27,87 @@ import erc20ABI from "../contracts/ERC20"; import { multiplyByPowerOfTen } from "../helpers/contracts"; import { useVortexAccount } from "./useVortexAccount"; +// Hook to get EVM native token balance +export const useEvmNativeBalance = (): EvmTokenDetailsWithBalance | null => { + const { address } = useVortexAccount(); + const { selectedNetwork } = useNetwork(); + const chainId = getNetworkId(selectedNetwork); + + const tokensForNetwork: EvmTokenDetails[] = useMemo(() => { + if (isNetworkEVM(selectedNetwork)) { + return Object.values(evmTokenConfig[selectedNetwork] ?? {}); + } else return []; + }, [selectedNetwork]); + + const nativeToken = useMemo(() => tokensForNetwork.find(t => t.isNative), [tokensForNetwork.find]); + + const { data: balance } = useBalance({ + address: address as `0x${string}`, + chainId: isNetworkEVM(selectedNetwork) ? chainId : undefined, + query: { + enabled: !!nativeToken && !!address && isNetworkEVM(selectedNetwork) + } + }); + + return useMemo(() => { + if (!nativeToken || !balance || !isNetworkEVM(selectedNetwork)) return null; + + return { + ...nativeToken, + balance: multiplyByPowerOfTen(Big(balance.value.toString()), -balance.decimals).toFixed(4, 0) + }; + }, [balance, selectedNetwork, nativeToken]); +}; + +// Hook to get AssetHub native DOT balance +export const useAssetHubNativeBalance = (): AssetHubTokenDetailsWithBalance | null => { + const [nativeBalance, setNativeBalance] = useState(null); + const { walletAccount } = usePolkadotWalletState(); + const { apiComponents: assetHubNode } = useAssetHubNode(); + const { selectedNetwork } = useNetwork(); + + const nativeToken = useMemo(() => { + const assethubTokens = Object.values(assetHubTokenConfig); + return assethubTokens.find(token => token.isNative); + }, []); + + useEffect(() => { + if (!nativeToken || !walletAccount || !assetHubNode || selectedNetwork !== "assethub") { + setNativeBalance(null); + return; + } + + const getNativeBalance = async () => { + try { + const { api } = assetHubNode; + const accountInfo = await api.query.system.account(walletAccount.address); + const accountData = accountInfo.toJSON() as { + data: { + free: number; + reserved: number; + frozen: number; + }; + }; + + const freeBalance = accountData.data.free || 0; + const formattedBalance = nativeToDecimal(freeBalance, -nativeToken.decimals).toFixed(4, 0).toString(); + + setNativeBalance({ + ...nativeToken, + balance: formattedBalance + }); + } catch (error) { + console.error("Error fetching AssetHub native balance:", error); + setNativeBalance(null); + } + }; + + getNativeBalance(); + }, [assetHubNode, walletAccount, selectedNetwork, nativeToken]); + + return nativeBalance; +}; + export const useEvmBalances = (tokens: EvmTokenDetails[]): EvmTokenDetailsWithBalance[] => { const { address } = useVortexAccount(); const { selectedNetwork } = useNetwork(); @@ -46,7 +131,11 @@ export const useEvmBalances = (tokens: EvmTokenDetails[]): EvmTokenDetailsWithBa const tokensWithBalances = tokens.reduce>((prev, curr, index) => { const tokenBalance = balances[index]?.result; - const balance = tokenBalance ? multiplyByPowerOfTen(Big(tokenBalance.toString()), -curr.decimals).toFixed(2, 0) : "0.00"; + // If we are dealing with a stablecoin, we show 2 decimals, otherwise 4 + const showDecimals = curr.assetSymbol.toLowerCase().includes("usd") ? 2 : 4; + const balance = tokenBalance + ? multiplyByPowerOfTen(Big(tokenBalance.toString()), -curr.decimals).toFixed(showDecimals, 0) + : "0.00"; prev.push({ ...curr, @@ -103,10 +192,37 @@ export const useAssetHubBalances = (tokens: AssetHubTokenDetails[]): AssetHubTok export const useOnchainTokenBalances = ( tokens: (FiatTokenDetails | OnChainTokenDetails)[] ): OnChainTokenDetailsWithBalance[] => { + const { selectedNetwork } = useNetwork(); + const evmTokens = useMemo(() => tokens.filter(isEvmTokenDetails) as EvmTokenDetailsWithBalance[], [tokens]); const substrateTokens = useMemo(() => tokens.filter(isAssetHubTokenDetails) as AssetHubTokenDetailsWithBalance[], [tokens]); + const evmBalances = useEvmBalances(evmTokens); const substrateBalances = useAssetHubBalances(substrateTokens); - - return evmBalances.length ? evmBalances : substrateBalances; + const evmNativeBalance = useEvmNativeBalance(); + const assetHubNativeBalance = useAssetHubNativeBalance(); + + const shouldIncludeEvmNativeBalance = evmTokens.some(token => token.isNative); + const shouldIncludeAssetHubNativeBalance = substrateTokens.some(token => token.isNative); + + return useMemo(() => { + const tokenBalances: OnChainTokenDetailsWithBalance[] = evmBalances.length ? evmBalances : substrateBalances; + + // Add native token balance based on the selected network + if (isNetworkEVM(selectedNetwork) && shouldIncludeEvmNativeBalance && evmNativeBalance) { + return [evmNativeBalance, ...tokenBalances]; + } else if (selectedNetwork === Networks.AssetHub && shouldIncludeAssetHubNativeBalance && assetHubNativeBalance) { + return [assetHubNativeBalance, ...tokenBalances]; + } + + return tokenBalances; + }, [ + evmBalances, + substrateBalances, + evmNativeBalance, + assetHubNativeBalance, + selectedNetwork, + shouldIncludeAssetHubNativeBalance, + shouldIncludeEvmNativeBalance + ]); }; diff --git a/apps/frontend/src/services/api/ramp.service.ts b/apps/frontend/src/services/api/ramp.service.ts index 90cb865bf..09d313577 100644 --- a/apps/frontend/src/services/api/ramp.service.ts +++ b/apps/frontend/src/services/api/ramp.service.ts @@ -56,7 +56,7 @@ export class RampService { presignedTxs, rampId }; - return apiRequest("post", `${this.BASE_PATH}/start`, request); + return apiRequest("post", `${this.BASE_PATH}/update`, request); } /** diff --git a/apps/frontend/src/translations/en.json b/apps/frontend/src/translations/en.json index 50bfa0e3a..0c5de294f 100644 --- a/apps/frontend/src/translations/en.json +++ b/apps/frontend/src/translations/en.json @@ -374,6 +374,7 @@ "ARS_tokenUnavailable": "Improving your ARS exit - back shortly! ", "BRL_tokenUnavailable": "Improving your BRL exit - back shortly! ", "EURC_tokenUnavailable": "Improving your EUR exit - back shortly! ", + "gasWarning": "Please choose a smaller amount to ensure you can pay the gas cost of the transaction.", "initializeFailed": { "default": "We're experiencing a digital traffic jam. Please hold tight while we clear the road and get things moving again!", "noAddress": "No address found.", diff --git a/apps/frontend/src/translations/pt.json b/apps/frontend/src/translations/pt.json index 45b2cb42b..833b9808d 100644 --- a/apps/frontend/src/translations/pt.json +++ b/apps/frontend/src/translations/pt.json @@ -373,6 +373,7 @@ "ARS_tokenUnavailable": "Ajustando sua saída ARS - em breve!", "BRL_tokenUnavailable": "Ajustando sua saída BRL - em breve!", "EURC_tokenUnavailable": "Ajustando sua saída EUR - em breve!", + "gasWarning": "Escolha um valor menor para garantir que você consiga pagar o custo do gás da transação.", "initializeFailed": { "default": "Estamos enfrentando um congestionamento digital. Por favor, aguarde enquanto liberamos o caminho e fazemos as coisas funcionarem novamente!", "noAddress": "Nenhum endereço encontrado.", diff --git a/biome.json b/biome.json index b5a96e0bc..c9fb510ca 100644 --- a/biome.json +++ b/biome.json @@ -16,16 +16,8 @@ "!**/dist/**", "!**/build/**", "!**/node_modules/", - "!**/lib/**", - "!**/types/**", "!**/coverage/**", "!**/gql/**", - "!**/signer-service/**", - "!**/api/dist/**", - "!**/apps/api/dist/**", - "!**/*.d.ts", - "!**/*.config.*", - "!**/apps/api/src/database/migrations/**", "!**/*.test.ts", "!**/*.test.tsx" ], diff --git a/package.json b/package.json index 027113fb3..0ba54aca8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ ] }, "name": "vortex-monorepo", + "packageManager": "bun@1.1.26", "private": true, "scripts": { "build": "bun run build:frontend && bun run build:backend", diff --git a/packages/shared/src/helpers/networks.ts b/packages/shared/src/helpers/networks.ts index 73cd1e6ca..5c92e3897 100644 --- a/packages/shared/src/helpers/networks.ts +++ b/packages/shared/src/helpers/networks.ts @@ -16,6 +16,16 @@ export enum Networks { Stellar = "stellar" } +// This type is used to represent all networks that can be used as a source or destination in the system. +export type EvmNetworks = + | Networks.Arbitrum + | Networks.Avalanche + | Networks.Base + | Networks.BSC + | Networks.Ethereum + | Networks.Moonbeam + | Networks.Polygon; + /** * Checks if a destination is a network and returns the network if it is. * Returns undefined if the destination is a payment method or not a valid network. @@ -34,8 +44,6 @@ export const ASSETHUB_CHAIN_ID = -1; export const PENDULUM_CHAIN_ID = -2; export const STELLAR_CHAIN_ID = -99; -type EVMNetworks = Exclude; - interface NetworkMetadata { id: number; displayName: string; @@ -105,7 +113,7 @@ export function getNetworkMetadata(network: string): NetworkMetadata | undefined return normalizedNetwork ? NETWORK_METADATA[normalizedNetwork] : undefined; } -export function isNetworkEVM(network: Networks): network is EVMNetworks { +export function isNetworkEVM(network: Networks): network is EvmNetworks { return getNetworkMetadata(network)?.isEVM ?? false; } diff --git a/packages/shared/src/helpers/parseNumbers.ts b/packages/shared/src/helpers/parseNumbers.ts index 1ed869c15..66365b43d 100644 --- a/packages/shared/src/helpers/parseNumbers.ts +++ b/packages/shared/src/helpers/parseNumbers.ts @@ -1,5 +1,5 @@ import { UInt, u128 } from "@polkadot/types-codec"; -import BigNumber from "big.js"; +import Big from "big.js"; // These are the decimals used for the native currency on the Amplitude network export const ChainDecimals = 12; @@ -13,78 +13,78 @@ export const StellarDecimals = ChainDecimals; export const FixedU128Decimals = 18; // Converts a decimal number to the native representation (a large integer) -export const decimalToNative = (value: BigNumber | number | string) => { +export const decimalToNative = (value: Big | number | string) => { let bigIntValue; try { - bigIntValue = new BigNumber(value); + bigIntValue = new Big(value); } catch (_error) { - bigIntValue = new BigNumber(0); + bigIntValue = new Big(0); } - const multiplier = new BigNumber(10).pow(ChainDecimals); + const multiplier = new Big(10).pow(ChainDecimals); return bigIntValue.mul(multiplier); }; -export const decimalToCustom = (value: BigNumber | number | string, decimals: number) => { +export const decimalToCustom = (value: Big | number | string, decimals: number) => { let bigIntValue; try { - bigIntValue = new BigNumber(value); + bigIntValue = new Big(value); } catch (_error) { - bigIntValue = new BigNumber(0); + bigIntValue = new Big(0); } - const multiplier = new BigNumber(10).pow(decimals); + const multiplier = new Big(10).pow(decimals); return bigIntValue.mul(multiplier); }; -export const decimalToStellarNative = (value: BigNumber | number | string) => { +export const decimalToStellarNative = (value: Big | number | string) => { let bigIntValue; try { - bigIntValue = new BigNumber(value); + bigIntValue = new Big(value); } catch (_error) { - bigIntValue = new BigNumber(0); + bigIntValue = new Big(0); } - const multiplier = new BigNumber(10).pow(StellarDecimals); + const multiplier = new Big(10).pow(StellarDecimals); return bigIntValue.mul(multiplier); }; -export const fixedPointToDecimal = (value: BigNumber | number | string) => { - const bigIntValue = new BigNumber(value); - const divisor = new BigNumber(10).pow(FixedU128Decimals); +export const fixedPointToDecimal = (value: Big | number | string) => { + const bigIntValue = new Big(value); + const divisor = new Big(10).pow(FixedU128Decimals); return bigIntValue.div(divisor); }; -export const sanitizeNative = (value: BigNumber | number | string | u128 | UInt) => { - if (!value) return new BigNumber(0); +export const sanitizeNative = (value: Big | number | string | u128 | UInt) => { + if (!value) return new Big(0); if (typeof value === "string" || value instanceof u128 || value instanceof UInt) { // Replace the unnecessary ',' with '' to prevent BigNumber from throwing an error - return new BigNumber(value.toString().replaceAll(",", "")); + return new Big(value.toString().replaceAll(",", "")); } - return new BigNumber(value); + return new Big(value); }; -export const nativeToDecimal = (value: BigNumber | number | string | u128 | UInt, decimals: number = ChainDecimals) => { +export const nativeToDecimal = (value: Big | number | string | u128 | UInt, decimals: number = ChainDecimals) => { const bigIntValue = sanitizeNative(value); - const divisor = new BigNumber(10).pow(decimals); + const divisor = new Big(10).pow(decimals); return bigIntValue.div(divisor); }; -export const nativeStellarToDecimal = (value: BigNumber | number | string) => { - const bigIntValue = new BigNumber(value); - const divisor = new BigNumber(10).pow(StellarDecimals); +export const nativeStellarToDecimal = (value: Big | number | string) => { + const bigIntValue = new Big(value); + const divisor = new Big(10).pow(StellarDecimals); return bigIntValue.div(divisor); }; -export const toBigNumber = (value: BigNumber | number | string, decimals: number) => { +export const toBigNumber = (value: Big | number | string, decimals: number) => { if (typeof value === "string" || value instanceof u128) { // Replace the unnecessary ',' with '' to prevent BigNumber from throwing an error - value = new BigNumber(value.toString().replaceAll(",", "")); + value = new Big(value.toString().replaceAll(",", "")); } - const bigIntValue = new BigNumber(value); + const bigIntValue = new Big(value); - const divisor = new BigNumber(10).pow(decimals); + const divisor = new Big(10).pow(decimals); return bigIntValue.div(divisor); }; @@ -107,7 +107,7 @@ export const format = (n: number, tokenSymbol: string | undefined, oneCharOnly = return prettyNumbers(n); }; -export const nativeToFormat = (value: BigNumber | number | string, tokenSymbol: string | undefined, oneCharOnly = false) => +export const nativeToFormat = (value: Big | number | string, tokenSymbol: string | undefined, oneCharOnly = false) => format(nativeToDecimal(value).toNumber(), tokenSymbol, oneCharOnly); export const prettyNumbers = (number: number, lang?: string, opts?: Intl.NumberFormatOptions) => @@ -121,10 +121,11 @@ export const roundNumber = (value: number | string = 0, round = 6) => { return +Number(value).toFixed(round); }; -export function roundDownToSignificantDecimals(big: BigNumber, decimals: number) { +export function roundDownToSignificantDecimals(number: Big.BigSource, decimals: number) { + const big = new Big(number); return big.prec(Math.max(0, big.e + 1) + decimals, 0); } -export function roundDownToTwoDecimals(big: BigNumber): string { +export function roundDownToTwoDecimals(big: Big): string { return roundDownToSignificantDecimals(big, 2).toFixed(2, 0); } diff --git a/packages/shared/src/tokens/assethub/config.ts b/packages/shared/src/tokens/assethub/config.ts index a38933364..874032c4f 100644 --- a/packages/shared/src/tokens/assethub/config.ts +++ b/packages/shared/src/tokens/assethub/config.ts @@ -11,9 +11,11 @@ export const assetHubTokenConfig: Record = [AssetHubToken.USDC]: { assetSymbol: "USDC", decimals: 6, + foreignAssetId: PENDULUM_USDC_ASSETHUB.foreignAssetId, + isNative: false, network: Networks.AssetHub, networkAssetIcon: "assethubUSDC", - type: TokenType.AssetHub, - ...PENDULUM_USDC_ASSETHUB + pendulumRepresentative: PENDULUM_USDC_ASSETHUB, + type: TokenType.AssetHub } }; diff --git a/packages/shared/src/tokens/constants/pendulum.ts b/packages/shared/src/tokens/constants/pendulum.ts index a0302c69b..e79b86285 100644 --- a/packages/shared/src/tokens/constants/pendulum.ts +++ b/packages/shared/src/tokens/constants/pendulum.ts @@ -2,30 +2,35 @@ * Pendulum-specific constants for token configuration */ -import { PendulumDetails } from "../types/base"; +import { FiatToken } from "../types/base"; +import { EvmToken } from "../types/evm"; +import { PendulumTokenDetails } from "../types/pendulum"; -export const PENDULUM_USDC_AXL: PendulumDetails = { - pendulumAssetSymbol: "USDC.axl", - pendulumCurrencyId: { XCM: 12 }, - pendulumDecimals: 6, - pendulumErc20WrapperAddress: "6eMCHeByJ3m2yPsXFkezBfCQtMs3ymUPqtAyCA41mNWmbNJe" +export const PENDULUM_USDC_AXL: PendulumTokenDetails = { + assetSymbol: "USDC.axl", + currency: EvmToken.USDC, + currencyId: { XCM: 12 }, + decimals: 6, + erc20WrapperAddress: "6eMCHeByJ3m2yPsXFkezBfCQtMs3ymUPqtAyCA41mNWmbNJe" }; -export const PENDULUM_USDC_ASSETHUB: PendulumDetails & { +export const PENDULUM_USDC_ASSETHUB: PendulumTokenDetails & { foreignAssetId: number; } = { - foreignAssetId: 1337, - pendulumAssetSymbol: "USDC", - pendulumCurrencyId: { XCM: 2 }, // USDC on AssetHub - pendulumDecimals: 6, - pendulumErc20WrapperAddress: "6dAegKXwGWEXkfhNbeqeKothqhe6G81McRxG8zvaDYrpdVHF" + assetSymbol: "USDC", + currency: EvmToken.USDC, + currencyId: { XCM: 2 }, + decimals: 6, // USDC on AssetHub + erc20WrapperAddress: "6dAegKXwGWEXkfhNbeqeKothqhe6G81McRxG8zvaDYrpdVHF", + foreignAssetId: 1337 }; -export const PENDULUM_BRLA_MOONBEAM: PendulumDetails = { - pendulumAssetSymbol: "BRLA", - pendulumCurrencyId: { XCM: 13 }, - pendulumDecimals: 18, - pendulumErc20WrapperAddress: "6eRq1yvty6KorGcJ3nKpNYrCBn9FQnzsBhFn4JmAFqWUwpnh" +export const PENDULUM_BRLA_MOONBEAM: PendulumTokenDetails = { + assetSymbol: "BRLA", + currency: FiatToken.BRL, + currencyId: { XCM: 13 }, + decimals: 18, + erc20WrapperAddress: "6eRq1yvty6KorGcJ3nKpNYrCBn9FQnzsBhFn4JmAFqWUwpnh" }; export const AXL_USDC_MOONBEAM = "0xca01a1d0993565291051daff390892518acfad3a"; diff --git a/packages/shared/src/tokens/evm/config.ts b/packages/shared/src/tokens/evm/config.ts index 3470716ce..d3c51df00 100644 --- a/packages/shared/src/tokens/evm/config.ts +++ b/packages/shared/src/tokens/evm/config.ts @@ -2,68 +2,74 @@ * EVM token configuration */ -import { Networks } from "../../helpers"; +import { EvmNetworks, Networks } from "../../helpers"; import { PENDULUM_USDC_AXL } from "../constants/pendulum"; import { TokenType } from "../types/base"; import { EvmToken, EvmTokenDetails } from "../types/evm"; -export const evmTokenConfig: Record> = { +export const evmTokenConfig: Record>> = { + [Networks.Ethereum]: { + [EvmToken.USDC]: { + assetSymbol: "USDC", + decimals: 6, // USDC on Ethereum + erc20AddressSourceChain: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + isNative: false, + network: Networks.Ethereum, + networkAssetIcon: "ethereumUSDC", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm + }, + [EvmToken.USDT]: { + assetSymbol: "USDT", + decimals: 6, // USDT on Ethereum + erc20AddressSourceChain: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + isNative: false, + network: Networks.Ethereum, + networkAssetIcon: "ethereumUSDT", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm + }, + [EvmToken.ETH]: { + assetSymbol: "ETH", + decimals: 18, // ETH on Ethereum + erc20AddressSourceChain: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + isNative: true, + network: Networks.Ethereum, + networkAssetIcon: "ethereumETH", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm + } + }, [Networks.Polygon]: { [EvmToken.USDC]: { assetSymbol: "USDC", decimals: 6, // USDC on Polygon erc20AddressSourceChain: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + isNative: false, network: Networks.Polygon, networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm }, [EvmToken.USDCE]: { assetSymbol: "USDC.e", decimals: 6, // USDC.e on Polygon erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + isNative: false, network: Networks.Polygon, networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm }, [EvmToken.USDT]: { assetSymbol: "USDT", decimals: 6, // USDT on Polygon erc20AddressSourceChain: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + isNative: false, network: Networks.Polygon, networkAssetIcon: "polygonUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - } - }, - [Networks.Ethereum]: { - [EvmToken.USDC]: { - assetSymbol: "USDC", - decimals: 6, // USDC on Ethereum - erc20AddressSourceChain: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - network: Networks.Ethereum, - networkAssetIcon: "ethereumUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDT]: { - assetSymbol: "USDT", - decimals: 6, // USDT on Ethereum - erc20AddressSourceChain: "0xdAC17F958D2ee523a2206206994597C13D831ec7", - network: Networks.Ethereum, - networkAssetIcon: "ethereumUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // Placeholder, update with actual address - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", - network: Networks.Ethereum, - networkAssetIcon: "ethereumUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm } }, [Networks.BSC]: { @@ -71,28 +77,31 @@ export const evmTokenConfig: Record> assetSymbol: "USDC", decimals: 18, // USDC on BSC erc20AddressSourceChain: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + isNative: false, network: Networks.BSC, networkAssetIcon: "bscUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm }, [EvmToken.USDT]: { assetSymbol: "USDT", decimals: 18, // USDT on BSC erc20AddressSourceChain: "0x55d398326f99059fF775485246999027B3197955", + isNative: false, network: Networks.BSC, networkAssetIcon: "bscUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 18, // Placeholder, update with actual address - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm + }, + [EvmToken.ETH]: { + assetSymbol: "ETH", + decimals: 18, // ETH on BSC + erc20AddressSourceChain: "0x2170ed0880ac9a755fd29b2688956bd959f933f8", + isNative: false, network: Networks.BSC, - networkAssetIcon: "bscUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + networkAssetIcon: "bscETH", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm } }, [Networks.Arbitrum]: { @@ -100,28 +109,31 @@ export const evmTokenConfig: Record> assetSymbol: "USDC", decimals: 6, // USDC on Arbitrum erc20AddressSourceChain: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + isNative: false, network: Networks.Arbitrum, networkAssetIcon: "arbitrumUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm }, [EvmToken.USDT]: { assetSymbol: "USDT", decimals: 6, // USDT on Arbitrum erc20AddressSourceChain: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + isNative: false, network: Networks.Arbitrum, networkAssetIcon: "arbitrumUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // Placeholder, update with actual address - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm + }, + [EvmToken.ETH]: { + assetSymbol: "ETH", + decimals: 18, // ETH on Arbitrum + erc20AddressSourceChain: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + isNative: true, network: Networks.Arbitrum, - networkAssetIcon: "arbitrumUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + networkAssetIcon: "arbitrumETH", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm } }, [Networks.Base]: { @@ -129,28 +141,31 @@ export const evmTokenConfig: Record> assetSymbol: "USDC", decimals: 6, // USDC on Base erc20AddressSourceChain: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + isNative: false, network: Networks.Base, networkAssetIcon: "baseUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm }, [EvmToken.USDT]: { assetSymbol: "USDT", decimals: 6, // USDT on Base erc20AddressSourceChain: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", + isNative: false, network: Networks.Base, networkAssetIcon: "baseUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // Placeholder, update with actual address - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm + }, + [EvmToken.ETH]: { + assetSymbol: "ETH", + decimals: 18, // ETH on Base + erc20AddressSourceChain: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + isNative: true, network: Networks.Base, - networkAssetIcon: "baseUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + networkAssetIcon: "baseETH", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm } }, [Networks.Avalanche]: { @@ -158,145 +173,33 @@ export const evmTokenConfig: Record> assetSymbol: "USDC", decimals: 6, // USDC on Avalanche erc20AddressSourceChain: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + isNative: false, network: Networks.Avalanche, networkAssetIcon: "avalancheUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm }, [EvmToken.USDT]: { assetSymbol: "USDT", decimals: 6, // USDT on Avalanche erc20AddressSourceChain: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", + isNative: false, network: Networks.Avalanche, networkAssetIcon: "avalancheUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // Placeholder, update with actual address - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", - network: Networks.Avalanche, - networkAssetIcon: "avalancheUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - } - }, - [Networks.AssetHub]: { - [EvmToken.USDC]: { - assetSymbol: "USDC", - decimals: 6, // Placeholder, not applicable - erc20AddressSourceChain: "0x0000000000000000000000000000000000000000", - network: Networks.AssetHub, - networkAssetIcon: "assethubUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDT]: { - assetSymbol: "USDT", - decimals: 6, // Placeholder, not applicable - erc20AddressSourceChain: "0x0000000000000000000000000000000000000000", - network: Networks.AssetHub, - networkAssetIcon: "assethubUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // Placeholder, not applicable - erc20AddressSourceChain: "0x0000000000000000000000000000000000000000", - network: Networks.AssetHub, - networkAssetIcon: "assethubUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm } }, [Networks.Moonbeam]: { [EvmToken.USDC]: { assetSymbol: "USDC", - decimals: 6, // USDC on Polygon - erc20AddressSourceChain: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - network: Networks.Moonbeam, - networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // USDC.e on Polygon - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", - network: Networks.Moonbeam, - networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDT]: { - assetSymbol: "USDT", - decimals: 6, // USDT on Polygon - erc20AddressSourceChain: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + decimals: 6, // USDC on Moonbeam + erc20AddressSourceChain: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + isNative: false, network: Networks.Moonbeam, - networkAssetIcon: "polygonUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - } - }, - [Networks.Pendulum]: { - [EvmToken.USDC]: { - assetSymbol: "USDC", - decimals: 6, // USDC on Polygon - erc20AddressSourceChain: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - network: Networks.Pendulum, - networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // USDC.e on Polygon - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", - network: Networks.Pendulum, - networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDT]: { - assetSymbol: "USDT", - decimals: 6, // USDT on Polygon - erc20AddressSourceChain: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", - network: Networks.Pendulum, - networkAssetIcon: "polygonUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - } - }, - // Todo Stellar is a placeholder network, should not need this. - [Networks.Stellar]: { - [EvmToken.USDC]: { - assetSymbol: "USDC", - decimals: 6, // USDC on Polygon - erc20AddressSourceChain: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - network: Networks.Pendulum, - networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDCE]: { - assetSymbol: "USDC.e", - decimals: 6, // USDC.e on Polygon - erc20AddressSourceChain: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", - network: Networks.Pendulum, - networkAssetIcon: "polygonUSDC", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL - }, - [EvmToken.USDT]: { - assetSymbol: "USDT", - decimals: 6, // USDT on Polygon - erc20AddressSourceChain: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", - network: Networks.Pendulum, - networkAssetIcon: "polygonUSDT", - type: TokenType.Evm, - ...PENDULUM_USDC_AXL + networkAssetIcon: "moonbeamUSDC", + pendulumRepresentative: PENDULUM_USDC_AXL, + type: TokenType.Evm } } }; diff --git a/packages/shared/src/tokens/moonbeam/config.ts b/packages/shared/src/tokens/moonbeam/config.ts index fb817035b..18997de6d 100644 --- a/packages/shared/src/tokens/moonbeam/config.ts +++ b/packages/shared/src/tokens/moonbeam/config.ts @@ -23,8 +23,8 @@ export const moonbeamTokenConfig: Partial> maxWithdrawalAmountRaw: "10000000000000000", minWithdrawalAmountRaw: "10000000000000", offrampFeesBasisPoints: 25, - pendulumAssetSymbol: "EURC", - pendulumCurrencyId: { - Stellar: { - AlphaNum4: { - code: "0x45555243", - issuer: "0xcf4f5a26e2090bb3adcf02c7a9d73dbfe6659cc690461475b86437fa49c71136" + pendulumRepresentative: { + assetSymbol: "EURC", + currency: FiatToken.EURC, + currencyId: { + Stellar: { + AlphaNum4: { + code: "0x45555243", + issuer: "0xcf4f5a26e2090bb3adcf02c7a9d73dbfe6659cc690461475b86437fa49c71136" + } } - } + }, + decimals: 12, + erc20WrapperAddress: "6eNUvRWCKE3kejoyrJTXiSM7NxtWi37eRXTnKhGKPsJevAj5" }, - pendulumDecimals: 12, - pendulumErc20WrapperAddress: "6eNUvRWCKE3kejoyrJTXiSM7NxtWi37eRXTnKhGKPsJevAj5", stellarAsset: { code: { hex: "0x45555243", @@ -58,17 +61,20 @@ export const stellarTokenConfig: Partial> minWithdrawalAmountRaw: "11000000000000", offrampFeesBasisPoints: 200, offrampFeesFixedComponent: 10, - pendulumAssetSymbol: "ARS", - pendulumCurrencyId: { - Stellar: { - AlphaNum4: { - code: "0x41525300", - issuer: "0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1" + pendulumRepresentative: { + assetSymbol: "ARS", + currency: FiatToken.ARS, + currencyId: { + Stellar: { + AlphaNum4: { + code: "0x41525300", + issuer: "0xb04f8bff207a0b001aec7b7659a8d106e54e659cdf9533528f468e079628fba1" + } } - } + }, + decimals: 12, + erc20WrapperAddress: "6f7VMG1ERxpZMvFE2CbdWb7phxDgnoXrdornbV3CCd51nFsj" }, - pendulumDecimals: 12, - pendulumErc20WrapperAddress: "6f7VMG1ERxpZMvFE2CbdWb7phxDgnoXrdornbV3CCd51nFsj", stellarAsset: { code: { hex: "0x41525300", diff --git a/packages/shared/src/tokens/types/assethub.ts b/packages/shared/src/tokens/types/assethub.ts index 083d87d60..62e02fa4b 100644 --- a/packages/shared/src/tokens/types/assethub.ts +++ b/packages/shared/src/tokens/types/assethub.ts @@ -3,14 +3,17 @@ */ import { Networks } from "../../helpers"; -import { BaseTokenDetails, PendulumDetails, TokenType } from "./base"; +import { PendulumTokenDetails } from "../types/pendulum"; +import { BaseTokenDetails, TokenType } from "./base"; -export interface AssetHubTokenDetails extends BaseTokenDetails, PendulumDetails { +export interface AssetHubTokenDetails extends BaseTokenDetails { type: TokenType.AssetHub; assetSymbol: string; networkAssetIcon: string; network: Networks; foreignAssetId: number; + isNative: boolean; + pendulumRepresentative: PendulumTokenDetails; } export interface AssetHubTokenDetailsWithBalance extends AssetHubTokenDetails { diff --git a/packages/shared/src/tokens/types/base.ts b/packages/shared/src/tokens/types/base.ts index 6ce108844..7c140660a 100644 --- a/packages/shared/src/tokens/types/base.ts +++ b/packages/shared/src/tokens/types/base.ts @@ -31,13 +31,6 @@ export interface BaseTokenDetails { assetSymbol: string; } -export interface PendulumDetails { - pendulumErc20WrapperAddress: string; - pendulumCurrencyId: PendulumCurrencyId; - pendulumAssetSymbol: string; - pendulumDecimals: number; -} - export interface FiatDetails { assetIcon: string; symbol: string; @@ -48,7 +41,6 @@ export interface BaseFiatTokenDetails { fiat: FiatDetails; minWithdrawalAmountRaw: string; maxWithdrawalAmountRaw: string; - pendulumErc20WrapperAddress: string; offrampFeesBasisPoints: number; offrampFeesFixedComponent?: number; onrampFeesBasisPoints?: number; diff --git a/packages/shared/src/tokens/types/evm.ts b/packages/shared/src/tokens/types/evm.ts index 40c3ca94a..d147624d0 100644 --- a/packages/shared/src/tokens/types/evm.ts +++ b/packages/shared/src/tokens/types/evm.ts @@ -2,14 +2,15 @@ * EVM token types */ -import { EvmAddress } from "../.."; +import { EvmAddress, PendulumTokenDetails } from "../.."; import { Networks } from "../../helpers"; -import { BaseTokenDetails, PendulumDetails, TokenType } from "./base"; +import { BaseTokenDetails, TokenType } from "./base"; export enum EvmToken { USDC = "usdc", USDT = "usdt", - USDCE = "usdce" + USDCE = "usdce", + ETH = "eth" } export enum UsdLikeEvmToken { @@ -18,12 +19,16 @@ export enum UsdLikeEvmToken { USDCE = EvmToken.USDCE } -export interface EvmTokenDetails extends BaseTokenDetails, PendulumDetails { +export interface EvmTokenDetails extends BaseTokenDetails { type: TokenType.Evm; assetSymbol: string; networkAssetIcon: string; network: Networks; erc20AddressSourceChain: EvmAddress; + isNative: boolean; + /// The metadata about the token when it's used in Pendulum. + /// For now, all EVM tokens are represented by axlUSDC on Pendulum. + pendulumRepresentative: PendulumTokenDetails; } export interface EvmTokenDetailsWithBalance extends EvmTokenDetails { diff --git a/packages/shared/src/tokens/types/moonbeam.ts b/packages/shared/src/tokens/types/moonbeam.ts index cfa453506..3410ee6ce 100644 --- a/packages/shared/src/tokens/types/moonbeam.ts +++ b/packages/shared/src/tokens/types/moonbeam.ts @@ -2,11 +2,13 @@ * Moonbeam token types */ -import { BaseFiatTokenDetails, BaseTokenDetails, PendulumDetails, TokenType } from "./base"; +import { PendulumTokenDetails } from "../types/pendulum"; +import { BaseFiatTokenDetails, BaseTokenDetails, TokenType } from "./base"; -export interface MoonbeamTokenDetails extends BaseTokenDetails, PendulumDetails, BaseFiatTokenDetails { +export interface MoonbeamTokenDetails extends BaseTokenDetails, BaseFiatTokenDetails { type: TokenType.Moonbeam; polygonErc20Address: string; moonbeamErc20Address: string; partnerUrl: string; + pendulumRepresentative: PendulumTokenDetails; } diff --git a/packages/shared/src/tokens/types/pendulum.ts b/packages/shared/src/tokens/types/pendulum.ts index 6d59fadc0..bedeae660 100644 --- a/packages/shared/src/tokens/types/pendulum.ts +++ b/packages/shared/src/tokens/types/pendulum.ts @@ -1,8 +1,9 @@ -import { PendulumCurrencyId } from "./base"; +import { PendulumCurrencyId, RampCurrency } from "./base"; export type PendulumTokenDetails = { - pendulumErc20WrapperAddress: string; - pendulumCurrencyId: PendulumCurrencyId; - pendulumAssetSymbol: string; - pendulumDecimals: number; + erc20WrapperAddress: string; + currencyId: PendulumCurrencyId; + assetSymbol: string; + decimals: number; + currency: RampCurrency; // Used for price conversions }; diff --git a/packages/shared/src/tokens/types/stellar.ts b/packages/shared/src/tokens/types/stellar.ts index 45bd773aa..ca683e122 100644 --- a/packages/shared/src/tokens/types/stellar.ts +++ b/packages/shared/src/tokens/types/stellar.ts @@ -2,9 +2,10 @@ * Stellar token types */ -import { BaseFiatTokenDetails, BaseTokenDetails, PendulumDetails, TokenType } from "./base"; +import { PendulumTokenDetails } from "../types/pendulum"; +import { BaseFiatTokenDetails, BaseTokenDetails, TokenType } from "./base"; -export interface StellarTokenDetails extends BaseTokenDetails, PendulumDetails, BaseFiatTokenDetails { +export interface StellarTokenDetails extends BaseTokenDetails, BaseFiatTokenDetails { type: TokenType.Stellar; stellarAsset: { code: { @@ -21,4 +22,5 @@ export interface StellarTokenDetails extends BaseTokenDetails, PendulumDetails, anchorHomepageUrl: string; tomlFileUrl: string; usesMemo: boolean; + pendulumRepresentative: PendulumTokenDetails; } diff --git a/packages/shared/src/tokens/utils/helpers.ts b/packages/shared/src/tokens/utils/helpers.ts index ed0d5abd9..320ee3cdf 100644 --- a/packages/shared/src/tokens/utils/helpers.ts +++ b/packages/shared/src/tokens/utils/helpers.ts @@ -2,12 +2,12 @@ * Helper functions for token configuration */ -import { Networks } from "../../helpers"; +import { EvmNetworks, isNetworkEVM, Networks } from "../../helpers"; import { assetHubTokenConfig } from "../assethub/config"; import { evmTokenConfig } from "../evm/config"; import { moonbeamTokenConfig } from "../moonbeam/config"; import { stellarTokenConfig } from "../stellar/config"; -import { AssetHubToken, FiatToken, OnChainToken, PendulumDetails, RampCurrency } from "../types/base"; +import { AssetHubToken, FiatToken, OnChainToken, RampCurrency } from "../types/base"; import { EvmToken } from "../types/evm"; import { MoonbeamTokenDetails } from "../types/moonbeam"; import { PendulumTokenDetails } from "../types/pendulum"; @@ -22,7 +22,9 @@ export function getOnChainTokenDetails(network: Networks, onChainToken: OnChainT if (network === Networks.AssetHub) { return assetHubTokenConfig[onChainToken as AssetHubToken]; } else { - return evmTokenConfig[network][onChainToken as EvmToken]; + if (isNetworkEVM(network)) { + return evmTokenConfig[network][onChainToken as EvmToken]; + } else throw new Error(`Network ${network} is not a valid EVM origin network`); } } catch (error) { console.error(`Error getting input token details: ${error}`); @@ -47,11 +49,14 @@ export function getOnChainTokenDetailsOrDefault(network: Networks, onChainToken: } return firstAvailableToken; } else { - const firstAvailableToken = Object.values(evmTokenConfig[network])[0]; - if (!firstAvailableToken) { - throw new Error(`No tokens configured for network ${network}`); - } - return firstAvailableToken; + if (isNetworkEVM(network)) { + const firstAvailableToken = Object.values(evmTokenConfig[network])[0]; + if (!firstAvailableToken) { + throw new Error(`No tokens configured for network ${network}`); + } + + return firstAvailableToken; + } else throw new Error(`Network ${network} is not a valid EVM origin network`); } } @@ -113,7 +118,7 @@ export function isFiatTokenEnum(token: string): token is FiatToken { */ export function getPendulumCurrencyId(fiatToken: FiatToken) { const tokenDetails = getAnyFiatTokenDetails(fiatToken); - return tokenDetails.pendulumCurrencyId; + return tokenDetails.pendulumRepresentative.currencyId; } /** @@ -130,10 +135,5 @@ export function getPendulumDetails(tokenType: RampCurrency, network?: Networks): throw new Error("Invalid token provided for pendulum details."); } - return { - pendulumAssetSymbol: tokenDetails.pendulumAssetSymbol, - pendulumCurrencyId: tokenDetails.pendulumCurrencyId, - pendulumDecimals: tokenDetails.pendulumDecimals, - pendulumErc20WrapperAddress: tokenDetails.pendulumErc20WrapperAddress - }; + return tokenDetails.pendulumRepresentative; }