104 lines
6.2 KiB
Markdown
104 lines
6.2 KiB
Markdown
# 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<RelayUrl> }` (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<Tag>` and `Into<Filter>`
|
|
- `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.
|