210 lines
6.0 KiB
TypeScript
210 lines
6.0 KiB
TypeScript
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<void> {
|
|
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<AlbyInvoice> {
|
|
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<AlbyPaymentStatus> {
|
|
// 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<AlbyPaymentStatus> {
|
|
// 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
|
|
}
|