diff --git a/src/contexts/PriceContext.tsx b/src/contexts/PriceContext.tsx index 92bd5e0bc..f49ee85da 100644 --- a/src/contexts/PriceContext.tsx +++ b/src/contexts/PriceContext.tsx @@ -1,5 +1,5 @@ import { useNetwork } from '@/hooks/useNetwork'; -import { dexieApiUrl } from '@/lib/urls'; +import { dexieApiUrl, forgeApiUrl } from '@/lib/urls'; import { createContext, ReactNode, @@ -56,32 +56,58 @@ export function PriceProvider({ children }: { children: ReactNode }) { intervalRef.current = null; } - const fetchCatPrices = async () => { - try { - const response = await fetch( - dexieApiUrl('v3/prices/tickers', isTestnet), - ); + const fetchTickers = async (url: string): Promise => { + const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - const data: DexieResponse = await response.json(); - const tickers = data.tickers.reduce( - (acc: Record, ticker: DexieTicker) => { - acc[ticker.base_currency.toLowerCase()] = { - lastPrice: ticker.last_price ? Number(ticker.last_price) : null, - askPrice: ticker.ask ? Number(ticker.ask) : null, - }; - return acc; - }, - {}, - ); - setCatPrices(tickers); - } catch (error) { - console.error('Failed to fetch CAT prices:', error); - setCatPrices({}); + const data: DexieResponse = await response.json(); + return Array.isArray(data.tickers) ? data.tickers : []; + }; + + const fetchCatPrices = async () => { + // Dexie prices tokens from offers completed on Dexie. A token that trades + // through Forge's pools never appears there, so Forge's tickers (the mid of + // each token's deepest XCH pool, in Dexie's ticker shape) fill the gaps. + // Dexie stays the primary source: Forge only prices tokens Dexie has no + // last price for, and either source failing leaves the other in place. + const [dexie, forge] = await Promise.allSettled([ + fetchTickers(dexieApiUrl('v3/prices/tickers', isTestnet)), + fetchTickers(forgeApiUrl('v1/prices/tickers', isTestnet)), + ]); + + if (dexie.status === 'rejected') { + console.error('Failed to fetch CAT prices from Dexie:', dexie.reason); + } + if (forge.status === 'rejected') { + console.warn('Failed to fetch CAT prices from Forge:', forge.reason); } + + const tickers: Record = {}; + const add = (ticker: DexieTicker) => { + const assetId = ticker.base_currency?.toLowerCase(); + if (!assetId) return; + const lastPrice = ticker.last_price ? Number(ticker.last_price) : null; + const askPrice = ticker.ask ? Number(ticker.ask) : null; + const prior = tickers[assetId]; + tickers[assetId] = { + lastPrice: + lastPrice !== null && Number.isFinite(lastPrice) + ? lastPrice + : (prior?.lastPrice ?? null), + askPrice: + askPrice !== null && Number.isFinite(askPrice) + ? askPrice + : (prior?.askPrice ?? null), + }; + }; + + if (forge.status === 'fulfilled') forge.value.forEach(add); + if (dexie.status === 'fulfilled') dexie.value.forEach(add); + + setCatPrices(tickers); }; const fetchChiaPrice = async () => { diff --git a/src/lib/urls.ts b/src/lib/urls.ts index adf1e93ce..de8209695 100644 --- a/src/lib/urls.ts +++ b/src/lib/urls.ts @@ -20,6 +20,12 @@ function mintGardenApiBase(isTestnet: boolean): string { return `https://api${isTestnet ? '.testnet' : ''}.mintgarden.io`; } +function forgeApiBase(isTestnet: boolean): string { + return isTestnet + ? 'https://forge-responder.up.railway.app' + : 'https://forge-ui-production.up.railway.app'; +} + // ── Spacescan ───────────────────────────────────────────────────────────────── export function spacescanCoinUrl(coinId: string, isTestnet: boolean): string { @@ -85,6 +91,13 @@ export function mintGardenApiUrl(path: string, isTestnet: boolean): string { return `${mintGardenApiBase(isTestnet)}/${path}`; } +// ── Forge — API ────────────────────────────────────────────────────────────── +// `path` must include the API version prefix (e.g. 'v1/prices/tickers'). + +export function forgeApiUrl(path: string, isTestnet: boolean): string { + return `${forgeApiBase(isTestnet)}/api/${path}`; +} + // ── ChiaOffer — API ───────────────────────────────────────────────────────── // `id` is the offer id.