149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
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<PaymentResult> {
|
|
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' }
|
|
}
|
|
|
|
await nostrService.createZapRequest(request.article.pubkey, request.article.id, request.article.zapAmount)
|
|
|
|
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(
|
|
_paymentHash: string,
|
|
articleId: string,
|
|
articlePubkey: string,
|
|
amount: number,
|
|
userPubkey?: string
|
|
): Promise<boolean> {
|
|
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(
|
|
articlePubkey,
|
|
articleId,
|
|
amount,
|
|
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(
|
|
paymentHash: string,
|
|
articleId: string,
|
|
articlePubkey: string,
|
|
amount: number,
|
|
recipientPubkey: string,
|
|
timeout: number = 300000 // 5 minutes
|
|
): Promise<boolean> {
|
|
return waitForArticlePaymentHelper(
|
|
paymentHash,
|
|
articleId,
|
|
articlePubkey,
|
|
amount,
|
|
recipientPubkey,
|
|
timeout
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get payment URL for display/QR code generation
|
|
*/
|
|
async getPaymentUrl(request: PaymentRequest): Promise<string | null> {
|
|
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()
|