Add dependency-free BOLT11 amount decoder; drop removed nip57 helper

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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-03 22:20:27 +07:00
parent 144227b88b
commit 36845b122d
3 changed files with 71 additions and 3 deletions

View file

@ -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 {

View file

@ -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) {

67
src/lib/bolt11.ts Normal file
View file

@ -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<network><amount><multiplier>`.
*
* BOLT11 amount format (from the spec):
* - HRP looks like: lnbc<amount?><multiplier?>1<data...>
* - <amount> is an integer; <multiplier> 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<string, number> = {
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);
}