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