Add vitepress docs
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# Nostr Address
|
||||
|
||||
The Address module provides utilities for working with Nostr Addresses (NIP-19 naddr format) and handles the conversion between different address formats.
|
||||
|
||||
## Address Class
|
||||
|
||||
```typescript
|
||||
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
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Working with Long-form Content
|
||||
```typescript
|
||||
// Create address for article
|
||||
const articleAddress = new Address(
|
||||
30023, // Long-form content kind
|
||||
authorPubkey,
|
||||
'my-article-slug',
|
||||
['wss://relay.example.com']
|
||||
)
|
||||
|
||||
// Convert to string format for storage
|
||||
const addressString = articleAddress.toString()
|
||||
|
||||
// Convert to naddr for sharing
|
||||
const shareableAddress = articleAddress.toNaddr()
|
||||
```
|
||||
|
||||
### Handling Replaceable Events
|
||||
```typescript
|
||||
// Create address from replaceable event
|
||||
const address = Address.fromEvent(event)
|
||||
|
||||
// Store latest version using address as key
|
||||
storage.set(address.toString(), event)
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
|
||||
```typescript
|
||||
class Encryptable<T extends EventTemplate> {
|
||||
constructor(
|
||||
readonly event: Partial<T>, // Base event template
|
||||
readonly updates: EncryptableUpdates // Plaintext updates
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
```typescript
|
||||
// Create private bookmark list
|
||||
const bookmarks = new Encryptable(
|
||||
{
|
||||
kind: 10003,
|
||||
tags: [['d', 'bookmarks']] // Public identifier
|
||||
},
|
||||
{
|
||||
content: JSON.stringify([
|
||||
{ id: 'note1', title: 'Secret Note' }
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
// Encrypt for publishing
|
||||
const event = await bookmarks.reconcile(async (content) => {
|
||||
return await myEncryptionFunction(content)
|
||||
})
|
||||
```
|
||||
|
||||
### Encrypted Group Membership
|
||||
```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])
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
```
|
||||
@@ -0,0 +1,185 @@
|
||||
# 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.
|
||||
|
||||
## Event Types Hierarchy
|
||||
|
||||
```typescript
|
||||
// Base event with content and tags
|
||||
interface EventContent {
|
||||
tags: string[][]
|
||||
content: string
|
||||
}
|
||||
|
||||
// Base event with kind
|
||||
interface EventTemplate extends EventContent {
|
||||
kind: number
|
||||
}
|
||||
|
||||
// Event with timestamp
|
||||
interface StampedEvent extends EventTemplate {
|
||||
created_at: number
|
||||
}
|
||||
|
||||
// Event with author
|
||||
interface OwnedEvent extends StampedEvent {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
// Event with ID
|
||||
interface HashedEvent extends OwnedEvent {
|
||||
id: string
|
||||
}
|
||||
|
||||
// Event with signature
|
||||
interface SignedEvent extends HashedEvent {
|
||||
sig: string
|
||||
[verifiedSymbol]?: boolean
|
||||
}
|
||||
|
||||
// Event with wrapped content
|
||||
interface UnwrappedEvent extends HashedEvent {
|
||||
wrap: SignedEvent
|
||||
}
|
||||
|
||||
// Event that can be either signed or wrapped
|
||||
type TrustedEvent = HashedEvent & {
|
||||
sig?: string
|
||||
wrap?: SignedEvent
|
||||
[verifiedSymbol]?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
// Check event types
|
||||
isEventTemplate(event): boolean
|
||||
isStampedEvent(event): boolean
|
||||
isOwnedEvent(event): boolean
|
||||
isHashedEvent(event): boolean
|
||||
isSignedEvent(event): boolean
|
||||
isUnwrappedEvent(event): boolean
|
||||
isTrustedEvent(event): boolean
|
||||
```
|
||||
|
||||
## Event Type Conversion
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Event Utilities
|
||||
|
||||
### Event Validation
|
||||
```typescript
|
||||
// Check if event has valid signature
|
||||
hasValidSignature(event: SignedEvent): boolean
|
||||
|
||||
// Get event identifier (d tag)
|
||||
getIdentifier(event: EventTemplate): string | undefined
|
||||
```
|
||||
|
||||
### Event References
|
||||
```typescript
|
||||
// Get event ID or address
|
||||
getIdOrAddress(event: HashedEvent): string
|
||||
|
||||
// Get both ID and address (if replaceable)
|
||||
getIdAndAddress(event: HashedEvent): string[]
|
||||
```
|
||||
|
||||
### Event Type Checking
|
||||
```typescript
|
||||
// Check event properties
|
||||
isEphemeral(event: EventTemplate): boolean
|
||||
isReplaceable(event: EventTemplate): boolean
|
||||
isPlainReplaceable(event: EventTemplate): boolean
|
||||
isParameterizedReplaceable(event: EventTemplate): boolean
|
||||
```
|
||||
|
||||
### Thread & Reply Handling
|
||||
```typescript
|
||||
// Get thread information
|
||||
getAncestors(event: EventTemplate): { roots: string[], replies: string[] }
|
||||
|
||||
// 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
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating and Processing Events
|
||||
|
||||
```typescript
|
||||
// Create new event
|
||||
const event = createEvent(1, {
|
||||
content: "Hello world!",
|
||||
tags: [["t", "greeting"]]
|
||||
})
|
||||
|
||||
// Process based on type
|
||||
if (isSignedEvent(event)) {
|
||||
// Handle signed event
|
||||
if (hasValidSignature(event)) {
|
||||
processValidEvent(event)
|
||||
}
|
||||
} else if (isUnwrappedEvent(event)) {
|
||||
// Handle wrapped event
|
||||
processWrappedEvent(event)
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Threads
|
||||
|
||||
```typescript
|
||||
// Get thread context
|
||||
const ancestors = getAncestors(event)
|
||||
const rootId = ancestors.roots[0]
|
||||
const replyTo = ancestors.replies[0]
|
||||
|
||||
// Check threading
|
||||
if (isChildOf(event, parentEvent)) {
|
||||
// Handle 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)
|
||||
```
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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.
|
||||
|
||||
## Core Types
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
## Filter Operations
|
||||
|
||||
### Match Events
|
||||
```typescript
|
||||
// Match single filter
|
||||
matchFilter(filter: Filter, event: HashedEvent): boolean
|
||||
|
||||
// Match multiple filters
|
||||
matchFilters(filters: Filter[], event: HashedEvent): boolean
|
||||
```
|
||||
|
||||
### Combine Filters
|
||||
```typescript
|
||||
// Combine filters with OR operation
|
||||
unionFilters(filters: Filter[]): Filter[]
|
||||
|
||||
// Combine filters with AND operation
|
||||
intersectFilters(groups: 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
|
||||
|
||||
```typescript
|
||||
// Unix epoch for Nostr (2021-01-01)
|
||||
export const EPOCH = 1609459200
|
||||
|
||||
// One day in seconds
|
||||
export const DAY = 86400
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Filtering
|
||||
|
||||
```typescript
|
||||
// Create basic filter
|
||||
const filter: Filter = {
|
||||
kinds: [1], // Text notes
|
||||
authors: ['pubkey1', 'pubkey2'],
|
||||
since: now() - 24 * 60 * 60, // Last 24 hours
|
||||
limit: 100
|
||||
}
|
||||
|
||||
// Match event against filter
|
||||
if (matchFilter(filter, event)) {
|
||||
processEvent(event)
|
||||
}
|
||||
```
|
||||
|
||||
### Combining Filters
|
||||
|
||||
```typescript
|
||||
// Union of filters (OR)
|
||||
const combinedFilters = unionFilters([
|
||||
{ kinds: [1], authors: ['pub1'] },
|
||||
{ kinds: [1], authors: ['pub2'] }
|
||||
])
|
||||
|
||||
// Intersection of filters (AND)
|
||||
const intersectedFilters = intersectFilters([
|
||||
[{ kinds: [1] }],
|
||||
[{ authors: ['pub1'] }]
|
||||
])
|
||||
```
|
||||
|
||||
### Time-based Filtering
|
||||
|
||||
```typescript
|
||||
// Filter events from specific time range
|
||||
const timeFilter: Filter = {
|
||||
since: now() - 7 * DAY, // Last week
|
||||
until: now(),
|
||||
limit: 100
|
||||
}
|
||||
|
||||
// Guess appropriate time window
|
||||
const delta = guessFilterDelta([timeFilter])
|
||||
```
|
||||
|
||||
### Tag Filtering
|
||||
|
||||
```typescript
|
||||
// Filter by tags
|
||||
const tagFilter: Filter = {
|
||||
'#t': ['nostr', 'bitcoin'], // Match hashtags
|
||||
'#p': ['pubkey1'], // Match mentions
|
||||
limit: 50
|
||||
}
|
||||
```
|
||||
|
||||
## Filter Optimization
|
||||
|
||||
### Trim Filters
|
||||
```typescript
|
||||
// Trim large filters to reasonable size
|
||||
const trimmedFilter = trimFilter(filter)
|
||||
const trimmedFilters = trimFilters(filters)
|
||||
```
|
||||
|
||||
### Filter Analysis
|
||||
```typescript
|
||||
// Get filter generality score
|
||||
const score = getFilterGenerality(filter)
|
||||
|
||||
// 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
|
||||
```
|
||||
@@ -0,0 +1,133 @@
|
||||
# Handlers (NIP-89)
|
||||
|
||||
The Handlers module provides functionality for working with handler recommendations and information (NIP-89).
|
||||
Handlers are events that describe which kinds a given application can display.
|
||||
|
||||
This module provides utilities for transforming these events into structured handler objects that applications can easily process.
|
||||
|
||||
|
||||
## Types
|
||||
|
||||
### Handler Definition
|
||||
|
||||
```typescript
|
||||
type Handler = {
|
||||
kind: number // Event kind this handler can process
|
||||
name: string // Display name of the handler
|
||||
about: string // Description
|
||||
image: string // Icon or image URL
|
||||
identifier: string // Unique identifier (d-tag)
|
||||
event: TrustedEvent // Original handler event
|
||||
website?: string // Optional website URL
|
||||
lud16?: string // Optional Lightning address
|
||||
nip05?: string // Optional NIP-05 identifier
|
||||
}
|
||||
```
|
||||
|
||||
## Core Functions
|
||||
|
||||
### Reading Handlers
|
||||
```typescript
|
||||
function readHandlers(event: TrustedEvent): Handler[]
|
||||
|
||||
// Example
|
||||
const handlers = readHandlers(handlerEvent)
|
||||
handlers.forEach(handler => {
|
||||
console.log(`Handler for kind ${handler.kind}: ${handler.name}`)
|
||||
})
|
||||
```
|
||||
|
||||
### Handler Identification
|
||||
```typescript
|
||||
function getHandlerKey(handler: Handler): string
|
||||
// Returns "kind:address" format
|
||||
|
||||
function getHandlerAddress(event: TrustedEvent): string | undefined
|
||||
// Gets handler address from event tags
|
||||
```
|
||||
|
||||
### Display Formatting
|
||||
```typescript
|
||||
function displayHandler(
|
||||
handler?: Handler,
|
||||
fallback = ""
|
||||
): string
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Reading Handler Information
|
||||
```typescript
|
||||
const event = {
|
||||
kind: 31990, // Handler Information kind
|
||||
content: JSON.stringify({
|
||||
name: "Note Viewer",
|
||||
about: "Displays text notes with formatting",
|
||||
image: "https://example.com/icon.png"
|
||||
}),
|
||||
tags: [
|
||||
['k', '1'], // Handles kind 1 (text notes)
|
||||
['d', 'note-viewer']
|
||||
]
|
||||
}
|
||||
|
||||
const handlers = readHandlers(event)
|
||||
// Returns array of handlers defined in the event
|
||||
```
|
||||
|
||||
### Working with Handlers
|
||||
```typescript
|
||||
// Get unique handler identifier
|
||||
const key = getHandlerKey(handler)
|
||||
// => "1:30023:note-viewer" (kind:pubkey:identifier)
|
||||
|
||||
// Display handler name
|
||||
const name = displayHandler(handler, "Unknown Handler")
|
||||
// => "Note Viewer" or fallback if handler undefined
|
||||
|
||||
// Get handler address
|
||||
const address = getHandlerAddress(event)
|
||||
// Returns address from tags with 'web' marker or first address
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
// Process handler information event
|
||||
function processHandlerEvent(event: TrustedEvent) {
|
||||
// Read all handlers from event
|
||||
const handlers = readHandlers(event)
|
||||
|
||||
// Process each handler
|
||||
handlers.forEach(handler => {
|
||||
// Generate unique key
|
||||
const key = getHandlerKey(handler)
|
||||
|
||||
// Store handler information
|
||||
handlerRegistry.set(key, {
|
||||
name: handler.name,
|
||||
kind: handler.kind,
|
||||
about: handler.about,
|
||||
image: handler.image,
|
||||
website: handler.website,
|
||||
address: getHandlerAddress(handler.event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Find handler for event kind
|
||||
function findHandler(kind: number): Handler | undefined {
|
||||
return Array.from(handlerRegistry.values())
|
||||
.find(h => h.kind === kind)
|
||||
}
|
||||
|
||||
// Display handler information
|
||||
function renderHandler(handler: Handler) {
|
||||
return {
|
||||
title: displayHandler(handler, "Unknown"),
|
||||
description: handler.about,
|
||||
icon: handler.image,
|
||||
website: handler.website || null
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# @welshman/util
|
||||
|
||||
A comprehensive utility package for Nostr application development, providing essential tools and types for working with Nostr events, addresses, profiles, and more.
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Event Management**: Create, validate, and process Nostr events
|
||||
- **Repository**: In-memory event storage with querying and indexing
|
||||
- **Filters**: Advanced event filtering and subscription management
|
||||
- **Profiles**: User profile handling and formatting
|
||||
- **Lists**: Public and private list management
|
||||
- **Zaps**: Lightning Network payment integration
|
||||
- **Tags**: Comprehensive tag parsing and manipulation
|
||||
- **Addresses**: NIP-19 address handling
|
||||
- **Relays**: Relay URL handling, event dispatching and in-memory storage
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm install @welshman/util
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# Event Kinds
|
||||
|
||||
This module provides a comprehensive collection of Nostr event kind definitions and utilities.
|
||||
It includes standard NIP event kinds as well as commonly used application-specific kinds.
|
||||
|
||||
|
||||
## Kind Type Checkers
|
||||
|
||||
```typescript
|
||||
// Check if kind is ephemeral (should not be stored)
|
||||
export const isEphemeralKind = (kind: number): boolean
|
||||
|
||||
// Check if kind is replaceable (only latest event matters)
|
||||
export const isReplaceableKind = (kind: number): boolean
|
||||
|
||||
// Check if kind is plain replaceable (no parameters)
|
||||
export const isPlainReplaceableKind = (kind: number): boolean
|
||||
|
||||
// Check if kind is parameterized replaceable
|
||||
export const isParameterizedReplaceableKind = (kind: number): boolean
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Checking Event Types
|
||||
```typescript
|
||||
import { isReplaceableKind, PROFILE, NOTE } from '@welshman/util'
|
||||
|
||||
// Profile events are replaceable
|
||||
isReplaceableKind(PROFILE) // => true
|
||||
|
||||
// Notes are not replaceable
|
||||
isReplaceableKind(NOTE) // => false
|
||||
```
|
||||
|
||||
### Working with DVMs
|
||||
```typescript
|
||||
import {
|
||||
DVM_REQUEST_TEXT_SUMMARY,
|
||||
DVM_RESPONSE_TEXT_SUMMARY,
|
||||
isDVMKind
|
||||
} from '@welshman/util'
|
||||
|
||||
// Create DVM request
|
||||
const request = {
|
||||
kind: DVM_REQUEST_TEXT_SUMMARY,
|
||||
content: "Text to summarize"
|
||||
}
|
||||
|
||||
// Check for DVM events
|
||||
isDVMKind(event.kind) // => true for kinds 5000-7000
|
||||
```
|
||||
|
||||
### Handling Replaceable Events
|
||||
```typescript
|
||||
import {
|
||||
isReplaceableKind,
|
||||
PROFILE,
|
||||
LONG_FORM
|
||||
} from '@welshman/util'
|
||||
|
||||
function handleEvent(event) {
|
||||
if (isReplaceableKind(event.kind)) {
|
||||
// Only keep latest version
|
||||
replaceExistingEvent(event)
|
||||
} else {
|
||||
// Keep all versions
|
||||
storeNewEvent(event)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Links
|
||||
|
||||
A small module for handling Nostr URI manipulation.
|
||||
|
||||
## Core Functions
|
||||
|
||||
### fromNostrURI
|
||||
```typescript
|
||||
function fromNostrURI(s: string): string
|
||||
|
||||
// Examples
|
||||
fromNostrURI('nostr:npub1...') // => 'npub1...'
|
||||
fromNostrURI('nostr://npub1...') // => 'npub1...'
|
||||
fromNostrURI('note1...') // => 'note1...'
|
||||
```
|
||||
Removes the `nostr:` or `nostr://` protocol prefix from a Nostr URI.
|
||||
|
||||
### toNostrURI
|
||||
```typescript
|
||||
function toNostrURI(s: string): string
|
||||
|
||||
// Examples
|
||||
toNostrURI('npub1...') // => 'nostr:npub1...'
|
||||
toNostrURI('nostr:npub1...') // => 'nostr:npub1...' (unchanged)
|
||||
```
|
||||
Ensures a string has the `nostr:` protocol prefix.
|
||||
@@ -0,0 +1,175 @@
|
||||
# Lists
|
||||
|
||||
The Lists module provides utilities for working with Nostr lists, including both public and private lists (like bookmarks, mute lists, etc.). It handles list creation, encryption, and manipulation.
|
||||
|
||||
## Core Types
|
||||
|
||||
### List Parameters
|
||||
```typescript
|
||||
interface ListParams {
|
||||
kind: number // List kind (e.g., 10000 for mutes)
|
||||
}
|
||||
```
|
||||
|
||||
### List Structure
|
||||
```typescript
|
||||
interface List extends ListParams {
|
||||
publicTags: string[][] // Publicly visible tags
|
||||
privateTags: string[][] // Encrypted tags
|
||||
event?: DecryptedEvent // Original event if list exists
|
||||
}
|
||||
```
|
||||
|
||||
### Published List
|
||||
```typescript
|
||||
interface PublishedList extends List {
|
||||
event: DecryptedEvent // Required event for published lists
|
||||
}
|
||||
```
|
||||
|
||||
## List Creation
|
||||
|
||||
### Create New List
|
||||
```typescript
|
||||
function makeList(list: ListParams & Partial<List>): List
|
||||
|
||||
// Example
|
||||
const muteList = makeList({
|
||||
kind: 10000,
|
||||
publicTags: [['d', 'mutes']],
|
||||
privateTags: [['p', 'pubkey1'], ['p', 'pubkey2']]
|
||||
})
|
||||
```
|
||||
|
||||
### Read Existing List
|
||||
```typescript
|
||||
function readList(event: DecryptedEvent): PublishedList
|
||||
|
||||
// Example
|
||||
const list = readList(decryptedEvent)
|
||||
```
|
||||
|
||||
## List Operations
|
||||
|
||||
### Get All Tags
|
||||
```typescript
|
||||
function getListTags(list: List | undefined): string[][]
|
||||
|
||||
// Example
|
||||
const allTags = getListTags(list) // Combines public and private tags
|
||||
```
|
||||
|
||||
### Remove Items
|
||||
```typescript
|
||||
// Remove by predicate
|
||||
function removeFromListByPredicate(
|
||||
list: List,
|
||||
pred: (t: string[]) => boolean
|
||||
): Encryptable
|
||||
|
||||
// Remove by value
|
||||
function removeFromList(
|
||||
list: List,
|
||||
value: string
|
||||
): Encryptable
|
||||
```
|
||||
|
||||
### Add Items
|
||||
```typescript
|
||||
// Add public items
|
||||
function addToListPublicly(
|
||||
list: List,
|
||||
...tags: string[][]
|
||||
): Encryptable
|
||||
|
||||
// Add private items
|
||||
function addToListPrivately(
|
||||
list: List,
|
||||
...tags: string[][]
|
||||
): Encryptable
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Private List
|
||||
```typescript
|
||||
// Create new mute list
|
||||
const muteList = makeList({
|
||||
kind: 10000,
|
||||
publicTags: [
|
||||
['d', 'mutes'],
|
||||
['name', 'My Mute List']
|
||||
]
|
||||
})
|
||||
|
||||
// Add items privately
|
||||
const updated = addToListPrivately(
|
||||
muteList,
|
||||
['p', 'pubkey1'],
|
||||
['p', 'pubkey2']
|
||||
)
|
||||
|
||||
// Encrypt and publish
|
||||
const encrypted = await updated.reconcile(encrypt)
|
||||
```
|
||||
|
||||
### Reading and Updating Lists
|
||||
```typescript
|
||||
// Read existing list
|
||||
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,
|
||||
tag => tag[0] === 'p'
|
||||
)
|
||||
```
|
||||
|
||||
## Common List Types
|
||||
|
||||
### Mute List
|
||||
```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']
|
||||
]
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
# Profile
|
||||
|
||||
The Profile module provides utilities for handling Nostr user profiles (kind 0 events), including profile creation, reading, and display formatting.
|
||||
|
||||
## Core Types
|
||||
|
||||
### Profile Structure
|
||||
```typescript
|
||||
interface Profile {
|
||||
name?: string // Display name
|
||||
nip05?: string // NIP-05 verification
|
||||
lud06?: string // Legacy Lightning address
|
||||
lud16?: string // Lightning address
|
||||
lnurl?: string // Lightning URL
|
||||
about?: string // Bio/description
|
||||
banner?: string // Banner image URL
|
||||
picture?: string // Profile picture URL
|
||||
website?: string // Website URL
|
||||
display_name?: string // Alternative display name
|
||||
event?: TrustedEvent // Original profile event
|
||||
}
|
||||
```
|
||||
|
||||
### Published Profile
|
||||
```typescript
|
||||
interface PublishedProfile extends Omit<Profile, "event"> {
|
||||
event: TrustedEvent // Required event for published profiles
|
||||
}
|
||||
```
|
||||
|
||||
## Core Functions
|
||||
|
||||
### Profile Creation & Reading
|
||||
```typescript
|
||||
// Create new profile
|
||||
function makeProfile(profile: Partial<Profile>): Profile
|
||||
|
||||
// Read profile from event
|
||||
function readProfile(event: TrustedEvent): PublishedProfile
|
||||
|
||||
// Create profile event
|
||||
function createProfile(profile: Profile): EventTemplate
|
||||
|
||||
// Edit existing profile
|
||||
function editProfile(profile: PublishedProfile): EventTemplate
|
||||
```
|
||||
|
||||
### Display Formatting
|
||||
```typescript
|
||||
// Format pubkey for display
|
||||
function displayPubkey(pubkey: string): string
|
||||
|
||||
// Format profile name for display
|
||||
function displayProfile(
|
||||
profile?: Profile,
|
||||
fallback = ""
|
||||
): string
|
||||
|
||||
// Check if profile has name
|
||||
function profileHasName(profile?: Profile): boolean
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating New Profile
|
||||
```typescript
|
||||
// Create basic profile
|
||||
const profile = makeProfile({
|
||||
name: "Alice",
|
||||
about: "Nostr user",
|
||||
picture: "https://example.com/avatar.jpg",
|
||||
lud16: "alice@getalby.com"
|
||||
})
|
||||
|
||||
// Create profile event
|
||||
const event = createProfile(profile)
|
||||
```
|
||||
|
||||
### Reading Profile
|
||||
```typescript
|
||||
// Read profile from event
|
||||
const profile = readProfile(profileEvent)
|
||||
|
||||
// Access profile data
|
||||
console.log(profile.name)
|
||||
console.log(profile.about)
|
||||
console.log(profile.lnurl) // Auto-generated from lud16/lud06
|
||||
```
|
||||
|
||||
### Displaying Profile
|
||||
```typescript
|
||||
// Display profile name
|
||||
const name = displayProfile(profile, "Anonymous")
|
||||
|
||||
// Display pubkey
|
||||
const shortPubkey = displayPubkey(profile.event.pubkey)
|
||||
// => "npub1abc...xyz"
|
||||
|
||||
// Check for name
|
||||
if (profileHasName(profile)) {
|
||||
showName(profile)
|
||||
} else {
|
||||
showPubkey(profile)
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Profile
|
||||
```typescript
|
||||
// Edit existing profile
|
||||
const updated = editProfile({
|
||||
...existingProfile,
|
||||
name: "New Name",
|
||||
about: "Updated bio"
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,147 @@
|
||||
# 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.
|
||||
|
||||
## Core Components
|
||||
|
||||
### 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 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## URL Utilities
|
||||
|
||||
### URL Validation
|
||||
```typescript
|
||||
// Check if URL is valid relay URL
|
||||
isRelayUrl(url: string): boolean
|
||||
|
||||
// Check if URL is .onion address
|
||||
isOnionUrl(url: string): boolean
|
||||
|
||||
// Check if URL is local
|
||||
isLocalUrl(url: string): boolean
|
||||
|
||||
// Check if URL is IP address
|
||||
isIPAddress(url: string): boolean
|
||||
|
||||
// Check if URL can be shared
|
||||
isShareableRelayUrl(url: string): boolean
|
||||
```
|
||||
|
||||
### URL Formatting
|
||||
```typescript
|
||||
// Normalize relay URL
|
||||
normalizeRelayUrl(url: string): string
|
||||
|
||||
// Format URL for display
|
||||
displayRelayUrl(url: string): string
|
||||
|
||||
// Format relay profile for display
|
||||
displayRelayProfile(profile?: RelayProfile, fallback = ""): string
|
||||
```
|
||||
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### URL Processing
|
||||
```typescript
|
||||
// Validate relay URL
|
||||
if (isRelayUrl(url)) {
|
||||
// Normalize for consistency
|
||||
const normalized = normalizeRelayUrl(url)
|
||||
|
||||
// Check if shareable
|
||||
if (isShareableRelayUrl(normalized)) {
|
||||
// Format for display
|
||||
const display = displayRelayUrl(normalized)
|
||||
showRelay(display)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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")
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
# Repository
|
||||
|
||||
The Repository module provides a robust in-memory event storage system with indexing, querying, and event replacement capabilities.
|
||||
|
||||
## Core Features
|
||||
|
||||
- Event storage and indexing
|
||||
- Query support with multiple filters
|
||||
- Event replacement and deletion tracking
|
||||
- Event update notifications
|
||||
- Optimized indexes for common queries
|
||||
|
||||
## Class Definition
|
||||
|
||||
```typescript
|
||||
class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
||||
// Storage indexes
|
||||
eventsById = new Map<string, E>()
|
||||
eventsByWrap = new Map<string, E>()
|
||||
eventsByAddress = new Map<string, E>()
|
||||
eventsByTag = new Map<string, E[]>()
|
||||
eventsByDay = new Map<number, E[]>()
|
||||
eventsByAuthor = new Map<string, E[]>()
|
||||
eventsByKind = new Map<number, E[]>()
|
||||
deletes = new Map<string, number>()
|
||||
}
|
||||
```
|
||||
|
||||
## Core Methods
|
||||
|
||||
### Event Management
|
||||
```typescript
|
||||
// Store or update event
|
||||
publish(event: E, opts = { shouldNotify: true }): boolean
|
||||
|
||||
// Get event by ID or address
|
||||
getEvent(idOrAddress: string): E | undefined
|
||||
|
||||
// Check if event exists
|
||||
hasEvent(event: E): boolean
|
||||
|
||||
// Remove event
|
||||
removeEvent(idOrAddress: string): void
|
||||
|
||||
// Check deletion status
|
||||
isDeleted(event: E): boolean
|
||||
isDeletedByAddress(event: E): boolean
|
||||
isDeletedById(event: E): boolean
|
||||
```
|
||||
|
||||
### Querying
|
||||
```typescript
|
||||
// Query events with filters
|
||||
query(
|
||||
filters: Filter[],
|
||||
opts = {
|
||||
includeDeleted: false,
|
||||
shouldSort: true
|
||||
}
|
||||
): E[]
|
||||
|
||||
// Dump all events
|
||||
dump(): E[]
|
||||
|
||||
// Load events in bulk
|
||||
load(events: E[], chunkSize = 1000): void
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Repository Operations
|
||||
```typescript
|
||||
// Create repository
|
||||
const repo = new Repository<TrustedEvent>()
|
||||
|
||||
// Add events
|
||||
repo.publish(event)
|
||||
|
||||
// Query events
|
||||
const events = repo.query([
|
||||
{ kinds: [1], limit: 100 }
|
||||
])
|
||||
|
||||
// Check event status
|
||||
if (!repo.isDeleted(event)) {
|
||||
processEvent(event)
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk Operations
|
||||
```typescript
|
||||
// Load multiple events
|
||||
repo.load(events, 500) // Process in chunks of 500
|
||||
|
||||
// Get all events
|
||||
const allEvents = repo.dump()
|
||||
```
|
||||
|
||||
### Query Examples
|
||||
```typescript
|
||||
// Query with multiple filters
|
||||
const events = repo.query([
|
||||
// Recent events from specific authors
|
||||
{
|
||||
kinds: [1],
|
||||
authors: ['pub1', 'pub2'],
|
||||
since: now() - 24 * 60 * 60
|
||||
},
|
||||
// Events with specific tags
|
||||
{
|
||||
'#t': ['bitcoin', 'nostr'],
|
||||
limit: 50
|
||||
}
|
||||
])
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
# Tags
|
||||
|
||||
The Tags module provides comprehensive utilities for working with Nostr event tags, including helpers for extracting, validating, and manipulating different types of tags.
|
||||
|
||||
## Core Functions
|
||||
|
||||
### Basic Tag Operations
|
||||
```typescript
|
||||
// Get tags by type(s)
|
||||
getTags(types: string | string[], tags: string[][]): string[][]
|
||||
|
||||
// Get single tag by type(s)
|
||||
getTag(types: string | string[], tags: string[][]): string[] | undefined
|
||||
|
||||
// Get tag values
|
||||
getTagValues(types: string | string[], tags: string[][]): string[]
|
||||
|
||||
// Get single tag value
|
||||
getTagValue(types: string | string[], tags: string[][]): string | undefined
|
||||
```
|
||||
|
||||
## Tag Type Extractors
|
||||
|
||||
### Event References
|
||||
```typescript
|
||||
// Get 'e' tags (event references)
|
||||
getEventTags(tags: string[][]): string[][]
|
||||
getEventTagValues(tags: string[][]): string[]
|
||||
|
||||
// Get 'a' tags (event addresses)
|
||||
getAddressTags(tags: string[][]): string[][]
|
||||
getAddressTagValues(tags: string[][]): string[]
|
||||
```
|
||||
|
||||
### Profile References
|
||||
```typescript
|
||||
// Get 'p' tags (pubkey references)
|
||||
getPubkeyTags(tags: string[][]): string[][]
|
||||
getPubkeyTagValues(tags: string[][]): string[]
|
||||
```
|
||||
|
||||
### Topics and Relays
|
||||
```typescript
|
||||
// Get 't' tags (topics/hashtags)
|
||||
getTopicTags(tags: string[][]): string[][]
|
||||
getTopicTagValues(tags: string[][]): string[]
|
||||
|
||||
// Get 'r' and 'relay' tags
|
||||
getRelayTags(tags: string[][]): string[][]
|
||||
getRelayTagValues(tags: string[][]): string[]
|
||||
```
|
||||
|
||||
### Groups and Kinds
|
||||
```typescript
|
||||
// Get group tags
|
||||
getGroupTags(tags: string[][]): string[][]
|
||||
getGroupTagValues(tags: string[][]): string[]
|
||||
|
||||
// Get 'k' tags (kind references)
|
||||
getKindTags(tags: string[][]): string[][]
|
||||
getKindTagValues(tags: string[][]): number[]
|
||||
```
|
||||
|
||||
## Thread Management
|
||||
|
||||
### Comment Tags
|
||||
```typescript
|
||||
// Get root and reply references
|
||||
getCommentTags(tags: string[][]): {
|
||||
roots: string[][],
|
||||
replies: string[][]
|
||||
}
|
||||
|
||||
getCommentTagValues(tags: string[][]): {
|
||||
roots: string[],
|
||||
replies: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### Reply Tags
|
||||
```typescript
|
||||
// Get detailed reply structure
|
||||
getReplyTags(tags: string[][]): {
|
||||
roots: string[][], // Thread roots
|
||||
replies: string[][], // Direct replies
|
||||
mentions: string[][] // Mentions
|
||||
}
|
||||
|
||||
getReplyTagValues(tags: string[][]): {
|
||||
roots: string[],
|
||||
replies: string[],
|
||||
mentions: string[]
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
```typescript
|
||||
// Remove duplicate tags
|
||||
uniqTags(tags: string[][]): string[][]
|
||||
|
||||
// Parse imeta tags into array of tag arrays
|
||||
tagsFromIMeta(imeta: string[]): string[][]
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Tag Handling
|
||||
```typescript
|
||||
// Get specific tag types
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
const topics = getTopicTagValues(event.tags)
|
||||
const relays = getRelayTagValues(event.tags)
|
||||
|
||||
// Get multiple tag types
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,192 @@
|
||||
# Zaps
|
||||
|
||||
The Zaps module provides utilities for working with Lightning Network payments (zaps) in Nostr, including LNURL handling, invoice amount parsing, and zap validation.
|
||||
|
||||
## Zapper Interface
|
||||
The Zapper interface represents a Lightning Network payment provider that can process zaps:
|
||||
|
||||
```typescript
|
||||
interface Zapper {
|
||||
// LNURL for payment processing
|
||||
lnurl: string
|
||||
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
### Finding Nostr Zappers
|
||||
|
||||
#### Getting Lightning Info
|
||||
|
||||
First, check the user's profile for Lightning addresses:
|
||||
|
||||
```typescript
|
||||
function getLightningInfo(profile: Profile) {
|
||||
// Check for Lightning Address (NIP-57)
|
||||
if (profile.lud16) {
|
||||
return {
|
||||
type: 'lud16',
|
||||
address: profile.lud16
|
||||
}
|
||||
}
|
||||
|
||||
// Check for LNURL
|
||||
if (profile.lud06) {
|
||||
return {
|
||||
type: 'lud06',
|
||||
url: profile.lud06
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
#### Fetching LNURL Metadata
|
||||
|
||||
Once you have the Lightning address or LNURL, fetch the metadata:
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Example Alby zapper configuration
|
||||
const albyZapper: Zapper = {
|
||||
lnurl: "lnurl1...",
|
||||
pubkey: "alby_user_pubkey",
|
||||
nostrPubkey: "alby_signing_key",
|
||||
allowsNostr: true,
|
||||
minSendable: 1000, // 1 sat minimum
|
||||
maxSendable: 100000000 // 100k sats maximum
|
||||
}
|
||||
|
||||
// Example LNbits zapper
|
||||
const lnbitsZapper: Zapper = {
|
||||
lnurl: "lnurl1...",
|
||||
callback: "https://lnbits.com/callback",
|
||||
nostrPubkey: "lnbits_signing_key",
|
||||
allowsNostr: true
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```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)
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user