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 node_modules
docs #docs
docs/reference docs/reference
docs/.vitepress/cache docs/.vitepress/cache
build build
+85 -89
View File
@@ -1,110 +1,106 @@
# Nostr Address # 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 ```typescript
class Address { // Address class for handling addressable events
export declare class Address {
constructor( constructor(
readonly kind: number, // Event kind kind: number,
readonly pubkey: string, // Author's public key pubkey: string,
readonly identifier: string, // Unique identifier (d-tag) identifier: string,
readonly relays?: string[], // Optional relay hints 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 // Utility function to get address string from event
export declare const getAddress: (e: AddressableEvent) => string;
### 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)
``` ```
## Examples ## Examples
### Working with Long-form Content ### Creating and parsing addresses
```typescript ```typescript
// Create address for article import { Address } from '@welshman/util';
const articleAddress = new Address(
30023, // Long-form content kind // Create address from components
authorPubkey, const address = new Address(
'my-article-slug', 30023,
'27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389',
'my-article',
['wss://relay.example.com'] ['wss://relay.example.com']
) );
// Convert to string format for storage // Parse from string format
const addressString = articleAddress.toString() const parsed = Address.from('30023:27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389:my-article');
console.log(parsed.kind); // 30023
console.log(parsed.identifier); // 'my-article'
// Convert to naddr for sharing // Check if string is valid address
const shareableAddress = articleAddress.toNaddr() const isValid = Address.isAddress('30023:27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389:my-article'); // true
const isInvalid = Address.isAddress('invalid-format'); // false
``` ```
### Handling Replaceable Events ### Converting between formats
```typescript ```typescript
// Create address from replaceable event import { Address } from '@welshman/util';
const address = Address.fromEvent(event)
// Store latest version using address as key const address = new Address(30023, '27067f0efd1b9ffc6d71672a1b69a4e5ac3b8ce3cc8428b06849448e38d69389', 'my-article');
storage.set(address.toString(), event)
// 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 # 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 ## API
### 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
```typescript ```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( constructor(
readonly event: Partial<T>, // Base event template event: Partial<T>,
readonly updates: EncryptableUpdates // Plaintext updates 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 ## Examples
### Private Bookmarks ### Basic Usage
```typescript ```typescript
// Create private bookmark list import { Encryptable } from '@welshman/util';
const bookmarks = new Encryptable(
// 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, content: JSON.stringify(['pubkey1', 'pubkey2']),
tags: [['d', 'bookmarks']] // Public identifier tags: [['p', 'sensitive-pubkey'], ['e', 'sensitive-event-id']]
},
{
content: JSON.stringify([
{ id: 'note1', title: 'Secret Note' }
])
} }
) );
// Encrypt for publishing // The reconcile method encrypts tag values at index 1
const event = await bookmarks.reconcile(async (content) => { const event = await encryptable.reconcile(encryptFn);
return await myEncryptionFunction(content) // event.tags[0] = ['p', 'encrypted-pubkey']
}) // event.tags[1] = ['e', 'encrypted-event-id']
``` ```
### Encrypted Group Membership ### Working with Decrypted Events
```typescript ```typescript
// Create private group member list import { asDecryptedEvent } from '@welshman/util';
const members = new Encryptable(
{
kind: 30000,
tags: [['d', 'group-members']]
},
{
tags: members.map(m => ['p', m.pubkey, m.role])
}
)
const encrypted = await members.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']] };
### Updating Private Content
```typescript const decryptedEvent = asDecryptedEvent(event, plaintext);
function updatePrivateList(event: DecryptedEvent, newItems: string[]) { console.log(decryptedEvent.plaintext.content); // "original content"
return new Encryptable(
event,
{
content: JSON.stringify(newItems)
}
)
}
// Usage
const updated = updatePrivateList(existingEvent, newItems)
const final = await updated.reconcile(encrypt)
``` ```
+156 -136
View File
@@ -1,185 +1,205 @@
# Nostr Events # 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 ```typescript
// Base event with content and tags // Base event content structure
interface EventContent { export type EventContent = {
tags: string[][] tags: string[][];
content: string content: string;
} };
// Base event with kind // Event template with kind
interface EventTemplate extends EventContent { export type EventTemplate = EventContent & {
kind: number kind: number;
} };
// Event with timestamp // Event with timestamp
interface StampedEvent extends EventTemplate { export type StampedEvent = EventTemplate & {
created_at: number created_at: number;
} };
// Event with author // Event with author
interface OwnedEvent extends StampedEvent { export type OwnedEvent = StampedEvent & {
pubkey: string pubkey: string;
} };
// Event with ID // Event with ID
interface HashedEvent extends OwnedEvent { export type HashedEvent = OwnedEvent & {
id: string id: string;
} };
// Event with signature // Signed event
interface SignedEvent extends HashedEvent { export type SignedEvent = HashedEvent & {
sig: string sig: string;
[verifiedSymbol]?: boolean };
}
// Event with wrapped content // Wrapped event (NIP-59)
interface UnwrappedEvent extends HashedEvent { export type UnwrappedEvent = HashedEvent & {
wrap: SignedEvent wrap: SignedEvent;
} };
// Event that can be either signed or wrapped // Event that can be either signed or wrapped
type TrustedEvent = HashedEvent & { export type TrustedEvent = HashedEvent & {
sig?: string sig?: string;
wrap?: SignedEvent wrap?: SignedEvent;
[verifiedSymbol]?: boolean };
}
``` ```
## Event Creation ### 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
```typescript ```typescript
// Check event types // Options for creating events
isEventTemplate(event): boolean export type MakeEventOpts = {
isStampedEvent(event): boolean content?: string;
isOwnedEvent(event): boolean tags?: string[][];
isHashedEvent(event): boolean created_at?: number;
isSignedEvent(event): boolean };
isUnwrappedEvent(event): boolean
isTrustedEvent(event): boolean // Creates a stamped event template
export declare const makeEvent: (kind: number, opts?: MakeEventOpts) => StampedEvent;
``` ```
## Event Type Conversion ### Type Guards
```typescript ```typescript
// Convert to specific event types export declare const isEventTemplate: (e: EventTemplate) => e is EventTemplate;
asEventTemplate(event): EventTemplate export declare const isStampedEvent: (e: StampedEvent) => e is StampedEvent;
asStampedEvent(event): StampedEvent export declare const isOwnedEvent: (e: OwnedEvent) => e is OwnedEvent;
asOwnedEvent(event): OwnedEvent export declare const isHashedEvent: (e: HashedEvent) => e is HashedEvent;
asHashedEvent(event): HashedEvent export declare const isSignedEvent: (e: TrustedEvent) => e is SignedEvent;
asSignedEvent(event): SignedEvent export declare const isUnwrappedEvent: (e: TrustedEvent) => e is UnwrappedEvent;
asUnwrappedEvent(event): UnwrappedEvent export declare const isTrustedEvent: (e: TrustedEvent) => e is TrustedEvent;
asTrustedEvent(event): TrustedEvent
``` ```
## Event Utilities ### Event Utilities
### Event Validation
```typescript ```typescript
// Check if event has valid signature // Event validation and signatures
hasValidSignature(event: SignedEvent): boolean export declare const verifyEvent: (event: TrustedEvent) => boolean;
// Get event identifier (d tag) // Event properties
getIdentifier(event: EventTemplate): string | undefined 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 ## Threading Protocols
```typescript
// Get event ID or address
getIdOrAddress(event: HashedEvent): string
// Get both ID and address (if replaceable) The `getAncestors` function handles two different threading protocols:
getIdAndAddress(event: HashedEvent): string[]
```
### Event Type Checking ### Regular Notes (NIP-10)
```typescript For regular notes and most event kinds, threading follows [NIP-10](https://github.com/nostr-protocol/nips/blob/master/10.md):
// Check event properties - Uses `e` and `a` tags with optional markers (`root`, `reply`, `mention`)
isEphemeral(event: EventTemplate): boolean - Positional rules apply when markers are absent:
isReplaceable(event: EventTemplate): boolean - First `e`/`a` tag = root
isPlainReplaceable(event: EventTemplate): boolean - Last `e`/`a` tag = reply target
isParameterizedReplaceable(event: EventTemplate): boolean - Middle tags = mentions
```
### Thread & Reply Handling ### Comments (NIP-22)
```typescript For comments (kind 1111), threading follows [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md):
// Get thread information - Uses uppercase tags (`E`, `A`, `P`, `K`) for root references
getAncestors(event: EventTemplate): { roots: string[], replies: string[] } - Uses lowercase tags (`e`, `a`, `p`, `k`) for reply references
- No positional rules - explicit tag types determine relationship
// Get parent references All `getParent*` functions and `isChildOf` include this logic, automatically handling both protocols based on event kind.
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
```
## Examples ## Examples
### Creating and Processing Events ### Creating Events
```typescript ```typescript
// Create new event import { makeEvent, NOTE, LONG_FORM } from '@welshman/util';
const event = createEvent(1, {
content: "Hello world!",
tags: [["t", "greeting"]]
})
// Process based on type // Create a basic note
if (isSignedEvent(event)) { const note = makeEvent(NOTE, {
// Handle signed event content: "Hello Nostr!",
if (hasValidSignature(event)) { tags: [["t", "nostr"]]
processValidEvent(event) });
}
} else if (isUnwrappedEvent(event)) { // Create a long-form article with custom timestamp
// Handle wrapped event const article = makeEvent(LONG_FORM, {
processWrappedEvent(event) 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 ### Working with Threads
```typescript ```typescript
// Get thread context import { getAncestors, isChildOf, NOTE, COMMENT } from '@welshman/util';
const ancestors = getAncestors(event)
const rootId = ancestors.roots[0]
const replyTo = ancestors.replies[0]
// Check threading // Regular note reply (NIP-10)
if (isChildOf(event, parentEvent)) { const noteReply = makeEvent(NOTE, {
// Handle reply 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 # Filters
The Filters module provides utilities for creating, manipulating, and matching Nostr event 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.
It includes support for filter operations, optimization, and time-based filtering.
## Core Types ## API
### Filter Matching
```typescript ```typescript
interface Filter { // Check if an event matches a filter
ids?: string[] // Match specific event IDs export declare const matchFilter: <E extends HashedEvent>(filter: Filter, event: E) => boolean;
kinds?: number[] // Match event kinds
authors?: string[] // Match author pubkeys // Check if an event matches any filter in array
since?: number // Match events since timestamp export declare const matchFilters: <E extends HashedEvent>(filters: Filter[], event: E) => boolean;
until?: number // Match events until timestamp
limit?: number // Limit number of results
search?: string // Text search
[key: `#${string}`]: string[] // Tag filters
}
``` ```
## Filter Operations ### Filter Operations
### Match Events
```typescript ```typescript
// Match single filter // Get a compact string representation of a filter
matchFilter(filter: Filter, event: HashedEvent): boolean export declare const getFilterId: (filter: Filter) => string;
// Match multiple filters // Combine multiple filters into minimal filter set
matchFilters(filters: Filter[], event: HashedEvent): boolean 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 ### Specialized Filter Creation
```typescript
// Combine filters with OR operation
unionFilters(filters: Filter[]): Filter[]
// Combine filters with AND operation ```typescript
intersectFilters(groups: Filter[][]): Filter[] // 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 ### Filter Analysis
```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
```typescript ```typescript
// Unix epoch for Nostr (2021-01-01) // Calculate filter generality (0 = specific, 1 = very general)
export const EPOCH = 1609459200 export declare const getFilterGenerality: (filter: Filter) => number;
// One day in seconds // Estimate time delta for filter results
export const DAY = 86400 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 ## Examples
### Basic Filtering ### Basic Filter Matching
```typescript ```typescript
// Create basic filter import { matchFilter, matchFilters, NOTE, LONG_FORM } from '@welshman/util';
const filter: Filter = {
kinds: [1], // Text notes
authors: ['pubkey1', 'pubkey2'],
since: now() - 24 * 60 * 60, // Last 24 hours
limit: 100
}
// Match event against filter const event = {
if (matchFilter(filter, event)) { id: 'abc123...',
processEvent(event) 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 ```typescript
// Union of filters (OR) import { getIdFilters } from '@welshman/util';
const combinedFilters = unionFilters([
{ kinds: [1], authors: ['pub1'] },
{ kinds: [1], authors: ['pub2'] }
])
// Intersection of filters (AND) // Mix of event IDs and addresses
const intersectedFilters = intersectFilters([ const references = [
[{ kinds: [1] }], 'abc123...', // event ID
[{ authors: ['pub1'] }] '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 ```typescript
// Filter events from specific time range import { getReplyFilters } from '@welshman/util';
const timeFilter: Filter = {
since: now() - 7 * DAY, // Last week
until: now(),
limit: 100
}
// Guess appropriate time window const originalEvents = [
const delta = guessFilterDelta([timeFilter]) { 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 ```typescript
// Filter by tags import { unionFilters, intersectFilters, trimFilters } from '@welshman/util';
const tagFilter: Filter = {
'#t': ['nostr', 'bitcoin'], // Match hashtags // Combine overlapping filters
'#p': ['pubkey1'], // Match mentions const filters = [
limit: 50 { 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 ```typescript
// Trim large filters to reasonable size import { addRepostFilters, NOTE, LONG_FORM } from '@welshman/util';
const trimmedFilter = trimFilter(filter)
const trimmedFilters = trimFilters(filters) 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 ### Filter Analysis
```typescript ```typescript
// Get filter generality score import { getFilterGenerality, guessFilterDelta, getFilterResultCardinality } from '@welshman/util';
const score = getFilterGenerality(filter)
// Get expected result count const specificFilter = { ids: ['abc123...'] };
const count = getFilterResultCardinality(filter) const generalFilter = { kinds: [1] };
```
console.log(getFilterGenerality(specificFilter)); // 0 (very specific)
## Advanced Usage console.log(getFilterGenerality(generalFilter)); // 1 (very general)
### Reply Chain Filters // Estimate appropriate time window
```typescript const filters = [{ authors: ['abc...', 'def...'] }];
// Get filters for replies const deltaSeconds = guessFilterDelta(filters); // ~21600 (6 hours)
const replyFilters = getReplyFilters(events, {
kinds: [1], // Check expected result count
limit: 100 const idFilter = { ids: ['abc...', 'def...', 'ghi...'] };
}) const resultCount = getFilterResultCardinality(idFilter); // 3
```
### Repost Handling
```typescript
// Add filters for reposts
const withReposts = addRepostFilters([
{ kinds: [1] } // Original filter
])
// Results in filters for kinds 1, 6, and 16
``` ```
+10 -43
View File
@@ -109,6 +109,13 @@ const updated = addToListPrivately(
['p', 'pubkey2'] ['p', 'pubkey2']
) )
// Add new items publicly
const addItems = addToListPublicly(
list,
['p', 'pubkey3'],
['p', 'pubkey4']
)
// Encrypt and publish // Encrypt and publish
const encrypted = await updated.reconcile(encrypt) const encrypted = await updated.reconcile(encrypt)
``` ```
@@ -121,19 +128,6 @@ const list = readList(decryptedEvent)
// Remove item // Remove item
const removeItem = removeFromList(list, 'pubkey1') 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 // Remove by predicate
const noMentions = removeFromListByPredicate( const noMentions = removeFromListByPredicate(
list, list,
@@ -141,35 +135,8 @@ const noMentions = removeFromListByPredicate(
) )
``` ```
## Common List Types ### Working with Tags
### Mute List
```typescript ```typescript
const muteList = makeList({ // Get all list tags
kind: 10000, const tags = getListTags(list)
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']
]
})
``` ```
+2 -2
View File
@@ -73,7 +73,7 @@ const profile = makeProfile({
}) })
// Create profile event // Create profile event
const event = createProfile(profile) const profileEvent = createProfile(profile)
``` ```
### Reading Profile ### Reading Profile
@@ -107,7 +107,7 @@ if (profileHasName(profile)) {
### Updating Profile ### Updating Profile
```typescript ```typescript
// Edit existing profile // Edit existing profile
const updated = editProfile({ const profileEvent = editProfile({
...existingProfile, ...existingProfile,
name: "New Name", name: "New Name",
about: "Updated bio" about: "Updated bio"
+126 -115
View File
@@ -1,147 +1,158 @@
# Relay # 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` module provides utilities for working with Nostr relays, including URL normalization, validation, and relay profile handling.
The Relay class extends EventEmitter to provide event-based communication.
## Core Components ## API
### Types and Enums
### Relay Class
```typescript ```typescript
class Relay<E extends HashedEvent = TrustedEvent> extends Emitter { // Relay operation modes
constructor(readonly repository: Repository<E>) export enum RelayMode {
Read = "read",
// Emit events: 'EVENT', 'EOSE', 'OK' Write = "write",
emit(type: string, ...args: any[]): boolean Inbox = "inbox"
// Handle relay messages
send(type: string, ...message: any[]): void
} }
// 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 ### 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 ```typescript
// Fetch relay information document // Get display name for relay profile
async function getRelayProfile(url: string): Promise<RelayProfile | null> { export declare const displayRelayProfile: (profile?: RelayProfile, fallback?: string) => string;
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
}
}
``` ```
## URL Utilities ## Examples
### URL Validation ### URL Validation
```typescript ```typescript
// Check if URL is valid relay URL import {
isRelayUrl(url: string): boolean isRelayUrl,
isOnionUrl,
isLocalUrl,
isShareableRelayUrl
} from '@welshman/util';
// Check if URL is .onion address // Valid relay URLs
isOnionUrl(url: string): boolean 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 // Invalid URLs
isLocalUrl(url: string): boolean console.log(isRelayUrl('https://example.com')); // false (not websocket)
console.log(isRelayUrl('invalid-url')); // false
// Check if URL is IP address // Special URL types
isIPAddress(url: string): boolean 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 // Safe to share publicly
isShareableRelayUrl(url: string): boolean console.log(isShareableRelayUrl('wss://relay.damus.io')); // true
console.log(isShareableRelayUrl('ws://localhost:8080')); // false (local)
``` ```
### URL Formatting ### URL Normalization
```typescript ```typescript
// Normalize relay URL import { normalizeRelayUrl, displayRelayUrl } from '@welshman/util';
normalizeRelayUrl(url: string): string
// Format URL for display // Normalize various URL formats
displayRelayUrl(url: string): string console.log(normalizeRelayUrl('relay.damus.io'));
// 'wss://relay.damus.io/'
// Format relay profile for display console.log(normalizeRelayUrl('ws://RELAY.EXAMPLE.COM/path'));
displayRelayProfile(profile?: RelayProfile, fallback = ""): string // '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 ```typescript
// Validate relay URL import { displayRelayProfile, RelayProfile } from '@welshman/util';
if (isRelayUrl(url)) {
// Normalize for consistency
const normalized = normalizeRelayUrl(url)
// Check if shareable const relayProfile: RelayProfile = {
if (isShareableRelayUrl(normalized)) { url: 'wss://relay.damus.io',
// Format for display name: 'Damus Relay',
const display = displayRelayUrl(normalized) description: 'A high-performance Nostr relay',
showRelay(display) 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
} }
} };
```
// Get display name
### Relay usage with Repository const displayName = displayRelayProfile(relayProfile);
console.log(displayName); // 'Damus Relay'
```typescript
// Create storage and relay interface // With fallback for unnamed relays
const repository = new Repository() const anonymousRelay: RelayProfile = {
const relay = new Relay(repository) url: 'wss://anonymous.relay.com'
};
// Subscribe to events
relay.send("REQ", "sub_id", { const name = displayRelayProfile(anonymousRelay, 'Unknown Relay');
kinds: [1], console.log(name); // 'Unknown Relay'
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")
``` ```
+1 -31
View File
@@ -103,9 +103,8 @@ uniqTags(tags: string[][]): string[][]
tagsFromIMeta(imeta: string[]): string[][] tagsFromIMeta(imeta: string[]): string[][]
``` ```
## Usage Examples ## Example
### Basic Tag Handling
```typescript ```typescript
// Get specific tag types // Get specific tag types
const pubkeys = getPubkeyTagValues(event.tags) const pubkeys = getPubkeyTagValues(event.tags)
@@ -118,32 +117,3 @@ const refs = getTags(['p', 'e'], event.tags)
// Get single tag // Get single tag
const topic = getTagValue('t', event.tags) 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 # 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 ## Protocol Overview
The Zapper interface represents a Lightning Network payment provider that can process zaps:
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 ```typescript
interface Zapper { // Zapper service information
// LNURL for payment processing export type Zapper = {
lnurl: string lnurl: string;
pubkey?: string;
callback?: string;
minSendable?: number;
maxSendable?: number;
nostrPubkey?: string;
allowsNostr?: boolean;
};
// User's pubkey on the payment service // Complete zap with request and receipt
pubkey?: string export type Zap = {
request: TrustedEvent; // kind 9734 (zap request)
// LNURL callback endpoint response: TrustedEvent; // kind 9735 (zap receipt)
callback?: string invoiceAmount: number; // amount in millisatoshis
};
// 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
}
``` ```
### Finding Nostr Zappers ### Lightning Network Utilities
#### Getting Lightning Info
First, check the user's profile for Lightning addresses:
```typescript ```typescript
function getLightningInfo(profile: Profile) { // Convert human-readable amount to millisatoshis
// Check for Lightning Address (NIP-57) export declare const hrpToMillisat: (hrpString: string) => bigint;
if (profile.lud16) {
return {
type: 'lud16',
address: profile.lud16
}
}
// Check for LNURL // Extract amount from BOLT11 lightning invoice
if (profile.lud06) { export declare const getInvoiceAmount: (bolt11: string) => number;
return {
type: 'lud06',
url: profile.lud06
}
}
return null // Convert lightning address or URL to LNURL
} export declare const getLnUrl: (address: string) => string | null;
``` ```
#### Fetching LNURL Metadata ### Zap Validation
Once you have the Lightning address or LNURL, fetch the metadata:
```typescript ```typescript
async function fetchZapper(address: string): Promise<Zapper | null> { // Create validated Zap from zap receipt event
// Convert Lightning address to LNURL if needed export declare const zapFromEvent: (response: TrustedEvent, zapper?: Zapper) => Zap | null;
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
}
}
``` ```
## Examples
### Converting Lightning Addresses
```typescript ```typescript
// Example Alby zapper configuration import { getLnUrl } from '@welshman/util';
const albyZapper: Zapper = {
lnurl: "lnurl1...", // Lightning address (LUD-16)
pubkey: "alby_user_pubkey", const lnurl1 = getLnUrl('satoshi@getalby.com');
nostrPubkey: "alby_signing_key", 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, allowsNostr: true,
minSendable: 1000, // 1 sat minimum minSendable: 1000,
maxSendable: 100000000 // 100k sats maximum 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 // Step 2: Create zap request (kind 9734)
const lnbitsZapper: Zapper = { const zapRequest = makeEvent(ZAP_REQUEST, {
lnurl: "lnurl1...", content: 'Amazing content!',
callback: "https://lnbits.com/callback", tags: [
nostrPubkey: "lnbits_signing_key", ['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 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 ### Zap Validation Rules
```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
}
```
## Core Functions The `zapFromEvent` function validates several aspects of a zap according to NIP-57:
### 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.
```typescript ```typescript
function zapFromEvent( import { zapFromEvent } from '@welshman/util';
response: TrustedEvent,
zapper: Zapper | undefined // Validation checks performed:
): Zap | null // 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
## Usage Examples // 4. LNURL matches the expected service (if provided in request)
// 5. Self-zaps are filtered out (sender != zapper)
### Processing Lightning Addresses
```typescript const zapReceipt = {
// Get LNURL from various formats // ... zap receipt event
const lnurl1 = getLnUrl("user@getalby.com") };
const lnurl2 = getLnUrl("https://getalby.com/.well-known/lnurlp/user")
const lnurl3 = getLnUrl("lnurl1...") const zapper = {
nostrPubkey: 'expected-zapper-pubkey',
// Check if conversion was successful lnurl: 'expected-lnurl'
if (lnurl1) { };
// Process LNURL
processLnurl(lnurl1) const validZap = zapFromEvent(zapReceipt, zapper);
}
``` // Returns null if any validation fails:
// - Malformed bolt11 invoice
### Invoice Amount Handling // - Amount mismatch
```typescript // - Wrong zapper pubkey
// Get invoice amount in millisats // - LNURL mismatch
const amount = getInvoiceAmount(bolt11Invoice) // - Self-zap detection
// 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)
}
``` ```