- 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.
290 lines
8.8 KiB
TypeScript
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()
|