aboutsummaryrefslogtreecommitdiffstats
path: root/tools/rates-test.js
blob: 88119f3fa54789a95a034d14aac48b587620686a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// What a payment was worth WHEN IT HAPPENED. The price is sampled as the app
// runs and kept as a series; a payment's fiat value is read off the sample
// nearest its timestamp, and a payment older than anything we sampled is
// priced at today's number and SAID to be — never quietly.
//
// Run: bun tools/rates-test.js
let fails = 0;
const check = (name, ok, detail = '') => {
  console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 90) : ''}`);
  if (!ok) fails++;
};
globalThis.localStorage = {
  _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); },
  removeItem(k) { delete this._d[k]; }, key(i) { return Object.keys(this._d)[i] ?? null; },
  get length() { return Object.keys(this._d).length; },
};
const HOUR = 3600;
const now = Math.floor(Date.now() / 1000);
// a week of hourly samples, climbing from 50k to 100k
const series = [];
for (let i = 7 * 24; i >= 0; i--) series.push([now - i * HOUR, 50_000 + (7 * 24 - i) * 300]);
localStorage.setItem('btc-wallet-rate-series', JSON.stringify(series));
localStorage.setItem('btc-wallet-rates', JSON.stringify({ USD: series[series.length - 1][1], EUR: 1234 }));

const { rateAt, rateNow, fmtFiat, getCurrency, setCurrency, currencies, RATE_TOLERANCE } = await import('../src/rates.js');
const { parseAmount, SATS } = await import('../src/format.js');

check('the live price is the newest sample', rateNow() === series[series.length - 1][1], String(rateNow()));

// a payment three days ago is priced at the price three days ago
const threeDays = now - 3 * 24 * HOUR;
const then = rateAt(threeDays);
const want = series.find(([ts]) => ts === threeDays)[1];
check('a payment is priced at the moment it happened', then.rate === want && !then.stale, `${then.rate} vs ${want}`);
check("...which isn't today's price", then.rate !== rateNow(), `${then.rate} vs ${rateNow()}`);

// a payment between samples takes the nearest one
const between = rateAt(threeDays + 900);
check('between samples, the nearest one wins', between.rate === want && !between.stale, String(between.rate));

// older than anything we have: today's price, flagged
const ancient = rateAt(now - 400 * 24 * HOUR);
check('an older payment than we have samples for is flagged', ancient.stale === true && ancient.rate === rateNow(), JSON.stringify(ancient));
check('...and the tolerance is a day', RATE_TOLERANCE === 24 * HOUR, String(RATE_TOLERANCE));

// no timestamp at all (a balance, a form) is simply the live price
check('an amount with no time is the live price', rateAt(null).rate === rateNow() && !rateAt(null).stale);

// typing money
check('typing 5 with the unit on fiat buys five dollars of bitcoin',
  parseAmount('5', 'fiat', 100_000) === Math.round((5 / 100_000) * SATS), String(parseAmount('5', 'fiat', 100_000)));
check('...and without a price it refuses rather than guessing', parseAmount('5', 'fiat', null) === null);
check('sats and BTC are untouched', parseAmount('2100', 'sats') === 2100 && parseAmount('0.001', 'btc') === 100_000);

// formatting
check('an amount formats as money', /\$1[,.]/.test(fmtFiat(SATS / 100, 100_000)), fmtFiat(SATS / 100, 100_000));
check('a small amount keeps more decimals', fmtFiat(100, 100_000).includes('0.10'), fmtFiat(100, 100_000));
check('a currency Intl has never heard of still renders', /XYZ/.test(fmtFiat(SATS, 10, 'XYZ')), fmtFiat(SATS, 10, 'XYZ'));

// changing currency drops the series rather than mislabelling it
check('the currency list comes from the rates map', currencies().includes('EUR'), currencies().join(','));
setCurrency('EUR');
check('changing currency starts the history again', JSON.parse(localStorage.getItem('btc-wallet-rate-series')).length <= 1,
  localStorage.getItem('btc-wallet-rate-series').slice(0, 40));
check('...and the live price follows it', rateNow() === 1234, String(rateNow()));
check('the choice sticks', getCurrency() === 'EUR');

console.log(fails ? `\n❌ ${fails} failed` : '\n✅ prices are remembered, not invented');
process.exit(fails ? 1 : 0);