- Introduced a new wallet module that includes components for sending and receiving Bitcoin payments. - Implemented WalletService to manage payment links and transactions, including methods for creating LNURL pay links and sending payments. - Added dialogs for receiving and sending payments, enhancing user interaction with the wallet. - Updated app configuration to enable the wallet module and integrated it into the main application flow. These changes provide users with a comprehensive wallet experience, allowing for seamless Bitcoin transactions.
320 lines
No EOL
8.5 KiB
TypeScript
320 lines
No EOL
8.5 KiB
TypeScript
import { ref, computed } from 'vue'
|
|
import { BaseService } from '@/core/base/BaseService'
|
|
import { payInvoiceWithWallet } from '@/lib/api/events'
|
|
import { toast } from 'vue-sonner'
|
|
|
|
export interface PaymentResult {
|
|
payment_hash: string
|
|
fee_msat: number
|
|
preimage: string
|
|
}
|
|
|
|
export interface QRCodeOptions {
|
|
width?: number
|
|
margin?: number
|
|
darkColor?: string
|
|
lightColor?: string
|
|
}
|
|
|
|
export interface PaymentOptions {
|
|
successMessage?: string
|
|
errorMessage?: string
|
|
showToast?: boolean
|
|
}
|
|
|
|
/**
|
|
* Centralized Payment Service
|
|
* Handles Lightning payments, QR code generation, and wallet management
|
|
*/
|
|
export class PaymentService extends BaseService {
|
|
// Service metadata
|
|
protected readonly metadata = {
|
|
name: 'PaymentService',
|
|
version: '1.0.0',
|
|
dependencies: ['AuthService']
|
|
}
|
|
|
|
// Payment state
|
|
private _isProcessingPayment = ref(false)
|
|
private _paymentError = ref<string | null>(null)
|
|
|
|
// Public reactive state
|
|
public readonly isProcessingPayment = computed(() => this._isProcessingPayment.value)
|
|
public readonly paymentError = computed(() => this._paymentError.value)
|
|
|
|
/**
|
|
* Service-specific initialization (called by BaseService)
|
|
*/
|
|
protected async onInitialize(): Promise<void> {
|
|
// Reset payment state on initialization to prevent stuck states
|
|
this.resetPaymentState()
|
|
this.debug('PaymentService initialized with clean state')
|
|
}
|
|
|
|
|
|
/**
|
|
* Get user wallets from authenticated user
|
|
*/
|
|
get userWallets() {
|
|
return this.authService?.user?.value?.wallets || []
|
|
}
|
|
|
|
/**
|
|
* Check if user has any wallet with balance
|
|
*/
|
|
get hasWalletWithBalance(): boolean {
|
|
return this.userWallets.some((wallet: any) => wallet.balance_msat > 0)
|
|
}
|
|
|
|
/**
|
|
* Find wallet with sufficient balance for payment
|
|
*/
|
|
getWalletWithBalance(requiredAmountSats?: number): any | null {
|
|
const wallets = this.userWallets
|
|
if (!wallets.length) return null
|
|
|
|
if (requiredAmountSats) {
|
|
// Convert sats to msat for comparison
|
|
const requiredMsat = requiredAmountSats * 1000
|
|
return wallets.find((wallet: any) => wallet.balance_msat >= requiredMsat)
|
|
}
|
|
|
|
return wallets.find((wallet: any) => wallet.balance_msat > 0)
|
|
}
|
|
|
|
/**
|
|
* Get the preferred wallet for operations (always use wallets[0] for consistency)
|
|
* This ensures consistent wallet selection across all modules
|
|
*/
|
|
getPreferredWallet(): any | null {
|
|
const wallets = this.userWallets
|
|
if (!wallets.length) return null
|
|
return wallets[0]
|
|
}
|
|
|
|
/**
|
|
* Get the preferred wallet's admin key for administrative operations
|
|
*/
|
|
getPreferredWalletAdminKey(): string | null {
|
|
const wallet = this.getPreferredWallet()
|
|
return wallet?.adminkey || null
|
|
}
|
|
|
|
/**
|
|
* Get the preferred wallet's invoice key for read-only operations
|
|
*/
|
|
getPreferredWalletInvoiceKey(): string | null {
|
|
const wallet = this.getPreferredWallet()
|
|
return wallet?.inkey || null
|
|
}
|
|
|
|
/**
|
|
* Generate QR code for Lightning payment request
|
|
*/
|
|
async generateQRCode(
|
|
paymentRequest: string,
|
|
options: QRCodeOptions = {}
|
|
): Promise<string> {
|
|
try {
|
|
const qrcode = await import('qrcode')
|
|
const dataUrl = await qrcode.toDataURL(paymentRequest, {
|
|
width: options.width || 256,
|
|
margin: options.margin || 2,
|
|
color: {
|
|
dark: options.darkColor || '#000000',
|
|
light: options.lightColor || '#FFFFFF'
|
|
}
|
|
})
|
|
return dataUrl
|
|
} catch (error) {
|
|
const err = this.handleError(error, 'generateQRCode')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate QR code for ticket or other custom data
|
|
*/
|
|
async generateCustomQRCode(
|
|
data: string,
|
|
options: QRCodeOptions = {}
|
|
): Promise<string> {
|
|
try {
|
|
const qrcode = await import('qrcode')
|
|
const dataUrl = await qrcode.toDataURL(data, {
|
|
width: options.width || 128,
|
|
margin: options.margin || 2,
|
|
color: {
|
|
dark: options.darkColor || '#000000',
|
|
light: options.lightColor || '#FFFFFF'
|
|
}
|
|
})
|
|
return dataUrl
|
|
} catch (error) {
|
|
const err = this.handleError(error, 'generateCustomQRCode')
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pay Lightning invoice with user's wallet
|
|
*/
|
|
async payWithWallet(
|
|
paymentRequest: string,
|
|
requiredAmountSats?: number,
|
|
options: PaymentOptions = {}
|
|
): Promise<PaymentResult> {
|
|
// Check authentication
|
|
if (!this.authService?.isAuthenticated?.value || !this.authService?.user?.value) {
|
|
throw new Error('User must be authenticated to pay with wallet')
|
|
}
|
|
|
|
// Find suitable wallet
|
|
const wallet = this.getWalletWithBalance(requiredAmountSats)
|
|
if (!wallet) {
|
|
throw new Error('No wallet with sufficient balance found')
|
|
}
|
|
|
|
try {
|
|
this._isProcessingPayment.value = true
|
|
this._paymentError.value = null
|
|
|
|
this.debug(`Paying invoice with wallet: ${wallet.id.slice(0, 8)}`)
|
|
|
|
// Make payment
|
|
const paymentResult = await payInvoiceWithWallet(
|
|
paymentRequest,
|
|
wallet.id,
|
|
wallet.adminkey
|
|
)
|
|
|
|
this.debug('Payment successful', {
|
|
paymentHash: paymentResult.payment_hash,
|
|
feeMsat: paymentResult.fee_msat
|
|
})
|
|
|
|
// Show success notification if enabled
|
|
if (options.showToast !== false) {
|
|
const message = options.successMessage || 'Payment successful!'
|
|
toast.success(message)
|
|
}
|
|
|
|
return paymentResult
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Payment failed'
|
|
this._paymentError.value = errorMessage
|
|
|
|
this.debug('Payment failed', error)
|
|
|
|
// Show error notification if enabled
|
|
if (options.showToast !== false) {
|
|
const message = options.errorMessage || errorMessage
|
|
toast.error('Payment failed', {
|
|
description: message
|
|
})
|
|
}
|
|
|
|
throw error
|
|
} finally {
|
|
this._isProcessingPayment.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open external Lightning wallet
|
|
*/
|
|
openExternalWallet(paymentRequest: string): void {
|
|
if (!paymentRequest) {
|
|
toast.error('No payment request available')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const lightningUrl = `lightning:${paymentRequest}`
|
|
window.open(lightningUrl, '_blank')
|
|
|
|
toast.info('Opening Lightning wallet...', {
|
|
description: 'If no wallet opens, copy the payment request manually'
|
|
})
|
|
} catch (error) {
|
|
console.warn('Failed to open lightning: URL:', error)
|
|
|
|
// Fallback: copy to clipboard
|
|
navigator.clipboard?.writeText(paymentRequest).then(() => {
|
|
toast.success('Payment request copied to clipboard')
|
|
}).catch(() => {
|
|
toast.info('Please copy the payment request manually')
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle payment with automatic fallback
|
|
* Tries wallet payment first, falls back to external wallet
|
|
*/
|
|
async handlePayment(
|
|
paymentRequest: string,
|
|
requiredAmountSats?: number,
|
|
options: PaymentOptions = {}
|
|
): Promise<PaymentResult | null> {
|
|
if (!paymentRequest) {
|
|
toast.error('No payment request available')
|
|
return null
|
|
}
|
|
|
|
// Try wallet payment first if user has balance
|
|
if (this.hasWalletWithBalance) {
|
|
try {
|
|
return await this.payWithWallet(
|
|
paymentRequest,
|
|
requiredAmountSats,
|
|
options
|
|
)
|
|
} catch (error) {
|
|
this.debug('Wallet payment failed, offering external wallet option:', error)
|
|
// Don't throw here, continue to external wallet option
|
|
}
|
|
}
|
|
|
|
// Fallback to external wallet
|
|
this.openExternalWallet(paymentRequest)
|
|
return null // External wallet payment status unknown
|
|
}
|
|
|
|
/**
|
|
* Clear payment error state
|
|
*/
|
|
clearPaymentError(): void {
|
|
this._paymentError.value = null
|
|
}
|
|
|
|
/**
|
|
* Reset payment state
|
|
*/
|
|
resetPaymentState(): void {
|
|
this._isProcessingPayment.value = false
|
|
this._paymentError.value = null
|
|
this.debug('Payment state reset to clean state')
|
|
}
|
|
|
|
/**
|
|
* Force reset payment state (public method for debugging)
|
|
*/
|
|
forceResetPaymentState(): void {
|
|
console.log('Force resetting payment state from:', this._isProcessingPayment.value)
|
|
this._isProcessingPayment.value = false
|
|
this._paymentError.value = null
|
|
console.log('Payment state after force reset:', this._isProcessingPayment.value)
|
|
}
|
|
|
|
/**
|
|
* Cleanup when service is disposed (called by BaseService)
|
|
*/
|
|
protected async onDispose(): Promise<void> {
|
|
this.resetPaymentState()
|
|
this.debug('PaymentService disposed')
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const paymentService = new PaymentService() |