diff --git a/apps/api/src/api/services/moonbeam/balance.ts b/apps/api/src/api/services/moonbeam/balance.ts index 59e585cac..08646898c 100644 --- a/apps/api/src/api/services/moonbeam/balance.ts +++ b/apps/api/src/api/services/moonbeam/balance.ts @@ -25,6 +25,32 @@ export class BalanceCheckError extends Error { } } +interface GetBalanceParams { + tokenAddress: `0x${string}`; + ownerAddress: `0x${string}`; + chain: any; +} + +export async function getEvmTokenBalance({ tokenAddress, ownerAddress, chain }: GetBalanceParams): Promise { + try { + const publicClient = createPublicClient({ + chain, + transport: http(), + }); + + const balanceResult = (await publicClient.readContract({ + address: tokenAddress, + abi: erc20ABI, + functionName: 'balanceOf', + args: [ownerAddress], + })) as string; + + return new Big(balanceResult); + } catch (err: any) { + throw new Error(`Failed to read token balance: ${err.message ?? err}`); + } +} + export function checkEvmBalancePeriodically( tokenAddress: string, brlaEvmAddress: string, 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 cb19bbba6..12334a55b 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 @@ -1,6 +1,8 @@ import { RampPhase, decodeSubmittableExtrinsic } from '@packages/shared'; +import { NABLA_ROUTER } from '@packages/shared'; import { ExecuteMessageResult, createExecuteMessageExtrinsic, submitExtrinsic } from '@pendulum-chain/api-solang'; import { Abi } from '@polkadot/api-contract'; +import Big from 'big.js'; import logger from '../../../../config/logger'; import { erc20WrapperAbi } from '../../../../contracts/ERC20Wrapper'; import RampState from '../../../../models/rampState.model'; @@ -17,6 +19,25 @@ export class NablaApprovePhaseHandler extends BasePhaseHandler { const networkName = 'pendulum'; const pendulumNode = await apiManager.getApi(networkName); + // Pre check: check if the approve has already been performed. + try { + const approval = await pendulumNode.api.query.tokenAllowance.approvals( + state.state.inputTokenPendulumDetails, + state.state.pendulumEphemeralAddress, + NABLA_ROUTER, + ); + const requiredAmount = new Big(state.state.inputAmountBeforeSwapRaw); + const approvedAmount = approval.toString() !== '' ? Big(approval.toString()) : Big(0); + if (approvedAmount.gte(requiredAmount)) { + logger.info(`NablaApprovePhaseHandler: Amount already approved. Skipping approval.`); + return this.transitionToNextPhase(state, 'nablaSwap'); + } + } catch (e) { + throw this.createRecoverableError( + `NablaApprovePhaseHandler: Could not check if the approve has already been performed. ${(e as Error).message}`, + ); + } + try { const { txData: nablaApproveTransaction } = this.getPresignedTransaction(state, 'nablaApprove'); // This is a new item that might not be available on old states. 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 143f59994..0ab0ed4d9 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 @@ -1,10 +1,19 @@ -import { RampPhase, decodeSubmittableExtrinsic, getAddressForFormat } from '@packages/shared'; +import { + AXL_USDC_MOONBEAM, + FiatToken, + RampPhase, + decodeSubmittableExtrinsic, + getAddressForFormat, + getAnyFiatTokenDetailsMoonbeam, +} from '@packages/shared'; import RampState from '../../../../models/rampState.model'; import { BasePhaseHandler } from '../base-phase-handler'; -import { submitXTokens } from '../../xcm/send'; - +import Big from 'big.js'; +import { moonbeam } from 'viem/chains'; +import { getEvmTokenBalance } from '../../moonbeam/balance'; import { ApiManager } from '../../pendulum/apiManager'; +import { submitXTokens } from '../../xcm/send'; import { StateMetadata } from '../meta-state-types'; export class PendulumToMoonbeamXCMPhaseHandler extends BasePhaseHandler { @@ -14,16 +23,33 @@ export class PendulumToMoonbeamXCMPhaseHandler extends BasePhaseHandler { protected async executePhase(state: RampState): Promise { const apiManager = ApiManager.getInstance(); - const networkName = 'pendulum'; - const pendulumNode = await apiManager.getApi(networkName); + const pendulumNode = await apiManager.getApi('pendulum'); - const { pendulumEphemeralAddress } = state.state as StateMetadata; + const { pendulumEphemeralAddress, moonbeamEphemeralAddress, outputAmountBeforeFinalStep, outputTokenType } = + state.state as StateMetadata; - if (!pendulumEphemeralAddress) { - throw new Error('Pendulum ephemeral address is not defined in the state. This is a bug.'); + if (!pendulumEphemeralAddress || !moonbeamEphemeralAddress) { + throw new Error('Ephemeral address(es) not defined in the state. This is a bug.'); } + const didInputTokenArrivedOnMoonbeam = async () => { + // Token is always either axlUSDC or BRL. + const tokenAddress = + state.type === 'off' ? getAnyFiatTokenDetailsMoonbeam(FiatToken.BRL).moonbeamErc20Address : AXL_USDC_MOONBEAM; + const balance = await getEvmTokenBalance({ + tokenAddress: tokenAddress as `0x${string}`, + ownerAddress: moonbeamEphemeralAddress as `0x${string}`, + chain: moonbeam, + }); + + return balance.gte(Big(outputAmountBeforeFinalStep.raw)); + }; + try { + if (await didInputTokenArrivedOnMoonbeam()) { + return this.transitionToNextPhase(state, this.nextPhaseSelector(state)); + } + const { txData: pendulumToMoonbeamTransaction } = this.getPresignedTransaction(state, 'pendulumToMoonbeam'); if (typeof pendulumToMoonbeamTransaction !== 'string') { diff --git a/packages/shared/src/tokens/utils/helpers.ts b/packages/shared/src/tokens/utils/helpers.ts index 439bad47d..a6cfc9abe 100644 --- a/packages/shared/src/tokens/utils/helpers.ts +++ b/packages/shared/src/tokens/utils/helpers.ts @@ -7,7 +7,7 @@ import { assetHubTokenConfig } from '../assethub/config'; import { evmTokenConfig } from '../evm/config'; import { moonbeamTokenConfig } from '../moonbeam/config'; import { stellarTokenConfig } from '../stellar/config'; -import { FiatToken, OnChainToken, RampCurrency } from '../types/base'; +import { FiatToken, OnChainToken, PendulumDetails, RampCurrency } from '../types/base'; import { AssetHubToken } from '../types/base'; import { EvmToken } from '../types/evm'; import { MoonbeamTokenDetails } from '../types/moonbeam'; @@ -115,7 +115,7 @@ export function getPendulumCurrencyId(fiatToken: FiatToken) { /** * Get Pendulum details for a token */ -export function getPendulumDetails(tokenType: RampCurrency, network?: Networks) { +export function getPendulumDetails(tokenType: RampCurrency, network?: Networks): PendulumDetails { const tokenDetails = isFiatTokenEnum(tokenType) ? getAnyFiatTokenDetails(tokenType) : network