From 762f98debd029c8d479003ee44c6e5ca65e674a5 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 26 Sep 2025 11:10:47 +0530 Subject: [PATCH 1/4] Feat: Update exchange rate calculation logic --- src/app/api/exchange-rate/route.ts | 76 ++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/src/app/api/exchange-rate/route.ts b/src/app/api/exchange-rate/route.ts index 459f5b803..d4a280a1d 100644 --- a/src/app/api/exchange-rate/route.ts +++ b/src/app/api/exchange-rate/route.ts @@ -39,8 +39,65 @@ export async function GET(request: NextRequest) { ) } - const pairKey = `${fromUc}-${toUc}` - const reversePairKey = `${toUc}-${fromUc}` + // If either currency is USD, handle direct conversion + if (fromUc === 'USD' || toUc === 'USD') { + const pairKey = `${fromUc}-${toUc}` + const reversePairKey = `${toUc}-${fromUc}` + + // Check if we should use Bridge for this pair or its reverse + const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) + const shouldUseBridgeReverse = BRIDGE_PAIRS.has(reversePairKey) + + if (shouldUseBridge || shouldUseBridgeReverse) { + // For Bridge pairs, we need to determine which rate to use + let bridgeResult + if (shouldUseBridge) { + // Direct pair (e.g., USD→EUR): use sell_rate + bridgeResult = await fetchFromBridge(fromUc, toUc, 'sell_rate', false) + } else { + // Reverse pair (e.g., EUR→USD): fetch USD→EUR and use buy_rate, then invert + bridgeResult = await fetchFromBridge(toUc, fromUc, 'buy_rate', true) + } + + if (bridgeResult) { + return bridgeResult + } + // Fall back to Frankfurter if Bridge fails + console.warn(`Bridge failed for ${pairKey}, falling back to Frankfurter`) + } + + // Use Frankfurter for all other pairs or as fallback + return await fetchFromFrankfurter(fromUc, toUc) + } + + // For non-USD pairs, convert through USD: from → USD → to + const fromToUsdRate = await getExchangeRate(fromUc, 'USD') + const usdToToRate = await getExchangeRate('USD', toUc) + + if (!fromToUsdRate || !usdToToRate) { + return NextResponse.json({ error: 'Failed to fetch intermediate USD rates' }, { status: 500 }) + } + + const combinedRate = fromToUsdRate * usdToToRate + + return NextResponse.json( + { rate: combinedRate }, + { + headers: { + 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', + }, + } + ) + } catch (error) { + console.error('Exchange rate API error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +async function getExchangeRate(from: string, to: string): Promise { + try { + const pairKey = `${from}-${to}` + const reversePairKey = `${to}-${from}` // Check if we should use Bridge for this pair or its reverse const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) @@ -51,24 +108,27 @@ export async function GET(request: NextRequest) { let bridgeResult if (shouldUseBridge) { // Direct pair (e.g., USD→EUR): use sell_rate - bridgeResult = await fetchFromBridge(fromUc, toUc, 'sell_rate', false) + bridgeResult = await fetchFromBridge(from, to, 'sell_rate', false) } else { // Reverse pair (e.g., EUR→USD): fetch USD→EUR and use buy_rate, then invert - bridgeResult = await fetchFromBridge(toUc, fromUc, 'buy_rate', true) + bridgeResult = await fetchFromBridge(to, from, 'buy_rate', true) } if (bridgeResult) { - return bridgeResult + const data = await bridgeResult.json() + return data.rate } // Fall back to Frankfurter if Bridge fails console.warn(`Bridge failed for ${pairKey}, falling back to Frankfurter`) } // Use Frankfurter for all other pairs or as fallback - return await fetchFromFrankfurter(fromUc, toUc) + const frankfurterResult = await fetchFromFrankfurter(from, to) + const data = await frankfurterResult.json() + return data.rate } catch (error) { - console.error('Exchange rate API error:', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error(`Failed to get exchange rate for ${from}-${to}:`, error) + return null } } From 022dc6b07b91860e26b7d28440cdab191ae640ed Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 26 Sep 2025 11:32:44 +0530 Subject: [PATCH 2/4] feat: integrate Manteca API for LATAM currency exchange rates --- src/app/api/exchange-rate/route.ts | 59 +++++++++++++++++++++++++ src/constants/countryCurrencyMapping.ts | 6 +-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/app/api/exchange-rate/route.ts b/src/app/api/exchange-rate/route.ts index d4a280a1d..24a55e3fa 100644 --- a/src/app/api/exchange-rate/route.ts +++ b/src/app/api/exchange-rate/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' +import { mantecaApi } from '@/services/manteca' interface ExchangeRateResponse { rate: number @@ -13,6 +14,9 @@ interface BridgeExchangeRateResponse { // Currency pairs that should use Bridge API (USD to these currencies only) const BRIDGE_PAIRS = new Set(['USD-EUR', 'USD-MXN', 'USD-BRL']) +// LATAM currencies that should use Manteca API +const MANTECA_CURRENCIES = new Set(['ARS', 'BRL', 'COP', 'CRC', 'PUSD', 'GTQ', 'PHP', 'BOB']) + export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams @@ -44,6 +48,23 @@ export async function GET(request: NextRequest) { const pairKey = `${fromUc}-${toUc}` const reversePairKey = `${toUc}-${fromUc}` + // Check if either currency is a LATAM currency that uses Manteca + if (MANTECA_CURRENCIES.has(fromUc) || MANTECA_CURRENCIES.has(toUc)) { + const mantecaRate = await fetchFromManteca(fromUc, toUc) + if (mantecaRate !== null) { + return NextResponse.json( + { rate: mantecaRate }, + { + headers: { + 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', + }, + } + ) + } + // Fall back to other providers if Manteca fails + console.warn(`Manteca failed for ${pairKey}, falling back to other providers`) + } + // Check if we should use Bridge for this pair or its reverse const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) const shouldUseBridgeReverse = BRIDGE_PAIRS.has(reversePairKey) @@ -99,6 +120,11 @@ async function getExchangeRate(from: string, to: string): Promise const pairKey = `${from}-${to}` const reversePairKey = `${to}-${from}` + // Check if either currency is a LATAM currency that uses Manteca + if (MANTECA_CURRENCIES.has(from) || MANTECA_CURRENCIES.has(to)) { + return await fetchFromManteca(from, to) + } + // Check if we should use Bridge for this pair or its reverse const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) const shouldUseBridgeReverse = BRIDGE_PAIRS.has(reversePairKey) @@ -132,6 +158,39 @@ async function getExchangeRate(from: string, to: string): Promise } } +async function fetchFromManteca(from: string, to: string): Promise { + console.log('Fetching from manteca') + try { + // Manteca API provides rates against USDC, so we need to handle different scenarios + if (from === 'USD' && MANTECA_CURRENCIES.has(to)) { + // USD → LATAM currency: use sell rate (selling USD to get LATAM currency) + const response = await mantecaApi.getPrices({ asset: 'USDC', against: to }) + return Number(response.effectiveSell) + } else if (MANTECA_CURRENCIES.has(from) && to === 'USD') { + // LATAM currency → USD: use buy rate (buying USD with LATAM currency) + const response = await mantecaApi.getPrices({ asset: 'USDC', against: from }) + return 1 / Number(response.effectiveBuy) + } else if (MANTECA_CURRENCIES.has(from) && MANTECA_CURRENCIES.has(to)) { + // LATAM currency → LATAM currency: convert through USD + const fromResponse = await mantecaApi.getPrices({ asset: 'USDC', against: from }) + const toResponse = await mantecaApi.getPrices({ asset: 'USDC', against: to }) + + // from → USD → to + const fromToUsd = 1 / Number(fromResponse.effectiveBuy) + const usdToTo = Number(toResponse.effectiveSell) + return fromToUsd * usdToTo + } else { + // One currency is LATAM, the other is not USD - this shouldn't happen in our flow + // but we'll return null to fall back to other providers + console.warn(`Unsupported Manteca conversion: ${from} → ${to}`) + return null + } + } catch (error) { + console.error(`Manteca API error for ${from}-${to}:`, error) + return null + } +} + async function fetchFromBridge( from: string, to: string, diff --git a/src/constants/countryCurrencyMapping.ts b/src/constants/countryCurrencyMapping.ts index 5c5e93f7a..baa92e918 100644 --- a/src/constants/countryCurrencyMapping.ts +++ b/src/constants/countryCurrencyMapping.ts @@ -42,9 +42,9 @@ export const countryCurrencyMappings: CountryCurrencyMapping[] = [ // Mexico { currencyCode: 'MXN', currencyName: 'Mexican Peso', country: 'Mexico', flagCode: 'mx', path: 'mexico' }, - // Coming Soon - { currencyCode: 'BRL', currencyName: 'Brazilian Real', country: 'Brazil', flagCode: 'br', comingSoon: true }, - { currencyCode: 'ARS', currencyName: 'Argentine Peso', country: 'Argentina', flagCode: 'ar', comingSoon: true }, + // LATAM Countries + { currencyCode: 'BRL', currencyName: 'Brazilian Real', country: 'Brazil', flagCode: 'br' }, + { currencyCode: 'ARS', currencyName: 'Argentine Peso', country: 'Argentina', flagCode: 'ar' }, ] export default countryCurrencyMappings From f68f0a86e1fca4e325cd2660c52da86758e63b7c Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 30 Sep 2025 11:16:46 +0530 Subject: [PATCH 3/4] remove direct API call to bridge and use `getCurrencyPrice` --- src/app/api/exchange-rate/route.ts | 193 +++++++---------------------- 1 file changed, 43 insertions(+), 150 deletions(-) diff --git a/src/app/api/exchange-rate/route.ts b/src/app/api/exchange-rate/route.ts index 24a55e3fa..b311e8912 100644 --- a/src/app/api/exchange-rate/route.ts +++ b/src/app/api/exchange-rate/route.ts @@ -1,19 +1,10 @@ import { NextRequest, NextResponse } from 'next/server' -import { mantecaApi } from '@/services/manteca' +import { getCurrencyPrice } from '@/app/actions/currency' interface ExchangeRateResponse { rate: number } -interface BridgeExchangeRateResponse { - midmarket_rate: string - buy_rate: string - sell_rate: string -} - -// Currency pairs that should use Bridge API (USD to these currencies only) -const BRIDGE_PAIRS = new Set(['USD-EUR', 'USD-MXN', 'USD-BRL']) - // LATAM currencies that should use Manteca API const MANTECA_CURRENCIES = new Set(['ARS', 'BRL', 'COP', 'CRC', 'PUSD', 'GTQ', 'PHP', 'BOB']) @@ -46,14 +37,18 @@ export async function GET(request: NextRequest) { // If either currency is USD, handle direct conversion if (fromUc === 'USD' || toUc === 'USD') { const pairKey = `${fromUc}-${toUc}` - const reversePairKey = `${toUc}-${fromUc}` - // Check if either currency is a LATAM currency that uses Manteca - if (MANTECA_CURRENCIES.has(fromUc) || MANTECA_CURRENCIES.has(toUc)) { - const mantecaRate = await fetchFromManteca(fromUc, toUc) - if (mantecaRate !== null) { + // Check if either currency uses getCurrencyPrice (Manteca or Bridge currencies) + if ( + MANTECA_CURRENCIES.has(fromUc) || + MANTECA_CURRENCIES.has(toUc) || + ['EUR', 'MXN'].includes(fromUc) || + ['EUR', 'MXN'].includes(toUc) + ) { + const currencyPriceRate = await fetchFromCurrencyPrice(fromUc, toUc) + if (currencyPriceRate !== null) { return NextResponse.json( - { rate: mantecaRate }, + { rate: currencyPriceRate }, { headers: { 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', @@ -61,30 +56,8 @@ export async function GET(request: NextRequest) { } ) } - // Fall back to other providers if Manteca fails - console.warn(`Manteca failed for ${pairKey}, falling back to other providers`) - } - - // Check if we should use Bridge for this pair or its reverse - const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) - const shouldUseBridgeReverse = BRIDGE_PAIRS.has(reversePairKey) - - if (shouldUseBridge || shouldUseBridgeReverse) { - // For Bridge pairs, we need to determine which rate to use - let bridgeResult - if (shouldUseBridge) { - // Direct pair (e.g., USD→EUR): use sell_rate - bridgeResult = await fetchFromBridge(fromUc, toUc, 'sell_rate', false) - } else { - // Reverse pair (e.g., EUR→USD): fetch USD→EUR and use buy_rate, then invert - bridgeResult = await fetchFromBridge(toUc, fromUc, 'buy_rate', true) - } - - if (bridgeResult) { - return bridgeResult - } - // Fall back to Frankfurter if Bridge fails - console.warn(`Bridge failed for ${pairKey}, falling back to Frankfurter`) + // Fall back to other providers if getCurrencyPrice fails + console.warn(`getCurrencyPrice failed for ${pairKey}, falling back to other providers`) } // Use Frankfurter for all other pairs or as fallback @@ -117,35 +90,14 @@ export async function GET(request: NextRequest) { async function getExchangeRate(from: string, to: string): Promise { try { - const pairKey = `${from}-${to}` - const reversePairKey = `${to}-${from}` - - // Check if either currency is a LATAM currency that uses Manteca - if (MANTECA_CURRENCIES.has(from) || MANTECA_CURRENCIES.has(to)) { - return await fetchFromManteca(from, to) - } - - // Check if we should use Bridge for this pair or its reverse - const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) - const shouldUseBridgeReverse = BRIDGE_PAIRS.has(reversePairKey) - - if (shouldUseBridge || shouldUseBridgeReverse) { - // For Bridge pairs, we need to determine which rate to use - let bridgeResult - if (shouldUseBridge) { - // Direct pair (e.g., USD→EUR): use sell_rate - bridgeResult = await fetchFromBridge(from, to, 'sell_rate', false) - } else { - // Reverse pair (e.g., EUR→USD): fetch USD→EUR and use buy_rate, then invert - bridgeResult = await fetchFromBridge(to, from, 'buy_rate', true) - } - - if (bridgeResult) { - const data = await bridgeResult.json() - return data.rate - } - // Fall back to Frankfurter if Bridge fails - console.warn(`Bridge failed for ${pairKey}, falling back to Frankfurter`) + // Check if either currency uses getCurrencyPrice (Manteca or Bridge currencies) + if ( + MANTECA_CURRENCIES.has(from) || + MANTECA_CURRENCIES.has(to) || + ['EUR', 'MXN'].includes(from) || + ['EUR', 'MXN'].includes(to) + ) { + return await fetchFromCurrencyPrice(from, to) } // Use Frankfurter for all other pairs or as fallback @@ -158,95 +110,36 @@ async function getExchangeRate(from: string, to: string): Promise } } -async function fetchFromManteca(from: string, to: string): Promise { - console.log('Fetching from manteca') +async function fetchFromCurrencyPrice(from: string, to: string): Promise { + console.log('Fetching from getCurrencyPrice') try { - // Manteca API provides rates against USDC, so we need to handle different scenarios - if (from === 'USD' && MANTECA_CURRENCIES.has(to)) { - // USD → LATAM currency: use sell rate (selling USD to get LATAM currency) - const response = await mantecaApi.getPrices({ asset: 'USDC', against: to }) - return Number(response.effectiveSell) - } else if (MANTECA_CURRENCIES.has(from) && to === 'USD') { - // LATAM currency → USD: use buy rate (buying USD with LATAM currency) - const response = await mantecaApi.getPrices({ asset: 'USDC', against: from }) - return 1 / Number(response.effectiveBuy) - } else if (MANTECA_CURRENCIES.has(from) && MANTECA_CURRENCIES.has(to)) { - // LATAM currency → LATAM currency: convert through USD - const fromResponse = await mantecaApi.getPrices({ asset: 'USDC', against: from }) - const toResponse = await mantecaApi.getPrices({ asset: 'USDC', against: to }) + if (from === 'USD' && (MANTECA_CURRENCIES.has(to) || ['EUR', 'MXN'].includes(to))) { + // USD → other currency: use sell rate (selling USD to get other currency) + const { sell } = await getCurrencyPrice(to) + return sell + } else if ((MANTECA_CURRENCIES.has(from) || ['EUR', 'MXN'].includes(from)) && to === 'USD') { + // Other currency → USD: use buy rate (buying USD with other currency) + const { buy } = await getCurrencyPrice(from) + return 1 / buy + } else if ( + (MANTECA_CURRENCIES.has(from) || ['EUR', 'MXN'].includes(from)) && + (MANTECA_CURRENCIES.has(to) || ['EUR', 'MXN'].includes(to)) + ) { + // Other currency → Other currency: convert through USD + const fromPrices = await getCurrencyPrice(from) + const toPrices = await getCurrencyPrice(to) // from → USD → to - const fromToUsd = 1 / Number(fromResponse.effectiveBuy) - const usdToTo = Number(toResponse.effectiveSell) + const fromToUsd = 1 / fromPrices.buy + const usdToTo = toPrices.sell return fromToUsd * usdToTo } else { - // One currency is LATAM, the other is not USD - this shouldn't happen in our flow - // but we'll return null to fall back to other providers - console.warn(`Unsupported Manteca conversion: ${from} → ${to}`) + // Unsupported conversion + console.warn(`Unsupported getCurrencyPrice conversion: ${from} → ${to}`) return null } } catch (error) { - console.error(`Manteca API error for ${from}-${to}:`, error) - return null - } -} - -async function fetchFromBridge( - from: string, - to: string, - rateType: 'buy_rate' | 'sell_rate', - shouldInvert: boolean -): Promise { - const bridgeAPIKey = process.env.BRIDGE_API_KEY - - if (!bridgeAPIKey) { - console.warn('Bridge API key not set') - return null - } - - try { - const url = `https://api.bridge.xyz/v0/exchange_rates?from=${from.toLowerCase()}&to=${to.toLowerCase()}` - const options: RequestInit & { next?: { revalidate?: number } } = { - method: 'GET', - // Bridge expects header name 'Api-Key' - headers: { 'Api-Key': bridgeAPIKey }, - next: { revalidate: 300 }, // Cache for 5 minutes - } - - const response = await fetch(url, options) - - if (!response.ok) { - console.error(`Bridge API error: ${response.status} ${response.statusText}`) - return null - } - - const bridgeData: BridgeExchangeRateResponse = await response.json() - - // Validate the response structure - if (!bridgeData[rateType]) { - console.error(`Invalid Bridge response: missing ${rateType}`) - return null - } - - let rate = parseFloat(bridgeData[rateType]) - - // If we fetched the reverse pair (e.g., fetched USD→EUR for EUR→USD request), - // we need to invert the rate - if (shouldInvert) { - rate = 1 / rate - } - - const exchangeRate: ExchangeRateResponse = { - rate, - } - - return NextResponse.json(exchangeRate, { - headers: { - 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', - }, - }) - } catch (error) { - console.error('Bridge API exception:', error) + console.error(`getCurrencyPrice error for ${from}-${to}:`, error) return null } } From c489193d6cfbc734064a71345a10163be1f6d7e4 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 30 Sep 2025 11:39:23 +0530 Subject: [PATCH 4/4] fix: ensure from -> usd -> to in fetchFromFrankfurter --- src/app/api/exchange-rate/route.ts | 86 ++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/src/app/api/exchange-rate/route.ts b/src/app/api/exchange-rate/route.ts index b311e8912..8131a2493 100644 --- a/src/app/api/exchange-rate/route.ts +++ b/src/app/api/exchange-rate/route.ts @@ -61,7 +61,18 @@ export async function GET(request: NextRequest) { } // Use Frankfurter for all other pairs or as fallback - return await fetchFromFrankfurter(fromUc, toUc) + const frankfurterRate = await fetchFromFrankfurter(fromUc, toUc) + if (frankfurterRate !== null) { + return NextResponse.json( + { rate: frankfurterRate }, + { + headers: { + 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', + }, + } + ) + } + return NextResponse.json({ error: 'Failed to fetch exchange rates' }, { status: 500 }) } // For non-USD pairs, convert through USD: from → USD → to @@ -101,9 +112,7 @@ async function getExchangeRate(from: string, to: string): Promise } // Use Frankfurter for all other pairs or as fallback - const frankfurterResult = await fetchFromFrankfurter(from, to) - const data = await frankfurterResult.json() - return data.rate + return await fetchFromFrankfurter(from, to) } catch (error) { console.error(`Failed to get exchange rate for ${from}-${to}:`, error) return null @@ -116,10 +125,18 @@ async function fetchFromCurrencyPrice(from: string, to: string): Promise { - const url = `https://api.frankfurter.app/latest?from=${from}&to=${to}` - const options: RequestInit & { next?: { revalidate?: number } } = { - method: 'GET', - next: { revalidate: 300 }, // Cache for 5 minutes - } +async function fetchFromFrankfurter(from: string, to: string): Promise { + try { + // If either currency is USD, do direct conversion + if (from === 'USD' || to === 'USD') { + return await fetchDirectFromFrankfurter(from, to) + } + + // For non-USD pairs, convert through USD: from → USD → to + const fromToUsdRate = await fetchDirectFromFrankfurter(from, 'USD') + const usdToToRate = await fetchDirectFromFrankfurter('USD', to) - const response = await fetch(url, options) + if (!fromToUsdRate || !usdToToRate) { + return null + } - if (!response.ok) { - console.error(`Frankfurter API error: ${response.status} ${response.statusText}`) - return NextResponse.json({ error: 'Failed to fetch exchange rates from API' }, { status: response.status }) + return fromToUsdRate * usdToToRate + } catch (error) { + console.error(`Frankfurter API exception for ${from}-${to}:`, error) + return null } +} - const data = await response.json() +async function fetchDirectFromFrankfurter(from: string, to: string): Promise { + try { + const url = `https://api.frankfurter.app/latest?from=${from}&to=${to}` + const options: RequestInit & { next?: { revalidate?: number } } = { + method: 'GET', + next: { revalidate: 300 }, // Cache for 5 minutes + } - const exchangeRate: ExchangeRateResponse = { - rate: data.rates[to] * 0.995, // Subtract 50bps - } + const response = await fetch(url, options) - return NextResponse.json(exchangeRate, { - headers: { - 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', - }, - }) + if (!response.ok) { + console.error(`Frankfurter API error: ${response.status} ${response.statusText}`) + return null + } + + const data = await response.json() + return data.rates[to] * 0.995 // Subtract 50bps + } catch (error) { + console.error(`Frankfurter direct API exception for ${from}-${to}:`, error) + return null + } }