Skip to content

Replace nabla swap price with fastforex#1190

Open
ebma wants to merge 13 commits into
stagingfrom
replace-nabla-swap-price-with-fastforex
Open

Replace nabla swap price with fastforex#1190
ebma wants to merge 13 commits into
stagingfrom
replace-nabla-swap-price-with-fastforex

Conversation

@ebma

@ebma ebma commented Jun 3, 2026

Copy link
Copy Markdown
Member

Replaces the fiat rates that currently are derived from the swap rates on the Pendulum DEX, with fiat rates from fastforex.

@netlify

netlify Bot commented Jun 3, 2026

Copy link
Copy Markdown

Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 11a7765
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6a3007c688b9a100089314fe
😎 Deploy Preview https://deploy-preview-1190--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jun 3, 2026

Copy link
Copy Markdown

Deploy Preview for vortex-sandbox failed. Why did it fail? →

Name Link
🔨 Latest commit 11a7765
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6a3007c68d95e00008a455f0

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR switches the API’s fiat FX rate sourcing from Pendulum/Nabla swap-derived rates to fastforex.io (with CoinGecko as a fallback), updating configuration, documentation, and tests accordingly.

Changes:

  • Added Fastforex configuration (API key + base URL) to the API config and .env.example.
  • Updated PriceFeedService to fetch USD→fiat rates from fastforex first, then fall back to CoinGecko, with caching preserved.
  • Updated the security spec and reworked unit tests to cover the new fastforex-first behavior and fallback paths.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
docs/security-spec/03-ramp-engine/quote-lifecycle.md Updates quote lifecycle/security invariants to reflect fastforex fiat FX sourcing with fallback.
apps/api/src/config/vars.ts Adds priceProviders.fastforex config (API key + base URL).
apps/api/src/api/services/priceFeed.service.ts Implements fastforex-backed USD→fiat exchange rate fetching with CoinGecko fallback and caching.
apps/api/src/api/services/priceFeed.service.test.ts Updates tests to validate fastforex primary + CoinGecko fallback behavior and caching.
apps/api/.env.example Documents FASTFOREX_API_KEY and FASTFOREX_API_URL.
Comments suppressed due to low confidence (1)

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

  • getUsdToFiatExchangeRate is documented/used as a USD→fiat helper, but it accepts RampCurrency (fiat or on-chain). If this is ever called with a non-fiat token (e.g. an EVM symbol), it will make an invalid fastforex/CoinGecko request and fail at runtime. Add an explicit fiat guard up front so misuse fails fast with a clear error.
  public async getUsdToFiatExchangeRate(toCurrency: RampCurrency): Promise<number> {
    const fromCurrency = "USD";

    const cacheKey = `fiat:${fromCurrency}:${toCurrency}`;
    const cachedEntry = this.fiatExchangeRateCache.get(cacheKey);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +435 to +444
const url = new URL(`${this.fastforexApiBaseUrl}/fetch-one`);
url.searchParams.append("from", fromCurrency);
url.searchParams.append("to", toCurrency);

const headers: HeadersInit = { Accept: "application/json" };
if (this.fastforexApiKey) {
headers["X-API-Key"] = this.fastforexApiKey;
}

const response = await fetchWithTimeout(url.toString(), { headers });
Comment on lines +270 to 273
it("should return the same amount when currencies match", async () => {
const result = await priceFeedService.convertCurrency("100", "BRL" as any, "BRL" as any);
expect(result).toBe("100.00");
});
Comment on lines 275 to 277
it("should perform 1:1 conversion between USD-like stablecoins", async () => {
const result = await priceFeedService.convertCurrency("100", "USDC" as any, "USDT" as any);
expect(result).toBe("100");
Comment on lines +4 to +6
const originalEnv = { ...process.env };
const originalFetch = global.fetch;

Comment on lines +73 to +77
beforeEach(() => {
// Store original env and Date.now
originalEnv = { ...process.env };
originalDateNow = Date.now;

// Mock environment variables for each test
process.env = {
...originalEnv, // Start with original to avoid missing Node internal vars
COINGECKO_API_KEY: "test-api-key",
COINGECKO_API_URL: "https://api.coingecko.com/api/v3",
CRYPTO_CACHE_TTL_MS: "300000", // 5 minutes
FIAT_CACHE_TTL_MS: "300000" // 5 minutes
} as any;

// Create a fresh fetch mock for each test
fetchMock = mock(() =>
Promise.resolve({
clone() {
return this;
},
headers: new Headers(),
json: () => Promise.resolve(mockCoinGeckoResponse),
ok: true,
redirected: false,
status: 200,
statusText: "OK",
text: () => Promise.resolve(JSON.stringify(mockCoinGeckoResponse)),
type: "basic",
url: ""
} as Response)
);

// Mock fetch
global.fetch = fetchMock as any;

// Reset mocks before each test
(getTokenOutAmountMock as any).mockClear();
// Reset Nabla mock to default implementation if needed (if tests modify its behavior)
(getTokenOutAmountMock as any).mockImplementation(async () => ({
effectiveExchangeRate: "1.25",
preciseQuotedAmountOut: { preciseBigDecimal: { toString: () => "1.25" } },
roundedDownQuotedAmountOut: { toString: () => "1.25" },
swapFee: { toString: () => "0.01" }
}));

// Ensure singleton is reset *before* each test to pick up fresh env vars/mocks
fetchMock = mock(async () => mockFastforexResponse(5.85));
global.fetch = fetchMock as unknown as typeof fetch;
getApiMock.mockClear();
Comment on lines +83 to +87
afterEach(() => {
// Restore fetch
Date.now = originalDateNow;
global.fetch = originalFetch;
// @ts-expect-error - accessing private property for testing
PriceFeedService.instance = undefined;
Copilot finished work on behalf of ebma June 3, 2026 08:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Comment thread apps/api/src/api/services/priceFeed.service.test.ts
Comment thread apps/api/src/api/services/priceFeed.service.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.

Comment on lines +483 to +499
private async assertFastforexRateWithinSanityBand(targetCurrency: RampCurrency, fastforexRate: number): Promise<void> {
this.assertValidFiatRate("fastforex", "USD", targetCurrency, fastforexRate);

const referenceRate = await this.getCryptoPrice("usd-coin", targetCurrency.toLowerCase());
this.assertValidFiatRate("CoinGecko", "USD", targetCurrency, referenceRate);

const spread = Big(fastforexRate).minus(referenceRate).abs().div(referenceRate).toNumber();
const limit = FASTFOREX_SANITY_SPREAD_LIMITS[targetCurrency] ?? 0.03;

if (spread > limit) {
throw new Error(
`fastforex USD-${targetCurrency} rate ${fastforexRate} differs from CoinGecko reference ${referenceRate} by ${(
spread * 100
).toFixed(2)}%, above ${(limit * 100).toFixed(2)}% limit`
);
}
}
Comment thread apps/api/src/api/services/priceFeed.service.ts
Comment on lines 325 to 333
} catch (error) {
if (error instanceof Error) {
logger.error(`Error converting ${amount} from ${fromCurrency} to ${toCurrency}: ${error.message}`);
} else {
logger.error(`Unknown error converting ${amount} from ${fromCurrency} to ${toCurrency}`);
}

// Return the original amount as fallback
logger.warn(`Returning original amount ${amount} as fallback due to conversion error`);
return amount;
throw error;
}
Comment on lines +15 to +25
global.setInterval = mock(() => 0) as unknown as typeof setInterval;

process.env = {
...originalEnv,
COINGECKO_API_KEY: "test-api-key",
COINGECKO_API_URL: "https://api.coingecko.com/api/v3",
CRYPTO_CACHE_TTL_MS: "300000",
FASTFOREX_API_KEY: "test-fastforex-key",
FASTFOREX_API_URL: "https://api.fastforex.io",
FIAT_CACHE_TTL_MS: "300000"
} as NodeJS.ProcessEnv;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants