# Research: Addresses ## Topic Summary Create an `Address` struct representing a nostr event address (`kind:pubkey:d-tag`). It should: - Be constructable from an event via `from_event` - Encode/decode to the `naddr` NIP-19 bech32 format (TLV-based) - Encode/decode to the `kind:pubkey:identifier` string format - Provide convenient property access (kind, pubkey, identifier) The reader already understands kind ranges and addressable events from the kinds chapter. ## Philosophy From `ref/building-nostr`: - Events are content-addressable via their ID (hash), giving referential transparency. But replaceability broke this — a replaceable event's ID changes on every update, so you need a stable reference. - An "address" is `kind:pubkey:identifier`, pointing to all events sharing the same kind, pubkey, and d-tag. - The `a` tag carries this triple as its value, with an optional relay hint in position 2. - Design principle: "you should have a slight bias towards regular, non-replaceable events because they don't break referential transparency." - The address includes the author's pubkey, which enables outbox-model relay lookup — unlike a bare event ID. ## Reference Implementation Analysis ### applesauce - No custom Address struct — re-exports `AddressPointer` from nostr-tools: `{ kind, pubkey, identifier, relays? }` - Also has `AddressPointerWithoutD` for replaceable (non-addressable) events - `createReplaceableAddress(kind, pubkey, identifier?)` concatenates with `:` - `parseReplaceableAddress(str)` splits on `:`, handles identifiers containing colons by joining remaining parts - `getAddressPointerForEvent(event, relays?)` extracts d-tag: `event.tags.find(t => t[0] === "d")?.[1] ?? ""` - Delegates naddr encoding to nostr-tools ### ndk - No dedicated Address type — handles addresses on NDKEvent directly - `tagAddress()` returns `"${kind}:${pubkey}:${dTag ?? ""}"` - `dTag` getter/setter on NDKEvent - Auto-generates d-tag for param replaceable events if missing (random string) - `filterFromId(id)` handles three input forms: raw address string, naddr bech32, or event ID - Regex: `NIP33_A_REGEX = /^(\d+):([0-9A-Fa-f]+)(?::(.*))?$/` - Delegates nip19 to nostr-tools ### nostr-gadgets - No dedicated Address type — addresses are plain `kind:pubkey:d-tag` strings - `isATag(input)` validates via regex: `/^\d+:[0-9a-f]{64}:[^:]+$/` - Imports `AddressPointer` from nostr-tools when needed - Inline parsing by splitting on `:` - d-tag defaults to empty string ### nostrlib (Go) - Type: `EntityPointer { PublicKey, Kind, Identifier, Relays []string }` - `AsTagReference()`: `fmt.Sprintf("%d:%s:%s", ep.Kind, ep.PublicKey.Hex(), ep.Identifier)` - `ParseAddrString(addr)`: splits on `:` (max 3 parts) - `Tags.GetD()` returns first d tag value or "" - `MatchesEvent` checks kind + pubkey + identifier - NIP-19 TLV: identifier=TLV0, relays=TLV1, author=TLV2(32 bytes), kind=TLV3(u32 BE) - No separate from_event — caller assembles EntityPointer ### nostr-tools - `AddressPointer = { identifier: string, pubkey: string, kind: number, relays?: string[] }` - naddr TLV: 0=identifier(UTF-8), 1=relay(multiple), 2=pubkey(32 bytes), 3=kind(u32 BE) - Strict decode: TLV 0, 2, 3 required; relays default to [] - a tag parsed as `tag[1].split(':')` ### rust-nostr - `Coordinate { kind: Kind, public_key: PublicKey, identifier: String }` - `Nip19Coordinate { coordinate: Coordinate, relays: Vec }` (separate from core Coordinate) - `from_kpi_format(str)` parses `kind:pubkey:d-tag` via split on `:` - `parse(str)` tries kpi format, then bech32, then NIP-21 URI - `Display` outputs `kind:pubkey:identifier` - `FromStr` delegates to `parse()` - `verify()` validates: replaceable must have empty identifier, addressable must have non-empty - TLV encode: SPECIAL(0)=identifier, AUTHOR(2)=32-byte pubkey, KIND(3)=u32 BE, RELAY(1)=URL strings - TLV decode: while loop consuming bytes, first occurrence wins for each field type - `Coordinate` converts `Into` and `Into` - `CoordinateBorrow<'a>` for zero-copy operations ### welshman - `Address` class with `kind, pubkey, identifier, relays` - `Address.from(str, relays?)` parses `kind:pubkey:identifier` via regex - `Address.fromNaddr(naddr)` decodes via nostr-tools - `Address.fromEvent(event, relays?)` extracts d-tag: `event.tags.find(t => t[0] === "d")?.[1] || ""` - `toString()` returns `kind:pubkey:identifier` - `toNaddr()` delegates to nostr-tools - `getAddress(event)` shorthand - `Address.isAddress(str)` regex test ## Common Patterns 1. **Three fields**: kind (u16), pubkey (32 bytes), identifier/d-tag (string). All implementations agree. 2. **String format**: `kind:pubkey:identifier` — universal, colon-separated, kind is decimal. 3. **NIP-19 TLV**: identifier=type 0, relay=type 1, author=type 2 (32 bytes), kind=type 3 (u32 big-endian). 4. **d-tag defaults to empty string**, not None/null — important for replaceable events that have no d-tag. 5. **from_event pattern**: extract d-tag from `tags.find("d")?.value() ?? ""`, combine with event.kind and event.pubkey. 6. **Relay hints are separate**: the core address is just the triple; relays are transport metadata. Most implementations keep them optional/separate. 7. **Parsing colon-split**: most implementations just split on `:` and take 3 parts. Some handle identifiers containing colons by joining remaining parts. ## Considerations for Our Implementation - Keep the core `Address` struct to the three fields (kind, pubkey, identifier). Relay hints can be added later as a wrapper or in the naddr encoding methods. - `from_event` should work on our existing `Event` type, using `tags.value("d")` which already exists. - NIP-19 TLV encoding is new infrastructure — the keys chapter only used simple bech32 (no TLV). This chapter introduces the TLV pattern that will be reused for `nprofile` and `nevent` later. - `Display`/`FromStr` should use the `kind:pubkey:identifier` format (the `a` tag value), since that's the most common string representation. - Unlike rust-nostr, we don't need to validate that addressable kinds have non-empty identifiers — an address is just a reference triple, and empty identifiers are valid for replaceable events. - The identifier field should handle colons in d-tags correctly during parsing.