import { Event, EventTemplate, getEventHash, signEvent } from 'nostr-tools' import { nostrService } from './nostr' import { PLATFORM_NPUB } from './platformConfig' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io' export interface ContentDeliveryTracking { articleId: string articlePubkey: string recipientPubkey: string messageEventId: string zapReceiptId?: string amount: number authorAmount?: number platformCommission?: number timestamp: number verified: boolean } /** * 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 readonly trackingKind = 30078 // Custom kind for platform tracking /** * 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 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 } const eventTemplate: EventTemplate = { kind: this.trackingKind, created_at: Math.floor(Date.now() / 1000), tags: [ ['p', this.platformPubkey], // Tag platform for querying ['article', tracking.articleId], ['author', tracking.articlePubkey], ['recipient', tracking.recipientPubkey], ['message', tracking.messageEventId], ['amount', tracking.amount.toString()], ...(tracking.authorAmount ? [['author_amount', tracking.authorAmount.toString()]] : []), ...(tracking.platformCommission ? [['platform_commission', tracking.platformCommission.toString()]] : []), ['verified', tracking.verified ? 'true' : 'false'], ['timestamp', tracking.timestamp.toString()], ...(tracking.zapReceiptId ? [['zap_receipt', tracking.zapReceiptId]] : []), ], content: JSON.stringify({ articleId: tracking.articleId, articlePubkey: tracking.articlePubkey, recipientPubkey: tracking.recipientPubkey, messageEventId: tracking.messageEventId, amount: tracking.amount, authorAmount: tracking.authorAmount, platformCommission: tracking.platformCommission, verified: tracking.verified, timestamp: tracking.timestamp, zapReceiptId: tracking.zapReceiptId, }), } const unsignedEvent = { pubkey: authorPubkey, ...eventTemplate, } const event: Event = { ...unsignedEvent, id: getEventHash(unsignedEvent), sig: signEvent(unsignedEvent, authorPrivateKey), } as Event const poolWithSub = pool as SimplePoolWithSub const pubs = poolWithSub.publish([RELAY_URL], event) await Promise.all(pubs) 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 [] } const filters = [ { kinds: [this.trackingKind], '#p': [this.platformPubkey], '#article': [articleId], limit: 100, }, ] return new Promise((resolve) => { const deliveries: ContentDeliveryTracking[] = [] let resolved = false const poolWithSub = pool as SimplePoolWithSub const sub = poolWithSub.sub([RELAY_URL], filters) const finalize = () => { if (resolved) { return } resolved = true sub.unsub() resolve(deliveries) } sub.on('event', (event: Event) => { try { const data = JSON.parse(event.content) as ContentDeliveryTracking const zapReceiptTag = event.tags.find((tag) => tag[0] === 'zap_receipt')?.[1] const authorAmountTag = event.tags.find((tag) => tag[0] === 'author_amount')?.[1] const platformCommissionTag = event.tags.find((tag) => tag[0] === 'platform_commission')?.[1] const delivery: ContentDeliveryTracking = { ...data, } if (authorAmountTag) { delivery.authorAmount = parseInt(authorAmountTag, 10) } if (platformCommissionTag) { delivery.platformCommission = parseInt(platformCommissionTag, 10) } if (zapReceiptTag) { delivery.zapReceiptId = zapReceiptTag } deliveries.push(delivery) } catch (error) { console.error('Error parsing tracking event', { eventId: event.id, error: error instanceof Error ? error.message : 'Unknown error', }) } }) 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 [] } const filters = [ { kinds: [this.trackingKind], '#p': [this.platformPubkey], '#recipient': [recipientPubkey], limit: 100, }, ] return new Promise((resolve) => { const deliveries: ContentDeliveryTracking[] = [] let resolved = false const poolWithSub = pool as SimplePoolWithSub const sub = poolWithSub.sub([RELAY_URL], filters) const finalize = () => { if (resolved) { return } resolved = true sub.unsub() resolve(deliveries) } sub.on('event', (event: Event) => { try { const data = JSON.parse(event.content) as ContentDeliveryTracking const zapReceiptTag = event.tags.find((tag) => tag[0] === 'zap_receipt')?.[1] const delivery: ContentDeliveryTracking = { ...data, } if (zapReceiptTag) { delivery.zapReceiptId = zapReceiptTag } deliveries.push(delivery) } catch (error) { console.error('Error parsing tracking event', { eventId: event.id, error: error instanceof Error ? error.message : 'Unknown error', }) } }) 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()