Consume NIP-19 relay hints for event fetching
Extract and use relay hints from bech32 entities (nevent, nprofile, naddr) and event tag references (e, q tags) to fetch events from hinted relays not in the user's relay pool. Changes: - Parse relay hints from bech32 TLV data in URLHandler - Pass relay hints through SearchType and NoteReference enums - Add ensureConnected() to RelayPool for ephemeral relay connections - Implement ephemeral relay lease management with race condition protection - Add repostTarget() helper to extract relay hints from repost e tags - Add QuoteRef struct to preserve relay hints from q tags (NIP-10/NIP-18) - Support relay hints in replies with author pubkey in e-tags (NIP-10) - Implement fallback broadcast when hinted relays don't respond - Add comprehensive test coverage for relay hint functionality - Add DEBUG logging for relay hint tracing during development Implementation details: - Connect to hinted relays as ephemeral, returning early when first connects - Use total deadline to prevent timeout accumulation across hint attempts - Decrement lease count before suspension points to ensure atomicity - Fall back to broadcast if hints don't resolve or respond Closes: https://github.com/damus-io/damus/issues/1147 Changelog-Added: Added relay hint support for nevent, nprofile, naddr links and event tag references (reposts, quotes, replies) Signed-off-by: alltheseas Signed-off-by: Daniel D'Aquino <daniel@daquino.me> Co-authored-by: alltheseas <alltheseas@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Daniel D'Aquino <daniel@daquino.me
This commit is contained in:
co-authored by
alltheseas
Claude Opus 4.5
Daniel D'Aquino <daniel@daquino.me
parent
6f8e2d3064
commit
9a1ae6f9b5
@@ -101,18 +101,18 @@ struct InnerSearchResults: View {
|
||||
case .nip05(let addr):
|
||||
SearchingEventView(state: damus_state, search_type: .nip05(addr))
|
||||
case .profile(let pubkey):
|
||||
SearchingEventView(state: damus_state, search_type: .profile(pubkey))
|
||||
SearchingEventView(state: damus_state, search_type: .profile(pubkey, relays: []))
|
||||
case .hex(let h):
|
||||
VStack(spacing: 10) {
|
||||
SearchingEventView(state: damus_state, search_type: .event(NoteId(h)))
|
||||
SearchingEventView(state: damus_state, search_type: .profile(Pubkey(h)))
|
||||
}
|
||||
SearchingEventView(state: damus_state, search_type: .event(NoteId(h), relays: []))
|
||||
SearchingEventView(state: damus_state, search_type: .profile(Pubkey(h), relays: []))
|
||||
}
|
||||
case .note(let nid):
|
||||
SearchingEventView(state: damus_state, search_type: .event(nid))
|
||||
SearchingEventView(state: damus_state, search_type: .event(nid, relays: []))
|
||||
case .nevent(let nevent):
|
||||
SearchingEventView(state: damus_state, search_type: .event(nevent.noteid))
|
||||
SearchingEventView(state: damus_state, search_type: .event(nevent.noteid, relays: nevent.relays))
|
||||
case .nprofile(let nprofile):
|
||||
SearchingEventView(state: damus_state, search_type: .profile(nprofile.author))
|
||||
SearchingEventView(state: damus_state, search_type: .profile(nprofile.author, relays: nprofile.relays))
|
||||
case .naddr(let naddr):
|
||||
SearchingEventView(state: damus_state, search_type: .naddr(naddr))
|
||||
case .multi(let multi):
|
||||
@@ -199,13 +199,12 @@ struct SearchResultsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct SearchResultsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchResultsView(damus_state: test_damus_state(), s)
|
||||
}
|
||||
}
|
||||
*/
|
||||
/// Interprets a raw search string and maps it to an appropriate `Search` case.
|
||||
/// - Parameters:
|
||||
/// - profiles: Profile index used when resolving profile-lookups from the query.
|
||||
/// - contacts: Contact list used to prioritize or resolve profile-lookups.
|
||||
/// - search new: The raw user-provided search string to interpret.
|
||||
/// - Returns: A `Search` value representing the parsed query (e.g., `.nip05`, `.hashtag`, `.hex`, `.profile`, `.note`, `.nevent`, `.nprofile`, `.naddr`, or `.multi`), or `nil` if the input string is empty.
|
||||
|
||||
@MainActor
|
||||
func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? {
|
||||
@@ -240,6 +239,10 @@ func search_for_string(profiles: Profiles, contacts: Contacts, search new: Strin
|
||||
}
|
||||
|
||||
if searchQuery.starts(with: "nevent"), case let .nevent(nevent) = Bech32Object.parse(searchQuery) {
|
||||
#if DEBUG
|
||||
print("[nevent] Parsed note ID: \(nevent.noteid.hex())")
|
||||
print("[nevent] Parsed \(nevent.relays.count) relay hints: \(nevent.relays.map { $0.absoluteString })")
|
||||
#endif
|
||||
return .nevent(nevent)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ enum SearchState {
|
||||
}
|
||||
|
||||
enum SearchType: Equatable {
|
||||
case event(NoteId)
|
||||
case profile(Pubkey)
|
||||
case event(NoteId, relays: [RelayURL])
|
||||
case profile(Pubkey, relays: [RelayURL])
|
||||
case nip05(String)
|
||||
case naddr(NAddr)
|
||||
}
|
||||
@@ -32,15 +32,23 @@ struct SearchingEventView: View {
|
||||
switch search_type {
|
||||
case .nip05:
|
||||
return "Nostr Address"
|
||||
case .profile:
|
||||
case .profile(_, _):
|
||||
return "Profile"
|
||||
case .event:
|
||||
case .event(_, _):
|
||||
return "Note"
|
||||
case .naddr:
|
||||
return "Naddr"
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs the search described by `search` and updates `search_state` with the outcome.
|
||||
///
|
||||
/// Starts the lookup for the provided SearchType and transitions `search_state` from `.searching` to one of:
|
||||
/// - `.found(event)` when the target event is located,
|
||||
/// - `.found_profile(pubkey)` when a profile pubkey is located (including nip05 resolution),
|
||||
/// - `.not_found` when the lookup fails or yields no result.
|
||||
/// - Parameters:
|
||||
/// - search: The SearchType to perform (nip05, event with optional relays, profile with optional relays, or naddr).
|
||||
func handle_search(search: SearchType) {
|
||||
self.search_state = .searching
|
||||
|
||||
@@ -76,18 +84,20 @@ struct SearchingEventView: View {
|
||||
}
|
||||
}
|
||||
|
||||
case .event(let note_id):
|
||||
case .event(let note_id, let relays):
|
||||
Task {
|
||||
let res = await state.nostrNetwork.reader.findEvent(query: .event(evid: note_id))
|
||||
let targetRelays = relays.isEmpty ? nil : relays
|
||||
let res = await state.nostrNetwork.reader.findEvent(query: .event(evid: note_id, find_from: targetRelays))
|
||||
guard case .event(let ev) = res else {
|
||||
self.search_state = .not_found
|
||||
return
|
||||
}
|
||||
self.search_state = .found(ev)
|
||||
}
|
||||
case .profile(let pubkey):
|
||||
case .profile(let pubkey, let relays):
|
||||
Task {
|
||||
let res = await state.nostrNetwork.reader.findEvent(query: .profile(pubkey: pubkey))
|
||||
let targetRelays = relays.isEmpty ? nil : relays
|
||||
let res = await state.nostrNetwork.reader.findEvent(query: .profile(pubkey: pubkey, find_from: targetRelays))
|
||||
guard case .profile(let pubkey) = res else {
|
||||
self.search_state = .not_found
|
||||
return
|
||||
@@ -96,7 +106,8 @@ struct SearchingEventView: View {
|
||||
}
|
||||
case .naddr(let naddr):
|
||||
Task {
|
||||
let res = await state.nostrNetwork.reader.lookup(naddr: naddr)
|
||||
let targetRelays = naddr.relays.isEmpty ? nil : naddr.relays
|
||||
let res = await state.nostrNetwork.reader.lookup(naddr: naddr, to: targetRelays)
|
||||
guard let res = res else {
|
||||
self.search_state = .not_found
|
||||
return
|
||||
@@ -141,6 +152,6 @@ struct SearchingEventView: View {
|
||||
struct SearchingEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state
|
||||
SearchingEventView(state: state, search_type: .event(test_note.id))
|
||||
SearchingEventView(state: state, search_type: .event(test_note.id, relays: []))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user