From 36845b122dd81ac5b565c88114ba0111af22d5c8 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 3 Sep 2026 22:20:27 +0700 Subject: [PATCH] Add dependency-free BOLT11 amount decoder; drop removed nip57 helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Newer nostr-tools removed nip57.getSatoshisAmountFromBolt11, breaking useZaps and useCrewGoal. Add src/lib/bolt11.ts (getSatoshisFromBolt11) that parses the sat amount straight from the invoice's human-readable prefix — no external dependency, immune to nostr-tools API churn — and use it in both hooks. Co-authored-by: shakespeare.diy --- src/hooks/useCrewGoal.ts | 4 +-- src/hooks/useZaps.ts | 3 +- src/lib/bolt11.ts | 67 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/lib/bolt11.ts diff --git a/src/hooks/useCrewGoal.ts b/src/hooks/useCrewGoal.ts index 37d674b..b932ee2 100644 --- a/src/hooks/useCrewGoal.ts +++ b/src/hooks/useCrewGoal.ts @@ -1,8 +1,8 @@ import { useQuery, type UseQueryResult } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; -import { nip57 } from 'nostr-tools'; import type { NostrEvent } from '@nostrify/nostrify'; import { ZAP_GOAL_KIND } from '@/lib/types'; +import { getSatoshisFromBolt11 } from '@/lib/bolt11'; export interface CrewGoal { id: string; @@ -43,7 +43,7 @@ function tallyZaps(receipts: NostrEvent[], closedAt?: number): { sats: number; c const bolt11 = z.tags.find(([n]) => n === 'bolt11')?.[1]; if (bolt11) { try { - sats += nip57.getSatoshisAmountFromBolt11(bolt11); + sats += getSatoshisFromBolt11(bolt11); count++; continue; } catch { diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts index 0677559..9f4db39 100644 --- a/src/hooks/useZaps.ts +++ b/src/hooks/useZaps.ts @@ -7,6 +7,7 @@ import { useNWC } from '@/hooks/useNWCContext'; import type { NWCConnection } from '@/hooks/useNWC'; import { nip57 } from 'nostr-tools'; import type { Event } from 'nostr-tools'; +import { getSatoshisFromBolt11 } from '@/lib/bolt11'; import type { WebLNProvider } from '@webbtc/webln-types'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; @@ -99,7 +100,7 @@ export function useZaps( const bolt11Tag = zap.tags.find(([name]) => name === 'bolt11')?.[1]; if (bolt11Tag) { try { - const invoiceSats = nip57.getSatoshisAmountFromBolt11(bolt11Tag); + const invoiceSats = getSatoshisFromBolt11(bolt11Tag); sats += invoiceSats; return; } catch (error) { diff --git a/src/lib/bolt11.ts b/src/lib/bolt11.ts new file mode 100644 index 0000000..70b62a0 --- /dev/null +++ b/src/lib/bolt11.ts @@ -0,0 +1,67 @@ +/** + * Minimal, dependency-free BOLT11 Lightning invoice amount decoder. + * + * WHY THIS EXISTS + * --------------- + * nostr-tools removed `nip57.getSatoshisAmountFromBolt11` in a newer minor, so + * we decode the amount ourselves. The amount is encoded in the invoice's + * human-readable prefix (HRP), NOT the bech32 data — so we don't need a full + * bech32 decode, just to parse the leading `ln`. + * + * BOLT11 amount format (from the spec): + * - HRP looks like: lnbc1 + * - is an integer; is one of: + * m (milli) = 0.001 BTC + * u (micro) = 0.000001 BTC + * n (nano) = 0.000000001 BTC + * p (pico) = 0.000000000001 BTC + * - No amount at all → a "donation" invoice (0 / unspecified). + * + * 1 BTC = 100_000_000 sats. + */ + +const MULTIPLIERS: Record = { + m: 0.001, + u: 0.000001, + n: 0.000000001, + p: 0.000000000001, +}; + +const SATS_PER_BTC = 100_000_000; + +/** + * Decode the amount (in whole satoshis) from a BOLT11 invoice string. + * Returns 0 for amountless invoices. Throws on a clearly malformed prefix so + * callers can fall back to other amount sources (e.g. the zap `amount` tag). + */ +export function getSatoshisFromBolt11(invoice: string): number { + if (!invoice || typeof invoice !== 'string') { + throw new Error('Invalid BOLT11 invoice'); + } + + const lower = invoice.trim().toLowerCase(); + + // The HRP is everything before the bech32 separator '1' that precedes the + // data part. BOLT11 HRPs start with "ln". Grab from the start up to (but not + // including) the final separator between HRP and data. In practice the amount + // block sits right after the network prefix (lnbc / lntb / lntbs / lnbcrt). + const match = lower.match(/^ln(?:bc|tb|tbs|bcrt|sb)(\d+)?([munp])?1/); + if (!match) { + throw new Error('Not a recognizable BOLT11 invoice'); + } + + const amountStr = match[1]; + const multiplier = match[2]; + + // No amount encoded → amountless invoice. + if (!amountStr) return 0; + + const amount = parseInt(amountStr, 10); + if (Number.isNaN(amount)) { + throw new Error('Invalid amount in BOLT11 invoice'); + } + + // btc value = amount * multiplier (multiplier absent means whole BTC units). + const btc = multiplier ? amount * MULTIPLIERS[multiplier] : amount; + return Math.round(btc * SATS_PER_BTC); +}