You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
4.6 KiB
178 lines
4.6 KiB
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); |
|
});
|
|
|