import { nostrService } from './nostr' import { articlePublisher } from './articlePublisher' import { getStoredPrivateContent } from './articleStorage' import { platformTracking } from './platformTracking' import { calculateArticleSplit } from './platformCommissions' import { automaticTransferService } from './automaticTransfer' import { lightningAddressService } from './lightningAddress' const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io' /** * Poll for payment completion via zap receipt verification * After payment is confirmed, sends private content to the user */ async function pollPaymentUntilDeadline( articleId: string, articlePubkey: string, amount: number, recipientPubkey: string, interval: number, deadline: number ): Promise { try { const zapReceiptExists = await nostrService.checkZapReceipt(articlePubkey, articleId, amount, recipientPubkey) if (zapReceiptExists) { const zapReceiptId = await getZapReceiptId(articlePubkey, articleId, amount, recipientPubkey) await sendPrivateContentAfterPayment(articleId, recipientPubkey, amount, zapReceiptId) return true } } catch (error) { console.error('Error checking zap receipt:', error) } if (Date.now() > deadline) { return false } return new Promise((resolve) => { setTimeout(() => { void pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline) .then(resolve) .catch(() => resolve(false)) }, interval) }) } export async function waitForArticlePayment( _paymentHash: string, articleId: string, articlePubkey: string, amount: number, recipientPubkey: string, timeout: number = 300000 // 5 minutes ): Promise { const interval = 2000 const deadline = Date.now() + timeout try { return await pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline) } catch (error) { console.error('Wait for payment error:', error) return false } } async function getZapReceiptId( articlePubkey: string, articleId: string, amount: number, recipientPubkey: string ): Promise { try { const pool = nostrService.getPool() if (!pool) { return undefined } const filters = [ { kinds: [9735], '#p': [articlePubkey], '#e': [articleId], limit: 1, }, ] return new Promise((resolve) => { let resolved = false const poolWithSub = pool as import('@/types/nostr-tools-extended').SimplePoolWithSub const sub = poolWithSub.sub([RELAY_URL], filters) const finalize = (value: string | undefined) => { if (resolved) { return } resolved = true sub.unsub() resolve(value) } sub.on('event', (event) => { const amountTag = event.tags.find((tag) => tag[0] === 'amount')?.[1] const amountInSats = amountTag ? Math.floor(parseInt(amountTag, 10) / 1000) : 0 if (amountInSats === amount && event.pubkey === recipientPubkey) { finalize(event.id) } }) sub.on('eose', () => finalize(undefined)) setTimeout(() => finalize(undefined), 3000) }) } catch (error) { console.error('Error getting zap receipt ID', { articleId, recipientPubkey, error: error instanceof Error ? error.message : 'Unknown error', }) return undefined } } /** * Send private content to user after payment confirmation * Returns true if content was successfully sent and verified */ async function sendPrivateContentAfterPayment( articleId: string, recipientPubkey: string, amount: number, zapReceiptId?: string ): Promise { const storedContent = await getStoredPrivateContent(articleId) if (!storedContent) { console.error('Stored private content not found for article', { articleId, recipientPubkey, timestamp: new Date().toISOString(), }) return false } const authorPrivateKey = nostrService.getPrivateKey() if (!authorPrivateKey) { console.error('Author private key not available, cannot send private content automatically', { articleId, recipientPubkey, authorPubkey: storedContent.authorPubkey, timestamp: new Date().toISOString(), }) return false } const result = await articlePublisher.sendPrivateContent(articleId, recipientPubkey, authorPrivateKey) if (result.success && result.messageEventId) { const timestamp = Math.floor(Date.now() / 1000) // Verify payment amount matches expected commission structure const expectedSplit = calculateArticleSplit() if (amount !== expectedSplit.total) { console.error('Payment amount does not match expected commission structure', { articleId, paidAmount: amount, expectedTotal: expectedSplit.total, expectedAuthor: expectedSplit.author, expectedPlatform: expectedSplit.platform, timestamp: new Date().toISOString(), }) } // Track content delivery with commission information const trackingData: import('./platformTracking').ContentDeliveryTracking = { articleId, articlePubkey: storedContent.authorPubkey, recipientPubkey, messageEventId: result.messageEventId, amount, authorAmount: expectedSplit.author, platformCommission: expectedSplit.platform, timestamp, verified: result.verified ?? false, } if (zapReceiptId) { trackingData.zapReceiptId = zapReceiptId } await platformTracking.trackContentDelivery(trackingData, authorPrivateKey) // Log commission information for platform tracking console.log('Article payment processed with commission', { articleId, totalAmount: amount, authorPortion: expectedSplit.author, platformCommission: expectedSplit.platform, recipientPubkey, timestamp: new Date().toISOString(), }) // Trigger automatic transfer of author portion try { // Get author's Lightning address from profile const authorLightningAddress = await lightningAddressService.getLightningAddress(storedContent.authorPubkey) if (authorLightningAddress) { const transferResult = await automaticTransferService.transferAuthorPortion( authorLightningAddress, articleId, storedContent.authorPubkey, amount ) if (!transferResult.success) { console.warn('Automatic transfer failed, will be retried later', { articleId, authorPubkey: storedContent.authorPubkey, error: transferResult.error, timestamp: new Date().toISOString(), }) } } else { console.warn('Author Lightning address not available for automatic transfer', { articleId, authorPubkey: storedContent.authorPubkey, timestamp: new Date().toISOString(), }) // Transfer will need to be done manually later } } catch (error) { console.error('Error triggering automatic transfer', { articleId, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date().toISOString(), }) // Don't fail the payment process if transfer fails } if (result.verified) { console.log('Private content sent and verified on relay', { articleId, recipientPubkey, messageEventId: result.messageEventId, timestamp: new Date().toISOString(), }) return true } else { console.warn('Private content sent but not yet verified on relay', { articleId, recipientPubkey, messageEventId: result.messageEventId, timestamp: new Date().toISOString(), }) return true } } else { console.error('Failed to send private content, but payment was confirmed', { articleId, recipientPubkey, error: result.error, timestamp: new Date().toISOString(), }) return false } }