feat: add fund-atm CLI for generating ATM funding invoices

Standalone Node.js script that generates a Lightning invoice for
the ATM's Lightning.Pub account. Reads config from .env, connects
to the relay, creates an invoice via Nostr RPC, displays a QR code
in the terminal, and prints the BOLT11.

Bundled as self-contained CJS with esbuild (all dependencies inlined)
so it works from the nix store without separate node_modules.

Usage: fund-atm <amount_sats>
  e.g. fund-atm 100000
       fund-atm 100000 sats

Added to NixOS systemPackages for both live and installed configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-28 00:45:45 -04:00
commit d3befc11c1
5 changed files with 406 additions and 6 deletions

View file

@ -0,0 +1,116 @@
#!/usr/bin/env node
/**
* fund-atm — Generate a Lightning invoice to fund the ATM's account.
*
* Usage:
* fund-atm <amount_sats>
* fund-atm 100000
* fund-atm 100000 sats
*
* Reads ATM identity and Lightning.Pub config from /var/lib/lamassu-atm/.env
* Connects to Lightning.Pub via Nostr relay, generates an invoice,
* displays a QR code in the terminal and prints the BOLT11.
*/
import { readFileSync } from 'node:fs'
import { NostrClient, loadIdentityFromHex } from '@lamassu/nostr-client'
import { LightningPubClient } from '@lamassu/lightning'
// @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() {
// Parse amount — accept "fund-atm 1000" or "fund-atm 1000 sats"
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')
console.error(' fund-atm 100000 sats')
process.exit(1)
}
const envPath = process.env.ATM_ENV_PATH || '/var/lib/lamassu-atm/.env'
const env = loadEnv(envPath)
const relayUrl = env.VITE_RELAY_URL
const lpPubkey = env.VITE_LIGHTNING_PUB_PUBKEY
const atmPrivateKey = env.VITE_ATM_PRIVATE_KEY
const appId = env.VITE_APP_ID
if (!relayUrl || !lpPubkey || !atmPrivateKey) {
console.error('Missing required config in', envPath)
console.error('Need: VITE_RELAY_URL, VITE_LIGHTNING_PUB_PUBKEY, VITE_ATM_PRIVATE_KEY')
process.exit(1)
}
console.error(`Generating invoice for ${amountSats} sats...`)
const identity = loadIdentityFromHex(atmPrivateKey)
const nostrClient = new NostrClient({
relays: [{ url: relayUrl }],
identity,
})
await nostrClient.connect()
const lightningPub = new LightningPubClient({
accountPubkey: lpPubkey,
relays: [relayUrl],
appId: appId || undefined,
})
lightningPub.initialize(nostrClient, identity)
const invoice = await lightningPub.createInvoice({
amountSats,
description: `Fund ATM (${amountSats} sats)`,
})
const bolt11 = invoice.paymentRequest
// Display QR code in terminal
try {
const qr = await QRCode.toString(bolt11.toUpperCase(), { type: 'terminal', small: true })
console.error('') // blank line
console.error(qr)
console.error(` ${amountSats} sats`)
console.error('')
} catch {
// Fall back to just printing the invoice
}
// Print BOLT11 to stdout (pipeable)
console.log(bolt11)
// Clean up
nostrClient.disconnect()
process.exit(0)
}
main().catch((err) => {
console.error('Error:', err.message || err)
process.exit(1)
})