refactor(v2): canonical sat-amount vocabulary + delete Lamassu-era reverse-derivation
Cross-codebase decision logged at memory `reference_sat_amount_vocabulary.md`
and at `~/dev/coordination/log.md` (2026-05-26). Canonical names with
explicit units across satmachineadmin, lamassu-next, atm-tui:
- `wire_sats` — actual Lightning payment amount (direction-agnostic;
was `gross_sats`, only "gross" for cash-out)
- `principal_sats` — market-rate sats before commission (unchanged)
- `fee_sats` — commission (was `commission_sats` internally;
already the wire format)
- `fee_fraction` — commission rate as unit fraction in [0, 1]
(was `*_pct` / `fee_percent`; eliminates the
latent 100x bug from `feePercent * 100` on the
lamassu-next side)
Invariants enforced in bitspire._assert_sat_invariants on every
parsed settlement — range (all sats >= 0, 0 <= fee_fraction <= 1) +
direction-specific sum:
- cash-out: wire_sats == principal_sats + fee_sats
- cash-in: wire_sats == principal_sats - fee_sats
AND fee_sats <= principal_sats
Breaches raise SettlementInvariantError; tasks._handle_payment
records the row as `status='rejected'` with the exception message
and skips distribution. Attribution failure path symmetric.
Schema changes (m001 + m006):
- dca_settlements.gross_sats -> wire_sats
- dca_settlements.commission_sats -> fee_sats
- super_config.super_fee_pct -> super_fee_fraction
- dca_commission_splits.pct -> fraction
- dca_machines.fallback_commission_pct DROPPED (obsolete)
- dca_settlements.used_fallback_split DROPPED (obsolete)
m006 idempotently renames + drops columns on existing installs;
m001 lays down the canonical schema for fresh installs.
Obsolete code removed (Lamassu-era reverse-derivation):
- calculations.calculate_commission — back-derived principal+fee
from gross-with-commission-baked-in. v2 stamps both directly.
- calculations.calculate_exchange_rate — bitSpire stamps directly.
- bitspire._parse_fallback — sole caller of calculate_commission.
- Machine.fallback_commission_fraction — only read by _parse_fallback.
- DcaSettlement.used_fallback_split — only written by _parse_fallback.
parse_settlement now raises SettlementMetadataError if Payment.extra
lacks the bitSpire stamp or required absolute sat fields. No silent
back-derivation; upstream-bug surfacing via dashboard rejection.
Frontend (JS + Quasar templates) updated for the column renames and
the removed fallback fields. Settlements table renders "Wire" + "Fee"
columns; the "(fallback split)" warning badge is gone.
Tests:
- test_calculations.py: kept distribution tests; deleted
calculate_commission + calculate_exchange_rate tests.
- test_two_stage_split.py: renamed variables; rewrote docstring
value literals (e.g. `super_fee_fraction=0.30` not `=30%`).
- test_nostr_attribution.py: dropped fallback_commission_fraction
from machine fixture.
- 72/72 pass on regtest container.
Cross-codebase follow-ups tracked in coordination log:
- lamassu-next: rename `fee_percent` -> `fee_fraction` on
Payment.extra + state.db; drop the `* 100` at lightning.ts:780.
- atm-tui: read `fee_fraction` column in db.zig.
Memory artefacts:
- reference_sat_amount_vocabulary.md (canonical + invariants)
- feedback_pct_to_fraction_renames_need_value_sweep.md (gotcha)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6348c55e37
commit
d717a6e214
12 changed files with 530 additions and 681 deletions
|
|
@ -62,7 +62,7 @@ async def m001_satmachine_v2_initial(db):
|
|||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS satoshimachine.super_config (
|
||||
id TEXT PRIMARY KEY,
|
||||
super_fee_pct DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
|
||||
super_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
|
||||
super_fee_wallet_id TEXT,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
|
|
@ -72,7 +72,7 @@ async def m001_satmachine_v2_initial(db):
|
|||
)
|
||||
if not existing:
|
||||
await db.execute(
|
||||
"INSERT INTO satoshimachine.super_config (id, super_fee_pct) "
|
||||
"INSERT INTO satoshimachine.super_config (id, super_fee_fraction) "
|
||||
"VALUES ('default', 0.0000)"
|
||||
)
|
||||
|
||||
|
|
@ -88,7 +88,6 @@ async def m001_satmachine_v2_initial(db):
|
|||
location TEXT,
|
||||
fiat_code TEXT NOT NULL DEFAULT 'GTQ',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
fallback_commission_pct DECIMAL(10,4) NOT NULL DEFAULT 0.0500,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
|
|
@ -180,10 +179,10 @@ async def m001_satmachine_v2_initial(db):
|
|||
# append-only audit memo for partial-dispense + operator notes.
|
||||
#
|
||||
# platform_fee_sats and operator_fee_sats are absolute BIGINT,
|
||||
# NOT derived percentages — when the v2 customer-discount engine
|
||||
# NOT derived fractions — when the v2 customer-discount engine
|
||||
# ships, these two columns are the audit-grade record of who
|
||||
# forgave what per transaction. Do not collapse them into a single
|
||||
# commission_pct. See plan section "Customer discounts" and #10.
|
||||
# fee_fraction. See plan section "Customer discounts" and #10.
|
||||
await db.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS satoshimachine.dca_settlements (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
|
@ -191,15 +190,14 @@ async def m001_satmachine_v2_initial(db):
|
|||
payment_hash TEXT NOT NULL UNIQUE,
|
||||
bitspire_event_id TEXT,
|
||||
bitspire_txid TEXT,
|
||||
gross_sats BIGINT NOT NULL,
|
||||
wire_sats BIGINT NOT NULL,
|
||||
fiat_amount DECIMAL(10,2) NOT NULL,
|
||||
fiat_code TEXT NOT NULL DEFAULT 'GTQ',
|
||||
exchange_rate REAL NOT NULL,
|
||||
principal_sats BIGINT NOT NULL,
|
||||
commission_sats BIGINT NOT NULL,
|
||||
fee_sats BIGINT NOT NULL,
|
||||
platform_fee_sats BIGINT NOT NULL,
|
||||
operator_fee_sats BIGINT NOT NULL,
|
||||
used_fallback_split BOOLEAN NOT NULL DEFAULT false,
|
||||
tx_type TEXT NOT NULL,
|
||||
bills_json TEXT,
|
||||
cassettes_json TEXT,
|
||||
|
|
@ -217,9 +215,9 @@ async def m001_satmachine_v2_initial(db):
|
|||
)
|
||||
|
||||
# 7. dca_commission_splits — operator's rules for distributing the
|
||||
# *remainder* (commission_sats - platform_fee_sats). One row per
|
||||
# *remainder* (fee_sats - platform_fee_sats). One row per
|
||||
# leg. machine_id=NULL = operator default; non-null = per-machine
|
||||
# override. Sum(pct) per (operator, machine) must equal 1.0 —
|
||||
# override. Sum(fraction) per (operator, machine) must equal 1.0 —
|
||||
# enforced at write-time in crud.py.
|
||||
#
|
||||
# `target` accepts any of (splitpayments-style):
|
||||
|
|
@ -235,7 +233,7 @@ async def m001_satmachine_v2_initial(db):
|
|||
operator_user_id TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
label TEXT,
|
||||
pct DECIMAL(10,4) NOT NULL,
|
||||
fraction DECIMAL(10,4) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
|
|
@ -439,6 +437,76 @@ async def m004_introduce_dca_lp_table(db):
|
|||
await db.execute(f"ALTER TABLE satoshimachine.dca_clients DROP COLUMN {col}")
|
||||
|
||||
|
||||
async def m006_rename_to_canonical_sat_vocabulary(db):
|
||||
"""Adopt the cross-codebase canonical sat-amount vocabulary AND drop
|
||||
the now-obsolete Lamassu-era fallback columns, per the decision at
|
||||
memory `reference_sat_amount_vocabulary.md` (2026-05-26):
|
||||
|
||||
Renames:
|
||||
- dca_settlements.gross_sats → wire_sats
|
||||
- dca_settlements.commission_sats → fee_sats
|
||||
- super_config.super_fee_pct → super_fee_fraction
|
||||
- dca_commission_splits.pct → fraction
|
||||
|
||||
Drops (Lamassu-era reverse-derivation is obsolete since bitSpire
|
||||
stamps both `principal_sats` AND `fee_sats` directly on
|
||||
Payment.extra per lamassu-next#44 — there's nothing to back-derive):
|
||||
- dca_machines.fallback_commission_pct (was the rate used by the
|
||||
deleted `_parse_fallback` path)
|
||||
- dca_settlements.used_fallback_split (was the per-row marker for
|
||||
that path)
|
||||
|
||||
Same canonical applies on the lamassu-next + atm-tui side; the
|
||||
rename is coordinated via `~/dev/coordination/log.md` (2026-05-26).
|
||||
|
||||
Each step is idempotent — probe for the OLD column; rename/drop only
|
||||
if present; otherwise no-op (covers fresh installs where m001
|
||||
already laid down the canonical schema).
|
||||
|
||||
Why a single migration: all driven by the same decision and any
|
||||
external code wants to see the whole rename + cleanup land at once.
|
||||
"""
|
||||
renames = [
|
||||
("dca_settlements", "gross_sats", "wire_sats"),
|
||||
("dca_settlements", "commission_sats", "fee_sats"),
|
||||
("super_config", "super_fee_pct", "super_fee_fraction"),
|
||||
("dca_commission_splits", "pct", "fraction"),
|
||||
]
|
||||
for table, old_col, new_col in renames:
|
||||
try:
|
||||
await db.fetchone(
|
||||
f"SELECT {old_col} FROM satoshimachine.{table} LIMIT 1"
|
||||
)
|
||||
except Exception:
|
||||
# old column doesn't exist; either rename already landed or
|
||||
# m001 produced the canonical schema directly on fresh install.
|
||||
continue
|
||||
await db.execute(
|
||||
f"ALTER TABLE satoshimachine.{table} "
|
||||
f"RENAME COLUMN {old_col} TO {new_col}"
|
||||
)
|
||||
|
||||
# Drop the Lamassu-era fallback columns. Same idempotency pattern.
|
||||
# Try both old (_pct) and new (_fraction) names for the dca_machines
|
||||
# column since an install could be at either rename state.
|
||||
drops = [
|
||||
("dca_machines", "fallback_commission_pct"),
|
||||
("dca_machines", "fallback_commission_fraction"),
|
||||
("dca_settlements", "used_fallback_split"),
|
||||
]
|
||||
for table, col in drops:
|
||||
try:
|
||||
await db.fetchone(
|
||||
f"SELECT {col} FROM satoshimachine.{table} LIMIT 1"
|
||||
)
|
||||
except Exception:
|
||||
# column doesn't exist; either already dropped or never present.
|
||||
continue
|
||||
await db.execute(
|
||||
f"ALTER TABLE satoshimachine.{table} DROP COLUMN {col}"
|
||||
)
|
||||
|
||||
|
||||
async def m005_lock_deposit_currency_to_machine_fiat_code(db):
|
||||
"""Rewrite every `dca_deposits.currency` row to match its joined
|
||||
`dca_machines.fiat_code`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue