refactor(machine): drop VITE_LNBITS_HTTP_URL — lnurl now arrives populated from LNbits (#57 gap 2)
Closes gap 2 from coord log 2026-06-01T18:30Z. The LNbits withdraw extension's nostr-transport RPC now populates `link.lnurl` from `settings.lnbits_baseurl` (aiolabs/withdraw#1 / commit e9d911e), so the ATM no longer needs a separate HTTP URL on the wire to compose the LNURL-withdraw callback itself. What goes: - `VITE_LNBITS_HTTP_URL` env var (renderer + Electron main) - `lnbitsHttpUrl` field on `LightningConfig`, `RuntimeConfig`, and the Window mirror in `src/types/electron.d.ts` - The manual `${lnbitsHttpUrl}/withdraw/api/v1/lnurl/${unique_hash}` composition in `generateLnurlWithdraw` - The `encodeLnurl` bech32 helper in `lightning.ts` (LNbits returns bech32-encoded; we just `.toUpperCase()` to match BOLT/LNURL convention) - `@scure/base` dep from `apps/machine/package.json` (only used by the removed helper; clink still uses it directly) - The `lnbitsHttpUrl` option + `LNBITS_HTTP_URL=…` env var + boot echo in `deploy/nixos/bitspire-atm.nix` - Doc references in CLAUDE.md, README.md, deploy/nixos/README.md, docs/architecture-comparison.md, and the lightning-check skill What stays: - `link.lnurl` consumption, with an explicit error if LNbits returns null (which signals `LNBITS_BASEURL` is unset on the server side — better to fail clearly than silently) - The receiver-side bech32 uppercasing (LNbits returns lowercase per the standard library) Why this is a net win: - Removes a config-drift surface — if LNbits's external URL moved (DNS, port, reverse-proxy rewrite), every ATM in the field would stop issuing redeemable LNURL-withdraw QRs until reconfigured. Now LNbits derives its own URL from `settings.lnbits_baseurl`, one source of truth. - Removes an extra provisioning step. No more `LNBITS_HTTP_URL=…` before running `provision-atm.sh`; the relay + server pubkey suffice. - Removes the misleading boot echo that triggered the §`18:30Z` smoke triage confusion ("LNbits HTTP: <url>" read like ATM-→-LNbits connectivity, when it was only ever a URL embedded in customer QRs). Also adds a `# pragma: allowlist secret` marker above the `VITE_ATM_PRIVATE_KEY` doc block in `.env.example` so the global secret scanner stops false-positiving on the documentation prose. Workspace typecheck + 24/24 apps/machine tests still green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9bdb9333fd
commit
4f68ddc40b
14 changed files with 63 additions and 91 deletions
|
|
@ -19,7 +19,6 @@ import {
|
|||
type MachineIdentity,
|
||||
} from '@bitSpire/nostr-client'
|
||||
import { LnbitsClient } from '@bitSpire/lnbits'
|
||||
import { bech32 } from '@scure/base'
|
||||
import { CLINKClient } from '@bitSpire/clink'
|
||||
import type { OfferRequest, ManagementRequest, ManagementResponse } from '@bitSpire/clink'
|
||||
import type { ATMServices, ATMContext } from '@bitSpire/state-machine'
|
||||
|
|
@ -50,13 +49,6 @@ interface LightningConfig {
|
|||
operatorPubkeys: string[]
|
||||
/** LNbits nostr-transport server pubkey (hex, 64 chars). */
|
||||
lnbitsServerPubkey: string
|
||||
/**
|
||||
* LNbits HTTP root (e.g. `https://lnbits.example`). Used purely to
|
||||
* compose the LNURL callback URL that customer wallets dereference
|
||||
* to redeem an LNURL-withdraw. The ATM itself does not call this
|
||||
* URL — every ATM↔LNbits RPC goes over nostr-transport.
|
||||
*/
|
||||
lnbitsHttpUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,7 +65,6 @@ async function loadLightningConfig(): Promise<LightningConfig> {
|
|||
appId: '30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097', // bitSpire ATM app ID
|
||||
operatorPubkeys: [],
|
||||
lnbitsServerPubkey: '',
|
||||
lnbitsHttpUrl: 'http://localhost:5000',
|
||||
}
|
||||
|
||||
if (isElectron && window.electronAPI) {
|
||||
|
|
@ -91,7 +82,6 @@ async function loadLightningConfig(): Promise<LightningConfig> {
|
|||
.filter(Boolean)
|
||||
: defaults.operatorPubkeys,
|
||||
lnbitsServerPubkey: rc.lnbitsServerPubkey || defaults.lnbitsServerPubkey,
|
||||
lnbitsHttpUrl: rc.lnbitsHttpUrl || defaults.lnbitsHttpUrl,
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Lightning] Failed to get runtime config from Electron:', e)
|
||||
|
|
@ -105,9 +95,6 @@ async function loadLightningConfig(): Promise<LightningConfig> {
|
|||
lnbitsServerPubkey:
|
||||
(import.meta.env.VITE_LNBITS_SERVER_PUBKEY as string | undefined) ||
|
||||
defaults.lnbitsServerPubkey,
|
||||
lnbitsHttpUrl:
|
||||
(import.meta.env.VITE_LNBITS_HTTP_URL as string | undefined) ||
|
||||
defaults.lnbitsHttpUrl,
|
||||
operatorPubkeys: import.meta.env.VITE_OPERATOR_PUBKEYS
|
||||
? (import.meta.env.VITE_OPERATOR_PUBKEYS as string)
|
||||
.split(',')
|
||||
|
|
@ -120,19 +107,6 @@ async function loadLightningConfig(): Promise<LightningConfig> {
|
|||
// Config is loaded async now - will be set in initializeLightningServices
|
||||
let CONFIG: LightningConfig
|
||||
|
||||
/**
|
||||
* Encode a callback URL as an LNURL (bech32 with HRP "lnurl", upper-cased
|
||||
* per BOLT/LNURL convention). Used for cash-in: customer wallet scans
|
||||
* the QR, decodes the URL, GETs it to receive the LNURL-withdraw params.
|
||||
*
|
||||
* Generous bech32 limit: LNURLs can run long (full origin + path + hash).
|
||||
*/
|
||||
function encodeLnurl(url: string): string {
|
||||
const bytes = new TextEncoder().encode(url)
|
||||
const words = bech32.toWords(bytes)
|
||||
return bech32.encode('lnurl', words, 2000).toUpperCase()
|
||||
}
|
||||
|
||||
/** Safety timeout in ms (15 minutes) — absolute maximum LNURL session lifetime.
|
||||
* Sessions are normally cleaned up by the state machine on idle transition.
|
||||
* This is a safety net in case the state machine doesn't clean up properly. */
|
||||
|
|
@ -677,22 +651,18 @@ function createATMServices(
|
|||
/**
|
||||
* Generate an LNURL-withdraw for cash-in.
|
||||
*
|
||||
* 1. Create the link via LNbits transport (lnurlw_create_link).
|
||||
* 2. Compose the customer-facing callback URL from VITE_LNBITS_HTTP_URL
|
||||
* plus the link's unique_hash (the transport returns null for
|
||||
* `lnurl`/`lnurl_url` — those are filled by HTTP views, not the
|
||||
* create RPC). Bech32-encode it ourselves with HRP "lnurl".
|
||||
* 3. Subscribe to settlement pushes filtered by tag="withdraw" +
|
||||
* 1. Create the link via LNbits transport (lnurlw_create_link). The
|
||||
* nostr-transport RPC returns `link.lnurl` populated from
|
||||
* `settings.lnbits_baseurl` on the LNbits side (see
|
||||
* aiolabs/withdraw#1 / commit e9d911e) — no need to compose
|
||||
* the callback URL ourselves anymore.
|
||||
* 2. Subscribe to settlement pushes filtered by tag="withdraw" +
|
||||
* link_id; customer wallet redeems via HTTP, LNbits pushes us
|
||||
* over nostr, we trigger dispense.
|
||||
*/
|
||||
generateLnurlWithdraw: async (context: ATMContext): Promise<string> => {
|
||||
console.log('[ATM Service] Generating LNURL-withdraw for', context.satsAmount, 'sats')
|
||||
|
||||
if (!CONFIG.lnbitsHttpUrl) {
|
||||
throw new Error('[ATM Service] VITE_LNBITS_HTTP_URL is required for LNbits cash-in')
|
||||
}
|
||||
|
||||
try {
|
||||
if (context.cashInSessionId) {
|
||||
invalidateLnurlSessionBySessionId(context.cashInSessionId)
|
||||
|
|
@ -707,8 +677,12 @@ function createATMServices(
|
|||
is_unique: false,
|
||||
})
|
||||
|
||||
const callbackUrl = `${CONFIG.lnbitsHttpUrl.replace(/\/+$/, '')}/withdraw/api/v1/lnurl/${link.unique_hash}`
|
||||
const lnurl = encodeLnurl(callbackUrl)
|
||||
if (!link.lnurl) {
|
||||
throw new Error(
|
||||
'[ATM Service] LNbits returned link.lnurl=null — check LNBITS_BASEURL on the server (aiolabs/withdraw#1)'
|
||||
)
|
||||
}
|
||||
const lnurl = link.lnurl.toUpperCase()
|
||||
|
||||
if (context.cashInSessionId) {
|
||||
registerLnurlSession(
|
||||
|
|
|
|||
2
apps/machine/src/types/electron.d.ts
vendored
2
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -6,8 +6,6 @@ export interface RuntimeConfig {
|
|||
relayUrl: string
|
||||
/** LNbits nostr-transport server pubkey (hex, 64 chars). */
|
||||
lnbitsServerPubkey: string
|
||||
/** LNbits HTTP root — used only to compose the LNURL-withdraw callback URL. */
|
||||
lnbitsHttpUrl: string
|
||||
/** Legacy LP fields — retained until 3d removes the LP backend. Optional. */
|
||||
lightningPubPubkey?: string
|
||||
lightningPubApiUrl?: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue