relay-selection.ts raw
1 import { RelayList } from '@/domain/relay/RelayList'
2 import relayListCacheService from '@/services/relay-list-cache.service'
3 import { Event } from 'nostr-tools'
4
5 /**
6 * Context for relay selection decisions
7 */
8 export interface RelaySelectionContext {
9 /** Current user's relay list */
10 userRelayList: RelayList | null
11 /** Current user's pubkey */
12 userPubkey: string | null
13 }
14
15 /**
16 * Options for publish relay selection
17 */
18 export interface PublishRelayOptions {
19 /** Include recipients' (p-tag) write relays */
20 includeRecipients?: boolean
21 /** Additional relay hints from nprofile/nevent */
22 hints?: string[]
23 }
24
25 /**
26 * Select relays for publishing an event
27 *
28 * Strategy:
29 * 1. Always include user's write relays
30 * 2. If includeRecipients, add recipients' write relays
31 * 3. Add any relay hints provided
32 */
33 export async function selectPublishRelays(
34 ctx: RelaySelectionContext,
35 event: Partial<Event>,
36 options: PublishRelayOptions = {}
37 ): Promise<string[]> {
38 const relays = new Set<string>()
39
40 // Add user's write relays
41 if (ctx.userRelayList) {
42 for (const relay of ctx.userRelayList.getWriteUrls()) {
43 relays.add(relay)
44 }
45 }
46
47 // Add hints
48 if (options.hints) {
49 for (const hint of options.hints) {
50 relays.add(hint)
51 }
52 }
53
54 // Add recipients' write relays if requested
55 if (options.includeRecipients && event.tags) {
56 const pTags = event.tags.filter((t) => t[0] === 'p' && t[1])
57
58 for (const tag of pTags) {
59 const pubkey = tag[1]
60 const hint = tag[2] // Optional relay hint in p-tag
61
62 // Try to get cached relay list
63 const recipientRelays = await relayListCacheService.getRelayList(pubkey)
64 if (recipientRelays) {
65 for (const relay of recipientRelays.write) {
66 relays.add(relay)
67 }
68 } else if (hint && hint.startsWith('wss://')) {
69 // Use hint as fallback
70 relays.add(hint)
71 }
72 }
73 }
74
75 return Array.from(relays)
76 }
77
78 /**
79 * Select relays for fetching events by author
80 *
81 * Strategy:
82 * 1. Use author's read relays if known
83 * 2. Fall back to hints if provided
84 * 3. Last resort: user's own relays
85 */
86 export async function selectReadRelays(
87 ctx: RelaySelectionContext,
88 authorPubkey: string,
89 hints?: string[]
90 ): Promise<string[]> {
91 // Try cached relay list first
92 const authorRelays = await relayListCacheService.getRelayList(authorPubkey)
93 if (authorRelays && authorRelays.read.length > 0) {
94 return authorRelays.read
95 }
96
97 // Use hints if provided
98 if (hints && hints.length > 0) {
99 return hints
100 }
101
102 // Last resort: user's own relays
103 if (ctx.userRelayList) {
104 return ctx.userRelayList.getReadUrls()
105 }
106
107 return []
108 }
109
110 /**
111 * Select relays for fetching a specific event by ID
112 *
113 * Strategy:
114 * 1. Use hints first (most likely to have the event)
115 * 2. Add author's read relays if author is known
116 * 3. Add user's read relays as fallback
117 */
118 export async function selectRelaysForEvent(
119 ctx: RelaySelectionContext,
120 _eventId: string,
121 hints?: string[],
122 authorPubkey?: string
123 ): Promise<string[]> {
124 const relays = new Set<string>()
125
126 // Use hints first (highest priority)
127 if (hints) {
128 for (const hint of hints) {
129 relays.add(hint)
130 }
131 }
132
133 // Add author's relays if known
134 if (authorPubkey) {
135 const authorRelays = await relayListCacheService.getRelayList(authorPubkey)
136 if (authorRelays) {
137 for (const relay of authorRelays.read) {
138 relays.add(relay)
139 }
140 }
141 }
142
143 // Add user's relays as fallback
144 if (ctx.userRelayList) {
145 for (const relay of ctx.userRelayList.getReadUrls()) {
146 relays.add(relay)
147 }
148 }
149
150 return Array.from(relays)
151 }
152
153 /**
154 * Select relays for fetching a thread
155 *
156 * Collects relays from:
157 * - Root event author's relays
158 * - All reply authors' relays
159 * - Any hints in e-tags
160 * - User's own relays
161 */
162 export async function selectThreadRelays(
163 ctx: RelaySelectionContext,
164 _rootEventId: string,
165 rootAuthorPubkey?: string,
166 replyAuthorPubkeys?: string[],
167 hints?: string[]
168 ): Promise<string[]> {
169 const relays = new Set<string>()
170
171 // Add hints
172 if (hints) {
173 for (const hint of hints) {
174 relays.add(hint)
175 }
176 }
177
178 // Add root author's relays
179 if (rootAuthorPubkey) {
180 const rootRelays = await relayListCacheService.getRelayList(rootAuthorPubkey)
181 if (rootRelays) {
182 for (const relay of rootRelays.read) {
183 relays.add(relay)
184 }
185 }
186 }
187
188 // Add reply authors' relays
189 if (replyAuthorPubkeys && replyAuthorPubkeys.length > 0) {
190 const authorRelays = await relayListCacheService.fetchRelayLists(replyAuthorPubkeys)
191 for (const cached of authorRelays.values()) {
192 for (const relay of cached.read) {
193 relays.add(relay)
194 }
195 }
196 }
197
198 // Add user's relays
199 if (ctx.userRelayList) {
200 for (const relay of ctx.userRelayList.getReadUrls()) {
201 relays.add(relay)
202 }
203 }
204
205 return Array.from(relays)
206 }
207
208 /**
209 * Select relays for DM operations
210 *
211 * For DMs, we need to ensure the message reaches the recipient's relays
212 */
213 export async function selectDMRelays(
214 ctx: RelaySelectionContext,
215 recipientPubkey: string
216 ): Promise<{ send: string[]; receive: string[] }> {
217 const recipientRelays = await relayListCacheService.getRelayList(recipientPubkey)
218
219 const sendRelays = new Set<string>()
220 const receiveRelays = new Set<string>()
221
222 // Send to recipient's write relays (where they check for incoming)
223 // and our own write relays
224 if (recipientRelays) {
225 for (const relay of recipientRelays.write) {
226 sendRelays.add(relay)
227 }
228 }
229
230 if (ctx.userRelayList) {
231 for (const relay of ctx.userRelayList.getWriteUrls()) {
232 sendRelays.add(relay)
233 }
234 // Receive from our own read relays
235 for (const relay of ctx.userRelayList.getReadUrls()) {
236 receiveRelays.add(relay)
237 }
238 }
239
240 return {
241 send: Array.from(sendRelays),
242 receive: Array.from(receiveRelays)
243 }
244 }
245
246 /**
247 * Extract relay hints from an nprofile, nevent, or naddr
248 */
249 export function extractRelayHints(decoded: {
250 type: string
251 data: { relays?: string[] }
252 }): string[] {
253 if ('relays' in decoded.data && Array.isArray(decoded.data.relays)) {
254 return decoded.data.relays.filter((r) => typeof r === 'string' && r.startsWith('wss://'))
255 }
256 return []
257 }
258
259 /**
260 * Extract relay hints from event e-tags
261 */
262 export function extractRelayHintsFromTags(tags: string[][]): string[] {
263 const hints: string[] = []
264
265 for (const tag of tags) {
266 if ((tag[0] === 'e' || tag[0] === 'a') && tag[2] && tag[2].startsWith('wss://')) {
267 hints.push(tag[2])
268 }
269 if (tag[0] === 'p' && tag[2] && tag[2].startsWith('wss://')) {
270 hints.push(tag[2])
271 }
272 }
273
274 return [...new Set(hints)]
275 }
276