helpers.ts raw

   1  /**
   2   * Helper utilities for the Directory Consensus Protocol
   3   */
   4  
   5  import type { NostrEvent } from 'applesauce-core/helpers';
   6  import type { EventStore } from 'applesauce-core';
   7  import type {
   8    RelayIdentity,
   9    TrustAct,
  10    GroupTagAct,
  11    TrustLevel,
  12  } from './types.js';
  13  import { EventKinds } from './types.js';
  14  import {
  15    parseRelayIdentity,
  16    parseTrustAct,
  17    parseGroupTagAct,
  18  } from './parsers.js';
  19  import { Observable, combineLatest, map } from 'rxjs';
  20  
  21  /**
  22   * Trust calculator for computing aggregate trust scores
  23   */
  24  export class TrustCalculator {
  25    private acts: Map<string, TrustAct[]> = new Map();
  26  
  27    /**
  28     * Add a trust act to the calculator
  29     */
  30    public addAct(act: TrustAct): void {
  31      const key = act.targetPubkey;
  32      if (!this.acts.has(key)) {
  33        this.acts.set(key, []);
  34      }
  35      this.acts.get(key)!.push(act);
  36    }
  37  
  38    /**
  39     * Calculate aggregate trust score for a pubkey
  40     * 
  41     * @param pubkey - The public key to calculate trust for
  42     * @returns Numeric trust score (0-100)
  43     */
  44    public calculateTrust(pubkey: string): number {
  45      const acts = this.acts.get(pubkey) || [];
  46      if (acts.length === 0) return 0;
  47  
  48      // Simple weighted average: high=100, medium=50, low=25
  49      const weights: Record<TrustLevel, number> = {
  50        [TrustLevel.High]: 100,
  51        [TrustLevel.Medium]: 50,
  52        [TrustLevel.Low]: 25,
  53      };
  54  
  55      let total = 0;
  56      let count = 0;
  57  
  58      for (const act of acts) {
  59        // Skip expired acts
  60        if (act.expiry && act.expiry < new Date()) {
  61          continue;
  62        }
  63  
  64        total += weights[act.trustLevel];
  65        count++;
  66      }
  67  
  68      return count > 0 ? total / count : 0;
  69    }
  70  
  71    /**
  72     * Get all acts for a pubkey
  73     */
  74    public getActs(pubkey: string): TrustAct[] {
  75      return this.acts.get(pubkey) || [];
  76    }
  77  
  78    /**
  79     * Clear all acts
  80     */
  81    public clear(): void {
  82      this.acts.clear();
  83    }
  84  }
  85  
  86  /**
  87   * Replication filter for managing which events to replicate
  88   */
  89  export class ReplicationFilter {
  90    private trustedRelays: Set<string> = new Set();
  91    private trustCalculator: TrustCalculator;
  92    private minTrustScore: number;
  93  
  94    constructor(minTrustScore = 50) {
  95      this.trustCalculator = new TrustCalculator();
  96      this.minTrustScore = minTrustScore;
  97    }
  98  
  99    /**
 100     * Add a trust act to influence replication decisions
 101     */
 102    public addTrustAct(act: TrustAct): void {
 103      this.trustCalculator.addAct(act);
 104      
 105      // Update trusted relays based on trust score
 106      const score = this.trustCalculator.calculateTrust(act.targetPubkey);
 107      if (score >= this.minTrustScore) {
 108        this.trustedRelays.add(act.targetPubkey);
 109      } else {
 110        this.trustedRelays.delete(act.targetPubkey);
 111      }
 112    }
 113  
 114    /**
 115     * Check if a relay is trusted enough for replication
 116     */
 117    public shouldReplicate(pubkey: string): boolean {
 118      return this.trustedRelays.has(pubkey);
 119    }
 120  
 121    /**
 122     * Get all trusted relay pubkeys
 123     */
 124    public getTrustedRelays(): string[] {
 125      return Array.from(this.trustedRelays);
 126    }
 127  
 128    /**
 129     * Get trust score for a relay
 130     */
 131    public getTrustScore(pubkey: string): number {
 132      return this.trustCalculator.calculateTrust(pubkey);
 133    }
 134  }
 135  
 136  /**
 137   * Helper to find all relay identities in an event store
 138   */
 139  export function findRelayIdentities(eventStore: EventStore): Observable<RelayIdentity[]> {
 140    return eventStore.stream({ kinds: [EventKinds.RelayIdentityAnnouncement] }).pipe(
 141      map(events => {
 142        const identities: RelayIdentity[] = [];
 143        for (const event of events as any) {
 144          try {
 145            identities.push(parseRelayIdentity(event));
 146          } catch (err) {
 147            // Skip invalid events
 148            console.warn('Invalid relay identity:', err);
 149          }
 150        }
 151        return identities;
 152      })
 153    );
 154  }
 155  
 156  /**
 157   * Helper to find all trust acts for a specific relay
 158   */
 159  export function findTrustActsForRelay(
 160    eventStore: EventStore,
 161    targetPubkey: string
 162  ): Observable<TrustAct[]> {
 163    return eventStore.stream({ kinds: [EventKinds.TrustAct] }).pipe(
 164      map(events => {
 165        const acts: TrustAct[] = [];
 166        for (const event of events as any) {
 167          try {
 168            const act = parseTrustAct(event);
 169            if (act.targetPubkey === targetPubkey) {
 170              acts.push(act);
 171            }
 172          } catch (err) {
 173            // Skip invalid events
 174            console.warn('Invalid trust act:', err);
 175          }
 176        }
 177        return acts;
 178      })
 179    );
 180  }
 181  
 182  /**
 183   * Helper to find all group tag acts for a specific relay
 184   */
 185  export function findGroupTagActsForRelay(
 186    eventStore: EventStore,
 187    targetPubkey: string
 188  ): Observable<GroupTagAct[]> {
 189    return eventStore.stream({ kinds: [EventKinds.GroupTagAct] }).pipe(
 190      map(events => {
 191        const acts: GroupTagAct[] = [];
 192        for (const event of events as any) {
 193          try {
 194            const act = parseGroupTagAct(event);
 195            if (act.targetPubkey === targetPubkey) {
 196              acts.push(act);
 197            }
 198          } catch (err) {
 199            // Skip invalid events
 200            console.warn('Invalid group tag act:', err);
 201          }
 202        }
 203        return acts;
 204      })
 205    );
 206  }
 207  
 208  /**
 209   * Helper to build a trust graph from an event store
 210   */
 211  export function buildTrustGraph(eventStore: EventStore): Observable<Map<string, TrustAct[]>> {
 212    return eventStore.stream({ kinds: [EventKinds.TrustAct] }).pipe(
 213      map(events => {
 214        const graph = new Map<string, TrustAct[]>();
 215        for (const event of events as any) {
 216          try {
 217            const act = parseTrustAct(event);
 218            const source = event.pubkey;
 219            if (!graph.has(source)) {
 220              graph.set(source, []);
 221            }
 222            graph.get(source)!.push(act);
 223          } catch (err) {
 224            // Skip invalid events
 225            console.warn('Invalid trust act:', err);
 226          }
 227        }
 228        return graph;
 229      })
 230    );
 231  }
 232  
 233  /**
 234   * Helper to check if an event is a directory event
 235   */
 236  export function isDirectoryEvent(event: NostrEvent): boolean {
 237    return Object.values(EventKinds).includes(event.kind as any);
 238  }
 239  
 240  /**
 241   * Helper to filter directory events from a stream
 242   */
 243  export function filterDirectoryEvents(eventStore: EventStore): Observable<NostrEvent> {
 244    return eventStore.stream({ kinds: Object.values(EventKinds) });
 245  }
 246  
 247  /**
 248   * Format a relay URL to canonical format (with trailing slash)
 249   */
 250  export function normalizeRelayURL(url: string): string {
 251    const trimmed = url.trim();
 252    return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
 253  }
 254  
 255  /**
 256   * Extract relay URL from a NIP-11 URL
 257   */
 258  export function extractRelayURL(nip11URL: string): string {
 259    try {
 260      const url = new URL(nip11URL);
 261      // Convert http(s) to ws(s)
 262      const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
 263      return normalizeRelayURL(`${protocol}//${url.host}${url.pathname}`);
 264    } catch (err) {
 265      throw new Error(`Invalid NIP-11 URL: ${nip11URL}`);
 266    }
 267  }
 268  
 269  /**
 270   * Create a NIP-11 URL from a relay WebSocket URL
 271   */
 272  export function createNIP11URL(relayURL: string): string {
 273    try {
 274      const url = new URL(relayURL);
 275      // Convert ws(s) to http(s)
 276      const protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
 277      return `${protocol}//${url.host}${url.pathname}`;
 278    } catch (err) {
 279      throw new Error(`Invalid relay URL: ${relayURL}`);
 280    }
 281  }
 282  
 283