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