Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
bd22d14
Add Eth to token config
ebma Jun 16, 2025
3b16b33
Add support for onramping ETH
ebma Jun 16, 2025
992f38b
Fix quote for offramping ETH
ebma Jun 17, 2025
3d707e6
Fix quote for on and offramping ETH
ebma Jun 17, 2025
4531527
Adjust decimals in exchange rate to accommodate lower numbers
ebma Jun 17, 2025
40481a7
Change wording of errors
ebma Jun 17, 2025
1a58074
Change logging of error
ebma Jun 17, 2025
1a6ed70
Add function to derive max decimals for token symbol
ebma Jun 17, 2025
a511693
Ensure that the max decimals are always reflected for the NumericInput
ebma Jun 17, 2025
9c28fe2
Remove log
ebma Jun 17, 2025
26a95e8
Merge branch 'staging' into 719-add-option-to-buy-or-sell-eth
ebma Jun 19, 2025
0fe8816
Merge branch 'staging' into 719-add-option-to-buy-or-sell-eth
ebma Jun 19, 2025
2e20ba3
Add support for native token balances in on-chain token hooks
ebma Jun 20, 2025
c42de5f
Fix query for evm and assethub native token
ebma Jun 20, 2025
33c1ff8
Fix wrong validator called for brl offramp
ebma Jun 20, 2025
17f5fd7
Fix rampId not passed in calls to API endpoints
ebma Jun 20, 2025
e8a5c9a
Add support for native token transfers by skipping approval checks
ebma Jun 22, 2025
b7a73df
Add network icons for eth
ebma Jun 23, 2025
e67c9b4
Streamline `updateRamp` and `startRamp` endpoints with respect to pas…
ebma Jun 23, 2025
360b6c8
Fix wrong input amount used for nabla swap in non-stablecoin case
ebma Jun 24, 2025
789a551
Improve check if token arrived for pendulumToMoonbeam phase
ebma Jun 24, 2025
85fab1a
Adjust token types and add new type `pendulumRepresentative`
ebma Jun 24, 2025
fac47c6
Use correct currency for nabla currency conversions related to soft a…
ebma Jun 24, 2025
1cddf01
Fix compilation issue
ebma Jun 24, 2025
3541ba5
Fix type issue
ebma Jun 24, 2025
fc4cb59
Fix wrong configuration of ETH on BSC and show 6 decimals for non-sta…
ebma Jun 26, 2025
56825f9
Remove unnecessary } from logged messages
ebma Jun 26, 2025
1127604
Adjust error message
ebma Jun 26, 2025
dd45c45
Query status from squidrouter
ebma Jun 26, 2025
8ecffd9
Adjust error message
ebma Jun 26, 2025
cd90498
Fix wrong import
ebma Jun 26, 2025
ad0651c
Fix 'includes' config of biome too strict
ebma Jun 26, 2025
04e6879
Define `bun` as packageManager in package.json
ebma Jun 26, 2025
bc0b97f
Remove logs
ebma Jun 26, 2025
6e46dce
Simplify
ebma Jun 26, 2025
57e53e2
Adjust decimals shown for eth to `4`
ebma Jun 26, 2025
2196eff
Fix typo
ebma Jun 26, 2025
5d7d2c8
Show warning for max amount of native token
ebma Jun 26, 2025
95e6b1a
Change log
ebma Jun 26, 2025
fa51b48
Refactor functions in gross-output.ts
ebma Jun 27, 2025
2978fa1
Refactor inputAmountForNablaSwap function
ebma Jun 27, 2025
4596e36
Remove some type assertions
ebma Jun 27, 2025
ee30ad6
Adjust comment
ebma Jun 27, 2025
ebd23c5
Refactor NumericInput component to use imported trimToMaxDecimals fun…
ebma Jun 27, 2025
fa602e5
Refactor number handling to use Big.js consistently and simplify deci…
ebma Jun 27, 2025
bff584e
Revert change in useOnchainTokenBalance.ts
ebma Jun 27, 2025
91cb2d4
Refactor useEvmNativeBalance and useAssetHubNativeBalance hooks to re…
ebma Jun 27, 2025
c92e1f1
Simplify error
ebma Jun 27, 2025
6051710
Fix conditions to include native tokens
ebma Jun 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/src/api/controllers/quote.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
Expand Down
5 changes: 2 additions & 3 deletions apps/api/src/api/controllers/ramp.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,12 @@ export const registerRamp = async (req: Request, res: Response<RampProcess>, nex
* @public
*/
export const updateRamp = async (
req: Request<{ rampId: string }, unknown, Omit<UpdateRampRequest, "rampId">>,
req: Request<unknown, unknown, UpdateRampRequest>,
res: Response<UpdateRampResponse>,
next: NextFunction
): Promise<void> => {
try {
const { rampId } = req.params;
const { presignedTxs, additionalData } = req.body;
const { rampId, presignedTxs, additionalData } = req.body;

// Validate required fields
if (!rampId || !presignedTxs) {
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/api/routes/v1/ramp.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api/services/brla/brlaTeleportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
16 changes: 8 additions & 8 deletions apps/api/src/api/services/nablaReads/outAmount.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<TokenOutData> {
const { api, fromAmountString, inputTokenDetails, outputTokenDetails, maximumFromAmount } = params;
const { api, fromAmountString, inputTokenPendulumDetails, outputTokenPendulumDetails, maximumFromAmount } = params;

let amountBig: Big;
try {
Expand All @@ -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");
}
Expand All @@ -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 => {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class NablaSwapPhaseHandler extends BasePhaseHandler {
limits: defaultReadLimits,
messageArguments: [
inputAmountBeforeSwapRaw,
[inputTokenPendulumDetails.pendulumErc20WrapperAddress, outputTokenPendulumDetails.pendulumErc20WrapperAddress]
[inputTokenPendulumDetails.erc20WrapperAddress, outputTokenPendulumDetails.erc20WrapperAddress]
],
messageName: "getAmountOut"
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.");
Expand All @@ -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;
Expand All @@ -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.
Comment thread
ebma marked this conversation as resolved.
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));
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -95,7 +95,8 @@ export class SquidRouterPayPhaseHandler extends BasePhaseHandler {
*/
private async checkStatus(state: RampState, swapHash: string): Promise<void> {
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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/api/services/phases/meta-state-types.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/api/services/priceFeed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading