Lightning
@welshman/util holds the low-level Lightning primitives used by the NIP-57 zap flow: LNURL encoding and BOLT11 amount parsing.
The Zapper class, the Zap result type, the ZapRequest / ZapReceipt / ZapGoal event kinds, and zap-split resolution live in @welshman/domain. See domain/Zaps.
Protocol Overview
A zap ties a Lightning payment to a Nostr event:
- Zap Request (kind 9734): Client creates a request specifying the amount and target
- Lightning Invoice: LNURL service generates an invoice with the request embedded
- Zap Receipt (kind 9735): Zapper publishes proof of payment to Nostr
API
Amount conversion
// Sats <-> millisats
export declare const toMsats: (sats: number) => number;
export declare const fromMsats: (msats: number) => number;
// Convert a BOLT11 human-readable part to millisatoshis
export declare const hrpToMillisat: (hrpString: string) => bigint;
// Extract the amount (in millisats) from a BOLT11 lightning invoice
export declare const getInvoiceAmount: (bolt11: string) => number;BOLT12
NIP-B1 zaps are modeled in @welshman/domain — see domain/Zaps. What lives here is the syntax of the strings they carry.
// Lowercase a bolt12 string and drop its separators and whitespace
export declare const canonicalBolt12: (encoded: string) => string;
export declare const isBolt12Offer: (offer: string) => boolean;
export declare const isBolt12PayerProof: (proof: string) => boolean;
export declare const isBolt12ZapId: (zapId: string) => boolean;
// Whether an amount tag is whole millisats, so `msatsTags` reads "21000.9" as absent
export declare const isMsats: (amount: string) => boolean;
// A fresh zap id, 128 bits of entropy as lowercase hex
export declare const makeBolt12ZapId: () => string;
// The invreq_payer_note a payment has to carry: `nostr:nipB1:<zap intent id>`
export declare const makeBolt12PayerNote: (zapIntentId: string) => string;Payer proofs
A BOLT12 payer proof is the lightning half of a NIP-B1 zap. It carries the invoice the recipient signed, the preimage of that invoice's payment, and the payer's own signature over both. Fields the payer withheld for privacy are merkle branches instead, so the recipient signature still verifies over what is left. decodeBolt12PayerProof runs every check the BOLT12 spec asks of a reader, so what it returns is a settled payment.
export type Bolt12PayerProof = {
fields: Map<number, Uint8Array> // every tlv in the proof, by type
paymentHash: string // invoice_payment_hash, which identifies the payment
preimage: string // proof_preimage, which hashes to it
payerId: string // invreq_payer_id, whose signature covers the proof
nodeId: string // invoice_node_id, whose signature covers the invoice
payerNote?: string // invreq_payer_note, `nostr:nipB1:<zap intent id>`
amount?: number // invoice_amount, in millisats
};
export declare const decodeBolt12PayerProof: (proof: string) => Bolt12PayerProof | undefined;
// Whether every field of a raw offer is in the proof unchanged, so the proof is of paying it
export declare const bolt12ProofPaysOffer: (proof: Bolt12PayerProof, offer: string) => boolean;The codec underneath is exported too: decodeBolt12(encoded, prefix) for bech32 with no checksum, and parseBolt12Tlvs(bytes) for the tlv stream.
LNURL
// Convert a lightning address (LUD-16) or URL to an encoded LNURL
export declare const getLnUrl: (address: string) => string | undefined;Examples
Converting Lightning Addresses
import {getLnUrl} from '@welshman/util';
// Lightning address (LUD-16)
getLnUrl('satoshi@getalby.com'); // 'lnurl1...' (encoded URL)
// Regular URL
getLnUrl('https://getalby.com/.well-known/lnurlp/satoshi'); // 'lnurl1...'
// Already-encoded LNURL is returned as-is
getLnUrl('lnurl1dp68gurn8ghj7...'); // 'lnurl1...' (same as input)
// Invalid address
getLnUrl('not-a-valid-address'); // undefinedParsing Invoice Amounts
import {getInvoiceAmount, hrpToMillisat} from '@welshman/util';
// Extract amount from a BOLT11 invoice
getInvoiceAmount('lnbc1500n1...'); // 150000 (1500 nBTC = 150 sats)
// Convert human-readable amounts to millisats
hrpToMillisat('1000'); // 100000000000000n (1000 BTC)
hrpToMillisat('1000m'); // 100000000000n (1000 mBTC = 1 BTC)
hrpToMillisat('1000u'); // 100000000n (1000 µBTC = 1 mBTC)
hrpToMillisat('1000n'); // 100000n (1000 nBTC = 100 sats)
hrpToMillisat('1000p'); // 100n (1000 pBTC = 100 msats)Validating and requesting zaps
Zap-receipt validation, invoice requests, and zap-split resolution live in @welshman/domain:
import {Domain} from '@welshman/app';
import {Zapper, ZapReceipt, ZapRequest} from '@welshman/domain';
const zapper = new Zapper({lnurl, pubkey, nostrPubkey, callback, allowsNostr: true});
// Validate a parsed kind-9735 receipt -> Zap | undefined
const receipt = await app.use(Domain).reader(ZapReceipt)(zapReceipt);
const zap = zapper.validate(receipt);
// Build, sign, and request an invoice for a zap
const {invoice, error} = await app.use(Domain).writer(ZapRequest)
.setAmount(5000)
.setRecipient(recipientPubkey)
.setLnurl(zapper.lnurl)
.requestInvoice(zapper); // {event, invoice?, error?}See domain/Zaps for the full Zapper, kind, and zap-split API.