import { nostrService } from './nostr' import { waitForArticlePayment as waitForArticlePaymentHelper } from './paymentPolling' import { resolveArticleInvoice } from './invoiceResolver' import { PLATFORM_COMMISSIONS, calculateArticleSplit } from './platformCommissions' import type { Article } from '@/types/nostr' import type { AlbyInvoice } from '@/types/alby' export interface PaymentRequest { article: Article userPubkey: string } export interface PaymentResult { success: boolean invoice?: AlbyInvoice paymentHash?: string error?: string } /** * Payment service integrating Alby/WebLN Lightning payments with Nostr articles */ export class PaymentService { /** * Create a Lightning invoice for an article payment * First checks if author has created an invoice in the event tags, otherwise creates a new one */ private validateArticleAmount(zapAmount: number): { valid: boolean; error?: string } { const expectedAmount = PLATFORM_COMMISSIONS.article.total if (zapAmount !== expectedAmount) { return { valid: false, error: `Invalid article payment amount: ${zapAmount} sats. Expected ${expectedAmount} sats (700 to author, 100 commission)`, } } return { valid: true } } private validateInvoiceAmount(invoice: AlbyInvoice): { valid: boolean; error?: string } { const split = calculateArticleSplit() if (invoice.amount !== split.total) { return { valid: false, error: `Invoice amount mismatch: ${invoice.amount} sats. Expected ${split.total} sats (${split.author} to author, ${split.platform} commission)`, } } return { valid: true } } async createArticlePayment(request: PaymentRequest): Promise { try { const amountValidation = this.validateArticleAmount(request.article.zapAmount) if (!amountValidation.valid) { return { success: false, error: amountValidation.error ?? 'Invalid amount' } } const invoice = await resolveArticleInvoice(request.article) const invoiceValidation = this.validateInvoiceAmount(invoice) if (!invoiceValidation.valid) { return { success: false, error: invoiceValidation.error ?? 'Invalid invoice amount' } } const { buildPurchaseZapRequestTags } = await import('./zapRequestBuilder') const purchaseTags = buildPurchaseZapRequestTags(request.article) await nostrService.createZapRequest(request.article.pubkey, request.article.id, request.article.zapAmount, purchaseTags) return { success: true, invoice, paymentHash: invoice.paymentHash, } } catch (error) { console.error('Payment creation error:', error) return { success: false, error: error instanceof Error ? error.message : 'Failed to create payment', } } } /** * Check if payment for an article has been completed */ async checkArticlePayment(params: { paymentHash: string articleId: string articlePubkey: string amount: number userPubkey?: string }): Promise { try { // With Alby/WebLN, we rely on zap receipts for payment verification // since WebLN doesn't provide payment status checking const zapReceiptExists = await nostrService.checkZapReceipt( params.articlePubkey, params.articleId, params.amount, params.userPubkey ) return zapReceiptExists } catch (error) { console.error('Payment check error:', error) return false } } /** * Wait for payment completion with polling * After payment is confirmed, sends private content to the user */ waitForArticlePayment(params: { paymentHash: string articleId: string articlePubkey: string amount: number recipientPubkey: string timeout?: number }): Promise { return waitForArticlePaymentHelper({ paymentHash: params.paymentHash, articleId: params.articleId, articlePubkey: params.articlePubkey, amount: params.amount, recipientPubkey: params.recipientPubkey, ...(params.timeout !== undefined ? { timeout: params.timeout } : {}), }) } /** * Get payment URL for display/QR code generation */ async getPaymentUrl(request: PaymentRequest): Promise { try { const result = await this.createArticlePayment(request) if (result.success && result.invoice) { // Return Lightning URI format return `lightning:${result.invoice.invoice}` } return null } catch (error) { console.error('Get payment URL error:', error) return null } } } export const paymentService = new PaymentService()