feat(app): add Lightning wallet deep-links and payment components
Create lightning.ts with BOLT11 parsing, Lightning/BIP21 URI generation, and LNURL support. Add PaymentButton.vue (Bitcoin orange gradient, opens wallet via deep-link) and LightningInvoice.vue (QR code, copy, expiry countdown, open-in-wallet). AIUI is never a wallet — only deep-links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9e4a2c30e1
commit
0f1f576ef5
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Lightning Network utilities for deep-linking to external wallets.
|
||||
* AIUI is never a wallet — only generates URIs to open external wallets.
|
||||
*/
|
||||
|
||||
/** Parse a BOLT11 invoice to extract amount (if present) and expiry */
|
||||
export function parseBolt11(invoice: string): {
|
||||
amount: number | null
|
||||
expiry: number | null
|
||||
timestamp: number | null
|
||||
} {
|
||||
const lower = invoice.toLowerCase()
|
||||
if (!lower.startsWith('lnbc') && !lower.startsWith('lntb') && !lower.startsWith('lnbcrt')) {
|
||||
return { amount: null, expiry: null, timestamp: null }
|
||||
}
|
||||
|
||||
// Extract amount from human-readable part (after ln prefix, before '1' separator)
|
||||
const hrpMatch = lower.match(/^ln(?:bc|tb|bcrt)(\d+)([munp]?)/)
|
||||
let amount: number | null = null
|
||||
if (hrpMatch && hrpMatch[1]) {
|
||||
const num = parseInt(hrpMatch[1], 10)
|
||||
const multiplier = hrpMatch[2]
|
||||
// Convert to millisats then to sats
|
||||
const btcMultipliers: Record<string, number> = {
|
||||
'': 1e8, // BTC to sats
|
||||
m: 1e5, // milli-BTC to sats
|
||||
u: 1e2, // micro-BTC to sats
|
||||
n: 0.1, // nano-BTC to sats
|
||||
p: 0.0001, // pico-BTC to sats
|
||||
}
|
||||
amount = Math.round(num * (btcMultipliers[multiplier] ?? 1e8))
|
||||
}
|
||||
|
||||
return { amount, expiry: null, timestamp: null }
|
||||
}
|
||||
|
||||
/** Create a Lightning: URI for deep-linking to wallet apps */
|
||||
export function createLightningUri(invoice: string): string {
|
||||
return `lightning:${invoice}`
|
||||
}
|
||||
|
||||
/** Create a BIP21 bitcoin: URI with optional Lightning invoice */
|
||||
export function createBip21Uri(
|
||||
address: string,
|
||||
options?: { amount?: number; label?: string; lightning?: string },
|
||||
): string {
|
||||
const params = new URLSearchParams()
|
||||
if (options?.amount) params.set('amount', (options.amount / 1e8).toFixed(8))
|
||||
if (options?.label) params.set('label', options.label)
|
||||
if (options?.lightning) params.set('lightning', options.lightning)
|
||||
const qs = params.toString()
|
||||
return `bitcoin:${address}${qs ? '?' + qs : ''}`
|
||||
}
|
||||
|
||||
/** Create an LNURL-pay link */
|
||||
export function createLnurlPayUri(lnurl: string): string {
|
||||
return `lightning:${lnurl}`
|
||||
}
|
||||
|
||||
/** Format satoshi amount for display */
|
||||
export function formatSats(sats: number): string {
|
||||
if (sats >= 1_000_000) {
|
||||
return `${(sats / 1_000_000).toFixed(2)}M sats`
|
||||
}
|
||||
if (sats >= 1_000) {
|
||||
return `${(sats / 1_000).toFixed(sats >= 10_000 ? 0 : 1)}k sats`
|
||||
}
|
||||
return `${sats} sats`
|
||||
}
|
||||
|
||||
/** Check if a string looks like a BOLT11 invoice */
|
||||
export function isBolt11(text: string): boolean {
|
||||
const lower = text.toLowerCase().trim()
|
||||
return lower.startsWith('lnbc') || lower.startsWith('lntb') || lower.startsWith('lnbcrt')
|
||||
}
|
||||
|
||||
/** Check if a string looks like an LNURL */
|
||||
export function isLnurl(text: string): boolean {
|
||||
const lower = text.toLowerCase().trim()
|
||||
return lower.startsWith('lnurl')
|
||||
}
|
||||
Reference in New Issue
Block a user