story-research-zapwall/lib/platformTracking.ts
Nicolas Cantu 90ff8282f1 feat: Implémentation système de commissions systématique et incontournable
- Création lib/platformCommissions.ts : configuration centralisée des commissions
  - Articles : 800 sats (700 auteur, 100 plateforme)
  - Avis : 70 sats (49 lecteur, 21 plateforme)
  - Sponsoring : 0.046 BTC (0.042 auteur, 0.004 plateforme)

- Validation des montants à chaque étape :
  - Publication : vérification du montant avant publication
  - Paiement : vérification du montant avant acceptation
  - Erreurs explicites si montant incorrect

- Tracking des commissions sur Nostr :
  - Tags author_amount et platform_commission dans événements
  - Interface ContentDeliveryTracking étendue
  - Traçabilité complète pour audit

- Logs structurés avec informations de commission
- Documentation complète du système

Les commissions sont maintenant systématiques, validées et traçables.
2025-12-27 21:11:09 +01:00

250 lines
8.0 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]
deliveries.push({
...data,
zapReceiptId: zapReceiptTag,
authorAmount: authorAmountTag ? parseInt(authorAmountTag, 10) : undefined,
platformCommission: platformCommissionTag ? parseInt(platformCommissionTag, 10) : undefined,
})
} 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]
deliveries.push({
...data,
zapReceiptId: zapReceiptTag,
})
} 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()