diff --git a/oracle/fetch_prices.js b/oracle/fetch_prices.js index a38f512..13b08f3 100644 --- a/oracle/fetch_prices.js +++ b/oracle/fetch_prices.js @@ -1,12 +1,13 @@ const { - updateCachedPrice + updateCachedPrice, + loadCache } = require('./price_engine'); async function fetchJson(url) { const res = await fetch(url, { headers: { 'accept': 'application/json', - 'user-agent': 'otb-oracle/0.1' + 'user-agent': 'otb-oracle/0.2' } }); @@ -31,56 +32,137 @@ async function fetchCoinGeckoSimplePrice() { 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 data = await fetchCoinGeckoSimplePrice(); const now = new Date().toISOString(); + const updatedPairs = []; - if (data['usd-coin']) { - updateCachedPrice('USDC_ARB', { - price_usd: data['usd-coin'].usd ?? null, - price_cad: data['usd-coin'].cad ?? null, - source: 'coingecko', - source_status: 'primary', - updated_at: now - }); + let cgData = {}; + try { + cgData = await fetchCoinGeckoSimplePrice(); + } catch (err) { + console.error(`CoinGecko fetch failed: ${err.message}`); + cgData = {}; } - if (data['ethereum']) { - updateCachedPrice('ETH_ETH', { - price_usd: data['ethereum'].usd ?? null, - price_cad: data['ethereum'].cad ?? null, - source: 'coingecko', - source_status: 'primary', - updated_at: now - }); + const cadPerUsd = getCadPerUsd(cgData); + + if (maybeUpdateFromCoinGecko('USDC_ARB', 'usd-coin', cgData['usd-coin'], now)) { + updatedPairs.push('USDC_ARB'); } - if (data['ether-1']) { - updateCachedPrice('ETHO_ETHO', { - price_usd: data['ether-1'].usd ?? null, - price_cad: data['ether-1'].cad ?? null, - source: 'coingecko', - source_status: 'primary', - updated_at: now - }); + if (maybeUpdateFromCoinGecko('ETH_ETH', 'ethereum', cgData['ethereum'], now)) { + updatedPairs.push('ETH_ETH'); } - if (data['etica']) { - updateCachedPrice('ETI_ETICA', { - price_usd: data['etica'].usd ?? null, - price_cad: data['etica'].cad ?? null, - source: 'coingecko', - source_status: 'primary', - updated_at: now - }); + 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, - updated_pairs: ['USDC_ARB', 'ETH_ETH', 'ETHO_ETHO', 'ETI_ETICA'] + cad_per_usd: cadPerUsd, + updated_pairs: updatedPairs }, null, 2)); } + main().catch(err => { console.error(`fetch_prices.js failed: ${err.message}`); process.exit(1); diff --git a/oracle/price_cache.json b/oracle/price_cache.json index 80246c5..72731fb 100644 --- a/oracle/price_cache.json +++ b/oracle/price_cache.json @@ -1,6 +1,6 @@ { "version": "0.1", - "updated_at": "2026-03-15T00:11:11.634Z", + "updated_at": "2026-03-15T00:19:19.555Z", "status": "fresh", "assets": { "USDC_ARB": { @@ -28,11 +28,11 @@ "ETHO_ETHO": { "symbol": "ETHO", "chain": "etho", - "price_usd": 0.00525255, - "price_cad": 0.00714931, - "source": "coingecko", - "source_status": "primary", - "updated_at": "2026-03-15T00:11:11.630Z", + "price_usd": 0.005678438337985232, + "price_cad": 0.00783624, + "source": "coinpaprika", + "source_status": "fallback", + "updated_at": "2026-03-15T00:19:18.791Z", "age_seconds": 0, "stale": false }, diff --git a/oracle/sources.json b/oracle/sources.json index a929d1c..86ef40d 100644 --- a/oracle/sources.json +++ b/oracle/sources.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "version": "0.2", "updated": "2026-03-14", "sources": { @@ -11,6 +11,13 @@ "rate_limit_seconds": 60 }, + "coinpaprika": { + "type": "api", + "url": "https://api.coinpaprika.com/v1/tickers", + "description": "Fallback market price source by coin ID", + "rate_limit_seconds": 60 + }, + "dexscreener": { "type": "api", "url": "https://api.dexscreener.com/latest/dex/tokens",