feat(machine): consume get_machine_config over the transport (#71)
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 <noreply@anthropic.com>
This commit is contained in:
parent
a01e64cc70
commit
fdb9a507c2
3 changed files with 79 additions and 0 deletions
|
|
@ -505,6 +505,46 @@ export async function initializeLightningServices(options?: {
|
||||||
}
|
}
|
||||||
console.log('[Lightning] LNbits wallet:', lnbitsWalletId)
|
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.
|
// CLINK client — kept in tree but not actively wired into LNbits flows.
|
||||||
// operatorPubkey is the operator allowlist for kind-21003 management
|
// operatorPubkey is the operator allowlist for kind-21003 management
|
||||||
// commands; it has no Lightning.Pub dependency.
|
// commands; it has no Lightning.Pub dependency.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import type {
|
||||||
CreateInvoiceBody,
|
CreateInvoiceBody,
|
||||||
PayInvoiceBody,
|
PayInvoiceBody,
|
||||||
WalletInfo,
|
WalletInfo,
|
||||||
|
MachineConfigResponse,
|
||||||
SubscribePaymentsBody,
|
SubscribePaymentsBody,
|
||||||
SubscribeAck,
|
SubscribeAck,
|
||||||
PaymentPushCallback,
|
PaymentPushCallback,
|
||||||
|
|
@ -210,6 +211,17 @@ export class LnbitsClient {
|
||||||
return data ?? []
|
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<MachineConfigResponse> {
|
||||||
|
return this.idempotent(() =>
|
||||||
|
this.sendRpc<MachineConfigResponse>('get_machine_config', {}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Invoices
|
// Invoices
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -274,3 +274,30 @@ export type PaymentPushCallback = (payment: LnbitsPayment) => void
|
||||||
|
|
||||||
/** Called when the subscription has been closed (by TTL or explicit unsubscribe). */
|
/** Called when the subscription has been closed (by TTL or explicit unsubscribe). */
|
||||||
export type SubscriptionCloseCallback = (reason: 'ttl' | 'unsubscribed') => void
|
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<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue