import type { AlbyInvoice, AlbyPaymentStatus, AlbyInvoiceRequest } from '@/types/alby' import { retryWithBackoff, isRetryableNetworkError } from './retry' /** * Check if WebLN (Lightning wallet) is available in the browser */ export function isWebLNAvailable(): boolean { return typeof window !== 'undefined' && typeof (window as any).webln !== 'undefined' } /** * Get WebLN provider (Alby or other Lightning wallet) */ function getWebLN(): any { if (typeof window === 'undefined') { throw new Error('WebLN is only available in the browser') } return (window as any).webln } /** * Alby/WebLN Service * Handles Lightning payments using the WebLN standard (works with Alby and other Lightning wallets) */ export class AlbyService { /** * Enable WebLN provider (request permission from user) */ async enable(): Promise { if (!isWebLNAvailable()) { throw new Error('WebLN provider not available. Please install Alby or another Lightning wallet extension.') } const webln = getWebLN() if (webln.enabled) { return } try { await webln.enable() } catch (error) { throw new Error('User rejected WebLN permission request') } } /** * Check if WebLN is enabled */ isEnabled(): boolean { if (!isWebLNAvailable()) { return false } return getWebLN().enabled === true } /** * Generate a Lightning invoice * This will prompt the user's wallet to create an invoice */ async createInvoice(request: AlbyInvoiceRequest): Promise { await this.enable() const webln = getWebLN() return retryWithBackoff( async () => { try { // Use makeInvoice method from WebLN const invoiceResponse = await webln.makeInvoice({ amount: request.amount, defaultMemo: request.description || 'Nostr Paywall Payment', }) // Extract payment hash from invoice using a simple approach // In production, decode BOLT11 properly const paymentHash = this.generatePaymentHash(invoiceResponse.paymentRequest) return { invoice: invoiceResponse.paymentRequest, paymentHash, amount: request.amount, expiresAt: Math.floor(Date.now() / 1000) + (request.expiry || 3600), } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)) console.error('Alby createInvoice error:', err) throw new Error(`Failed to create invoice: ${err.message}`) } }, { maxRetries: 3, initialDelay: 1000, retryable: isRetryableNetworkError, } ) } /** * Request payment (user pays an invoice) */ async sendPayment(invoice: string): Promise<{ preimage: string }> { await this.enable() const webln = getWebLN() return retryWithBackoff( async () => { try { const response = await webln.sendPayment(invoice) return { preimage: response.preimage } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)) console.error('Alby sendPayment error:', err) throw new Error(`Failed to send payment: ${err.message}`) } }, { maxRetries: 2, // Fewer retries for payment operations initialDelay: 2000, retryable: isRetryableNetworkError, } ) } /** * Check payment status by verifying if invoice is paid * Note: With WebLN, we typically verify payment by checking zap receipts on Nostr * since WebLN doesn't provide a direct way to check invoice status */ async checkPaymentStatus(paymentHash: string): Promise { // WebLN doesn't have a direct method to check payment status // We'll rely on Nostr zap receipts for verification return { paid: false, paymentHash, } } /** * Wait for payment completion * Since WebLN doesn't provide payment status checking, * we'll rely on zap receipt verification on Nostr */ async waitForPayment( paymentHash: string, timeout: number = 300000, // 5 minutes default interval: number = 2000 // 2 seconds default ): Promise { // With WebLN, payment verification is typically done via zap receipts // This method is kept for API compatibility but should be used with zap receipt verification const startTime = Date.now() return new Promise((resolve) => { const checkPayment = async () => { try { const status = await this.checkPaymentStatus(paymentHash) if (status.paid) { resolve(status) return } if (Date.now() - startTime > timeout) { resolve({ paid: false, paymentHash }) return } setTimeout(checkPayment, interval) } catch (error) { resolve({ paid: false, paymentHash }) } } checkPayment() }) } /** * Generate a simple payment hash identifier from invoice * Note: This is not the actual payment hash from the invoice * In production, decode BOLT11 properly or use zap receipts for verification */ private generatePaymentHash(invoice: string): string { // Generate a simple hash-like identifier // In practice, payment verification should be done via zap receipts if (typeof window !== 'undefined' && window.crypto) { // Use a simple hash for identification let hash = 0 for (let i = 0; i < invoice.length; i++) { const char = invoice.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash // Convert to 32-bit integer } return Math.abs(hash).toString(16).padStart(16, '0') } return Date.now().toString(16) } } // Singleton instance let albyServiceInstance: AlbyService | null = null export function getAlbyService(): AlbyService { if (!albyServiceInstance) { albyServiceInstance = new AlbyService() } return albyServiceInstance }