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.
 
 
 

169 lines
4.1 KiB

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