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

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);
});