99 lines
3 KiB
JavaScript
99 lines
3 KiB
JavaScript
|
|
// Fiat prices for every Aegis-supported coin — CoinGecko's free /simple/price
|
||
|
|
// endpoint, one request covers the lot. Opt-in via Settings so a
|
||
|
|
// privacy-conscious user isn't quietly telling CoinGecko when Aegis is open.
|
||
|
|
//
|
||
|
|
// Cache is in-memory (returned by fullState() → panel). The addon polls
|
||
|
|
// every 5 min while enabled; each fetch is cheap (~200 B response) and
|
||
|
|
// the free tier tolerates one call/5 min per client easily.
|
||
|
|
//
|
||
|
|
// Trade-off named in the settings copy: CoinGecko sees the browser's IP
|
||
|
|
// + a User-Agent every poll. Not seed-linked, not address-linked, but a
|
||
|
|
// data point. Off by default.
|
||
|
|
|
||
|
|
const COIN_GECKO_IDS = {
|
||
|
|
bch: "bitcoin-cash",
|
||
|
|
btc: "bitcoin",
|
||
|
|
trx: "tron",
|
||
|
|
eth: "ethereum",
|
||
|
|
sol: "solana",
|
||
|
|
sc: "siacoin",
|
||
|
|
dgb: "digibyte",
|
||
|
|
};
|
||
|
|
|
||
|
|
const ENDPOINT = "https://api.coingecko.com/api/v3/simple/price";
|
||
|
|
const POLL_MS = 5 * 60 * 1000;
|
||
|
|
|
||
|
|
module.exports = function makePriceFeed({ log = () => {}, onChange = () => {} } = {}) {
|
||
|
|
const state = {
|
||
|
|
enabled: false,
|
||
|
|
prices: {}, // { <chain>: usd (number) }
|
||
|
|
fetchedAt: null,
|
||
|
|
error: null,
|
||
|
|
loading: false,
|
||
|
|
};
|
||
|
|
let timer = null;
|
||
|
|
|
||
|
|
async function fetchOnce() {
|
||
|
|
if (!state.enabled) return;
|
||
|
|
state.loading = true; state.error = null; onChange();
|
||
|
|
try {
|
||
|
|
const ids = Object.values(COIN_GECKO_IDS).join(",");
|
||
|
|
const url = `${ENDPOINT}?ids=${encodeURIComponent(ids)}&vs_currencies=usd`;
|
||
|
|
const r = await fetch(url);
|
||
|
|
if (!r.ok) throw new Error(`CoinGecko HTTP ${r.status}`);
|
||
|
|
const body = await r.json();
|
||
|
|
const next = {};
|
||
|
|
for (const [chain, cgId] of Object.entries(COIN_GECKO_IDS)) {
|
||
|
|
const usd = body?.[cgId]?.usd;
|
||
|
|
if (typeof usd === "number") next[chain] = usd;
|
||
|
|
}
|
||
|
|
state.prices = next;
|
||
|
|
state.fetchedAt = Date.now();
|
||
|
|
state.error = null;
|
||
|
|
} catch (e) {
|
||
|
|
state.error = e?.message || String(e);
|
||
|
|
log("price fetch failed:", state.error);
|
||
|
|
} finally {
|
||
|
|
state.loading = false;
|
||
|
|
onChange();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function schedule() {
|
||
|
|
clearTimeout(timer);
|
||
|
|
if (!state.enabled) return;
|
||
|
|
timer = setTimeout(async () => { await fetchOnce(); schedule(); }, POLL_MS);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
// Snapshot for the panel: only what the UI needs.
|
||
|
|
snapshot() {
|
||
|
|
return {
|
||
|
|
enabled: state.enabled,
|
||
|
|
prices: state.prices,
|
||
|
|
fetchedAt: state.fetchedAt,
|
||
|
|
error: state.error,
|
||
|
|
loading: state.loading,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
// Turn the feed on/off. Enabling triggers an immediate fetch so the
|
||
|
|
// panel doesn't wait 5 minutes for the first price.
|
||
|
|
async setEnabled(on) {
|
||
|
|
const changed = !!on !== state.enabled;
|
||
|
|
state.enabled = !!on;
|
||
|
|
if (!state.enabled) {
|
||
|
|
state.prices = {}; state.fetchedAt = null; state.error = null;
|
||
|
|
clearTimeout(timer);
|
||
|
|
if (changed) onChange();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
onChange();
|
||
|
|
await fetchOnce();
|
||
|
|
schedule();
|
||
|
|
},
|
||
|
|
// Force-refresh — bound to a manual "refresh" button in the panel.
|
||
|
|
refresh() { return fetchOnce(); },
|
||
|
|
dispose() { clearTimeout(timer); state.enabled = false; },
|
||
|
|
};
|
||
|
|
};
|