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