- Fusion tous documents commissions en technical.md - Suppression documents redondants : - architecture.md, commissions.md, commission-system.md - commission-implementation.md, split-and-transfer.md - implementation-summary.md, content-delivery-verification.md - Documentation fidèle au code actuel - remaining-tasks.md mis à jour avec état réel - Documentation centralisée et sans répétitions
268 lines
8.3 KiB
TypeScript
268 lines
8.3 KiB
TypeScript
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<string | null> {
|
|
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<ContentDeliveryTracking[]> {
|
|
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<ContentDeliveryTracking[]> {
|
|
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()
|