Add skills

This commit is contained in:
Jon Staab
2026-06-09 17:24:24 -07:00
parent f2a54cee49
commit a33af11b1b
12 changed files with 3971 additions and 0 deletions
+675
View File
@@ -0,0 +1,675 @@
---
name: welshman-util
description: Use this skill when working with @welshman/util: nostr event types, kinds, tags, filters, addresses, NIPs (42/86/98), profiles, relays, zaps, wallets, or any core nostr data structures.
---
# welshman/util — Core Nostr Utilities
`@welshman/util` is the foundational layer of the welshman nostr stack, providing types, constants, and helpers for every nostr primitive: events, kinds, tags, filters, addresses, profiles, lists, zaps, relays, and Lightning wallet integration. Higher level welshman packages (`@welshman/net`, `@welshman/app`, `@welshman/store`, etc.) depend on the types and utilities defined here.
## Installation
```bash
npm install @welshman/util
# or
pnpm add @welshman/util
# or
yarn add @welshman/util
```
---
## Key Exports
### Event Types
| Type | Description |
|------|-------------|
| `EventContent` | `{ tags, content }` — base content structure |
| `EventTemplate` | `EventContent + kind` |
| `StampedEvent` | `EventTemplate + created_at` |
| `OwnedEvent` | `StampedEvent + pubkey` |
| `HashedEvent` | `OwnedEvent + id` |
| `SignedEvent` | `HashedEvent + sig` |
| `TrustedEvent` | `HashedEvent + optional sig` — most common in-app type |
| `DecryptedEvent` | `TrustedEvent + plaintext` (for encrypted lists/events) |
### Event Utilities
| Export | Description |
|--------|-------------|
| `verifiedSymbol` | Symbol (re-exported from `nostr-tools`) used as a key on events; set `event[verifiedSymbol] = true` to skip signature re-validation |
| `makeEvent(kind, opts?)` | Create a `StampedEvent` with optional content, tags, created_at |
| `verifyEvent(event)` | Verify event signature; returns `false` for unsigned events (no `sig` field) even if `verifiedSymbol` is set, because `isSignedEvent` is checked first; returns `true` immediately for signed events where `event[verifiedSymbol]` is already set |
| `getIdentifier(event)` | Get `d` tag value |
| `getIdOrAddress(event)` | Returns address string for replaceable events, id otherwise |
| `getIdAndAddress(event)` | Returns array with both id and address (if applicable) |
| `deduplicateEvents(events)` | Deduplicate by id or address |
| `isEphemeral(event)` | True for ephemeral kinds (2000029999) |
| `isReplaceable(event)` | True for plain or parameterized replaceable |
| `isPlainReplaceable(event)` | True for kinds 1000019999 and metadata/contacts |
| `isParameterizedReplaceable(event)` | True for kinds 3000039999 |
| `getAncestors(event)` | Returns `{ roots, replies, mentions }` for NIP-10 events (mentions may be empty `[]` but is always present); NIP-22/COMMENT path returns `{ roots, replies }` without mentions |
| `getParentIdOrAddr(event)` | Immediate parent id or address |
| `isChildOf(child, parent)` | Check if child replies to parent |
### Type Guards
`isEventTemplate`, `isStampedEvent`, `isOwnedEvent`, `isHashedEvent`, `isSignedEvent`
### Event Kinds (constants)
All constants are exported by name from `@welshman/util`.
**Core / NIP-01**
```
PROFILE = 0 NOTE = 1 FOLLOWS = 3
DELETE = 5 REPOST = 6 REACTION = 7
BADGE_AWARD = 8 MESSAGE = 9 THREAD = 11
SEAL = 13 DIRECT_MESSAGE = 14 DIRECT_MESSAGE_FILE = 15
GENERIC_REPOST = 16 PICTURE_NOTE = 20 VANISH = 62
COMMENT = 1111 GENERIC_REPOST = 16
```
**Channels (NIP-28)**
```
CHANNEL_CREATE = 40 CHANNEL_UPDATE = 41 CHANNEL_MESSAGE = 42
CHANNEL_HIDE_MESSAGE = 43 CHANNEL_MUTE_USER = 44
```
**Wrapped / encrypted (NIP-59)**
```
WRAP = 1059 WRAP_NIP04 = 1060
WRAPPED_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE] // convenience array
```
**Media / files**
```
FILE_METADATA = 1063 PICTURE_NOTE = 20 AUDIO = 31337
```
**Polls**
```
POLL = 1068 POLL_RESPONSE = 1018
```
**Marketplace / auction**
```
BID = 1021 BID_CONFIRMATION = 1022
STALL = 30017 PRODUCT = 30018 MARKET_UI = 30019
PRODUCT_SOLD_AS_AUCTION = 30020
CLASSIFIED = 30402 DRAFT_CLASSIFIED = 30403
```
**Git (NIP-34)**
```
GIT_PATCH = 1617 GIT_ISSUE = 1621 GIT_REPLY = 1622
GIT_STATUS_OPEN = 1630 GIT_STATUS_COMPLETE = 1631
GIT_STATUS_CLOSED = 1632 GIT_STATUS_DRAFT = 1633
GIT_REPOSITORY = 30403
```
**Social / community**
```
REMIX = 1808 REPORT = 1984 LABEL = 1985
REVIEW = 1986 HIGHLIGHT = 9802 APPROVAL = 4550
NOSTROCKET_PROBLEM = 1971
COMMUNITY = 34550
BADGE_DEFINITION = 30009 BADGES = 30008
LIVE_EVENT = 30311 LIVE_CHAT_MESSAGE = 1311
```
**Rooms (NIP-29)**
```
ROOM_CREATE = 9007 ROOM_DELETE = 9008 ROOM = 35834
ROOM_JOIN = 9021 ROOM_LEAVE = 9022 ROOM_META = 39000
ROOM_ADMINS = 39001 ROOM_MEMBERS = 39002 ROOM_EDIT_META = 9002
ROOM_ADD_MEMBER = 9000 ROOM_REMOVE_MEMBER = 9001
ROOM_ADD_PERM = 9003 ROOM_REMOVE_PERM = 9004
ROOM_DELETE_EVENT = 9005 ROOM_EDIT_STATUS = 9006
ROOM_CREATE_PERMISSION = 19004
RELAY_MEMBERS = 13534 RELAY_ADD_MEMBER = 8000 RELAY_REMOVE_MEMBER = 8001
RELAY_JOIN = 28934 RELAY_INVITE = 28935 RELAY_LEAVE = 28936
```
**Replaceable lists (kinds 1000010099)**
```
MUTES = 10000 PINS = 10001 RELAYS = 10002
BOOKMARKS = 10003 COMMUNITIES = 10004 CHANNELS = 10005
BLOCKED_RELAYS = 10006 SEARCH_RELAYS = 10007 ROOMS = 10009
FEEDS = 10014 TOPICS = 10015 EMOJIS = 10030
MESSAGING_RELAYS = 10050 BLOSSOM_SERVERS = 10063
FILE_SERVERS = 10096
```
**Parameterized replaceable lists (kinds 3000030102)**
```
NAMED_PEOPLE = 30000 NAMED_RELAYS = 30002 NAMED_BOOKMARKS = 30003
NAMED_CURATIONS = 30004 NAMED_TOPICS = 30015
NAMED_WIKI_AUTHORS = 30101 NAMED_WIKI_RELAYS = 30102
NAMED_EMOJIS = 30030 NAMED_ARTIFACTS = 30063
NAMED_COMMUNITIES = 30064
```
**Long-form / wiki / publishing (NIP-23)**
```
LONG_FORM = 30023 LONG_FORM_DRAFT = 30024
WIKI = 30818 APP_DATA = 30078
FEED = 31890
```
**Calendar (NIP-52)**
```
CALENDAR = 31924 EVENT_DATE = 31922 EVENT_TIME = 31923
EVENT_RSVP = 31925
```
**Handlers (NIP-89)**
```
HANDLER_INFORMATION = 31990 HANDLER_RECOMMENDATION = 31989
```
**Status / alerts**
```
STATUS = 30315
ALERT_EMAIL = 32830 ALERT_STATUS = 32831 ALERT_WEB = 32832
ALERT_ANDROID = 32833 ALERT_IOS = 32834
```
**Zaps / wallet / Lightning**
```
ZAP_GOAL = 9041 ZAP_REQUEST = 9734 ZAP_RESPONSE = 9735
WALLET_INFO = 13194 WALLET_REQUEST = 23194 WALLET_RESPONSE = 23195
LIGHTNING_PUB_RPC = 21000
OTS = 1040
```
**Auth**
```
CLIENT_AUTH = 22242 BLOSSOM_AUTH = 24242 HTTP_AUTH = 27235
NOSTR_CONNECT = 24133
```
**Follow packs**
```
FOLLOW_PACK = 39089
```
**Promenade protocol**
```
PROMENADE_REGISTER_ACCOUNT = 16430 PROMENADE_SHARD_SHARE = 26428
PROMENADE_SHARD_ACK = 26429 PROMENADE_CONFIG = 26430
PROMENADE_COMMIT = 26431 PROMENADE_REQUEST = 26432
PROMENADE_RESULT = 26433
```
**Deprecated**
```
DEPRECATED_RELAY_RECOMMENDATION = 2
DEPRECATED_DIRECT_MESSAGE = 4
DEPRECATED_NAMED_GENERIC = 30001
```
**DVM — Data Vending Machines (NIP-90, kinds 50007000)**
Requests (`5xxx`) and their paired responses (`6xxx`):
```
DVM_REQUEST_TEXT_EXTRACTION = 5000 DVM_RESPONSE_TEXT_EXTRACTION = 6000
DVM_REQUEST_TEXT_SUMMARY = 5001 DVM_RESPONSE_TEXT_SUMMARY = 6001
DVM_REQUEST_TEXT_TRANSLATION = 5002 DVM_RESPONSE_TEXT_TRANSLATION = 6002
DVM_REQUEST_TEXT_GENERATION = 5050 DVM_RESPONSE_TEXT_GENERATION = 6050
DVM_REQUEST_IMAGE_GENERATION = 5100 DVM_RESPONSE_IMAGE_GENERATION = 6100
DVM_REQUEST_VIDEO_CONVERSION = 5200 DVM_RESPONSE_VIDEO_CONVERSION = 6200
DVM_REQUEST_VIDEO_TRANSLATION = 5201 DVM_RESPONSE_VIDEO_TRANSLATION = 6201
DVM_REQUEST_IMAGE_TO_VIDEO_CONVERSION = 5202
DVM_RESPONSE_IMAGE_TO_VIDEO_CONVERSION = 6202
DVM_REQUEST_TEXT_TO_SPEECH = 5250 DVM_RESPONSE_TEXT_TO_SPEECH = 6250
DVM_REQUEST_DISCOVER_CONTENT = 5300 DVM_RESPONSE_DISCOVER_CONTENT = 6300
DVM_REQUEST_DISCOVER_PEOPLE = 5301 DVM_RESPONSE_DISCOVER_PEOPLE = 6301
DVM_REQUEST_SEARCH_CONTENT = 5302 DVM_RESPONSE_SEARCH_CONTENT = 6302
DVM_REQUEST_SEARCH_PEOPLE = 5303 DVM_RESPONSE_SEARCH_PEOPLE = 6303
DVM_REQUEST_COUNT = 5400 DVM_RESPONSE_COUNT = 6400
DVM_REQUEST_MALWARE_SCAN = 5500 DVM_RESPONSE_MALWARE_SCAN = 6500
DVM_REQUEST_OTS = 5900 DVM_RESPONSE_OTS = 6900
DVM_REQUEST_OP_RETURN = 5901 DVM_RESPONSE_OP_RETURN = 6901
DVM_REQUEST_PUBLISH_SCHEDULE = 5905 DVM_RESPONSE_PUBLISH_SCHEDULE = 6905
DVM_FEEDBACK = 7000
```
Use `isDVMKind(kind)` to test if a kind falls in the DVM range (50007000).
**Kind classifiers**
```typescript
isRegularKind(kind) // 10009999 and select low kinds
isPlainReplaceableKind(kind) // 0, 3, and 1000019999
isEphemeralKind(kind) // 2000029999
isParameterizedReplaceableKind(kind) // 3000039999
isReplaceableKind(kind) // plain OR parameterized replaceable
isDVMKind(kind) // 50007000
```
### Tags
| Export | Description |
|--------|-------------|
| `getTags(types, tags)` | Get all tags matching one or more type strings |
| `getTag(types, tags)` | Get first matching tag |
| `getTagValues(types, tags)` | Get value (index 1) of all matching tags — types first, then the tags array |
| `getTagValue(types, tags)` | Get value of first matching tag — types first, then the tags array |
| `getEventTags(tags)` | `e` tags |
| `getEventTagValues(tags)` | Values of `e` tags |
| `getAddressTags(tags)` | `a` tags |
| `getAddressTagValues(tags)` | Values of `a` tags |
| `getPubkeyTags(tags)` | `p` tags |
| `getPubkeyTagValues(tags)` | Values of `p` tags |
| `getTopicTags(tags)` / `getTopicTagValues(tags)` | `t` (hashtag) tags |
| `getRelayTags(tags)` / `getRelayTagValues(tags)` | `r` and `relay` tags |
| `getKindTags(tags)` / `getKindTagValues(tags)` | `k` tags (returns `number[]`) |
| `getGroupTags(tags)` / `getGroupTagValues(tags)` | group tags |
| `getReplyTags(tags)` | `{ roots, replies, mentions }` — NIP-10 threading |
| `getCommentTags(tags)` | `{ roots, replies }` — NIP-22 uppercase/lowercase tags |
| `uniqTags(tags)` | Remove duplicate tags |
| `tagsFromIMeta(imeta)` | Parse `imeta` tag into array of tag arrays |
### Filters
| Export | Description |
|--------|-------------|
| `matchFilter(filter, event)` | Test if event matches a single filter |
| `matchFilters(filters, event)` | Test if event matches any filter |
| `getIdFilters(idsOrAddresses)` | Build filters from mixed ids and addresses |
| `getReplyFilters(events, filter?)` | Build filters to find replies |
| `addRepostFilters(filters)` | Add kind 6/16 repost filters |
| `unionFilters(filters)` | Merge overlapping filters |
| `intersectFilters(groups)` | Intersect arrays of filter groups |
| `trimFilter(filter)` / `trimFilters(filters)` | Limit array fields to 1000 items |
| `getFilterId(filter)` | Compact string key for a filter |
| `getFilterGenerality(filter)` | 0 = specific, 1 = general |
| `guessFilterDelta(filters, max?)` | Estimate appropriate time window in seconds |
| `getFilterResultCardinality(filter)` | Expected result count for id-based filters |
### Address
| Export | Description |
|--------|-------------|
| `Address` class | Handles `kind:pubkey:identifier` and NIP-19 naddr format |
| `Address.isAddress(s)` | Validate address string format |
| `Address.from(s, relays?)` | Parse from `kind:pubkey:identifier` string |
| `Address.fromNaddr(naddr)` | Parse from NIP-19 naddr |
| `Address.fromEvent(event, relays?)` | Create from addressable event |
| `address.toString()` | Serialize to `kind:pubkey:identifier` |
| `address.toNaddr()` | Serialize to NIP-19 naddr |
| `getAddress(event)` | Convenience: get address string from event |
### Profile
| Export | Description |
|--------|-------------|
| `makeProfile(partial)` | Create a profile object |
| `readProfile(event)` | Parse `PublishedProfile` from kind 0 event |
| `createProfile(profile)` | Create kind 0 `EventTemplate` |
| `editProfile(published)` | Update existing profile event |
| `displayProfile(profile?, fallback?)` | Get best display name string |
| `displayPubkey(pubkey)` | Shorten pubkey to `npub1abc...xyz` |
| `profileHasName(profile?)` | Check if profile has a name field |
Profile fields: `name`, `display_name`, `about`, `picture`, `banner`, `website`, `nip05`, `lud06`, `lud16`, `lnurl`
### Lists (kind 10000+)
| Export | Description |
|--------|-------------|
| `makeList(params)` | Create a new list |
| `readList(event)` | Parse `PublishedList` from `DecryptedEvent` |
| `getListTags(list)` | Combined public + private tags |
| `addToListPublicly(list, ...tags)` | Returns `Encryptable` with tag added publicly |
| `addToListPrivately(list, ...tags)` | Returns `Encryptable` with tag added privately |
| `removeFromList(list, value)` | Returns `Encryptable` with tag removed |
| `removeFromListByPredicate(list, pred)` | Returns `Encryptable` with matching tags removed |
| `updateList(list, { publicTags?, privateTags? })` | Bulk update tags |
### Encryptable
| Export | Description |
|--------|-------------|
| `Encryptable<T>` | Wraps a partial event with plaintext updates; call `.reconcile(encrypt)` to produce encrypted event |
| `asDecryptedEvent(event, plaintext?)` | Attach plaintext data to a `TrustedEvent` |
### Relay
| Export | Description |
|--------|-------------|
| `RelayMode` | Enum: `Read`, `Write`, `Search`, `Blocked`, `Messaging` |
| `RelayProfile` | NIP-11 relay info type |
| `isRelayUrl(url)` | Validate relay URL |
| `isShareableRelayUrl(url)` | True if valid relay URL and not a local address |
| `isOnionUrl(url)` | Tor address check |
| `isLocalUrl(url)` | Local address check |
| `isIPAddress(url)` | IP address check |
| `normalizeRelayUrl(url)` | Normalize to standard wss:// format |
| `displayRelayUrl(url)` | Strip protocol and trailing slash |
| `displayRelayProfile(profile?, fallback?)` | Get display name for relay |
### Zaps (NIP-57)
| Export | Description |
|--------|-------------|
| `getLnUrl(address)` | Convert lightning address or URL to LNURL; returns `undefined` if invalid |
| `getInvoiceAmount(bolt11)` | Extract millisatoshi amount from BOLT11 invoice |
| `hrpToMillisat(hrpString)` | Convert human-readable BTC amount to millisats (`bigint`) |
| `zapFromEvent(response, zapper)` | Validate zap receipt and return `Zap` or `undefined` |
| `Zapper` type | `{ lnurl, pubkey?, callback?, minSendable?, maxSendable?, nostrPubkey?, allowsNostr? }` |
| `Zap` type | `{ request: TrustedEvent, response: TrustedEvent, invoiceAmount: number }` |
### Wallet
| Export | Description |
|--------|-------------|
| `WalletType` | Enum: `WebLN`, `NWC` |
| `Wallet` | Union: `WebLNWallet | NWCWallet` |
| `isWebLNWallet(wallet)` | Type guard |
| `isNWCWallet(wallet)` | Type guard |
### NIP-42 (Relay Auth)
```typescript
makeRelayAuth(url: string, challenge: string): StampedEvent
// Creates kind 22242 auth event; sign before sending
```
### NIP-98 (HTTP Auth)
```typescript
makeHttpAuth(url: string, method?: string, body?: string): Promise<StampedEvent>
makeHttpAuthHeader(event: SignedEvent): string // Returns "Nostr <base64>"
```
### NIP-86 (Relay Management)
```typescript
sendManagementRequest(url: string, request: ManagementRequest, authEvent: SignedEvent): Promise<ManagementResponse>
// ManagementResponse = { result?: any; error?: string }
// ManagementMethod enum covers: BanPubkey, AllowPubkey, BanEvent, AllowEvent, etc.
```
### Handlers (NIP-89)
```typescript
readHandlers(event: TrustedEvent): Handler[]
getHandlerKey(handler: Handler): string // "kind:address" format
getHandlerAddress(event: TrustedEvent): string | undefined
displayHandler(handler?: Handler, fallback?: string): string
```
### Links
```typescript
fromNostrURI(s: string): string // strips "nostr:" or "nostr://" prefix
toNostrURI(s: string): string // ensures "nostr:" prefix
```
### Blossom (Media Servers)
```typescript
makeBlossomAuthEvent(opts: BlossomAuthEventOpts): StampedEvent
uploadBlob(server, blob, opts?): Promise<Response>
getBlob(server, sha256, opts?): Promise<Response>
deleteBlob(server, sha256, opts?): Promise<Response>
listBlobs(server, pubkey, opts?): Promise<Response>
checkBlobExists(server, sha256, opts?): Promise<{exists, size?}>
buildBlobUrl(server, sha256, extension?): string
encryptFile(file: Blob): Promise<EncryptedFile>
decryptFile(encryptedFile: EncryptedFile): Promise<Uint8Array>
```
---
## Common Patterns
### Creating and inspecting events
```typescript
import { makeEvent, NOTE, PROFILE, RELAYS, LONG_FORM, getIdentifier, getIdOrAddress } from '@welshman/util'
// Text note (kind 1)
const note = makeEvent(NOTE, {
content: 'Hello Nostr!',
tags: [['t', 'nostr']],
})
// Profile update (kind 0)
const profile = makeEvent(PROFILE, {
content: JSON.stringify({ name: 'Alice', about: 'Nostr dev' }),
tags: [],
})
// Relay list (kind 10002)
const relayList = makeEvent(RELAYS, {
content: '',
tags: [
['r', 'wss://relay.example.com', 'read'],
['r', 'wss://relay2.example.com', 'write'],
],
})
```
### Pre-verifying persisted events with verifiedSymbol
When loading events from a local store (IndexedDB, localStorage, etc.) at startup, you
can skip expensive signature re-validation by marking them as already verified:
```typescript
import { verifiedSymbol } from '@welshman/util'
import type { TrustedEvent } from '@welshman/util'
// Load from storage
const storedEvents: TrustedEvent[] = await db.getAll('events')
// Mark as pre-verified — verifyEvent() will return true immediately (without
// re-running the cryptographic check) for events that have a sig field
for (const event of storedEvents) {
event[verifiedSymbol] = true
}
repository.load(storedEvents)
```
Only do this for events you persisted yourself after they were validated. Never set
`verifiedSymbol` on events received directly from untrusted external sources.
### Working with tags
```typescript
import {
getTagValue,
getTagValues,
getPubkeyTagValues,
getTopicTagValues,
getRelayTagValues,
getReplyTags,
uniqTags,
} from '@welshman/util'
// getTagValue and getTagValues: types argument FIRST, then the tags array
const title = getTagValue('title', event.tags) // string | undefined
const urls = getTagValues('r', event.tags) // string[]
// Multiple types at once
const ids = getTagValues(['e', 'a'], event.tags) // string[]
const mentions = getPubkeyTagValues(event.tags) // string[]
const topics = getTopicTagValues(event.tags) // string[]
const relays = getRelayTagValues(event.tags) // string[]
// NIP-10 thread context
const { roots, replies, mentions: threadMentions } = getReplyTags(event.tags)
```
### Matching and building filters
```typescript
import { matchFilters, getIdFilters, getReplyFilters, addRepostFilters, NOTE } from '@welshman/util'
import { ago, HOUR } from '@welshman/lib'
// Does this event match our subscription?
const active = matchFilters([{ kinds: [NOTE], authors: [myPubkey] }], event)
// Fetch a set of events by id or address
const filters = getIdFilters([
'abc123', // event id
'30023:deadbeef:my-slug', // address
])
// Find all replies to a set of events
const replyFilters = getReplyFilters(events, { since: ago(HOUR) })
// Automatically include repost kinds
const withReposts = addRepostFilters([{ kinds: [NOTE] }])
```
### Addresses for replaceable events
```typescript
import { Address, getAddress } from '@welshman/util'
// From an addressable event
const addr = Address.fromEvent(event, ['wss://relay.example.com'])
console.log(addr.toString()) // '30023:deadbeef:my-slug'
console.log(addr.toNaddr()) // 'naddr1...'
// Round-trip from naddr
const parsed = Address.fromNaddr('naddr1...')
// Quick string form
const addressStr = getAddress(event) // '30023:deadbeef:my-slug'
```
### Profiles
```typescript
import { readProfile, displayProfile, displayPubkey, editProfile } from '@welshman/util'
const profile = readProfile(kind0Event)
console.log(displayProfile(profile, 'Anonymous')) // name or fallback
console.log(displayPubkey(pubkey)) // 'npub1abc...xyz'
// Update profile
const updatedEvent = editProfile({ ...profile, name: 'New Name', about: 'Updated bio' })
// sign and publish updatedEvent
```
### Zap flow
```typescript
import { getLnUrl, makeEvent, ZAP_REQUEST, zapFromEvent } from '@welshman/util'
// Step 1: resolve LNURL
const lnurl = getLnUrl('satoshi@getalby.com')
if (!lnurl) throw new Error('Invalid lightning address')
// Step 2: build zap request (kind 9734)
const zapRequest = makeEvent(ZAP_REQUEST, {
content: 'Great post!',
tags: [
['p', recipientPubkey],
['e', targetEventId],
['amount', '5000'], // millisats
['lnurl', lnurl],
['relays', 'wss://relay.damus.io'],
],
})
// Step 3: sign, send to LNURL callback, pay invoice...
// Step 4: validate receipt (kind 9735)
const zap = zapFromEvent(zapReceipt, { nostrPubkey: zapperPubkey, allowsNostr: true, lnurl })
if (zap) {
console.log(`Received ${zap.invoiceAmount} msat`, zap.request.content)
}
```
### NIP-42 relay authentication
```typescript
import { makeRelayAuth } from '@welshman/util'
// Inside relay AUTH handler
const authEvent = makeRelayAuth('wss://relay.example.com', challengeFromRelay)
const signed = await signer.sign(authEvent)
// send signed AUTH message to relay
```
### NIP-98 HTTP authentication
```typescript
import { makeHttpAuth, makeHttpAuthHeader } from '@welshman/util'
const body = JSON.stringify({ data: 'example' })
const authEvent = await makeHttpAuth('https://api.example.com/upload', 'POST', body)
const signed = await signer.signEvent(authEvent)
await fetch('https://api.example.com/upload', {
method: 'POST',
body,
headers: {
Authorization: makeHttpAuthHeader(signed),
'Content-Type': 'application/json',
},
})
```
---
## Integration Notes
- **`@welshman/net`** — uses `TrustedEvent`, `Filter`, `SignedEvent` from this package as the wire types for relay connections and subscriptions.
- **`@welshman/store`** — provides Svelte stores over repositories built on `TrustedEvent`; relies on `isReplaceable`, `getAddress`, etc. for deduplication.
- **`@welshman/app`** — high-level application layer; wraps net/store/router and uses profile, list, zap, and handler helpers from this package.
- **`@welshman/router`** — uses `RelayMode` and relay URL helpers when computing relay selections.
- **`@welshman/signer`** — produces `SignedEvent` objects that satisfy types defined here; the `Encrypt` function type used by `Encryptable` is typically provided by a signer.
---
## Gotchas & Tips
- **`TrustedEvent` vs `SignedEvent`**: Most in-app code should accept `TrustedEvent` (has id, may have sig). Only require `SignedEvent` when you need to ensure the event has a signature.
- **Replaceable event identity**: Use `getIdOrAddress` rather than `event.id` when referencing events that may be addressable — the address string is stable across updates, the id is not.
- **`getAncestors` handles two protocols**: Kind 1111 (comment/NIP-22) uses uppercase `E`/`A` for roots and lowercase for replies, returning `{ roots, replies }`. All other kinds use NIP-10 positional rules, returning `{ roots, replies, mentions }` where `mentions` is always present but may be an empty array. You do not need to branch on this; `getAncestors`, `getParentIdOrAddr`, and `isChildOf` handle it automatically.
- **List mutations return `Encryptable`**: `addToListPrivately`, `removeFromList`, etc. do not return an event directly. Call `.reconcile(encryptFn)` on the result to get the final `EventTemplate` ready to sign.
- **`zapFromEvent` returns `undefined` on any validation failure** including amount mismatch, wrong zapper pubkey, malformed invoice, or self-zap. Always check the result.
- **`getLnUrl` handles three input forms**: bare lightning address (`user@domain`), full HTTPS URL, or already-encoded `lnurl1...`. Returns `undefined` for anything else.
- **`normalizeRelayUrl` vs `displayRelayUrl`**: Use `normalizeRelayUrl` before storing or comparing relay URLs. Use `displayRelayUrl` only for human-readable display (strips protocol/trailing slash).
- **`Address.isAddress`** checks the `kind:pubkey:identifier` format only, not naddr. To validate an naddr string, use `Address.fromNaddr` inside a try/catch.
- **`getTagValue` / `getTagValues` argument order**: the type(s) come **first**, the tags array comes **second**`getTagValue('title', event.tags)`. This is the opposite of the specialized helpers like `getEventTags(tags)` which take only the tags array. Mixing up the order produces no TypeScript error but silently returns `undefined` or `[]`.
- **`verifiedSymbol` is a Symbol key**: you must import `verifiedSymbol` from `@welshman/util` and use it as a computed property key — `event[verifiedSymbol] = true`. You cannot use a string key. The symbol is re-exported from `nostr-tools/pure`, so it is the same identity as the one used internally by `verifyEvent`.