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 { 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 { 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 { 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 { 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((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()