Update util docs

This commit is contained in:
Jon Staab
2025-06-04 17:52:25 -07:00
parent 95eae509cc
commit 59db0eda9d
10 changed files with 780 additions and 837 deletions
+189 -162
View File
@@ -1,192 +1,219 @@
# Zaps
The Zaps module provides utilities for working with Lightning Network payments (zaps) in Nostr, including LNURL handling, invoice amount parsing, and zap validation.
The Zaps module provides utilities for working with Lightning Network payments (zaps) in Nostr, following [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md). It includes LNURL handling, invoice amount parsing, and zap validation.
## Zapper Interface
The Zapper interface represents a Lightning Network payment provider that can process zaps:
## Protocol Overview
Zaps enable Lightning Network payments to be associated with Nostr events through a standardized flow:
1. **Zap Request** (kind 9734): Client creates a request specifying the amount and target
2. **Lightning Invoice**: LNURL service generates an invoice with the request embedded
3. **Zap Receipt** (kind 9735): Zapper publishes proof of payment to Nostr
## API
### Types
```typescript
interface Zapper {
// LNURL for payment processing
lnurl: string
// Zapper service information
export type Zapper = {
lnurl: string;
pubkey?: string;
callback?: string;
minSendable?: number;
maxSendable?: number;
nostrPubkey?: string;
allowsNostr?: boolean;
};
// User's pubkey on the payment service
pubkey?: string
// LNURL callback endpoint
callback?: string
// Minimum payment amount in millisatoshis
minSendable?: number
// Maximum payment amount in millisatoshis
maxSendable?: number
// Pubkey used to sign zap receipts
nostrPubkey?: string
// Whether provider supports Nostr zaps
allowsNostr?: boolean
}
// Complete zap with request and receipt
export type Zap = {
request: TrustedEvent; // kind 9734 (zap request)
response: TrustedEvent; // kind 9735 (zap receipt)
invoiceAmount: number; // amount in millisatoshis
};
```
### Finding Nostr Zappers
#### Getting Lightning Info
First, check the user's profile for Lightning addresses:
### Lightning Network Utilities
```typescript
function getLightningInfo(profile: Profile) {
// Check for Lightning Address (NIP-57)
if (profile.lud16) {
return {
type: 'lud16',
address: profile.lud16
}
}
// Convert human-readable amount to millisatoshis
export declare const hrpToMillisat: (hrpString: string) => bigint;
// Check for LNURL
if (profile.lud06) {
return {
type: 'lud06',
url: profile.lud06
}
}
// Extract amount from BOLT11 lightning invoice
export declare const getInvoiceAmount: (bolt11: string) => number;
return null
}
// Convert lightning address or URL to LNURL
export declare const getLnUrl: (address: string) => string | null;
```
#### Fetching LNURL Metadata
Once you have the Lightning address or LNURL, fetch the metadata:
### Zap Validation
```typescript
async function fetchZapper(address: string): Promise<Zapper | null> {
// Convert Lightning address to LNURL if needed
const lnurl = getLnUrl(address)
if (!lnurl) return null
try {
// Decode and fetch LNURL metadata
const url = new URL(bech32.decode(lnurl).data)
const response = await fetch(url.toString())
const metadata = await response.json()
// Extract zapper details
return {
lnurl,
callback: metadata.callback,
minSendable: metadata.minSendable,
maxSendable: metadata.maxSendable,
nostrPubkey: metadata.nostrPubkey,
allowsNostr: Boolean(metadata.allowsNostr),
}
} catch (error) {
console.error('Failed to fetch zapper:', error)
return null
}
}
// Create validated Zap from zap receipt event
export declare const zapFromEvent: (response: TrustedEvent, zapper?: Zapper) => Zap | null;
```
## Examples
### Converting Lightning Addresses
```typescript
// Example Alby zapper configuration
const albyZapper: Zapper = {
lnurl: "lnurl1...",
pubkey: "alby_user_pubkey",
nostrPubkey: "alby_signing_key",
import { getLnUrl } from '@welshman/util';
// Lightning address (LUD-16)
const lnurl1 = getLnUrl('satoshi@getalby.com');
console.log(lnurl1); // 'lnurl1...' (encoded URL)
// Regular URL
const lnurl2 = getLnUrl('https://getalby.com/.well-known/lnurlp/satoshi');
console.log(lnurl2); // 'lnurl1...' (encoded URL)
// Already encoded LNURL
const lnurl3 = getLnUrl('lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf');
console.log(lnurl3); // 'lnurl1...' (same as input)
// Invalid address
const invalid = getLnUrl('not-a-valid-address');
console.log(invalid); // null
```
### Parsing Invoice Amounts
```typescript
import { getInvoiceAmount, hrpToMillisat } from '@welshman/util';
// Extract amount from BOLT11 invoice
const invoice = 'lnbc1500n1...'; // 1500 nanosats = 1.5 sats
const amount = getInvoiceAmount(invoice);
console.log(amount); // 1500 (millisatoshis)
// Convert human-readable amounts
console.log(hrpToMillisat('1000')); // 100000000000n (1000 BTC in millisats)
console.log(hrpToMillisat('1000m')); // 100000000n (1000 mBTC = 1 BTC in millisats)
console.log(hrpToMillisat('1000u')); // 100000n (1000 µBTC = 1 mBTC in millisats)
console.log(hrpToMillisat('1000n')); // 100n (1000 nBTC = 1000 sats in millisats)
console.log(hrpToMillisat('1000p')); // 0.1n (1000 pBTC = 1 msat, but must be divisible by 10)
```
### Validating Zaps
```typescript
import { zapFromEvent, ZAP_RESPONSE } from '@welshman/util';
// Zapper service configuration
const zapper: Zapper = {
lnurl: 'lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf',
nostrPubkey: 'zapper-pubkey-hex',
allowsNostr: true,
minSendable: 1000, // 1 sat minimum
maxSendable: 100000000 // 100k sats maximum
minSendable: 1000,
maxSendable: 10000000
};
// Zap receipt event (kind 9735)
const zapReceipt = {
kind: ZAP_RESPONSE,
pubkey: 'zapper-pubkey-hex',
tags: [
['bolt11', 'lnbc1500n1...'],
['description', '{"kind":9734,"pubkey":"sender-pubkey","tags":[["p","recipient-pubkey"],["amount","1500"],["relays","wss://relay.com"]],"content":"Great post!","created_at":1234567890}'],
['p', 'recipient-pubkey']
],
// ... other event fields
};
// Validate the zap
const validZap = zapFromEvent(zapReceipt, zapper);
if (validZap) {
console.log('Amount:', validZap.invoiceAmount); // 1500 millisats
console.log('Request:', validZap.request.content); // "Great post!"
console.log('Recipient:', validZap.request.tags.find(t => t[0] === 'p')?.[1]);
} else {
console.log('Invalid zap - failed validation');
}
```
### Complete Zap Flow Example
```typescript
import { getLnUrl, zapFromEvent, makeEvent, ZAP_REQUEST } from '@welshman/util';
// Step 1: Get LNURL from lightning address
const lightningAddress = 'satoshi@getalby.com';
const lnurl = getLnUrl(lightningAddress);
if (!lnurl) {
throw new Error('Invalid lightning address');
}
// Example LNbits zapper
const lnbitsZapper: Zapper = {
lnurl: "lnurl1...",
callback: "https://lnbits.com/callback",
nostrPubkey: "lnbits_signing_key",
// Step 2: Create zap request (kind 9734)
const zapRequest = makeEvent(ZAP_REQUEST, {
content: 'Amazing content!',
tags: [
['p', 'recipient-pubkey-hex'], // recipient
['amount', '5000'], // 5000 millisats = 5 sats
['lnurl', lnurl],
['relays', 'wss://relay.damus.io', 'wss://relay.snort.social']
]
});
// Step 3: Send to LNURL service (implementation specific)
// The service will generate an invoice with the zap request in description
// Step 4: Pay the invoice (using Lightning wallet)
// Step 5: Validate received zap receipt
const zapperInfo = {
lnurl,
nostrPubkey: 'zapper-service-pubkey',
allowsNostr: true
};
// When zap receipt arrives (kind 9735)
function handleZapReceipt(zapReceipt: TrustedEvent) {
const validatedZap = zapFromEvent(zapReceipt, zapperInfo);
if (validatedZap) {
console.log(`Received ${validatedZap.invoiceAmount} msat zap!`);
console.log(`Message: ${validatedZap.request.content}`);
return validatedZap;
} else {
console.log('Invalid zap receipt');
return null;
}
}
```
### Zap Structure
```typescript
interface Zap {
request: TrustedEvent // Zap request event kind 9734
response: TrustedEvent // Zap receipt/response event kind 9735 sent by the zapper
invoiceAmount: number // Amount in millisats
}
```
### Zap Validation Rules
## Core Functions
### Lightning Address Handling
```typescript
// Convert address to LNURL
function getLnUrl(address: string): string | null
// Examples:
getLnUrl("user@domain.com") // => lnurl1...
getLnUrl("https://domain.com/.well-known/lnurlp/user") // => lnurl1...
getLnUrl("lnurl1...") // => returns unchanged
```
### Invoice Processing
```typescript
// Parse amount from BOLT11 invoice
function getInvoiceAmount(bolt11: string): number
// Convert human readable amount to millisats
function hrpToMillisat(hrpString: string): bigint
```
### Zap Validation
The `zapFromEvent` function validates a zap receipt event, against an expected zapper.
It returns a `Zap` object if the zap is valid, or `null` if not.
The `zapFromEvent` function validates several aspects of a zap according to NIP-57:
```typescript
function zapFromEvent(
response: TrustedEvent,
zapper: Zapper | undefined
): Zap | null
```
## Usage Examples
### Processing Lightning Addresses
```typescript
// Get LNURL from various formats
const lnurl1 = getLnUrl("user@getalby.com")
const lnurl2 = getLnUrl("https://getalby.com/.well-known/lnurlp/user")
const lnurl3 = getLnUrl("lnurl1...")
// Check if conversion was successful
if (lnurl1) {
// Process LNURL
processLnurl(lnurl1)
}
```
### Invoice Amount Handling
```typescript
// Get invoice amount in millisats
const amount = getInvoiceAmount(bolt11Invoice)
// Convert string amount to millisats
const millisats = hrpToMillisat("1000") // 1000 sats
const millisats = hrpToMillisat("1m") // 1 million sats
```
### Zap Validation
```typescript
// Validate zap event
const zap = zapFromEvent(zapResponse, albyZapper)
if (zap) {
// Process valid zap
processZap(zap)
}
import { zapFromEvent } from '@welshman/util';
// Validation checks performed:
// 1. Invoice amount matches requested amount (if specified)
// 2. Zap request is properly embedded in invoice description
// 3. Zapper pubkey matches the expected zapper service
// 4. LNURL matches the expected service (if provided in request)
// 5. Self-zaps are filtered out (sender != zapper)
const zapReceipt = {
// ... zap receipt event
};
const zapper = {
nostrPubkey: 'expected-zapper-pubkey',
lnurl: 'expected-lnurl'
};
const validZap = zapFromEvent(zapReceipt, zapper);
// Returns null if any validation fails:
// - Malformed bolt11 invoice
// - Amount mismatch
// - Wrong zapper pubkey
// - LNURL mismatch
// - Self-zap detection
```