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
|
|
@ -581,8 +581,29 @@ window.app = Vue.createApp({
|
|||
this.superFeeDialog.show = true
|
||||
},
|
||||
|
||||
// Guard the decimal-vs-percent trap shared by the super + operator fee
|
||||
// forms: fees are decimal fractions (3% = 0.03), capped at 0.15. A value
|
||||
// > 0.15 almost always means a percent was typed (3 instead of 0.03).
|
||||
// Returns false + shows a clear toast so the operator never sees a raw 400.
|
||||
_assertFeesDecimal(...fracs) {
|
||||
if (fracs.some((v) => !Number.isFinite(v) || v < 0 || v > 0.15)) {
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Enter each fee as a decimal fraction (e.g. 3% = 0.03)',
|
||||
caption:
|
||||
'Range 0–0.15. A value above 0.15 usually means a percent was typed (3 instead of 0.03).'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
async submitSuperFee() {
|
||||
const d = this.superFeeDialog.data
|
||||
if (!this._assertFeesDecimal(
|
||||
Number(d.super_cash_in_fee_fraction),
|
||||
Number(d.super_cash_out_fee_fraction)
|
||||
)) return
|
||||
this.superFeeDialog.saving = true
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
|
|
@ -699,13 +720,17 @@ window.app = Vue.createApp({
|
|||
|
||||
async submitAddMachine() {
|
||||
const body = this._cleanMachineForm(this.addMachineDialog.data)
|
||||
if (!body.machine_npub || !body.wallet_id) {
|
||||
if (!body.wallet_id) {
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'machine_npub and wallet_id are required'
|
||||
message: 'A wallet is required'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!this._assertFeesDecimal(
|
||||
Number(body.operator_cash_in_fee_fraction) || 0,
|
||||
Number(body.operator_cash_out_fee_fraction) || 0
|
||||
)) return
|
||||
this.addMachineDialog.saving = true
|
||||
try {
|
||||
const {data} = await LNbits.api.request('POST', MACHINES_PATH, null, body)
|
||||
|
|
@ -713,7 +738,7 @@ window.app = Vue.createApp({
|
|||
this.addMachineDialog.show = false
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: `Machine ${data.name || data.machine_npub.slice(0, 12)} added`
|
||||
message: `Machine ${data.name || (data.machine_npub || 'unpaired').slice(0, 12)} added`
|
||||
})
|
||||
} catch (e) {
|
||||
this._notifyError(e, 'Failed to add machine')
|
||||
|
|
@ -741,6 +766,10 @@ window.app = Vue.createApp({
|
|||
|
||||
async submitEditMachine() {
|
||||
const d = this.editMachineDialog.data
|
||||
if (!this._assertFeesDecimal(
|
||||
Number(d.operator_cash_in_fee_fraction) || 0,
|
||||
Number(d.operator_cash_out_fee_fraction) || 0
|
||||
)) return
|
||||
this.editMachineDialog.saving = true
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
|
|
@ -772,7 +801,7 @@ window.app = Vue.createApp({
|
|||
Quasar.Dialog.create({
|
||||
title: 'Delete machine?',
|
||||
message:
|
||||
`This removes <b>${machine.name || machine.machine_npub.slice(0, 12)}</b>` +
|
||||
`This removes <b>${machine.name || (machine.machine_npub || 'unpaired').slice(0, 12)}</b>` +
|
||||
' from your fleet. Existing settlements and payment history are preserved' +
|
||||
' — only the machine row itself is removed. Continue?',
|
||||
html: true,
|
||||
|
|
@ -848,7 +877,7 @@ window.app = Vue.createApp({
|
|||
Quasar.Dialog.create({
|
||||
title: 'Revoke spire access?',
|
||||
message:
|
||||
`This cuts <b>${machine.name || machine.machine_npub.slice(0, 12)}</b>'s` +
|
||||
`This cuts <b>${machine.name || (machine.machine_npub || 'unpaired').slice(0, 12)}</b>'s` +
|
||||
' signing access at the bunker — the spire can no longer submit' +
|
||||
' cash-outs until you re-pair it. Continue?',
|
||||
html: true,
|
||||
|
|
@ -1515,7 +1544,7 @@ window.app = Vue.createApp({
|
|||
// Helpers
|
||||
// -----------------------------------------------------------------
|
||||
shortNpub(npub) {
|
||||
if (!npub) return ''
|
||||
if (!npub) return 'unpaired'
|
||||
if (npub.length <= 16) return npub
|
||||
return npub.slice(0, 8) + '…' + npub.slice(-6)
|
||||
},
|
||||
|
|
@ -1601,7 +1630,7 @@ window.app = Vue.createApp({
|
|||
|
||||
_cleanMachineForm(d) {
|
||||
return {
|
||||
machine_npub: (d.machine_npub || '').trim(),
|
||||
machine_npub: (d.machine_npub || '').trim() || null,
|
||||
wallet_id: d.wallet_id,
|
||||
name: (d.name || '').trim() || null,
|
||||
location: (d.location || '').trim() || null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue