const { updateCachedPrice, loadCache } = require('./price_engine'); const { fetchPair } = require('../backend/providers/klingex'); async function fetchJson(url) { const res = await fetch(url, { headers: { 'accept': 'application/json', 'user-agent': 'otb-oracle/0.3' } }); if (!res.ok) { throw new Error(`HTTP ${res.status} from ${url}`); } return res.json(); } async function fetchCoinGeckoSimplePrice(ids) { const joined = Array.isArray(ids) ? ids.join(',') : String(ids || ''); const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(joined)}&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 hasFinitePrice(value) { return value !== null && value !== undefined && Number.isFinite(Number(value)) && Number(value) > 0; } function getCachedCadPerUsd() { try { const cache = loadCache(); const usdc = cache?.assets?.USDC_ARB || {}; const usd = Number(usdc.price_usd); const cad = Number(usdc.price_cad); if (hasFinitePrice(usd) && hasFinitePrice(cad)) { return cad / usd; } if (hasFinitePrice(cad)) { return cad; } } catch (_) { } return 1.38; } function maybeUpdateFromCoinGecko(pairKey, sourceData, now, sourceStatus = 'primary') { 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: sourceStatus, updated_at: now }); return true; } function maybeUpdateFromCoinPaprika(pairKey, tickerData, cadPerUsd, now, sourceStatus = 'primary') { 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: sourceStatus, updated_at: now }); return true; } async function updateFromPaprika(pairKey, coinId, cadPerUsd, now, updatedPairs, sourceStatus = 'primary') { try { const ticker = await fetchCoinPaprikaTicker(coinId); if (maybeUpdateFromCoinPaprika(pairKey, ticker, cadPerUsd, now, sourceStatus)) { updatedPairs.push(pairKey); return true; } } catch (err) { console.error(`CoinPaprika ${pairKey} fetch failed: ${err.message}`); } return false; } async function updateETHOFromKlingEx(now, updatedPairs, cadPerUsd) { try { const r = await fetchPair('ETHO-USDT'); const usd = Number(r.price); if (!hasFinitePrice(usd)) { throw new Error(`KlingEx returned non-usable ETHO price: ${r.price}`); } const cad = Number((usd * cadPerUsd).toFixed(8)); updateCachedPrice('ETHO_ETHO', { price_usd: usd, price_cad: cad, source: r.source || 'klingex', source_status: 'primary', updated_at: now }); updatedPairs.push('ETHO_ETHO'); return true; } catch (err) { console.error(`KlingEx ETHO fetch failed: ${err.message}`); } // Last-resort fallback only. try { const cgData = await fetchCoinGeckoSimplePrice(['ether-1']); if (maybeUpdateFromCoinGecko('ETHO_ETHO', cgData['ether-1'], now, 'fallback')) { updatedPairs.push('ETHO_ETHO'); return true; } console.error('CoinGecko ETHO fallback returned no usable ether-1 price'); } catch (err) { console.error(`CoinGecko ETHO fallback failed: ${err.message}`); } return false; } async function main() { const now = new Date().toISOString(); const updatedPairs = []; let cadPerUsd = getCachedCadPerUsd(); await updateFromPaprika('USDC_ARB', 'usdc-usd-coin', cadPerUsd, now, updatedPairs, 'primary'); cadPerUsd = getCachedCadPerUsd(); await updateFromPaprika('ETH_ETH', 'eth-ethereum', cadPerUsd, now, updatedPairs, 'primary'); // ETHO uses KlingEx primary; CoinGecko only as last fallback. await updateETHOFromKlingEx(now, updatedPairs, cadPerUsd); await updateFromPaprika('ETI_ETICA', 'eti-etica', cadPerUsd, now, updatedPairs, 'fallback'); await updateFromPaprika('EGAZ_ETICA', 'egaz-egaz', cadPerUsd, now, updatedPairs, 'fallback'); 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); });