feat(pairing,ui): optional machine_npub + bunker_relay override + fee decimal-input UX
Some checks failed
ci.yml / feat(pairing,ui): optional machine_npub + bunker_relay override + fee decimal-input UX (pull_request) Failing after 0s
Some checks failed
ci.yml / feat(pairing,ui): optional machine_npub + bunker_relay override + fee decimal-input UX (pull_request) Failing after 0s
Three changes from the nsecbunkerd#27 bunker-pairing smoke (validated
end-to-end on the Sintra, 2026-06-21); intermingled per-file, so landed
together.
1. Optional machine_npub (model A1) — register UNPAIRED, bunker mints the
identity at pairing:
- machine_npub now nullable (migration m011 rebuilds dca_machines for
sqlite / ALTER ... DROP NOT NULL for postgres; UNIQUE stays, NULLs
don't collide so any number of unpaired machines coexist).
- CreateMachineData.machine_npub -> str | None; create skips the
collision-check + fee publish when blank; api_pair_machine now
publishes the fee config after minting, so an unpaired machine clears
its awaiting-fees gate once paired.
- Supplying an npub up front is the DEVELOPMENT self-key path (a machine
holding its own signing key) — available to anyone but the form field
is explicitly marked DEVELOPMENT ONLY.
- Frontend: npub field optional, required rule dropped, null-safe
display (shortNpub -> "unpaired", guarded slices), empty -> null.
2. bunker_relay override on POST /machines/{id}/pair: PairMachineData gains
bunker_relay; api_pair_machine threads it to pair_spire. Lets the seed's
bunker:// relay differ from the relay lnbits uses to reach the bunker
(internal docker host vs LAN/public) — needed for split-relay / dev
deploys. Without it the smoke had to mint via a script.
3. Fees are decimal fractions, not percents: relabel super + operator fee
inputs ("decimal fraction, 0-0.15") + a shared _assertFeesDecimal()
guard (super/add/edit submits) so a percent typo (3 instead of 0.03)
gets a clear toast, not a raw 400.
refs: nsecbunkerd#27/#36; aiolabs/bitspire#52; coordination smoke 2026-06-21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47b7efc53c
commit
73bd274979
5 changed files with 153 additions and 29 deletions
28
views_api.py
28
views_api.py
|
|
@ -248,7 +248,7 @@ async def _assert_super_config_cap_safe(
|
|||
HTTPStatus.BAD_REQUEST,
|
||||
(
|
||||
f"super cash-in fee {effective_in:.4f} would exceed cap "
|
||||
f"on machine {m.id} ({m.name or m.machine_npub[:12]}): "
|
||||
f"on machine {m.id} ({m.name or (m.machine_npub or m.id)[:12]}): "
|
||||
f"+ operator {op_in:.4f} = "
|
||||
f"{total_in:.4f} > {MAX_FEE_FRACTION_PER_DIRECTION}"
|
||||
),
|
||||
|
|
@ -258,7 +258,7 @@ async def _assert_super_config_cap_safe(
|
|||
HTTPStatus.BAD_REQUEST,
|
||||
(
|
||||
f"super cash-out fee {effective_out:.4f} would exceed cap "
|
||||
f"on machine {m.id} ({m.name or m.machine_npub[:12]}): "
|
||||
f"on machine {m.id} ({m.name or (m.machine_npub or m.id)[:12]}): "
|
||||
f"+ operator {op_out:.4f} = "
|
||||
f"{total_out:.4f} > {MAX_FEE_FRACTION_PER_DIRECTION}"
|
||||
),
|
||||
|
|
@ -275,7 +275,13 @@ async def api_create_machine(
|
|||
data: CreateMachineData, user: User = Depends(check_user_exists)
|
||||
) -> Machine:
|
||||
await _assert_wallet_owned_by(data.wallet_id, user.id)
|
||||
await _assert_no_pubkey_collision(data.machine_npub)
|
||||
# machine_npub is optional: blank = register UNPAIRED — the bunker mints
|
||||
# the identity at pairing (the normal path). An npub supplied up front is
|
||||
# the development self-key path; only then do we collision-check + publish
|
||||
# a fee config now (an unpaired machine has no target yet, so it gets its
|
||||
# config at pairing instead — see api_pair_machine).
|
||||
if data.machine_npub:
|
||||
await _assert_no_pubkey_collision(data.machine_npub)
|
||||
await _assert_machine_fee_cap_safe(
|
||||
data.operator_cash_in_fee_fraction,
|
||||
data.operator_cash_out_fee_fraction,
|
||||
|
|
@ -284,9 +290,10 @@ async def api_create_machine(
|
|||
# Layer 2 (#39): publish initial fee config to the ATM so it can
|
||||
# unblock past its `awaiting-fees` maintenance gate. Soft-fails on
|
||||
# transport errors — machine creation has already succeeded.
|
||||
super_config = await get_super_config()
|
||||
if super_config is not None:
|
||||
await publish_fee_config(machine, super_config, user.id)
|
||||
if machine.machine_npub:
|
||||
super_config = await get_super_config()
|
||||
if super_config is not None:
|
||||
await publish_fee_config(machine, super_config, user.id)
|
||||
return machine
|
||||
|
||||
|
||||
|
|
@ -317,6 +324,7 @@ async def api_pair_machine(
|
|||
machine,
|
||||
relays=data.relays,
|
||||
admin_client=client,
|
||||
bunker_relay=data.bunker_relay,
|
||||
duration_hours=data.duration_hours,
|
||||
)
|
||||
except NsecBunkerNotConfiguredError as exc:
|
||||
|
|
@ -337,6 +345,14 @@ async def api_pair_machine(
|
|||
bunker_spire_key_name=result.bunker_key_name,
|
||||
paired_at=datetime.now(timezone.utc),
|
||||
)
|
||||
# Now that the machine has a bunker identity, publish its fee config so
|
||||
# the spire can clear its `awaiting-fees` gate. For a machine created
|
||||
# unpaired, this is the first time it has a target. Soft-fails (mirrors
|
||||
# create); pairing has already succeeded.
|
||||
super_config = await get_super_config()
|
||||
if super_config is not None:
|
||||
paired = await _machine_owned_by(machine_id, user.id)
|
||||
await publish_fee_config(paired, super_config, user.id)
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue