refactor(machine): route cash-out methods through LnbitsClient when wired
3b.2 of the LP→LNbits migration. With the LnbitsClient parallel-wired
in 3b.1, this commit routes three of the ATMServices methods through
LNbits when CONFIG.lnbitsServerPubkey is set:
generateInvoice → lnbits.createInvoice(walletId, {amount, memo, unit})
getAvailableBalance → lnbits.getBalance(walletId)
watchInvoice → lnbits.decodePayment(bolt11) + subscribePayments(
{payment_hash, max_seconds: 600}
)
Each method keeps its LP path as the fallback when LNbits isn't
configured (CONFIG.lnbitsServerPubkey empty). So:
- VITE_LNBITS_SERVER_PUBKEY unset → behaves exactly like before
this PR (LP for everything).
- VITE_LNBITS_SERVER_PUBKEY set → cash-out (invoice + payment
observation) routes through
LNbits. Cash-in (ndebit) still
on LP until 3b.3.
Init flow change: at startup, after LnbitsClient is instantiated, we
call `list_wallets` to discover the account's default wallet id. This
is the wallet that auto-account-creation lands the account in (and
where LNBITS_DEMO_MODE deposits the auto-credit). It's then passed
into createATMServices alongside the LnbitsClient reference.
createATMServices signature gained two parameters (`lnbits`,
`lnbitsWalletId`). When both are present, `lnbitsActive` flips and the
LNbits paths fire.
Verified:
pnpm typecheck clean (14/14, machine task cache miss → exec OK)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d0c19ed7f
commit
967f24f864
1 changed files with 113 additions and 6 deletions
|
|
@ -1064,12 +1064,42 @@ export async function initializeLightningServices(options?: {
|
|||
clink.startListening()
|
||||
console.log('[Lightning] CLINK client initialized with offer + management handlers')
|
||||
|
||||
// Create ATM services using Lightning.Pub's native LNURL-withdraw
|
||||
const atmServices = createATMServices(lightningPub, clink, identity, (preimage) => {
|
||||
if (paymentReceivedCallback) {
|
||||
paymentReceivedCallback(preimage)
|
||||
// Resolve the ATM's LNbits wallet id, if LNbits is wired. First contact
|
||||
// auto-creates the account on the LNbits side (with prvkey=NULL, per
|
||||
// issue aiolabs/lnbits#9 alignment) and `list_wallets` returns the
|
||||
// auto-created default wallet — which is where LNBITS_DEMO_MODE deposits
|
||||
// the auto-credit, and where any subsequent `create_invoice` /
|
||||
// `pay_invoice` will hit.
|
||||
let lnbitsWalletId: string | null = null
|
||||
if (_lnbitsRef) {
|
||||
try {
|
||||
const wallets = await _lnbitsRef.listWallets()
|
||||
lnbitsWalletId = wallets[0]?.id ?? null
|
||||
if (lnbitsWalletId) {
|
||||
console.log('[Lightning] LNbits wallet:', lnbitsWalletId)
|
||||
} else {
|
||||
console.warn('[Lightning] LNbits list_wallets returned empty — falling back to LP for cash-out')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Lightning] LNbits list_wallets failed; falling back to LP for cash-out:', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Create ATM services. Cash-out (generateInvoice / getAvailableBalance /
|
||||
// watchInvoice) routes through LNbits if available; cash-in (ndebit)
|
||||
// stays on LP until 3b.3 replaces it with lnurlw + subscribe_payments.
|
||||
const atmServices = createATMServices(
|
||||
lightningPub,
|
||||
clink,
|
||||
identity,
|
||||
(preimage) => {
|
||||
if (paymentReceivedCallback) {
|
||||
paymentReceivedCallback(preimage)
|
||||
}
|
||||
},
|
||||
_lnbitsRef,
|
||||
lnbitsWalletId,
|
||||
)
|
||||
|
||||
return {
|
||||
nostrClient,
|
||||
|
|
@ -1106,10 +1136,18 @@ function createATMServices(
|
|||
lightningPub: LightningPubClient,
|
||||
clink: CLINKClient,
|
||||
_identity: MachineIdentity,
|
||||
onPaymentSuccess: (preimage: string) => void
|
||||
onPaymentSuccess: (preimage: string) => void,
|
||||
/**
|
||||
* Migration: when an LNbits client + wallet id are wired (3b.2+),
|
||||
* cash-out paths route through them. Cash-in still goes through LP
|
||||
* (ndebit/CLINK) until 3b.3.
|
||||
*/
|
||||
lnbits: LnbitsClient | null,
|
||||
lnbitsWalletId: string | null,
|
||||
): ATMServices {
|
||||
// Store callback for LNURL-withdraw polling to use
|
||||
const onPaymentCallback = onPaymentSuccess
|
||||
const lnbitsActive = lnbits !== null && lnbitsWalletId !== null
|
||||
|
||||
return {
|
||||
/**
|
||||
|
|
@ -1265,6 +1303,26 @@ function createATMServices(
|
|||
const amountSats = Math.floor(amountMsat / 1000)
|
||||
console.log('[ATM Service] Amount in sats:', amountSats)
|
||||
|
||||
// LNbits path (3b.2): cash-out invoice via the nostr-transport.
|
||||
if (lnbitsActive) {
|
||||
try {
|
||||
const payment = await lnbits!.createInvoice(lnbitsWalletId!, {
|
||||
amount: amountSats,
|
||||
memo: 'bitSpire - Cash Out',
|
||||
unit: 'sat',
|
||||
})
|
||||
if (!payment.payment_request) {
|
||||
throw new Error('LNbits createInvoice returned empty payment_request')
|
||||
}
|
||||
console.log('[ATM Service] Generated invoice via LNbits:', payment.payment_request.slice(0, 32) + '...')
|
||||
return payment.payment_request
|
||||
} catch (error) {
|
||||
console.error('[ATM Service] LNbits createInvoice failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// LP path (legacy, kept until LNbits is fully wired).
|
||||
try {
|
||||
const invoice = await lightningPub.createInvoice({
|
||||
amountSats,
|
||||
|
|
@ -1306,6 +1364,18 @@ function createATMServices(
|
|||
* Used to limit cash-in transactions to what the ATM can pay out
|
||||
*/
|
||||
getAvailableBalance: async (): Promise<number> => {
|
||||
// LNbits path (3b.2): balance straight from get_wallet over transport.
|
||||
if (lnbitsActive) {
|
||||
try {
|
||||
const { balanceSats } = await lnbits!.getBalance(lnbitsWalletId!)
|
||||
console.log('[ATM Service] Available balance (LNbits):', balanceSats, 'sats')
|
||||
return balanceSats
|
||||
} catch (error) {
|
||||
console.error('[ATM Service] LNbits getBalance failed:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ATM Service] Fetching available balance from Lightning.Pub')
|
||||
try {
|
||||
const { balanceSats } = await lightningPub.getBalance()
|
||||
|
|
@ -1466,6 +1536,43 @@ function createATMServices(
|
|||
return () => {}
|
||||
}
|
||||
|
||||
// LNbits path (3b.2): subscribe_payments by payment_hash.
|
||||
if (lnbitsActive) {
|
||||
let cancelled = false
|
||||
let subId: string | null = null
|
||||
;(async () => {
|
||||
try {
|
||||
// Extract payment_hash from the bolt11. LNbits has a `decode_payment`
|
||||
// RPC for this but a plain bolt11 decode is cheap and avoids a roundtrip.
|
||||
const decoded = await lnbits!.decodePayment(invoice)
|
||||
const paymentHash = (decoded as { payment_hash?: string }).payment_hash
|
||||
if (!paymentHash) {
|
||||
console.error('[ATM Service] LNbits decode_payment did not return payment_hash')
|
||||
return
|
||||
}
|
||||
if (cancelled) return
|
||||
subId = await lnbits!.subscribePayments(
|
||||
lnbitsWalletId!,
|
||||
{ payment_hash: paymentHash, max_seconds: 600 },
|
||||
(push) => {
|
||||
if (push.payment_hash !== paymentHash) return
|
||||
if (push.status !== 'success') return
|
||||
console.log('[ATM Service] Invoice paid (LNbits push)!')
|
||||
callback(push.preimage ?? 'payment-confirmed')
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('[ATM Service] LNbits watchInvoice failed:', e)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (subId) {
|
||||
void lnbits!.unsubscribe(lnbitsWalletId!, subId).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lightning.Pub's watchInvoice subscribes to LiveUserOperation events
|
||||
// and returns a cleanup function
|
||||
const cleanup = lightningPub.watchInvoice(invoice, (paidInvoice) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue