Skip to content
57 changes: 57 additions & 0 deletions apps/api/src/api/services/phases/handlers/nabla-swap-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { createExecuteMessageExtrinsic, ExecuteMessageResult, readMessage, submi
import { Abi } from "@polkadot/api-contract";
import {
ApiManager,
checkEvmBalanceForToken,
decodeSubmittableExtrinsic,
defaultReadLimits,
EvmClientManager,
EvmTokenDetails,
evmTokenConfig,
FiatToken,
NABLA_ROUTER,
Networks,
Expand Down Expand Up @@ -138,6 +141,60 @@ export class NablaSwapPhaseHandler extends BasePhaseHandler {
const evmClientManager = EvmClientManager.getInstance();
const baseClient = evmClientManager.getClient(Networks.Base);

if (!quote.metadata.nablaSwapEvm?.inputAmountForSwapRaw || !quote.metadata.nablaSwapEvm.inputCurrency) {
throw new Error("Missing nablaSwapEvm input metadata required to validate pre-swap balance");
}

const evmEphemeralAddress = state.state.evmEphemeralAddress;
if (!evmEphemeralAddress) {
throw new Error("Missing EVM ephemeral address to validate nabla swap input balance");
}

const inputTokenDetails = evmTokenConfig[Networks.Base]?.[quote.metadata.nablaSwapEvm.inputCurrency] as
| EvmTokenDetails
| undefined;
if (!inputTokenDetails) {
throw new Error(`Invalid input token ${quote.metadata.nablaSwapEvm.inputCurrency} for Base nabla swap`);
}

const isRecoverableBalanceCheckFailure = (error: unknown): boolean => {
if (!(error instanceof Error)) {
return false;
}

const normalizedMessage = error.message.toLowerCase();
return (
error.name === "BalanceCheckError" ||
normalizedMessage.includes("timeout") ||
normalizedMessage.includes("timed out") ||
normalizedMessage.includes("read failure") ||
normalizedMessage.includes("failed to read") ||
normalizedMessage.includes("network") ||
normalizedMessage.includes("rpc") ||
normalizedMessage.includes("fetch")
);
};

try {
await checkEvmBalanceForToken({
amountDesiredRaw: quote.metadata.nablaSwapEvm.inputAmountForSwapRaw,
chain: Networks.Base,
intervalMs: 1000,
ownerAddress: evmEphemeralAddress,
timeoutMs: 5000,
tokenDetails: inputTokenDetails
});
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
logger.error(`Could not validate EVM input balance before swap: ${errorMessage}`);

if (isRecoverableBalanceCheckFailure(e)) {
throw this.createRecoverableError(`Could not validate EVM input balance before swap: ${errorMessage}`);
}

throw this.createUnrecoverableError(`Could not validate EVM input balance before swap: ${errorMessage}`);
}

try {
const { txData: nablaSwapTransaction } = this.getPresignedTransaction(state, "nablaSwapEvm");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {
checkEvmBalanceForToken,
EvmClientManager,
EvmNetworks,
EvmToken,
EvmTokenDetails,
evmTokenConfig,
FiatToken,
getNetworkFromDestination,
getNetworkId,
Expand Down Expand Up @@ -72,7 +76,43 @@ export class SquidRouterPhaseHandler extends BasePhaseHandler {
return this.transitionToNextPhase(state, "destinationTransfer");
}

const bridgeMeta = quote.metadata.evmToEvm || quote.metadata.moonbeamToEvm;
if (!bridgeMeta?.inputAmountRaw || !bridgeMeta.fromNetwork || !bridgeMeta.fromToken) {
throw new Error("Missing bridge metadata required to validate squidRouter input balance");
}

const evmEphemeralAddress = state.state.evmEphemeralAddress;
if (!evmEphemeralAddress) {
throw new Error("Missing EVM ephemeral address to validate squidRouter input balance");
}

const sourceNetwork = bridgeMeta.fromNetwork as EvmNetworks;
const sourceTokenDetails = Object.values(evmTokenConfig[sourceNetwork] || {}).find(
token => token.erc20AddressSourceChain.toLowerCase() === bridgeMeta.fromToken.toLowerCase()
) as EvmTokenDetails | undefined;

if (!sourceTokenDetails) {
throw new Error(
`Could not resolve source token details on ${bridgeMeta.fromNetwork} for token ${bridgeMeta.fromToken} in squidRouter phase`
);
}

try {
try {
await checkEvmBalanceForToken({
amountDesiredRaw: bridgeMeta.inputAmountRaw,
chain: sourceNetwork,
intervalMs: 1000,
ownerAddress: evmEphemeralAddress,
timeoutMs: 15000,
tokenDetails: sourceTokenDetails
});
} catch (error) {
throw this.createRecoverableError(
`Unable to verify squidRouter input balance for ${evmEphemeralAddress} on ${sourceNetwork}; balance may not be settled yet`
);
}

// Get the presigned transactions for this phase
const approveTransaction = this.getPresignedTransaction(state, "squidRouterApprove");
const swapTransaction = this.getPresignedTransaction(state, "squidRouterSwap");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class SubsidizePostSwapEvmPhaseHandler extends BasePhaseHandler {
);
}

await new Promise(resolve => setTimeout(resolve, 15000));

// Check current balance on EVM
const currentBalance = await checkEvmBalanceForToken({
amountDesiredRaw: "1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { HORIZON_URL, PaymentData, STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS, StellarTokenDetails } from "@vortexfi/shared";
import {
HORIZON_URL,
NUMBER_OF_PRESIGNED_TXS,
PaymentData,
STELLAR_EPHEMERAL_STARTING_BALANCE_UNITS,
StellarTokenDetails
} from "@vortexfi/shared";
import Big from "big.js";
import { Account, Asset, Horizon, Keypair, Memo, Networks, Operation, TransactionBuilder } from "stellar-sdk";
import logger from "../../../../config/logger";
Expand Down Expand Up @@ -39,8 +45,6 @@ export async function buildPaymentAndMergeTx({
createAccountTransactions: Array<{ sequence: string; tx: string }>;
}> {
const baseFee = STELLAR_BASE_FEE;
const NUMBER_OF_PRESIGNED_TXS = 5;

if (!FUNDING_SECRET) {
logger.error("Stellar funding secret not defined");
throw new Error("Stellar funding secret not defined");
Expand Down
Loading
Loading