Skip to content

Commit fa6cdf7

Browse files
authored
Merge pull request #933 from pendulum-chain/staging
Create new production release
2 parents 025404d + 30eaee6 commit fa6cdf7

File tree

9 files changed

+229
-12
lines changed

9 files changed

+229
-12
lines changed

apps/api/src/api/services/phases/handlers/subsidize-post-swap-handler.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,11 @@ export class SubsidizePostSwapPhaseHandler extends BasePhaseHandler {
9292
networkName
9393
);
9494

95+
await waitUntilTrueWithTimeout(didBalanceReachExpected, 5000);
96+
9597
const subsidyAmount = nativeToDecimal(requiredAmount, quote.metadata.nablaSwap.outputDecimals).toNumber();
9698
const subsidyToken = quote.metadata.nablaSwap.outputCurrency as unknown as SubsidyToken;
97-
9899
await this.createSubsidy(state, subsidyAmount, subsidyToken, fundingAccountKeypair.address, result.hash);
99-
100-
await waitUntilTrueWithTimeout(didBalanceReachExpected, 5000);
101100
}
102101

103102
return this.transitionToNextPhase(state, this.nextPhaseSelector(state, quote));

apps/api/src/api/services/phases/meta-state-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,7 @@ export interface StateMetadata {
4747
// Used for webhook notifications
4848
sessionId?: string;
4949
squidRouterQuoteId: string;
50+
// Final transaction hash and explorer link (computed once when ramp is complete)
51+
finalTransactionHash?: string;
52+
finalTransactionExplorerLink?: string;
5053
}

apps/api/src/api/services/priceFeed.service.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,74 @@ export class PriceFeedService {
369369
}
370370
}
371371

372+
/**
373+
* Get the onchain oracle price for a specific currency
374+
*
375+
* @param currency - The RampCurrency to get the oracle price for
376+
* @returns The oracle price data including price value and last update timestamp
377+
* @throws Error if the price cannot be fetched or currency is not found
378+
*/
379+
public async getOnchainOraclePrice(currency: RampCurrency): Promise<{
380+
price: Big;
381+
lastUpdateTimestamp: number;
382+
name: string;
383+
}> {
384+
logger.debug(`Fetching onchain oracle price for ${currency}`);
385+
386+
const apiManager = ApiManager.getInstance();
387+
const pendulumApi = await apiManager.getApi("pendulum");
388+
const pendulumApiInstance = pendulumApi.api;
389+
390+
try {
391+
// Construct the query parameters
392+
const blockchain = "FIAT";
393+
const symbol = `${currency}-USD`;
394+
395+
logger.debug(`Querying oracle with blockchain: ${blockchain}, symbol: ${symbol}`);
396+
397+
// Query the oracle for the specific currency
398+
const priceDataEncoded = await pendulumApiInstance.query.diaOracleModule.coinInfosMap({
399+
blockchain,
400+
symbol
401+
});
402+
403+
// Check if price data exists
404+
if (priceDataEncoded.isEmpty) {
405+
throw new Error(`No oracle price found for currency ${currency} (${blockchain}/${symbol})`);
406+
}
407+
408+
// Parse the price data
409+
const priceData = priceDataEncoded.toHuman() as {
410+
name: string;
411+
price: string;
412+
lastUpdateTimestamp: string;
413+
};
414+
415+
// Remove commas from numeric strings and parse
416+
const priceRaw = parseFloat(priceData.price.replaceAll(",", ""));
417+
const lastUpdateTimestamp = parseInt(priceData.lastUpdateTimestamp.replaceAll(",", ""), 10);
418+
419+
// Convert price from raw to decimal number by dividing by 10^12
420+
const price = Big(priceRaw).div(1_000_000_000_000);
421+
422+
logger.debug(`Oracle price for ${currency}: ${price}, Last update: ${lastUpdateTimestamp}, Name: ${priceData.name}`);
423+
424+
return {
425+
lastUpdateTimestamp,
426+
name: priceData.name,
427+
price
428+
};
429+
} catch (error) {
430+
if (error instanceof Error) {
431+
logger.error(`Error fetching onchain oracle price for ${currency}: ${error.message}`);
432+
} else {
433+
logger.error(`Unknown error fetching onchain oracle price for ${currency}`);
434+
}
435+
436+
throw error;
437+
}
438+
}
439+
372440
/**
373441
* Helper function to map RampCurrency to CoinGecko token ID
374442
* @param currency - The RampCurrency to map

apps/api/src/api/services/quote/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export interface QuoteContext {
122122
outputToken: string; // ERC20 wrapper address
123123
effectiveExchangeRate?: string;
124124
outputCurrency: RampCurrency;
125+
oraclePrice?: Big;
125126
};
126127

127128
hydrationSwap?: {

apps/api/src/api/services/quote/engines/nabla-swap/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { PendulumTokenDetails, RampDirection } from "@vortexfi/shared";
22
import { Big } from "big.js";
3+
import logger from "../../../../../config/logger";
4+
import { priceFeedService } from "../../../priceFeed.service";
35
import { calculateNablaSwapOutput } from "../../core/nabla";
46
import { QuoteContext, Stage, StageKey } from "../../core/types";
57

@@ -9,6 +11,7 @@ export interface NablaSwapConfig {
911
}
1012

1113
export interface NablaSwapComputation {
14+
oraclePrice?: Big;
1215
inputAmountPreFees: Big;
1316
inputTokenPendulumDetails: PendulumTokenDetails;
1417
outputTokenPendulumDetails: PendulumTokenDetails;
@@ -43,13 +46,25 @@ export abstract class BaseNablaSwapEngine implements Stage {
4346
rampType: request.rampType
4447
});
4548

49+
let oraclePrice;
50+
try {
51+
oraclePrice = await priceFeedService.getOnchainOraclePrice(
52+
request.rampType === RampDirection.BUY ? request.inputCurrency : request.outputCurrency
53+
);
54+
} catch (error) {
55+
logger.warn(
56+
`OffRampSwapEngine: Unable to fetch on-chain oracle price for ${request.outputCurrency}, proceeding without it. Error: ${error}`
57+
);
58+
}
59+
4660
this.assignNablaSwapContext(
4761
ctx,
4862
result,
4963
inputAmountForSwap,
5064
inputAmountForSwapRaw,
5165
inputTokenPendulumDetails,
52-
outputTokenPendulumDetails
66+
outputTokenPendulumDetails,
67+
oraclePrice?.price
5368
);
5469

5570
this.addNote(ctx, inputTokenPendulumDetails, outputTokenPendulumDetails, inputAmountForSwap, result);
@@ -77,7 +92,8 @@ export abstract class BaseNablaSwapEngine implements Stage {
7792
inputAmountForSwapDecimal: string,
7893
inputAmountForSwapRaw: string,
7994
inputToken: PendulumTokenDetails,
80-
outputToken: PendulumTokenDetails
95+
outputToken: PendulumTokenDetails,
96+
oraclePrice?: Big
8197
): void {
8298
ctx.nablaSwap = {
8399
...ctx.nablaSwap,
@@ -88,6 +104,7 @@ export abstract class BaseNablaSwapEngine implements Stage {
88104
inputCurrencyId: inputToken.currencyId,
89105
inputDecimals: inputToken.decimals,
90106
inputToken: inputToken.erc20WrapperAddress,
107+
oraclePrice,
91108
outputAmountDecimal: result.nablaOutputAmountDecimal,
92109
outputAmountRaw: result.nablaOutputAmountRaw,
93110
outputCurrency: outputToken.currency,

apps/api/src/api/services/ramp/helpers.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FiatToken, Networks } from "@vortexfi/shared";
2+
import logger from "../../../config/logger";
23
import { SANDBOX_ENABLED } from "../../../constants/constants";
34
import QuoteTicket from "../../../models/quoteTicket.model";
45
import RampState from "../../../models/rampState.model";
@@ -11,6 +12,76 @@ enum TransactionHashKey {
1112

1213
type ExplorerLinkBuilder = (hash: string, rampState: RampState, quote: QuoteTicket) => string;
1314

15+
// Map chain names from AxelarScan to their respective explorer URLs
16+
const CHAIN_EXPLORERS: Record<string, string> = {
17+
arbitrum: "https://arbiscan.io/tx",
18+
avalanche: "https://snowtrace.io/tx",
19+
base: "https://basescan.org/tx",
20+
binance: "https://bscscan.com/tx",
21+
bsc: "https://bscscan.com/tx",
22+
ethereum: "https://etherscan.io/tx",
23+
moonbeam: "https://moonscan.io/tx",
24+
polygon: "https://polygonscan.com/tx"
25+
};
26+
27+
async function getAxelarScanExecutionLink(hash: string): Promise<{ explorerLink: string; executionHash: string }> {
28+
const url = "https://api.axelarscan.io/gmp/searchGMP";
29+
const response = await fetch(url, {
30+
body: JSON.stringify({ txHash: hash }),
31+
headers: {
32+
"Content-Type": "application/json"
33+
},
34+
method: "POST"
35+
});
36+
37+
if (!response.ok) {
38+
logger.error(`Failed to fetch AxelarScan link for hash ${hash}: ${response.statusText}`);
39+
// Fallback to AxelarScan link
40+
return {
41+
executionHash: hash,
42+
explorerLink: `https://axelarscan.io/gmp/${hash}`
43+
};
44+
}
45+
46+
try {
47+
const data = (await response.json()).data;
48+
const chain = data[0]?.express_executed?.chain || data[0]?.executed?.chain;
49+
const executionHash = data[0]?.express_executed?.transactionHash || data[0]?.executed?.transactionHash;
50+
51+
if (!executionHash) {
52+
logger.warn(`No execution hash found in AxelarScan response for ${hash}`);
53+
return {
54+
executionHash: hash,
55+
explorerLink: `https://axelarscan.io/gmp/${hash}`
56+
};
57+
}
58+
59+
// Normalize chain name to lowercase for matching
60+
const normalizedChain = chain?.toLowerCase();
61+
const explorerBaseUrl = normalizedChain ? CHAIN_EXPLORERS[normalizedChain] : undefined;
62+
63+
if (explorerBaseUrl) {
64+
return {
65+
executionHash,
66+
explorerLink: `${explorerBaseUrl}/${executionHash}`
67+
};
68+
}
69+
70+
// Fallback to AxelarScan if chain is not recognized
71+
logger.warn(`Unknown chain "${chain}" in AxelarScan response for hash ${hash}, using AxelarScan link`);
72+
return {
73+
executionHash,
74+
explorerLink: `https://axelarscan.io/gmp/${executionHash}`
75+
};
76+
} catch (error) {
77+
logger.error(`Failed to parse AxelarScan response for hash ${hash}: ${error}`);
78+
return {
79+
executionHash: hash,
80+
explorerLink: `https://axelarscan.io/gmp/${hash}`
81+
};
82+
}
83+
}
84+
1485
const EXPLORER_LINK_BUILDERS: Record<TransactionHashKey, ExplorerLinkBuilder> = {
1586
[TransactionHashKey.HydrationToAssethubXcmHash]: hash => `https://hydration.subscan.io/block/${hash}`,
1687

@@ -41,7 +112,10 @@ function deriveSandboxTransactionHash(rampState: RampState): string {
41112
/// For now, this will be the hash of the last transaction on the second-last network, ie. the outgoing transfer
42113
/// and not the incoming one.
43114
/// Only works for ramping processes that have reached the "complete" phase.
44-
export function getFinalTransactionHashForRamp(rampState: RampState, quote: QuoteTicket) {
115+
export async function getFinalTransactionHashForRamp(
116+
rampState: RampState,
117+
quote: QuoteTicket
118+
): Promise<{ transactionExplorerLink: string | undefined; transactionHash: string | undefined }> {
45119
if (rampState.currentPhase !== "complete") {
46120
return { transactionExplorerLink: undefined, transactionHash: undefined };
47121
}
@@ -56,7 +130,40 @@ export function getFinalTransactionHashForRamp(rampState: RampState, quote: Quot
56130

57131
for (const hashKey of TRANSACTION_HASH_PRIORITY) {
58132
const hash = rampState.state[hashKey];
133+
59134
if (hash) {
135+
// For SquidRouter swaps, query the execution hash from AxelarScan
136+
if (hashKey === TransactionHashKey.SquidRouterSwapHash) {
137+
try {
138+
const isMoneriumPolygonOnramp =
139+
rampState.from === "sepa" && quote.inputCurrency === FiatToken.EURC && rampState.to === Networks.Polygon;
140+
141+
if (isMoneriumPolygonOnramp) {
142+
// For Monerium Polygon onramp, use the hash directly
143+
return {
144+
transactionExplorerLink: `https://polygonscan.com/tx/${hash}`,
145+
transactionHash: hash
146+
};
147+
}
148+
149+
// For other cases, query AxelarScan for the execution hash and chain-specific explorer
150+
const { explorerLink, executionHash } = await getAxelarScanExecutionLink(hash);
151+
152+
return {
153+
transactionExplorerLink: explorerLink,
154+
transactionHash: executionHash
155+
};
156+
} catch (error) {
157+
logger.error(`Error fetching AxelarScan execution hash for ${hash}: ${error}`);
158+
// Fallback to original hash if fetching fails
159+
return {
160+
transactionExplorerLink: EXPLORER_LINK_BUILDERS[hashKey](hash, rampState, quote),
161+
transactionHash: hash
162+
};
163+
}
164+
}
165+
166+
// For other hash types, use them directly
60167
return {
61168
transactionExplorerLink: EXPLORER_LINK_BUILDERS[hashKey](hash, rampState, quote),
62169
transactionHash: hash

apps/api/src/api/services/ramp/ramp.service.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,31 @@ export class RampService extends BaseRampService {
430430
? rampState.phaseHistory[rampState.phaseHistory.length - 2].phase
431431
: "initial";
432432

433-
const { transactionExplorerLink, transactionHash } = getFinalTransactionHashForRamp(rampState, quote);
433+
// Get or compute final transaction hash and explorer link
434+
let transactionHash = rampState.state.finalTransactionHash;
435+
let transactionExplorerLink = rampState.state.finalTransactionExplorerLink;
436+
437+
// If not stored yet and ramp is complete, compute and store them
438+
if (
439+
rampState.type === RampDirection.BUY &&
440+
rampState.currentPhase === "complete" &&
441+
(!transactionHash || !transactionExplorerLink)
442+
) {
443+
const result = await getFinalTransactionHashForRamp(rampState, quote);
444+
transactionHash = result.transactionHash;
445+
transactionExplorerLink = result.transactionExplorerLink;
446+
447+
// Store the computed values in the state for future use
448+
if (transactionHash && transactionExplorerLink) {
449+
await rampState.update({
450+
state: {
451+
...rampState.state,
452+
finalTransactionExplorerLink: transactionExplorerLink,
453+
finalTransactionHash: transactionHash
454+
}
455+
});
456+
}
457+
}
434458

435459
const response: GetRampStatusResponse = {
436460
anchorFeeFiat: fiatFees.anchor,

apps/api/src/models/subsidy.model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export enum SubsidyToken {
1111
BRLA = "BRLA",
1212
EURC = "EURC",
1313
USDC = "USDC",
14-
MATIC = "MATIC"
14+
MATIC = "MATIC",
15+
BRL = "BRL"
1516
}
1617

1718
export interface SubsidyAttributes {

apps/rebalancer/src/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ async function checkForRebalancing() {
1919
}
2020

2121
const config = getConfig();
22-
if (
23-
brlaPool.coverageRatio >= 1 + config.rebalancingThreshold ||
24-
usdcAxlPool.coverageRatio <= 1 - config.rebalancingThreshold
25-
) {
22+
if (brlaPool.coverageRatio >= 1 + config.rebalancingThreshold && usdcAxlPool.coverageRatio <= 1) {
2623
console.log("Coverage ratios of BRLA and USDC.axl require rebalancing.");
2724
// Proceed with rebalancing
2825
const amountAxlUsdc = config.rebalancingUsdToBrlAmount;

0 commit comments

Comments
 (0)