import { Event } from 'nostr-tools' import { nostrService } from './nostr' import { PLATFORM_NPUB } from './platformConfig' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { ContentDeliveryTracking } from './platformTrackingTypes' import { buildTrackingEvent } from './platformTrackingEvents' import { parseTrackingEvent, createArticleDeliveriesSubscription, createRecipientDeliveriesSubscription } from './platformTrackingQueries' export type { ContentDeliveryTracking } from './platformTrackingTypes' /** * Platform tracking service * Publishes tracking events on Nostr for content delivery verification * These events are signed by the platform and can be queried for audit purposes */ export class PlatformTrackingService { private readonly platformPubkey: string = PLATFORM_NPUB private async publishTrackingEvent(event: Event): Promise { const pool = nostrService.getPool() if (!pool) { throw new Error('Pool not initialized') } // Publish to all active relays (enabled and not marked inactive for this session) const { relaySessionManager } = await import('./relaySessionManager') const activeRelays = await relaySessionManager.getActiveRelays() if (activeRelays.length === 0) { // Fallback to primary relay if no active relays const { getPrimaryRelaySync } = await import('./config') const relayUrl = getPrimaryRelaySync() const pubs = pool.publish([relayUrl], event) await Promise.all(pubs) } else { // Publish to all active relays console.log(`[PlatformTracking] Publishing tracking event ${event.id} to ${activeRelays.length} active relay(s)`) const pubs = pool.publish(activeRelays, event) // Track failed relays and mark them inactive for the session const results = await Promise.allSettled(pubs) results.forEach((result, index) => { const relayUrl = activeRelays[index] if (!relayUrl) { return } if (result.status === 'rejected') { const error = result.reason console.error(`[PlatformTracking] Relay ${relayUrl} failed during publish:`, error) relaySessionManager.markRelayFailed(relayUrl) } }) } } private validateTrackingPool(): { pool: SimplePoolWithSub; authorPubkey: string } | null { const pool = nostrService.getPool() if (!pool) { console.error('Pool not initialized for platform tracking') return null } const authorPubkey = nostrService.getPublicKey() if (!authorPubkey) { console.error('Author public key not available for tracking') return null } return { pool, authorPubkey } } /** * Publish a content delivery tracking event * This event is published by the author but tagged for platform tracking * The platform can query these events to track all content deliveries */ async trackContentDelivery( tracking: ContentDeliveryTracking, authorPrivateKey: string ): Promise { try { const validation = this.validateTrackingPool() if (!validation) { return null } const { authorPubkey } = validation const event = buildTrackingEvent(tracking, authorPubkey, authorPrivateKey, this.platformPubkey) await this.publishTrackingEvent(event) console.log('Platform tracking event published', { eventId: event.id, articleId: tracking.articleId, recipientPubkey: tracking.recipientPubkey, messageEventId: tracking.messageEventId, authorAmount: tracking.authorAmount, platformCommission: tracking.platformCommission, timestamp: new Date().toISOString(), }) return event.id } catch (error) { console.error('Error publishing platform tracking event', { articleId: tracking.articleId, recipientPubkey: tracking.recipientPubkey, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date().toISOString(), }) return null } } /** * Query tracking events for an article * Returns all delivery tracking events for a specific article */ async getArticleDeliveries(articleId: string): Promise { try { const pool = nostrService.getPool() if (!pool) { return [] } return new Promise((resolve) => { const deliveries: ContentDeliveryTracking[] = [] let resolved = false const sub = createArticleDeliveriesSubscription(pool, articleId, this.platformPubkey) const finalize = (): void => { if (resolved) { return } resolved = true sub.unsub() resolve(deliveries) } sub.on('event', (event: Event) => { const delivery = parseTrackingEvent(event) if (delivery) { deliveries.push(delivery) } }) sub.on('eose', finalize) setTimeout(finalize, 5000) }) } catch (error) { console.error('Error querying article deliveries', { articleId, error: error instanceof Error ? error.message : 'Unknown error', }) return [] } } /** * Query all deliveries for a recipient */ async getRecipientDeliveries(recipientPubkey: string): Promise { try { const pool = nostrService.getPool() if (!pool) { return [] } return new Promise((resolve) => { const deliveries: ContentDeliveryTracking[] = [] let resolved = false const sub = createRecipientDeliveriesSubscription(pool, recipientPubkey, this.platformPubkey) const finalize = (): void => { if (resolved) { return } resolved = true sub.unsub() resolve(deliveries) } sub.on('event', (event: Event) => { const delivery = parseTrackingEvent(event) if (delivery) { deliveries.push(delivery) } }) sub.on('eose', finalize) setTimeout(finalize, 5000) }) } catch (error) { console.error('Error querying recipient deliveries', { recipientPubkey, error: error instanceof Error ? error.message : 'Unknown error', }) return [] } } } export const platformTracking = new PlatformTrackingService()