aboutsummaryrefslogtreecommitdiffstats
path: root/src/nwc-budget.js
blob: a16e0681e7c5fd463d0ad17d9e6d26267c730e96 (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
// An NWC connection's spending budget resets on a calendar boundary — a day,
// a week or a month — in UTC, so every device of a wallet (and the service
// worker answering while the app is closed) agrees on which window a spend
// fell in without a timezone in the picture. The window key is what the
// connection's spent counter is stamped with: a counter stamped with another
// key is from a window that has passed.

export const PERIODS = ['day', 'week', 'month'];

const pad = (n) => String(n).padStart(2, '0');

// The week runs Monday to Sunday (ISO), keyed by its Monday.
function mondayOf(d) {
  const m = new Date(d);
  const dow = (m.getUTCDay() + 6) % 7; // Monday = 0
  m.setUTCDate(m.getUTCDate() - dow);
  m.setUTCHours(0, 0, 0, 0);
  return m;
}

export function budgetWindow(period, ts = Date.now()) {
  const d = new Date(ts);
  if (period === 'month') return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}`;
  if (period === 'week') { const m = mondayOf(d); return `W${m.getUTCFullYear()}-${pad(m.getUTCMonth() + 1)}-${pad(m.getUTCDate())}`; }
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
}

// When the current window ends, in unix seconds (NIP-47's renews_at).
export function budgetRenewsAt(period, ts = Date.now()) {
  const d = new Date(ts);
  let end;
  if (period === 'month') end = Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
  else if (period === 'week') { const m = mondayOf(d); end = m.getTime() + 7 * 86_400_000; }
  else end = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
  return Math.floor(end / 1000);
}

// NIP-47 names the period as an adverb.
export const renewalPeriod = (period) => (period === 'month' ? 'monthly' : period === 'week' ? 'weekly' : 'daily');

// What a connection has spent in the window that is on now: the counter
// carried on the connection (stamped with its window) plus, for the worker,
// its own spends the app has not yet folded into that counter.
export function spentInWindow(conn, extraSpends = [], ts = Date.now()) {
  const period = conn.period || 'day';
  const win = budgetWindow(period, ts);
  let spent = conn.spentDate === win ? (conn.spentToday || 0) : 0;
  for (const s of extraSpends) {
    if (s.absorbed || s.connId !== conn.id) continue;
    if (budgetWindow(period, s.ts) !== win) continue;
    spent += (s.amountSat || 0) + (s.feeSat || 0);
  }
  return { window: win, period, spent, left: Math.max(0, (conn.dailySat || 0) - spent) };
}