story-research-zapwall/lib/reviewReward.ts
Nicolas Cantu f7bd7faa73 fix: Correction erreurs TypeScript, nettoyage et réorganisation documentation
- Correction toutes erreurs TypeScript :
  - Variables non utilisées supprimées
  - Types optionnels corrigés (exactOptionalPropertyTypes)
  - Imports corrigés (PLATFORM_BITCOIN_ADDRESS depuis platformConfig)
  - Gestion correcte des propriétés optionnelles

- Suppression fichiers obsolètes :
  - code-cleanup-summary.md (redondant)
  - todo-implementation*.md (todos obsolètes)
  - corrections-completed.md, fallbacks-found.md (corrections faites)
  - implementation-summary.md (redondant)
  - documentation-plan.md (plan, pas documentation)

- Suppression scripts temporaires :
  - add-ssh-key.sh
  - add-ssh-key-plink.sh

- Réorganisation documentation dans docs/ :
  - architecture.md (nouveau)
  - commissions.md (nouveau)
  - implementation-summary.md
  - remaining-tasks.md
  - split-and-transfer.md
  - commission-system.md
  - commission-implementation.md
  - content-delivery-verification.md

Toutes erreurs TypeScript corrigées, documentation centralisée.
2025-12-27 21:25:19 +01:00

290 lines
8.8 KiB
TypeScript

import { getAlbyService } from './alby'
import { calculateReviewSplit, PLATFORM_COMMISSIONS } from './platformCommissions'
import { automaticTransferService } from './automaticTransfer'
import { nostrService } from './nostr'
import { lightningAddressService } from './lightningAddress'
import type { AlbyInvoice } from '@/types/alby'
import type { Event } from 'nostr-tools'
/**
* Review reward service
* Handles Lightning payments for rewarding reviews with automatic commission split
*
* Review reward: 70 sats total
* - Reviewer: 49 sats
* - Platform: 21 sats
*/
export interface ReviewRewardRequest {
reviewId: string
articleId: string
reviewerPubkey: string
reviewerLightningAddress?: string
authorPubkey: string
authorPrivateKey: string
}
export interface ReviewRewardResult {
success: boolean
invoice?: AlbyInvoice
paymentHash?: string
error?: string
split: {
reviewer: number
platform: number
total: number
}
}
export class ReviewRewardService {
/**
* Create review reward payment with commission split
*/
async createReviewRewardPayment(request: ReviewRewardRequest): Promise<ReviewRewardResult> {
try {
const split = calculateReviewSplit()
// Verify author has permission to reward this review
// (should be verified before calling this function)
const alby = getAlbyService()
await alby.enable()
const invoice = await alby.createInvoice({
amount: split.total,
description: `Review reward: ${request.reviewId} (${split.reviewer} sats to reviewer, ${split.platform} sats commission)`,
expiry: 3600, // 1 hour
})
console.log('Review reward invoice created', {
reviewId: request.reviewId,
articleId: request.articleId,
reviewerPubkey: request.reviewerPubkey,
amount: split.total,
reviewerPortion: split.reviewer,
platformCommission: split.platform,
timestamp: new Date().toISOString(),
})
return {
success: true,
invoice,
paymentHash: invoice.paymentHash,
split,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error creating review reward payment', {
reviewId: request.reviewId,
articleId: request.articleId,
error: errorMessage,
timestamp: new Date().toISOString(),
})
return {
success: false,
error: errorMessage,
split: calculateReviewSplit(),
}
}
}
/**
* Process review reward payment after confirmation
* Transfers reviewer portion and tracks commission
*/
async processReviewRewardPayment(
request: ReviewRewardRequest,
paymentHash: string
): Promise<boolean> {
try {
const split = calculateReviewSplit()
// Get reviewer Lightning address if not provided
let reviewerLightningAddress: string | undefined = request.reviewerLightningAddress
if (!reviewerLightningAddress) {
const address = await lightningAddressService.getLightningAddress(request.reviewerPubkey)
reviewerLightningAddress = address ?? undefined
}
// Transfer reviewer portion
if (reviewerLightningAddress) {
const transferResult = await automaticTransferService.transferReviewerPortion(
reviewerLightningAddress,
request.reviewId,
request.reviewerPubkey,
split.total
)
if (!transferResult.success) {
console.error('Failed to transfer reviewer portion', {
reviewId: request.reviewId,
error: transferResult.error,
timestamp: new Date().toISOString(),
})
// Continue anyway - transfer can be done manually later
}
} else {
console.warn('Reviewer Lightning address not available for automatic transfer', {
reviewId: request.reviewId,
reviewerPubkey: request.reviewerPubkey,
timestamp: new Date().toISOString(),
})
}
// Track the reward payment
await this.trackReviewReward(request, split, paymentHash)
// Update review event with reward tag
await this.updateReviewWithReward(request.reviewId, request.authorPrivateKey)
console.log('Review reward processed', {
reviewId: request.reviewId,
articleId: request.articleId,
reviewerPubkey: request.reviewerPubkey,
reviewerAmount: split.reviewer,
platformCommission: split.platform,
timestamp: new Date().toISOString(),
})
return true
} catch (error) {
console.error('Error processing review reward payment', {
reviewId: request.reviewId,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
return false
}
}
/**
* Track review reward payment
*/
private async trackReviewReward(
request: ReviewRewardRequest,
split: { reviewer: number; platform: number; total: number },
paymentHash: string
): Promise<void> {
try {
// In production, publish tracking event on Nostr similar to article payments
console.log('Review reward tracked', {
reviewId: request.reviewId,
articleId: request.articleId,
reviewerPubkey: request.reviewerPubkey,
authorPubkey: request.authorPubkey,
reviewerAmount: split.reviewer,
platformCommission: split.platform,
paymentHash,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error tracking review reward', {
reviewId: request.reviewId,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
}
}
/**
* Update review event with reward tag
* Publishes a new event that references the original review with reward tags
*/
private async updateReviewWithReward(reviewId: string, authorPrivateKey: string): Promise<void> {
try {
const pool = nostrService.getPool()
if (!pool) {
throw new Error('Pool not initialized')
}
// Get the original event from pool
const poolWithSub = pool as import('@/types/nostr-tools-extended').SimplePoolWithSub
const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io'
const filters = [
{
kinds: [1],
ids: [reviewId],
limit: 1,
},
]
const originalEvent = await new Promise<Event | null>((resolve) => {
let resolved = false
const sub = poolWithSub.sub([RELAY_URL], filters)
const finalize = (value: Event | null) => {
if (resolved) {
return
}
resolved = true
sub.unsub()
resolve(value)
}
sub.on('event', (event: Event) => {
finalize(event)
})
sub.on('eose', () => finalize(null))
setTimeout(() => finalize(null), 5000)
})
if (!originalEvent) {
console.error('Original review event not found', {
reviewId,
timestamp: new Date().toISOString(),
})
return
}
// Check if already rewarded
const alreadyRewarded = originalEvent.tags.some((tag) => tag[0] === 'rewarded' && tag[1] === 'true')
if (alreadyRewarded) {
console.log('Review already marked as rewarded', {
reviewId,
timestamp: new Date().toISOString(),
})
return
}
// Create updated event with reward tags
nostrService.setPrivateKey(authorPrivateKey)
nostrService.setPublicKey(originalEvent.pubkey)
const updatedEvent = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
...originalEvent.tags.filter((tag) => tag[0] !== 'rewarded' && tag[0] !== 'reward_amount'),
['e', reviewId], // Reference to original review
['rewarded', 'true'],
['reward_amount', PLATFORM_COMMISSIONS.review.total.toString()],
],
content: originalEvent.content, // Keep original content
}
const publishedEvent = await nostrService.publishEvent(updatedEvent)
if (publishedEvent) {
console.log('Review updated with reward tag', {
reviewId,
updatedEventId: publishedEvent.id,
timestamp: new Date().toISOString(),
})
} else {
console.error('Failed to publish updated review event', {
reviewId,
timestamp: new Date().toISOString(),
})
}
} catch (error) {
console.error('Error updating review with reward', {
reviewId,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
}
}
}
export const reviewRewardService = new ReviewRewardService()