diff --git a/migrations.py b/migrations.py
index d3bcece..da8ba07 100644
--- a/migrations.py
+++ b/migrations.py
@@ -82,7 +82,7 @@ async def m001_satmachine_v2_initial(db):
CREATE TABLE IF NOT EXISTS spirekeeper.dca_machines (
id TEXT PRIMARY KEY,
operator_user_id TEXT NOT NULL,
- machine_npub TEXT UNIQUE,
+ machine_npub TEXT NOT NULL UNIQUE,
wallet_id TEXT NOT NULL,
name TEXT,
location TEXT,
@@ -776,72 +776,3 @@ async def m010_add_machine_bunker_pairing(db):
await db.execute(
f"ALTER TABLE spirekeeper.{table} ADD COLUMN {col} {coltype}"
)
-
-
-async def m011_machine_npub_nullable(db):
- """Make dca_machines.machine_npub nullable so an operator can register a
- machine *unpaired* (no npub) and have its identity minted by the bunker
- at pairing time (model A1, aiolabs/spirekeeper#9). The npub is only
- supplied up front on the development self-key path (a machine that holds
- its own signing key). UNIQUE stays — NULLs don't collide, so any number
- of unpaired machines coexist.
-
- Pre-public-launch: no back-compat shim. Existing rows are preserved by
- the rebuild; the column simply loses NOT NULL.
- """
- if db.type != "SQLITE":
- # Postgres / Cockroach can drop the constraint in place.
- await db.execute(
- "ALTER TABLE spirekeeper.dca_machines "
- "ALTER COLUMN machine_npub DROP NOT NULL"
- )
- return
-
- # SQLite can't drop NOT NULL in place — rebuild the table (same pattern
- # as m008/m009), preserving every row + the indexes.
- await db.execute(
- f"""
- CREATE TABLE spirekeeper.dca_machines_new (
- id TEXT PRIMARY KEY,
- operator_user_id TEXT NOT NULL,
- machine_npub TEXT UNIQUE,
- wallet_id TEXT NOT NULL,
- name TEXT,
- location TEXT,
- fiat_code TEXT NOT NULL DEFAULT 'GTQ',
- is_active BOOLEAN NOT NULL DEFAULT true,
- created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
- updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
- operator_cash_in_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
- operator_cash_out_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
- bunker_spire_key_name TEXT,
- paired_at TIMESTAMP
- )
- """
- )
- await db.execute(
- """
- INSERT INTO spirekeeper.dca_machines_new
- (id, operator_user_id, machine_npub, wallet_id, name, location,
- fiat_code, is_active, created_at, updated_at,
- operator_cash_in_fee_fraction, operator_cash_out_fee_fraction,
- bunker_spire_key_name, paired_at)
- SELECT id, operator_user_id, machine_npub, wallet_id, name, location,
- fiat_code, is_active, created_at, updated_at,
- operator_cash_in_fee_fraction, operator_cash_out_fee_fraction,
- bunker_spire_key_name, paired_at
- FROM spirekeeper.dca_machines
- """
- )
- await db.execute("DROP TABLE spirekeeper.dca_machines")
- await db.execute(
- "ALTER TABLE spirekeeper.dca_machines_new RENAME TO dca_machines"
- )
- await db.execute(
- "CREATE INDEX IF NOT EXISTS dca_machines_operator_idx "
- "ON dca_machines (operator_user_id)"
- )
- await db.execute(
- "CREATE UNIQUE INDEX IF NOT EXISTS dca_machines_wallet_id_uq "
- "ON dca_machines (wallet_id)"
- )
diff --git a/models.py b/models.py
index d779633..f56cbcd 100644
--- a/models.py
+++ b/models.py
@@ -28,11 +28,7 @@ class CreateMachineData(BaseModel):
not against any fee total. See aiolabs/satmachineadmin#37 / #38.
"""
- # Optional: blank = register the machine UNPAIRED — the bunker mints its
- # identity at pairing (model A1, the normal path). Supplying an npub here
- # is the development self-key path (a machine that holds its own signing
- # key); see views_api.api_create_machine.
- machine_npub: str | None = None
+ machine_npub: str
wallet_id: str
name: str | None = None
location: str | None = None
@@ -52,7 +48,7 @@ class CreateMachineData(BaseModel):
class Machine(BaseModel):
id: str
operator_user_id: str
- machine_npub: str | None # NULL until paired (or supplied on the dev self-key path)
+ machine_npub: str
wallet_id: str
name: str | None
location: str | None
@@ -88,17 +84,11 @@ class UpdateMachineData(BaseModel):
class PairMachineData(BaseModel):
"""Body for POST /machines/{id}/pair (S0 / #9). `relays` are the relays
the spire will use for its own events (kind-21000/30078) — typically the
- operator's nostrrelay. `bunker_relay` overrides the relay embedded in the
- seed's `bunker://` URL (the relay the spire uses to *reach* the bunker);
- when omitted it defaults to `settings.lnbits_nsec_bunker_url`. Set it when
- the relay lnbits uses to reach the bunker differs from the one the spire
- must reach — e.g. an internal docker hostname (`ws://lnbits:5001/…`) vs a
- LAN/public URL (`ws://192.168.0.32:5001/…`), or any split-relay deploy.
- `duration_hours` optionally time-bounds the spire's connect token
- (None = non-expiring)."""
+ operator's nostrrelay; the bunker connection relay is added separately
+ from the lnbits bunker settings. `duration_hours` optionally time-bounds
+ the spire's connect token (None = non-expiring)."""
relays: list[str]
- bunker_relay: str | None = None
duration_hours: int | None = None
@validator("duration_hours")
diff --git a/static/js/index.js b/static/js/index.js
index c9878d0..52d3a3b 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -581,29 +581,8 @@ 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(
@@ -720,17 +699,13 @@ window.app = Vue.createApp({
async submitAddMachine() {
const body = this._cleanMachineForm(this.addMachineDialog.data)
- if (!body.wallet_id) {
+ if (!body.machine_npub || !body.wallet_id) {
Quasar.Notify.create({
type: 'negative',
- message: 'A wallet is required'
+ message: 'machine_npub and wallet_id are 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)
@@ -738,7 +713,7 @@ window.app = Vue.createApp({
this.addMachineDialog.show = false
Quasar.Notify.create({
type: 'positive',
- message: `Machine ${data.name || (data.machine_npub || 'unpaired').slice(0, 12)} added`
+ message: `Machine ${data.name || data.machine_npub.slice(0, 12)} added`
})
} catch (e) {
this._notifyError(e, 'Failed to add machine')
@@ -766,10 +741,6 @@ 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(
@@ -801,7 +772,7 @@ window.app = Vue.createApp({
Quasar.Dialog.create({
title: 'Delete machine?',
message:
- `This removes ${machine.name || (machine.machine_npub || 'unpaired').slice(0, 12)}` +
+ `This removes ${machine.name || machine.machine_npub.slice(0, 12)}` +
' from your fleet. Existing settlements and payment history are preserved' +
' — only the machine row itself is removed. Continue?',
html: true,
@@ -877,7 +848,7 @@ window.app = Vue.createApp({
Quasar.Dialog.create({
title: 'Revoke spire access?',
message:
- `This cuts ${machine.name || (machine.machine_npub || 'unpaired').slice(0, 12)}'s` +
+ `This cuts ${machine.name || machine.machine_npub.slice(0, 12)}'s` +
' signing access at the bunker — the spire can no longer submit' +
' cash-outs until you re-pair it. Continue?',
html: true,
@@ -1544,7 +1515,7 @@ window.app = Vue.createApp({
// Helpers
// -----------------------------------------------------------------
shortNpub(npub) {
- if (!npub) return 'unpaired'
+ if (!npub) return ''
if (npub.length <= 16) return npub
return npub.slice(0, 8) + '…' + npub.slice(-6)
},
@@ -1630,7 +1601,7 @@ window.app = Vue.createApp({
_cleanMachineForm(d) {
return {
- machine_npub: (d.machine_npub || '').trim() || null,
+ machine_npub: (d.machine_npub || '').trim(),
wallet_id: d.wallet_id,
name: (d.name || '').trim() || null,
location: (d.location || '').trim() || null,
diff --git a/templates/spirekeeper/index.html b/templates/spirekeeper/index.html
index 53c30d1..45d47a7 100644
--- a/templates/spirekeeper/index.html
+++ b/templates/spirekeeper/index.html
@@ -792,13 +792,13 @@
@@ -1627,12 +1627,12 @@
diff --git a/views_api.py b/views_api.py
index 1a5bc62..8dff07e 100644
--- a/views_api.py
+++ b/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 or m.id)[:12]}): "
+ f"on machine {m.id} ({m.name or m.machine_npub[: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 or m.id)[:12]}): "
+ f"on machine {m.id} ({m.name or m.machine_npub[:12]}): "
f"+ operator {op_out:.4f} = "
f"{total_out:.4f} > {MAX_FEE_FRACTION_PER_DIRECTION}"
),
@@ -275,13 +275,7 @@ async def api_create_machine(
data: CreateMachineData, user: User = Depends(check_user_exists)
) -> Machine:
await _assert_wallet_owned_by(data.wallet_id, user.id)
- # 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_no_pubkey_collision(data.machine_npub)
await _assert_machine_fee_cap_safe(
data.operator_cash_in_fee_fraction,
data.operator_cash_out_fee_fraction,
@@ -290,10 +284,9 @@ 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.
- 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)
+ super_config = await get_super_config()
+ if super_config is not None:
+ await publish_fee_config(machine, super_config, user.id)
return machine
@@ -324,7 +317,6 @@ 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:
@@ -345,14 +337,6 @@ 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