Add wallet module with receive and send functionality
- 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.
This commit is contained in:
parent
c74945874c
commit
f75aae6be6
12 changed files with 1294 additions and 3 deletions
327
src/modules/wallet/services/WalletService.ts
Normal file
327
src/modules/wallet/services/WalletService.ts
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { BaseService } from '@/core/services/BaseService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
|
||||
export interface PayLink {
|
||||
id: string
|
||||
wallet: string
|
||||
description: string
|
||||
min: number
|
||||
max: number
|
||||
comment_chars: number
|
||||
username?: string
|
||||
lnurl?: string
|
||||
lnaddress?: string
|
||||
}
|
||||
|
||||
export interface SendPaymentRequest {
|
||||
amount: number
|
||||
destination: string // Can be invoice, lnurl, or lightning address
|
||||
comment?: string
|
||||
}
|
||||
|
||||
export interface PaymentTransaction {
|
||||
id: string
|
||||
amount: number
|
||||
description: string
|
||||
timestamp: Date
|
||||
type: 'sent' | 'received'
|
||||
status: 'pending' | 'confirmed' | 'failed'
|
||||
fee?: number
|
||||
}
|
||||
|
||||
export default class WalletService extends BaseService {
|
||||
private paymentService: any = null
|
||||
|
||||
// Reactive state
|
||||
private _payLinks = ref<PayLink[]>([])
|
||||
private _transactions = ref<PaymentTransaction[]>([])
|
||||
private _isCreatingPayLink = ref(false)
|
||||
private _isSendingPayment = ref(false)
|
||||
private _error = ref<string | null>(null)
|
||||
|
||||
// Public reactive getters
|
||||
readonly payLinks = computed(() => this._payLinks.value)
|
||||
readonly transactions = computed(() => this._transactions.value)
|
||||
readonly isCreatingPayLink = computed(() => this._isCreatingPayLink.value)
|
||||
readonly isSendingPayment = computed(() => this._isSendingPayment.value)
|
||||
readonly error = computed(() => this._error.value)
|
||||
|
||||
constructor() {
|
||||
super('WalletService')
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
this.paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
|
||||
if (!this.paymentService) {
|
||||
throw new Error('Payment service not available')
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
await this.loadPayLinks()
|
||||
await this.loadTransactions()
|
||||
|
||||
this.logger.info('WalletService initialized successfully')
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize WalletService:', error)
|
||||
this._error.value = error instanceof Error ? error.message : 'Initialization failed'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new LNURL pay link for receiving payments
|
||||
*/
|
||||
async createReceiveAddress(params: {
|
||||
description: string
|
||||
minAmount: number
|
||||
maxAmount: number
|
||||
username?: string
|
||||
allowComments?: boolean
|
||||
}): Promise<PayLink | null> {
|
||||
this._isCreatingPayLink.value = true
|
||||
this._error.value = null
|
||||
|
||||
try {
|
||||
const wallet = this.paymentService?.getPreferredWallet()
|
||||
if (!wallet) {
|
||||
throw new Error('No wallet available')
|
||||
}
|
||||
|
||||
const adminKey = this.paymentService?.getPreferredWalletAdminKey()
|
||||
if (!adminKey) {
|
||||
throw new Error('No admin key available')
|
||||
}
|
||||
|
||||
// Create pay link via LNbits LNURLP extension API
|
||||
const response = await fetch(`${this.paymentService.config.baseUrl}/lnurlp/api/v1/links`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': adminKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
description: params.description,
|
||||
wallet: wallet.id,
|
||||
min: params.minAmount,
|
||||
max: params.maxAmount,
|
||||
comment_chars: params.allowComments ? 200 : 0,
|
||||
username: params.username || null,
|
||||
disposable: false // Reusable pay link
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || 'Failed to create pay link')
|
||||
}
|
||||
|
||||
const payLink: PayLink = await response.json()
|
||||
|
||||
// Generate the LNURL and Lightning Address
|
||||
const baseUrl = this.paymentService.config.baseUrl
|
||||
payLink.lnurl = `${baseUrl}/lnurlp/${payLink.id}`
|
||||
|
||||
if (payLink.username) {
|
||||
// Extract domain from base URL
|
||||
const domain = new URL(baseUrl).hostname
|
||||
payLink.lnaddress = `${payLink.username}@${domain}`
|
||||
}
|
||||
|
||||
this._payLinks.value.unshift(payLink)
|
||||
this.logger.info('Created new pay link:', payLink.id)
|
||||
|
||||
return payLink
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to create receive address:', error)
|
||||
this._error.value = error instanceof Error ? error.message : 'Failed to create receive address'
|
||||
return null
|
||||
} finally {
|
||||
this._isCreatingPayLink.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Lightning payment
|
||||
*/
|
||||
async sendPayment(request: SendPaymentRequest): Promise<boolean> {
|
||||
this._isSendingPayment.value = true
|
||||
this._error.value = null
|
||||
|
||||
try {
|
||||
const adminKey = this.paymentService?.getPreferredWalletAdminKey()
|
||||
if (!adminKey) {
|
||||
throw new Error('No admin key available')
|
||||
}
|
||||
|
||||
let endpoint = ''
|
||||
let body: any = {}
|
||||
|
||||
// Determine payment type and prepare request
|
||||
if (request.destination.startsWith('ln')) {
|
||||
// Lightning invoice
|
||||
endpoint = `${this.paymentService.config.baseUrl}/api/v1/payments`
|
||||
body = {
|
||||
out: true,
|
||||
bolt11: request.destination
|
||||
}
|
||||
} else if (request.destination.includes('@') || request.destination.toLowerCase().startsWith('lnurl')) {
|
||||
// Lightning address or LNURL
|
||||
endpoint = `${this.paymentService.config.baseUrl}/api/v1/payments/lnurl`
|
||||
body = {
|
||||
lnurl: request.destination.includes('@')
|
||||
? `https://${request.destination.split('@')[1]}/.well-known/lnurlp/${request.destination.split('@')[0]}`
|
||||
: request.destination,
|
||||
amount: request.amount * 1000, // Convert to millisats
|
||||
comment: request.comment || ''
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid payment destination format')
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': adminKey
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || 'Payment failed')
|
||||
}
|
||||
|
||||
const payment = await response.json()
|
||||
this.logger.info('Payment sent successfully:', payment.payment_hash)
|
||||
|
||||
// Refresh transactions
|
||||
await this.loadTransactions()
|
||||
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send payment:', error)
|
||||
this._error.value = error instanceof Error ? error.message : 'Payment failed'
|
||||
return false
|
||||
} finally {
|
||||
this._isSendingPayment.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing pay links
|
||||
*/
|
||||
private async loadPayLinks(): Promise<void> {
|
||||
try {
|
||||
const adminKey = this.paymentService?.getPreferredWalletAdminKey()
|
||||
if (!adminKey) return
|
||||
|
||||
const response = await fetch(`${this.paymentService.config.baseUrl}/lnurlp/api/v1/links`, {
|
||||
headers: {
|
||||
'X-Api-Key': adminKey
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const links = await response.json()
|
||||
const baseUrl = this.paymentService.config.baseUrl
|
||||
const domain = new URL(baseUrl).hostname
|
||||
|
||||
// Add LNURL and Lightning Address to each link
|
||||
this._payLinks.value = links.map((link: PayLink) => ({
|
||||
...link,
|
||||
lnurl: `${baseUrl}/lnurlp/${link.id}`,
|
||||
lnaddress: link.username ? `${link.username}@${domain}` : undefined
|
||||
}))
|
||||
|
||||
this.logger.info(`Loaded ${links.length} pay links`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to load pay links:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load payment transactions
|
||||
*/
|
||||
private async loadTransactions(): Promise<void> {
|
||||
try {
|
||||
const invoiceKey = this.paymentService?.getPreferredWalletInvoiceKey()
|
||||
if (!invoiceKey) return
|
||||
|
||||
const response = await fetch(`${this.paymentService.config.baseUrl}/api/v1/payments`, {
|
||||
headers: {
|
||||
'X-Api-Key': invoiceKey
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const payments = await response.json()
|
||||
|
||||
// Transform to our transaction format
|
||||
this._transactions.value = payments.map((payment: any) => ({
|
||||
id: payment.payment_hash,
|
||||
amount: Math.abs(payment.amount),
|
||||
description: payment.memo || payment.description || 'No description',
|
||||
timestamp: new Date(payment.time * 1000),
|
||||
type: payment.amount > 0 ? 'received' : 'sent',
|
||||
status: payment.pending ? 'pending' : 'confirmed',
|
||||
fee: payment.fee
|
||||
})).sort((a: PaymentTransaction, b: PaymentTransaction) =>
|
||||
b.timestamp.getTime() - a.timestamp.getTime()
|
||||
)
|
||||
|
||||
this.logger.info(`Loaded ${payments.length} transactions`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to load transactions:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a pay link
|
||||
*/
|
||||
async deletePayLink(linkId: string): Promise<boolean> {
|
||||
try {
|
||||
const adminKey = this.paymentService?.getPreferredWalletAdminKey()
|
||||
if (!adminKey) {
|
||||
throw new Error('No admin key available')
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.paymentService.config.baseUrl}/lnurlp/api/v1/links/${linkId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Api-Key': adminKey
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete pay link')
|
||||
}
|
||||
|
||||
// Remove from local state
|
||||
this._payLinks.value = this._payLinks.value.filter(link => link.id !== linkId)
|
||||
this.logger.info('Deleted pay link:', linkId)
|
||||
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to delete pay link:', error)
|
||||
this._error.value = error instanceof Error ? error.message : 'Failed to delete pay link'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all data
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.loadPayLinks(),
|
||||
this.loadTransactions()
|
||||
])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue