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
+1 -1
View File
@@ -1,5 +1,5 @@
node_modules
docs
#docs
docs/reference
docs/.vitepress/cache
build
+85 -89
View File
@@ -1,110 +1,106 @@
# Nostr Address
The Address module provides utilities for working with Nostr Addresses (NIP-19 naddr format) and handles the conversion between different address formats.
The Address module provides utilities for working with Nostr Addresses (NIP-19 naddr format) and handles the conversion between different address formats. Addresses are used to reference addressable events (kinds 10000-39999) in a format that includes kind, pubkey, and identifier.
## Address Class
## API
```typescript
class Address {
// Address class for handling addressable events
export declare class Address {
constructor(
readonly kind: number, // Event kind
readonly pubkey: string, // Author's public key
readonly identifier: string, // Unique identifier (d-tag)
readonly relays?: string[], // Optional relay hints
)
kind: number,
pubkey: string,
identifier: string,
relays?: string[]
);
// Check if string is a valid address format
static isAddress(address: string): boolean;
// Parse address from string format "kind:pubkey:identifier"
static from(address: string, relays?: string[]): Address;
// Parse address from naddr (NIP-19 format)
static fromNaddr(naddr: string): Address;
// Create address from addressable event
static fromEvent(event: AddressableEvent, relays?: string[]): Address;
// Convert to string format "kind:pubkey:identifier"
toString(): string;
// Convert to naddr (NIP-19 format)
toNaddr(): string;
}
```
## Creating Addresses
### From Components
```typescript
const address = new Address(
30023, // kind (e.g., long-form article)
'ab82...123', // pubkey
'my-article-title', // identifier
['wss://relay.example.com'] // relays
)
```
### From String Format
```typescript
// Parse "kind:pubkey:identifier" format
const address = Address.from('30023:ab82...123:my-article-title')
// With optional relays
const address = Address.from(
'30023:ab82...123:my-article-title',
['wss://relay.example.com']
)
```
### From Naddr
```typescript
// Parse naddr format
const address = Address.fromNaddr('naddr1...')
```
### From Event
```typescript
const address = Address.fromEvent(event, relays)
```
## Converting Addresses
### To String
```typescript
const address = new Address(kind, pubkey, identifier)
address.toString() // => "kind:pubkey:identifier"
```
### To Naddr
```typescript
const address = new Address(kind, pubkey, identifier, relays)
address.toNaddr() // => "naddr1..."
```
## Utility Functions
### Check Address Format
```typescript
// Check if string is valid address format
Address.isAddress('30023:abc...123:title') // => true
Address.isAddress('not-an-address') // => false
```
### Get Address from Event
```typescript
import { getAddress } from '@welshman/util'
// Extract address from event
const address = getAddress(event)
// Utility function to get address string from event
export declare const getAddress: (e: AddressableEvent) => string;
```
## Examples
### Working with Long-form Content
### Creating and parsing addresses
```typescript
// Create address for article
const articleAddress = new Address(
30023, // Long-form content kind
authorPubkey,
'my-article-slug',
import { Address } from '@welshman/util';
// Create address from components
const address = new Address(
30023,
'27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389',
'my-article',
['wss://relay.example.com']
)
);
// Convert to string format for storage
const addressString = articleAddress.toString()
// Parse from string format
const parsed = Address.from('30023:27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389:my-article');
console.log(parsed.kind); // 30023
console.log(parsed.identifier); // 'my-article'
// Convert to naddr for sharing
const shareableAddress = articleAddress.toNaddr()
// Check if string is valid address
const isValid = Address.isAddress('30023:27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389:my-article'); // true
const isInvalid = Address.isAddress('invalid-format'); // false
```
### Handling Replaceable Events
### Converting between formats
```typescript
// Create address from replaceable event
const address = Address.fromEvent(event)
import { Address } from '@welshman/util';
// Store latest version using address as key
storage.set(address.toString(), event)
const address = new Address(30023, '27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389', 'my-article');
// Convert to string format
const addressString = address.toString();
console.log(addressString); // '30023:27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389:my-article'
// Convert to naddr format (NIP-19)
const naddr = address.toNaddr();
console.log(naddr); // 'naddr1...'
// Parse back from naddr
const fromNaddr = Address.fromNaddr(naddr);
console.log(fromNaddr.kind); // 30023
```
### Working with events
```typescript
import { Address, getAddress } from '@welshman/util';
const event = {
kind: 30023,
pubkey: '27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389',
tags: [
['d', 'my-article'],
['title', 'My Article Title']
]
};
// Create address from event
const address = Address.fromEvent(event, ['wss://relay.example.com']);
console.log(address.identifier); // 'my-article'
// Get address string directly
const addressString = getAddress(event);
console.log(addressString); // '30023:27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389:my-article'
```
+71 -142
View File
@@ -1,162 +1,91 @@
# Encryptable
The Encryptable module provides a system for handling encrypted Nostr events, particularly useful for private content like muted lists, bookmarks, or other encrypted user data.
The Encryptable module provides utilities for handling encrypted Nostr events, allowing you to merge plaintext updates into events and encrypt them before publishing.
## Core Types
### Encrypt Function
```typescript
type Encrypt = (x: string) => Promise<string>
```
### Encryptable Updates
```typescript
type EncryptableUpdates = {
content?: string
tags?: string[][]
}
```
### Decrypted Event
```typescript
type DecryptedEvent = TrustedEvent & {
plaintext: EncryptableUpdates
}
```
## Encryptable Class
## API
```typescript
class Encryptable<T extends EventTemplate> {
// Encryption function type
export type Encrypt = (x: string) => Promise<string>;
// Partial event content for updates
export type EncryptableUpdates = Partial<EventContent>;
// Event with attached plaintext data
export type DecryptedEvent = TrustedEvent & {
plaintext: EncryptableUpdates;
};
// Creates a DecryptedEvent by attaching plaintext to an event
export declare const asDecryptedEvent: (
event: TrustedEvent,
plaintext?: EncryptableUpdates
) => DecryptedEvent;
// Encryptable class for handling encrypted events
export declare class Encryptable<T extends EventTemplate> {
constructor(
readonly event: Partial<T>, // Base event template
readonly updates: EncryptableUpdates // Plaintext updates
)
event: Partial<T>,
updates: EncryptableUpdates
);
// Encrypts updates and merges them into the event
reconcile(encrypt: Encrypt): Promise<T>;
}
```
## Usage
### Basic Encryption
```typescript
// Create encryptable event
const encryptable = new Encryptable(
{ kind: 10000 }, // Base event
{ content: "secret content" } // Plaintext updates
)
// Encrypt and get final event
const event = await encryptable.reconcile(encryptFn)
```
### Private Lists
```typescript
// Create private mute list
const muteList = new Encryptable(
{
kind: 10000, // Mute list kind
tags: [] // Public tags
},
{
content: JSON.stringify(['pubkey1', 'pubkey2']), // Private content
tags: [['p', 'pubkey1'], ['p', 'pubkey2']] // Private tags
}
)
// Encrypt for publishing
const encrypted = await muteList.reconcile(async (content) => {
return await nip04.encrypt(pubkey, content)
})
```
### Updating Encrypted Content
```typescript
// Create encryptable from existing event
const existing = {
kind: 10000,
content: encryptedContent,
tags: publicTags
}
// Add new encrypted content
const updated = new Encryptable(
existing,
{
content: JSON.stringify(newContent),
tags: newPrivateTags
}
)
const final = await updated.reconcile(encrypt)
```
## Helper Functions
### Create Decrypted Event
```typescript
import { asDecryptedEvent } from '@welshman/util'
// Add plaintext content to event
const decrypted = asDecryptedEvent(
event,
{
content: decryptedContent,
tags: decryptedTags
}
)
```
## Examples
### Private Bookmarks
### Basic Usage
```typescript
// Create private bookmark list
const bookmarks = new Encryptable(
import { Encryptable } from '@welshman/util';
// Create encryptable with plaintext updates
const encryptable = new Encryptable(
{ kind: 10000 }, // Base event template
{ content: "secret mute list data" } // Plaintext content to encrypt
);
// Encrypt and get final event
const encryptFn = async (text: string) => {
// Your encryption logic here
return await encrypt(text);
};
const event = await encryptable.reconcile(encryptFn);
// event.content is now encrypted
```
### Encrypting Tags
```typescript
import { Encryptable } from '@welshman/util';
// Encrypt both content and tag values
const encryptable = new Encryptable(
{ kind: 10000, tags: [] },
{
kind: 10003,
tags: [['d', 'bookmarks']] // Public identifier
},
{
content: JSON.stringify([
{ id: 'note1', title: 'Secret Note' }
])
content: JSON.stringify(['pubkey1', 'pubkey2']),
tags: [['p', 'sensitive-pubkey'], ['e', 'sensitive-event-id']]
}
)
);
// Encrypt for publishing
const event = await bookmarks.reconcile(async (content) => {
return await myEncryptionFunction(content)
})
// The reconcile method encrypts tag values at index 1
const event = await encryptable.reconcile(encryptFn);
// event.tags[0] = ['p', 'encrypted-pubkey']
// event.tags[1] = ['e', 'encrypted-event-id']
```
### Encrypted Group Membership
### Working with Decrypted Events
```typescript
// Create private group member list
const members = new Encryptable(
{
kind: 30000,
tags: [['d', 'group-members']]
},
{
tags: members.map(m => ['p', m.pubkey, m.role])
}
)
import { asDecryptedEvent } from '@welshman/util';
const encrypted = await members.reconcile(encrypt)
```
### Updating Private Content
```typescript
function updatePrivateList(event: DecryptedEvent, newItems: string[]) {
return new Encryptable(
event,
{
content: JSON.stringify(newItems)
}
)
}
// Usage
const updated = updatePrivateList(existingEvent, newItems)
const final = await updated.reconcile(encrypt)
// Add plaintext data to an event for reference
const event = { kind: 10000, content: "encrypted...", tags: [] };
const plaintext = { content: "original content", tags: [['p', 'pubkey']] };
const decryptedEvent = asDecryptedEvent(event, plaintext);
console.log(decryptedEvent.plaintext.content); // "original content"
```
+156 -136
View File
@@ -1,185 +1,205 @@
# Nostr Events
The Events module provides comprehensive type definitions and utilities for working with Nostr events, including helper functions for event creation, validation, and manipulation.
The Events module provides type definitions and utilities for working with Nostr events, including creation, validation, and manipulation functions.
## Event Types Hierarchy
## API
### Event Types
```typescript
// Base event with content and tags
interface EventContent {
tags: string[][]
content: string
}
// Base event content structure
export type EventContent = {
tags: string[][];
content: string;
};
// Base event with kind
interface EventTemplate extends EventContent {
kind: number
}
// Event template with kind
export type EventTemplate = EventContent & {
kind: number;
};
// Event with timestamp
interface StampedEvent extends EventTemplate {
created_at: number
}
export type StampedEvent = EventTemplate & {
created_at: number;
};
// Event with author
interface OwnedEvent extends StampedEvent {
pubkey: string
}
export type OwnedEvent = StampedEvent & {
pubkey: string;
};
// Event with ID
interface HashedEvent extends OwnedEvent {
id: string
}
export type HashedEvent = OwnedEvent & {
id: string;
};
// Event with signature
interface SignedEvent extends HashedEvent {
sig: string
[verifiedSymbol]?: boolean
}
// Signed event
export type SignedEvent = HashedEvent & {
sig: string;
};
// Event with wrapped content
interface UnwrappedEvent extends HashedEvent {
wrap: SignedEvent
}
// Wrapped event (NIP-59)
export type UnwrappedEvent = HashedEvent & {
wrap: SignedEvent;
};
// Event that can be either signed or wrapped
type TrustedEvent = HashedEvent & {
sig?: string
wrap?: SignedEvent
[verifiedSymbol]?: boolean
}
export type TrustedEvent = HashedEvent & {
sig?: string;
wrap?: SignedEvent;
};
```
## Event Creation
### Create Basic Event
```typescript
import { createEvent } from '@welshman/util'
const event = createEvent(
1, // kind
{
content: "Hello Nostr!",
tags: [["t", "nostr"]],
created_at: now() // Optional, defaults to current time
}
)
```
## Type Guards
### Event Creation
```typescript
// Check event types
isEventTemplate(event): boolean
isStampedEvent(event): boolean
isOwnedEvent(event): boolean
isHashedEvent(event): boolean
isSignedEvent(event): boolean
isUnwrappedEvent(event): boolean
isTrustedEvent(event): boolean
// Options for creating events
export type MakeEventOpts = {
content?: string;
tags?: string[][];
created_at?: number;
};
// Creates a stamped event template
export declare const makeEvent: (kind: number, opts?: MakeEventOpts) => StampedEvent;
```
## Event Type Conversion
### Type Guards
```typescript
// Convert to specific event types
asEventTemplate(event): EventTemplate
asStampedEvent(event): StampedEvent
asOwnedEvent(event): OwnedEvent
asHashedEvent(event): HashedEvent
asSignedEvent(event): SignedEvent
asUnwrappedEvent(event): UnwrappedEvent
asTrustedEvent(event): TrustedEvent
export declare const isEventTemplate: (e: EventTemplate) => e is EventTemplate;
export declare const isStampedEvent: (e: StampedEvent) => e is StampedEvent;
export declare const isOwnedEvent: (e: OwnedEvent) => e is OwnedEvent;
export declare const isHashedEvent: (e: HashedEvent) => e is HashedEvent;
export declare const isSignedEvent: (e: TrustedEvent) => e is SignedEvent;
export declare const isUnwrappedEvent: (e: TrustedEvent) => e is UnwrappedEvent;
export declare const isTrustedEvent: (e: TrustedEvent) => e is TrustedEvent;
```
## Event Utilities
### Event Utilities
### Event Validation
```typescript
// Check if event has valid signature
hasValidSignature(event: SignedEvent): boolean
// Event validation and signatures
export declare const verifyEvent: (event: TrustedEvent) => boolean;
// Get event identifier (d tag)
getIdentifier(event: EventTemplate): string | undefined
// Event properties
export declare const getIdentifier: (e: EventTemplate) => string | undefined;
export declare const getIdOrAddress: (e: HashedEvent) => string;
export declare const getIdAndAddress: (e: HashedEvent) => string[];
// Event type checking
export declare const isEphemeral: (e: EventTemplate) => boolean;
export declare const isReplaceable: (e: EventTemplate) => boolean;
export declare const isPlainReplaceable: (e: EventTemplate) => boolean;
export declare const isParameterizedReplaceable: (e: EventTemplate) => boolean;
// Thread and reply handling
// Note: getAncestors handles comments (kind 1111) differently from regular notes
export declare const getAncestors: (event: EventTemplate) => { roots: string[]; replies: string[] };
export declare const getParentIdsAndAddrs: (event: EventTemplate) => string[];
export declare const getParentIdOrAddr: (event: EventTemplate) => string | undefined;
export declare const getParentId: (event: EventTemplate) => string | undefined;
export declare const getParentAddr: (event: EventTemplate) => string | undefined;
export declare const isChildOf: (child: EventTemplate, parent: HashedEvent) => boolean;
```
### Event References
```typescript
// Get event ID or address
getIdOrAddress(event: HashedEvent): string
## Threading Protocols
// Get both ID and address (if replaceable)
getIdAndAddress(event: HashedEvent): string[]
```
The `getAncestors` function handles two different threading protocols:
### Event Type Checking
```typescript
// Check event properties
isEphemeral(event: EventTemplate): boolean
isReplaceable(event: EventTemplate): boolean
isPlainReplaceable(event: EventTemplate): boolean
isParameterizedReplaceable(event: EventTemplate): boolean
```
### Regular Notes (NIP-10)
For regular notes and most event kinds, threading follows [NIP-10](https://github.com/nostr-protocol/nips/blob/master/10.md):
- Uses `e` and `a` tags with optional markers (`root`, `reply`, `mention`)
- Positional rules apply when markers are absent:
- First `e`/`a` tag = root
- Last `e`/`a` tag = reply target
- Middle tags = mentions
### Thread & Reply Handling
```typescript
// Get thread information
getAncestors(event: EventTemplate): { roots: string[], replies: string[] }
### Comments (NIP-22)
For comments (kind 1111), threading follows [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md):
- Uses uppercase tags (`E`, `A`, `P`, `K`) for root references
- Uses lowercase tags (`e`, `a`, `p`, `k`) for reply references
- No positional rules - explicit tag types determine relationship
// Get parent references
getParentIdsAndAddrs(event: EventTemplate): string[]
getParentIdOrAddr(event: EventTemplate): string | undefined
getParentId(event: EventTemplate): string | undefined
getParentAddr(event: EventTemplate): string | undefined
// Check reply relationship
isChildOf(child: EventTemplate, parent: HashedEvent): boolean
```
All `getParent*` functions and `isChildOf` include this logic, automatically handling both protocols based on event kind.
## Examples
### Creating and Processing Events
### Creating Events
```typescript
// Create new event
const event = createEvent(1, {
content: "Hello world!",
tags: [["t", "greeting"]]
})
import { makeEvent, NOTE, LONG_FORM } from '@welshman/util';
// Process based on type
if (isSignedEvent(event)) {
// Handle signed event
if (hasValidSignature(event)) {
processValidEvent(event)
}
} else if (isUnwrappedEvent(event)) {
// Handle wrapped event
processWrappedEvent(event)
}
// Create a basic note
const note = makeEvent(NOTE, {
content: "Hello Nostr!",
tags: [["t", "nostr"]]
});
// Create a long-form article with custom timestamp
const article = makeEvent(LONG_FORM, {
content: "# My Article\n\nThis is my article content...",
tags: [["d", "my-article"], ["title", "My Article"]],
created_at: 1234567890
});
```
### Event Properties
```typescript
import { getIdentifier, getIdOrAddress, LONG_FORM } from '@welshman/util';
const article = makeEvent(LONG_FORM, {
content: "Article content...",
tags: [["d", "my-unique-id"]]
});
// Get the identifier (d tag value)
const identifier = getIdentifier(article); // "my-unique-id"
// For a hashed event, get ID or address
const reference = getIdOrAddress(hashedArticle);
// Returns address for replaceable events, ID for others
```
### Working with Threads
```typescript
// Get thread context
const ancestors = getAncestors(event)
const rootId = ancestors.roots[0]
const replyTo = ancestors.replies[0]
import { getAncestors, isChildOf, NOTE, COMMENT } from '@welshman/util';
// Check threading
if (isChildOf(event, parentEvent)) {
// Handle reply
// Regular note reply (NIP-10)
const noteReply = makeEvent(NOTE, {
content: "This is a reply to a note",
tags: [
["e", "root-event-id", "", "root"],
["e", "parent-event-id", "", "reply"]
]
});
// Comment reply (NIP-22)
const commentReply = makeEvent(COMMENT, {
content: "This is a reply comment",
tags: [
["E", "root-event-id"], // uppercase = root reference
["e", "parent-event-id"] // lowercase = reply reference
]
});
// Both work the same way
const noteAncestors = getAncestors(noteReply);
const commentAncestors = getAncestors(commentReply);
console.log('Note roots:', noteAncestors.roots); // ["root-event-id"]
console.log('Note replies:', noteAncestors.replies); // ["parent-event-id"]
console.log('Comment roots:', commentAncestors.roots); // ["root-event-id"]
console.log('Comment replies:', commentAncestors.replies); // ["parent-event-id"]
// Parent checking works for both protocols
if (isChildOf(noteReply, parentEvent)) {
console.log('Note is a reply');
}
if (isChildOf(commentReply, parentEvent)) {
console.log('Comment is a reply');
}
```
### Type Conversion
```typescript
// Convert to needed type
const template = asEventTemplate(event)
const stamped = asStampedEvent(event)
const owned = asOwnedEvent(event)
const hashed = asHashedEvent(event)
const signed = asSignedEvent(event)
```
+139 -116
View File
@@ -1,165 +1,188 @@
# Filters
The Filters module provides utilities for creating, manipulating, and matching Nostr event filters.
It includes support for filter operations, optimization, and time-based filtering.
The Filters module provides utilities for creating, manipulating, and matching Nostr event filters. It includes support for filter operations, optimization, and time-based filtering.
## Core Types
## API
### Filter Matching
```typescript
interface Filter {
ids?: string[] // Match specific event IDs
kinds?: number[] // Match event kinds
authors?: string[] // Match author pubkeys
since?: number // Match events since timestamp
until?: number // Match events until timestamp
limit?: number // Limit number of results
search?: string // Text search
[key: `#${string}`]: string[] // Tag filters
}
// Check if an event matches a filter
export declare const matchFilter: <E extends HashedEvent>(filter: Filter, event: E) => boolean;
// Check if an event matches any filter in array
export declare const matchFilters: <E extends HashedEvent>(filters: Filter[], event: E) => boolean;
```
## Filter Operations
### Filter Operations
### Match Events
```typescript
// Match single filter
matchFilter(filter: Filter, event: HashedEvent): boolean
// Get a compact string representation of a filter
export declare const getFilterId: (filter: Filter) => string;
// Match multiple filters
matchFilters(filters: Filter[], event: HashedEvent): boolean
// Combine multiple filters into minimal filter set
export declare const unionFilters: (filters: Filter[]) => Filter[];
// Create intersection of filter groups
export declare const intersectFilters: (groups: Filter[][]) => Filter[];
// Trim large filter arrays to avoid relay limits
export declare const trimFilter: (filter: Filter) => Filter;
export declare const trimFilters: (filters: Filter[]) => Filter[];
```
### Combine Filters
```typescript
// Combine filters with OR operation
unionFilters(filters: Filter[]): Filter[]
### Specialized Filter Creation
// Combine filters with AND operation
intersectFilters(groups: Filter[][]): Filter[]
```typescript
// Create filters for finding events by ID or address
export declare const getIdFilters: (idsOrAddresses: string[]) => Filter[];
// Create filters for finding replies to events
export declare const getReplyFilters: (events: TrustedEvent[], filter?: Filter) => Filter[];
// Add repost filters (kinds 6, 16) to existing filters
export declare const addRepostFilters: (filters: Filter[]) => Filter[];
```
### Filter Utilities
```typescript
// Get unique filter ID
getFilterId(filter: Filter): string
// Calculate filter group
calculateFilterGroup(filter: Filter): string
// Get filters for event IDs or addresses
getIdFilters(idsOrAddresses: string[]): Filter[]
// Get filters for reply events
getReplyFilters(events: TrustedEvent[], filter?: Filter): Filter[]
// Add repost filters
addRepostFilters(filters: Filter[]): Filter[]
```
## Time Constants
### Filter Analysis
```typescript
// Unix epoch for Nostr (2021-01-01)
export const EPOCH = 1609459200
// Calculate filter generality (0 = specific, 1 = very general)
export declare const getFilterGenerality: (filter: Filter) => number;
// One day in seconds
export const DAY = 86400
// Estimate time delta for filter results
export declare const guessFilterDelta: (filters: Filter[], max?: number) => number;
// Get expected result count for ID-based filters
export declare const getFilterResultCardinality: (filter: Filter) => number | undefined;
```
## Examples
### Basic Filtering
### Basic Filter Matching
```typescript
// Create basic filter
const filter: Filter = {
kinds: [1], // Text notes
authors: ['pubkey1', 'pubkey2'],
since: now() - 24 * 60 * 60, // Last 24 hours
limit: 100
}
import { matchFilter, matchFilters, NOTE, LONG_FORM } from '@welshman/util';
// Match event against filter
if (matchFilter(filter, event)) {
processEvent(event)
}
const event = {
id: 'abc123...',
kind: 1,
pubkey: 'def456...',
created_at: 1234567890,
content: 'Hello Nostr!',
tags: [['t', 'nostr']]
};
// Single filter matching
const filter = { kinds: [NOTE], authors: ['def456...'] };
const matches = matchFilter(filter, event); // true
// Multiple filter matching
const filters = [
{ kinds: [NOTE] },
{ kinds: [LONG_FORM], authors: ['def456...'] }
];
const matchesAny = matchFilters(filters, event); // true (matches first filter)
```
### Combining Filters
### Creating Filters for IDs and Addresses
```typescript
// Union of filters (OR)
const combinedFilters = unionFilters([
{ kinds: [1], authors: ['pub1'] },
{ kinds: [1], authors: ['pub2'] }
])
import { getIdFilters } from '@welshman/util';
// Intersection of filters (AND)
const intersectedFilters = intersectFilters([
[{ kinds: [1] }],
[{ authors: ['pub1'] }]
])
// Mix of event IDs and addresses
const references = [
'abc123...', // event ID
'30023:def456...:my-article', // address
'ghi789...', // another event ID
];
const filters = getIdFilters(references);
// Returns: [
// { ids: ['abc123...', 'ghi789...'] },
// { kinds: [30023], authors: ['def456...'], '#d': ['my-article'] }
// ]
```
### Time-based Filtering
### Finding Replies
```typescript
// Filter events from specific time range
const timeFilter: Filter = {
since: now() - 7 * DAY, // Last week
until: now(),
limit: 100
}
import { getReplyFilters } from '@welshman/util';
// Guess appropriate time window
const delta = guessFilterDelta([timeFilter])
const originalEvents = [
{ id: 'abc123...', kind: 1, /* ... */ },
{ id: 'def456...', kind: 30023, /* ... */ }
];
// Find all replies to these events
const replyFilters = getReplyFilters(originalEvents);
// Returns filters with #e and #a tags pointing to the original events
// Add additional constraints
const recentReplies = getReplyFilters(originalEvents, {
since: Math.floor(Date.now() / 1000) - 3600 // last hour
});
```
### Tag Filtering
### Filter Operations
```typescript
// Filter by tags
const tagFilter: Filter = {
'#t': ['nostr', 'bitcoin'], // Match hashtags
'#p': ['pubkey1'], // Match mentions
limit: 50
}
import { unionFilters, intersectFilters, trimFilters } from '@welshman/util';
// Combine overlapping filters
const filters = [
{ kinds: [1], authors: ['abc...'] },
{ kinds: [1], authors: ['def...'] },
{ kinds: [6], authors: ['abc...'] }
];
const combined = unionFilters(filters);
// Results in more efficient filter set
// Intersect filter groups
const group1 = [{ kinds: [1, 6] }];
const group2 = [{ authors: ['abc...', 'def...'] }];
const intersection = intersectFilters([group1, group2]);
// Returns: [{ kinds: [1, 6], authors: ['abc...', 'def...'] }]
// Trim oversized filters
const largeFilters = [{ authors: new Array(2000).fill('pubkey') }];
const trimmed = trimFilters(largeFilters);
// Limits arrays to 1000 items max
```
## Filter Optimization
### Adding Repost Support
### Trim Filters
```typescript
// Trim large filters to reasonable size
const trimmedFilter = trimFilter(filter)
const trimmedFilters = trimFilters(filters)
import { addRepostFilters, NOTE, LONG_FORM } from '@welshman/util';
const baseFilters = [
{ kinds: [NOTE] },
{ kinds: [LONG_FORM], authors: ['abc...'] }
];
const withReposts = addRepostFilters(baseFilters);
// Automatically adds:
// - kind 6 filters for note reposts
// - kind 16 filters with #k tags for other reposts
```
### Filter Analysis
```typescript
// Get filter generality score
const score = getFilterGenerality(filter)
import { getFilterGenerality, guessFilterDelta, getFilterResultCardinality } from '@welshman/util';
// Get expected result count
const count = getFilterResultCardinality(filter)
```
## Advanced Usage
### Reply Chain Filters
```typescript
// Get filters for replies
const replyFilters = getReplyFilters(events, {
kinds: [1],
limit: 100
})
```
### Repost Handling
```typescript
// Add filters for reposts
const withReposts = addRepostFilters([
{ kinds: [1] } // Original filter
])
// Results in filters for kinds 1, 6, and 16
const specificFilter = { ids: ['abc123...'] };
const generalFilter = { kinds: [1] };
console.log(getFilterGenerality(specificFilter)); // 0 (very specific)
console.log(getFilterGenerality(generalFilter)); // 1 (very general)
// Estimate appropriate time window
const filters = [{ authors: ['abc...', 'def...'] }];
const deltaSeconds = guessFilterDelta(filters); // ~21600 (6 hours)
// Check expected result count
const idFilter = { ids: ['abc...', 'def...', 'ghi...'] };
const resultCount = getFilterResultCardinality(idFilter); // 3
```
+10 -43
View File
@@ -109,6 +109,13 @@ const updated = addToListPrivately(
['p', 'pubkey2']
)
// Add new items publicly
const addItems = addToListPublicly(
list,
['p', 'pubkey3'],
['p', 'pubkey4']
)
// Encrypt and publish
const encrypted = await updated.reconcile(encrypt)
```
@@ -121,19 +128,6 @@ const list = readList(decryptedEvent)
// Remove item
const removeItem = removeFromList(list, 'pubkey1')
// Add new items publicly
const addItems = addToListPublicly(
list,
['p', 'pubkey3'],
['p', 'pubkey4']
)
```
### Working with Tags
```typescript
// Get all list tags
const tags = getListTags(list)
// Remove by predicate
const noMentions = removeFromListByPredicate(
list,
@@ -141,35 +135,8 @@ const noMentions = removeFromListByPredicate(
)
```
## Common List Types
### Mute List
### Working with Tags
```typescript
const muteList = makeList({
kind: 10000,
publicTags: [['d', 'mutes']],
privateTags: [] // Keep muted users private
})
```
### Bookmark List
```typescript
const bookmarks = makeList({
kind: 10003,
privateTags: [
['e', 'id1'],
['e', 'id2']
]
})
```
### Relay List
```typescript
const relays = makeList({
kind: 10002,
publicTags: [[
['r', 'wss://relay1.com'],
['r', 'wss://relay2.com', 'write']
]
})
// Get all list tags
const tags = getListTags(list)
```
+2 -2
View File
@@ -73,7 +73,7 @@ const profile = makeProfile({
})
// Create profile event
const event = createProfile(profile)
const profileEvent = createProfile(profile)
```
### Reading Profile
@@ -107,7 +107,7 @@ if (profileHasName(profile)) {
### Updating Profile
```typescript
// Edit existing profile
const updated = editProfile({
const profileEvent = editProfile({
...existingProfile,
name: "New Name",
about: "Updated bio"
+126 -115
View File
@@ -1,147 +1,158 @@
# Relay
The `Relay` module provides utilities for working with Nostr relays, including a local in-memory relay implementation that integrates with [Repository](/util/repository) for event storage.
The Relay class extends EventEmitter to provide event-based communication.
The `Relay` module provides utilities for working with Nostr relays, including URL normalization, validation, and relay profile handling.
## Core Components
## API
### Types and Enums
### Relay Class
```typescript
class Relay<E extends HashedEvent = TrustedEvent> extends Emitter {
constructor(readonly repository: Repository<E>)
// Emit events: 'EVENT', 'EOSE', 'OK'
emit(type: string, ...args: any[]): boolean
// Handle relay messages
send(type: string, ...message: any[]): void
// Relay operation modes
export enum RelayMode {
Read = "read",
Write = "write",
Inbox = "inbox"
}
// Relay information from NIP-11
export type RelayProfile = {
url: string;
icon?: string;
banner?: string;
name?: string;
pubkey?: string;
contact?: string;
software?: string;
version?: string;
negentropy?: number;
description?: string;
supported_nips?: number[];
limitation?: {
min_pow_difficulty?: number;
payment_required?: boolean;
auth_required?: boolean;
};
};
```
### URL Validation
```typescript
// Check if URL is a valid relay URL
export declare const isRelayUrl: (url: string) => boolean;
// Check if URL is an onion (Tor) address
export declare const isOnionUrl: (url: string) => boolean;
// Check if URL is a local address
export declare const isLocalUrl: (url: string) => boolean;
// Check if URL contains an IP address
export declare const isIPAddress: (url: string) => boolean;
// Check if URL is safe to share publicly
export declare const isShareableRelayUrl: (url: string) => boolean;
```
### URL Normalization
```typescript
// Normalize relay URL to standard format
export declare const normalizeRelayUrl: (url: string) => string;
// Format URL for display (remove protocol, trailing slash)
export declare const displayRelayUrl: (url: string) => string;
```
### Relay Profile
```typescript
interface RelayProfile {
url: string // Relay URL
name?: string // Display name
description?: string // Description
pubkey?: string // Operator's pubkey
contact?: string // Contact information
software?: string // Software name
version?: string // Software version
supported_nips?: number[] // Supported NIPs
limitation?: {
min_pow_difficulty?: number
payment_required?: boolean
auth_required?: boolean
}
}
```
### Finding Relay Information
```typescript
// Fetch relay information document
async function getRelayProfile(url: string): Promise<RelayProfile | null> {
try {
const normalized = normalizeRelayUrl(url)
// Convert ws/wss to http/https
const httpUrl = normalized.replace(/^ws(s)?:\/\//, 'http$1://')
// Fetch relay information document
const response = await fetch(`${httpUrl}`)
const info = await response.json()
return {
url: normalized,
name: info.name,
description: info.description,
pubkey: info.pubkey,
contact: info.contact,
software: info.software,
version: info.version,
supported_nips: info.supported_nips,
limitation: info.limitation
}
} catch (error) {
console.error(`Failed to fetch relay info for ${url}:`, error)
return null
}
}
// Get display name for relay profile
export declare const displayRelayProfile: (profile?: RelayProfile, fallback?: string) => string;
```
## URL Utilities
## Examples
### URL Validation
```typescript
// Check if URL is valid relay URL
isRelayUrl(url: string): boolean
import {
isRelayUrl,
isOnionUrl,
isLocalUrl,
isShareableRelayUrl
} from '@welshman/util';
// Check if URL is .onion address
isOnionUrl(url: string): boolean
// Valid relay URLs
console.log(isRelayUrl('wss://relay.damus.io')); // true
console.log(isRelayUrl('relay.damus.io')); // true (auto-adds wss://)
console.log(isRelayUrl('ws://localhost:8080')); // true
// Check if URL is local
isLocalUrl(url: string): boolean
// Invalid URLs
console.log(isRelayUrl('https://example.com')); // false (not websocket)
console.log(isRelayUrl('invalid-url')); // false
// Check if URL is IP address
isIPAddress(url: string): boolean
// Special URL types
console.log(isOnionUrl('wss://7rqsrjfmyb3n2k72.onion')); // true
console.log(isLocalUrl('ws://localhost:8080')); // true
console.log(isLocalUrl('wss://relay.local')); // true
// Check if URL can be shared
isShareableRelayUrl(url: string): boolean
// Safe to share publicly
console.log(isShareableRelayUrl('wss://relay.damus.io')); // true
console.log(isShareableRelayUrl('ws://localhost:8080')); // false (local)
```
### URL Formatting
### URL Normalization
```typescript
// Normalize relay URL
normalizeRelayUrl(url: string): string
import { normalizeRelayUrl, displayRelayUrl } from '@welshman/util';
// Format URL for display
displayRelayUrl(url: string): string
// Normalize various URL formats
console.log(normalizeRelayUrl('relay.damus.io'));
// 'wss://relay.damus.io/'
// Format relay profile for display
displayRelayProfile(profile?: RelayProfile, fallback = ""): string
console.log(normalizeRelayUrl('ws://RELAY.EXAMPLE.COM/path'));
// 'ws://relay.example.com/path'
console.log(normalizeRelayUrl('wss://relay.damus.io/?ref=123'));
// 'wss://relay.damus.io/' (strips query params)
// Format for display
console.log(displayRelayUrl('wss://relay.damus.io/'));
// 'relay.damus.io'
console.log(displayRelayUrl('ws://localhost:8080/'));
// 'localhost:8080'
```
### Working with Relay Profiles
## Usage Examples
### URL Processing
```typescript
// Validate relay URL
if (isRelayUrl(url)) {
// Normalize for consistency
const normalized = normalizeRelayUrl(url)
import { displayRelayProfile, RelayProfile } from '@welshman/util';
// Check if shareable
if (isShareableRelayUrl(normalized)) {
// Format for display
const display = displayRelayUrl(normalized)
showRelay(display)
const relayProfile: RelayProfile = {
url: 'wss://relay.damus.io',
name: 'Damus Relay',
description: 'A high-performance Nostr relay',
software: 'strfry',
version: '1.0.0',
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22],
limitation: {
payment_required: false,
auth_required: false,
min_pow_difficulty: 0
}
}
```
### Relay usage with Repository
```typescript
// Create storage and relay interface
const repository = new Repository()
const relay = new Relay(repository)
// Subscribe to events
relay.send("REQ", "sub_id", {
kinds: [1],
limit: 100
})
// Listen for events
relay.on("EVENT", (subId, event) => {
console.log(`Received event for ${subId}:`, event)
})
// Publish event
// Will be stored in repository and sent to matching subscribers
relay.send("EVENT", signedEvent)
// Close subscription
relay.send("CLOSE", "sub_id")
};
// Get display name
const displayName = displayRelayProfile(relayProfile);
console.log(displayName); // 'Damus Relay'
// With fallback for unnamed relays
const anonymousRelay: RelayProfile = {
url: 'wss://anonymous.relay.com'
};
const name = displayRelayProfile(anonymousRelay, 'Unknown Relay');
console.log(name); // 'Unknown Relay'
```
+1 -31
View File
@@ -103,9 +103,8 @@ uniqTags(tags: string[][]): string[][]
tagsFromIMeta(imeta: string[]): string[][]
```
## Usage Examples
## Example
### Basic Tag Handling
```typescript
// Get specific tag types
const pubkeys = getPubkeyTagValues(event.tags)
@@ -118,32 +117,3 @@ const refs = getTags(['p', 'e'], event.tags)
// Get single tag
const topic = getTagValue('t', event.tags)
```
### Thread Processing
```typescript
// Get thread context
const {roots, replies} = getReplyTags(event.tags)
// Process thread structure
function processThread(tags: string[][]) {
const thread = getReplyTags(tags)
return {
rootEvents: thread.roots.map(t => t[1]),
replyTo: thread.replies.map(t => t[1]),
mentions: thread.mentions.map(t => t[1])
}
}
```
### Tag Collection
```typescript
// Collect all references
function collectReferences(tags: string[][]) {
return {
events: getEventTagValues(tags),
profiles: getPubkeyTagValues(tags),
addresses: getAddressTagValues(tags)
}
}
```
+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
```