const { updateCachedPrice, loadCache } = require('./price_engine'); async function fetchJson(url) { const res = await fetch(url, { headers: { 'accept': 'application/json', 'user-agent': 'otb-oracle/0.2' } }); if (!res.ok) { throw new Error(`HTTP ${res.status} from ${url}`); } return res.json(); } async function fetchCoinGeckoSimplePrice() { const ids = [ 'usd-coin', 'ethereum', 'ether-1', 'etica' ].join(','); const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,cad`; return fetchJson(url); } async function fetchCoinPaprikaTicker(coinId) { const url = `https://api.coinpaprika.com/v1/tickers/${encodeURIComponent(coinId)}`; return fetchJson(url); } function getCadPerUsd(coingeckoData) { if (coingeckoData?.['usd-coin']?.cad && Number.isFinite(Number(coingeckoData['usd-coin'].cad))) { return Number(coingeckoData['usd-coin'].cad); } try { const cache = loadCache(); const cachedCad = cache?.assets?.USDC_ARB?.price_cad; if (cachedCad && Number.isFinite(Number(cachedCad))) { return Number(cachedCad); } } catch (_) { } return 1.38; } function hasFinitePrice(value) { return value !== null && value !== undefined && Number.isFinite(Number(value)) && Number(value) > 0; } function maybeUpdateFromCoinGecko(pairKey, cgKey, sourceData, now) { if (!sourceData || !hasFinitePrice(sourceData.usd) || !hasFinitePrice(sourceData.cad)) { return false; } updateCachedPrice(pairKey, { price_usd: Number(sourceData.usd), price_cad: Number(sourceData.cad), source: 'coingecko', source_status: 'primary', updated_at: now }); return true; } function maybeUpdateFromCoinPaprika(pairKey, tickerData, cadPerUsd, now) { const usd = Number(tickerData?.quotes?.USD?.price); if (!hasFinitePrice(usd)) { return false; } const cad = Number((usd * cadPerUsd).toFixed(8)); updateCachedPrice(pairKey, { price_usd: usd, price_cad: cad, source: 'coinpaprika', source_status: 'fallback', updated_at: now }); return true; } async function main() { const now = new Date().toISOString(); const updatedPairs = []; let cgData = {}; try { cgData = await fetchCoinGeckoSimplePrice(); } catch (err) { console.error(`CoinGecko fetch failed: ${err.message}`); cgData = {}; } const cadPerUsd = getCadPerUsd(cgData); if (maybeUpdateFromCoinGecko('USDC_ARB', 'usd-coin', cgData['usd-coin'], now)) { updatedPairs.push('USDC_ARB'); } if (maybeUpdateFromCoinGecko('ETH_ETH', 'ethereum', cgData['ethereum'], now)) { updatedPairs.push('ETH_ETH'); } let ethoUpdated = maybeUpdateFromCoinGecko('ETHO_ETHO', 'ether-1', cgData['ether-1'], now); if (!ethoUpdated) { try { const pap = await fetchCoinPaprikaTicker('etho-ethoprotocol'); if (maybeUpdateFromCoinPaprika('ETHO_ETHO', pap, cadPerUsd, now)) { updatedPairs.push('ETHO_ETHO'); ethoUpdated = true; } } catch (err) { console.error(`Coinpaprika ETHO fallback failed: ${err.message}`); } } else { updatedPairs.push('ETHO_ETHO'); } let etiUpdated = maybeUpdateFromCoinGecko('ETI_ETICA', 'etica', cgData['etica'], now); if (!etiUpdated) { try { const pap = await fetchCoinPaprikaTicker('eti-etica'); if (maybeUpdateFromCoinPaprika('ETI_ETICA', pap, cadPerUsd, now)) { updatedPairs.push('ETI_ETICA'); etiUpdated = true; } } catch (err) { console.error(`Coinpaprika ETI fallback failed: ${err.message}`); } } else { updatedPairs.push('ETI_ETICA'); } try { const papEgaz = await fetchCoinPaprikaTicker('egaz-egaz'); if (maybeUpdateFromCoinPaprika('EGAZ_ETICA', papEgaz, cadPerUsd, now)) { updatedPairs.push('EGAZ_ETICA'); } } catch (err) { console.error(`Coinpaprika EGAZ fetch failed: ${err.message}`); } console.log(JSON.stringify({ ok: true, fetched_at: now, cad_per_usd: cadPerUsd, updated_pairs: updatedPairs }, null, 2)); } main().catch(err => { console.error(`fetch_prices.js failed: ${err.message}`); process.exit(1); });