From 9d0e8d8fea7437aa560fd6f67a1769f6eaaf76a4 Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 2 Jul 2026 22:00:49 +0200 Subject: [PATCH] feat(machine): consume get_machine_config over the transport (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Source the operator pubkey + fee config from LNbits via the get_machine_config kind-21000 RPC (spirekeeper#41) right after list_wallets, instead of the operator pubkey coming only from VITE_OPERATOR_PUBKEYS (env). A seed-only machine (blank .env) had an empty operator allowlist → the fees/operator-config services disabled themselves → permanent "awaiting configuration". Now it pulls its config over the already-authenticated channel and configures itself with zero per-machine provisioning — closing bitspire#70 P1. - LnbitsClient.getMachineConfig() → sendRpc('get_machine_config') + the MachineConfigResponse / FeeConfigWire types. - lightning.ts, only when VITE_OPERATOR_PUBKEYS is empty (env override still wins): set CONFIG.operatorPubkeys from operator_pubkey (re-enables the services), and persist fee_config via the existing applyFeeConfig IPC (mapping snake_case → camelCase) so atm.ts's awaiting-fees gate clears immediately — robust to the replaceable kind-30078 not being fetchable from the relay. The live kind-30078 subscription still handles mid-run fee updates. - Soft-fail: older spirekeeper (no RPC) or a transport error falls back to the env/kind-30078 path. lnbits + machine typecheck clean; lnbits suite 29 pass. Co-Authored-By: Claude Opus 4.8 --- apps/machine/src/services/lightning.ts | 40 ++++++++++++++++++++++++++ packages/lnbits/src/client.ts | 12 ++++++++ packages/lnbits/src/types.ts | 27 +++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/apps/machine/src/services/lightning.ts b/apps/machine/src/services/lightning.ts index 6509031..4eca3e2 100644 --- a/apps/machine/src/services/lightning.ts +++ b/apps/machine/src/services/lightning.ts @@ -505,6 +505,46 @@ export async function initializeLightningServices(options?: { } console.log('[Lightning] LNbits wallet:', lnbitsWalletId) + // #70 P1: pull operator pubkey + fee config from LNbits over the authenticated + // transport (spirekeeper#41 `get_machine_config`). A seed-only machine has no + // VITE_OPERATOR_PUBKEYS, so without this it can't trust its fee config and sits + // at "awaiting configuration". Only for the seed-only case — an explicit + // VITE_OPERATOR_PUBKEYS override keeps the env/kind-30078 path untouched. + // Soft-fail: an older spirekeeper (no RPC) or a transport error falls back to + // whatever the operator services can pull from kind-30078. + if (CONFIG.operatorPubkeys.length === 0) { + try { + const mc = await lnbits.getMachineConfig() + if (mc.operator_pubkey) { + CONFIG.operatorPubkeys = [mc.operator_pubkey] + console.log('[Lightning] Operator pubkey(s):', mc.operator_pubkey, '(server-delivered, #70 P1)') + } + if (mc.fee_config && isElectron && window.electronAPI) { + // Persist the server-delivered fee config so atm.ts's awaiting-fees gate + // (getFeeConfig) clears immediately — robust to the replaceable kind-30078 + // event not being fetchable from the relay. The live kind-30078 + // subscription still handles mid-run fee updates. + const applied = await window.electronAPI.applyFeeConfig( + { + cashInFeeFraction: mc.fee_config.cash_in_fee_fraction, + cashOutFeeFraction: mc.fee_config.cash_out_fee_fraction, + schemaVersion: mc.fee_config.schema_version, + }, + mc.created_at, + ) + console.log( + '[Lightning] Server-delivered fee config:', + applied.applied ? 'applied' : `skipped (${applied.reason})`, + ) + } + } catch (e) { + console.warn( + '[Lightning] get_machine_config unavailable; falling back to env/kind-30078 for operator config:', + (e as Error).message, + ) + } + } + // CLINK client — kept in tree but not actively wired into LNbits flows. // operatorPubkey is the operator allowlist for kind-21003 management // commands; it has no Lightning.Pub dependency. diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts index f75db3f..cb3645d 100644 --- a/packages/lnbits/src/client.ts +++ b/packages/lnbits/src/client.ts @@ -37,6 +37,7 @@ import type { CreateInvoiceBody, PayInvoiceBody, WalletInfo, + MachineConfigResponse, SubscribePaymentsBody, SubscribeAck, PaymentPushCallback, @@ -210,6 +211,17 @@ export class LnbitsClient { return data ?? [] } + /** Pull server-delivered machine config (operator pubkey + fee config) over + * the authenticated transport — spirekeeper's `get_machine_config` RPC + * (bitspire#70 P1). Lets a seed-only ATM configure itself with no per-machine + * env provisioning. Rejects (LnbitsRpcError) if the server hasn't registered + * the RPC (older spirekeeper) — callers should soft-fall-back. */ + async getMachineConfig(): Promise { + return this.idempotent(() => + this.sendRpc('get_machine_config', {}), + ) + } + // ============================================================================ // Invoices // ============================================================================ diff --git a/packages/lnbits/src/types.ts b/packages/lnbits/src/types.ts index 17af86f..dd5e7a9 100644 --- a/packages/lnbits/src/types.ts +++ b/packages/lnbits/src/types.ts @@ -274,3 +274,30 @@ export type PaymentPushCallback = (payment: LnbitsPayment) => void /** Called when the subscription has been closed (by TTL or explicit unsubscribe). */ export type SubscriptionCloseCallback = (reason: 'ttl' | 'unsubscribed') => void + +// ============================================================================ +// get_machine_config RPC (spirekeeper#41 / bitspire#70 P1) +// ============================================================================ + +/** Fee-config wire shape inside `get_machine_config` — mirrors spirekeeper's + * `FeeConfigPayload.to_wire_dict()` (snake_case). */ +export interface FeeConfigWire { + schema_version: number + cash_in_fee_fraction: number + cash_out_fee_fraction: number + components?: Record +} + +/** Response of the `get_machine_config` RPC: the operator pubkey + fee config + * (+ fiat, ids) LNbits delivers to a paired ATM over the authenticated + * transport, so a seed-only machine needs no per-machine env provisioning. + * `fee_config` is null until the operator has a super-config. */ +export interface MachineConfigResponse { + operator_pubkey: string + fee_config: FeeConfigWire | null + fiat_code: string + machine_npub: string + wallet_id: string + /** Freshness watermark (unix s) for the consumer's fee-config replay guard. */ + created_at: number +}