fund-atm resolves its signer by resuming the bunker binding from state.db (the connect token is already spent by the main app, so it can't re-pair); falls back to a dev nsec via VITE_ATM_PRIVATE_KEY. better-sqlite3 marked external in the esbuild bundle. .env.example + CLAUDE.md document VITE_SPIRE_SEED as the prod identity, VITE_ATM_PRIVATE_KEY as dev-only. (fund-atm is slated for deprecation in favour of the operator funding the wallet directly via the LNbits UI — kept working for now.) Part of Phase C, aiolabs/bitspire#52. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
4 KiB
JavaScript
144 lines
4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* fund-atm — Generate a Lightning invoice to fund the ATM's LNbits wallet.
|
|
*
|
|
* Usage:
|
|
* fund-atm <amount_sats>
|
|
* fund-atm 100000
|
|
* fund-atm 100000 sats
|
|
*
|
|
* Reads ATM identity and LNbits transport config from /var/lib/bitspire/.env.
|
|
* Connects to LNbits via the Nostr nostr-native-transport, generates an
|
|
* invoice, displays a QR code in the terminal, and prints the BOLT11.
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
import {
|
|
NostrClient,
|
|
LocalSigner,
|
|
loadIdentityFromHex,
|
|
resumeFromBinding,
|
|
type Signer,
|
|
} from '@bitSpire/nostr-client'
|
|
import { LnbitsClient } from '@bitSpire/lnbits'
|
|
import { initDatabase, getBunkerBinding } from './state-store.js'
|
|
|
|
// @ts-ignore — qrcode is a transitive dep (via qrcode.vue), no types needed
|
|
import QRCode from 'qrcode'
|
|
|
|
function loadEnv(path: string): Record<string, string> {
|
|
const env: Record<string, string> = {}
|
|
try {
|
|
const content = readFileSync(path, 'utf-8')
|
|
for (const line of content.split('\n')) {
|
|
const trimmed = line.trim()
|
|
if (trimmed && !trimmed.startsWith('#')) {
|
|
const eqIdx = trimmed.indexOf('=')
|
|
if (eqIdx > 0) {
|
|
const key = trimmed.slice(0, eqIdx)
|
|
const value = trimmed.slice(eqIdx + 1)
|
|
env[key] = value
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return env
|
|
}
|
|
|
|
async function main() {
|
|
const arg = process.argv[2] || ''
|
|
const amountSats = parseInt(arg, 10)
|
|
if (!amountSats || amountSats <= 0) {
|
|
console.error('Usage: fund-atm <amount_sats>')
|
|
console.error(' e.g., fund-atm 100000')
|
|
process.exit(1)
|
|
}
|
|
|
|
const envPath = process.env['ATM_ENV_PATH'] || '/var/lib/bitspire/.env'
|
|
const env = loadEnv(envPath)
|
|
|
|
const relayUrl = env['VITE_RELAY_URL']
|
|
const lnbitsServerPubkey = env['VITE_LNBITS_SERVER_PUBKEY']
|
|
const atmPrivateKey = env['VITE_ATM_PRIVATE_KEY']
|
|
|
|
if (!relayUrl || !lnbitsServerPubkey) {
|
|
console.error('Missing required config in', envPath)
|
|
console.error('Need: VITE_RELAY_URL, VITE_LNBITS_SERVER_PUBKEY')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.error(`Generating invoice for ${amountSats} sats...`)
|
|
|
|
// Resolve the signer. Prod: resume the bunker binding from state.db (the
|
|
// ATM's transport key — the connect token was already redeemed by the main
|
|
// app, so we can't re-pair here). Dev: a local nsec via VITE_ATM_PRIVATE_KEY.
|
|
let signer: Signer
|
|
if (atmPrivateKey) {
|
|
signer = new LocalSigner(loadIdentityFromHex(atmPrivateKey))
|
|
} else {
|
|
initDatabase()
|
|
const binding = getBunkerBinding()
|
|
if (!binding) {
|
|
console.error('ATM is not paired (no bunker binding in state.db) and no')
|
|
console.error('VITE_ATM_PRIVATE_KEY set. Pair the ATM via the main app first.')
|
|
process.exit(1)
|
|
}
|
|
signer = await resumeFromBinding({
|
|
clientSecretHex: binding.clientSecretHex,
|
|
spirePubkey: binding.spirePubkey,
|
|
bunkerUrl: binding.bunkerUrl,
|
|
})
|
|
}
|
|
|
|
const nostrClient = new NostrClient({
|
|
relays: [{ url: relayUrl }],
|
|
signer,
|
|
})
|
|
await nostrClient.connect()
|
|
|
|
const lnbits = new LnbitsClient({
|
|
serverPubkey: lnbitsServerPubkey,
|
|
relays: [relayUrl],
|
|
})
|
|
lnbits.initialize(nostrClient, signer)
|
|
|
|
const wallets = await lnbits.listWallets()
|
|
const wallet = wallets[0]
|
|
if (!wallet) {
|
|
console.error('No LNbits wallet found for this ATM identity')
|
|
process.exit(1)
|
|
}
|
|
|
|
const payment = await lnbits.createInvoice(wallet.id, {
|
|
amount: amountSats,
|
|
memo: `Fund bitSpire (${amountSats} sats)`,
|
|
unit: 'sat',
|
|
})
|
|
const bolt11 = payment.payment_request
|
|
if (!bolt11) {
|
|
console.error('LNbits create_invoice returned empty payment_request')
|
|
process.exit(1)
|
|
}
|
|
|
|
try {
|
|
const qr = await QRCode.toString(bolt11.toUpperCase(), { type: 'terminal', small: true })
|
|
console.error('')
|
|
console.error(qr)
|
|
console.error(` ${amountSats} sats`)
|
|
console.error('')
|
|
} catch {
|
|
// Fall back to just printing the invoice
|
|
}
|
|
|
|
console.log(bolt11)
|
|
|
|
nostrClient.disconnect()
|
|
process.exit(0)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Error:', err.message || err)
|
|
process.exit(1)
|
|
})
|