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
|
|
@ -31,13 +31,13 @@
|
|||
<q-banner
|
||||
v-if="superConfig"
|
||||
class="q-mb-md"
|
||||
:class="superConfig.super_fee_pct > 0 ? 'bg-blue-1 text-grey-9' : 'bg-grey-2 text-grey-9'">
|
||||
:class="superConfig.super_fee_fraction > 0 ? 'bg-blue-1 text-grey-9' : 'bg-grey-2 text-grey-9'">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="account_balance" :color="superConfig.super_fee_pct > 0 ? 'blue' : 'grey'"></q-icon>
|
||||
<q-icon name="account_balance" :color="superConfig.super_fee_fraction > 0 ? 'blue' : 'grey'"></q-icon>
|
||||
</template>
|
||||
<span :style="{fontWeight: 500}">
|
||||
LNbits platform fee:
|
||||
<span :style="{color: '#1976d2'}">${ (superConfig.super_fee_pct * 100).toFixed(2) }%</span>
|
||||
<span :style="{color: '#1976d2'}">${ (superConfig.super_fee_fraction * 100).toFixed(2) }%</span>
|
||||
of each transaction's commission.
|
||||
</span>
|
||||
<span :style="{opacity: 0.7, fontSize: '0.85em'}" class="q-ml-sm">
|
||||
|
|
@ -143,12 +143,6 @@
|
|||
v-text="shortId(props.row.wallet_id)"></code>
|
||||
</q-td>
|
||||
<q-td key="fiat_code" v-text="props.row.fiat_code"></q-td>
|
||||
<q-td key="fallback_commission_pct">
|
||||
<span v-text="(props.row.fallback_commission_pct * 100).toFixed(2) + '%'"></span>
|
||||
<q-tooltip>
|
||||
Used only when bitSpire doesn't supply a per-tx split.
|
||||
</q-tooltip>
|
||||
</q-td>
|
||||
<q-td key="actions" auto-width>
|
||||
<q-btn flat dense round size="sm" icon="visibility"
|
||||
color="primary"
|
||||
|
|
@ -529,13 +523,13 @@
|
|||
dense outlined></q-input>
|
||||
</div>
|
||||
<div class="col-5 col-md-3">
|
||||
<q-input v-model.number="leg.pct"
|
||||
<q-input v-model.number="leg.fraction"
|
||||
label="% (0..1)"
|
||||
type="number" step="0.01" min="0" max="1"
|
||||
dense outlined>
|
||||
<template v-slot:append>
|
||||
<span :style="{fontSize: '0.75em', opacity: 0.6}"
|
||||
v-text="((leg.pct || 0) * 100).toFixed(1) + '%'"></span>
|
||||
v-text="((leg.fraction || 0) * 100).toFixed(1) + '%'"></span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
|
@ -630,8 +624,8 @@
|
|||
<span :style="{fontSize: '0.85em'}"
|
||||
v-text="formatTime(props.row.created_at)"></span>
|
||||
</q-td>
|
||||
<q-td key="gross_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.gross_sats)"></span>
|
||||
<q-td key="wire_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.wire_sats)"></span>
|
||||
</q-td>
|
||||
<q-td key="error_message">
|
||||
<span :style="{fontSize: '0.85em', opacity: 0.8}"
|
||||
|
|
@ -792,14 +786,6 @@
|
|||
dense outlined
|
||||
:rules="[v => !!v || 'Pick a wallet']"></q-select>
|
||||
|
||||
<q-input
|
||||
v-model.number="addMachineDialog.data.fallback_commission_pct"
|
||||
label="Fallback commission % (decimal: 0.05 = 5%)"
|
||||
hint="Only used if bitSpire doesn't supply a per-tx split (lamassu-next#44)."
|
||||
type="number" step="0.0001" min="0" max="1"
|
||||
class="q-mb-md"
|
||||
dense outlined></q-input>
|
||||
|
||||
<q-input
|
||||
v-model="addMachineDialog.data.fiat_code"
|
||||
label="Fiat code"
|
||||
|
|
@ -855,12 +841,6 @@
|
|||
<div class="text-caption" :style="{opacity: 0.6}">Location</div>
|
||||
<span v-text="machineDetail.machine.location || '—'"></span>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="text-caption" :style="{opacity: 0.6}">
|
||||
Fallback commission %
|
||||
</div>
|
||||
<span v-text="(machineDetail.machine.fallback_commission_pct * 100).toFixed(2) + '%'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mb-md"></q-separator>
|
||||
|
|
@ -893,27 +873,19 @@
|
|||
<q-td key="status">
|
||||
<q-badge :color="settlementStatusColor(props.row.status)"
|
||||
:label="props.row.status"></q-badge>
|
||||
<q-icon v-if="props.row.used_fallback_split"
|
||||
name="warning_amber" color="orange" size="sm"
|
||||
class="q-ml-xs">
|
||||
<q-tooltip>
|
||||
Fallback split — bitSpire didn't supply per-tx
|
||||
net/fee. See lamassu-next#44.
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td key="created_at">
|
||||
<span :style="{fontSize: '0.85em'}"
|
||||
v-text="formatTime(props.row.created_at)"></span>
|
||||
</q-td>
|
||||
<q-td key="gross_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.gross_sats)"></span>
|
||||
<q-td key="wire_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.wire_sats)"></span>
|
||||
</q-td>
|
||||
<q-td key="principal_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.principal_sats)"></span>
|
||||
</q-td>
|
||||
<q-td key="commission_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.commission_sats)"></span>
|
||||
<q-td key="fee_sats" class="text-right">
|
||||
<span v-text="formatSats(props.row.fee_sats)"></span>
|
||||
<div :style="{fontSize: '0.75em', opacity: 0.6}">
|
||||
super
|
||||
<span v-text="formatSats(props.row.platform_fee_sats)"></span>
|
||||
|
|
@ -996,7 +968,7 @@
|
|||
<q-icon name="info" color="blue"></q-icon>
|
||||
</template>
|
||||
Original gross:
|
||||
<b v-text="formatSats(partialDispenseDialog.settlement.gross_sats)"></b>.
|
||||
<b v-text="formatSats(partialDispenseDialog.settlement.wire_sats)"></b>.
|
||||
Provide what was actually dispensed. Sat amounts will scale linearly,
|
||||
the commission split will recompute, and distribution will re-run.
|
||||
</q-banner>
|
||||
|
|
@ -1019,7 +991,7 @@
|
|||
label="Dispensed sats"
|
||||
hint="Exact sat amount actually dispensed (≤ original gross)"
|
||||
type="number" step="1" min="0"
|
||||
:max="partialDispenseDialog.settlement.gross_sats"
|
||||
:max="partialDispenseDialog.settlement.wire_sats"
|
||||
dense outlined></q-input>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
|
@ -1085,7 +1057,7 @@
|
|||
Operators see this as a read-only banner. Wallet ID is where the
|
||||
collected fee lands; typically a wallet you (the super) own.
|
||||
</p>
|
||||
<q-input v-model.number="superFeeDialog.data.super_fee_pct"
|
||||
<q-input v-model.number="superFeeDialog.data.super_fee_fraction"
|
||||
label="Fee % (decimal, 0..1)"
|
||||
hint="0.30 = 30% of every operator's commission"
|
||||
type="number" step="0.0001" min="0" max="1"
|
||||
|
|
@ -1337,10 +1309,6 @@
|
|||
emit-value map-options
|
||||
class="q-mb-md"
|
||||
dense outlined></q-select>
|
||||
<q-input v-model.number="editMachineDialog.data.fallback_commission_pct"
|
||||
label="Fallback commission %"
|
||||
type="number" step="0.0001" min="0" max="1"
|
||||
class="q-mb-md" dense outlined></q-input>
|
||||
<q-input v-model="editMachineDialog.data.fiat_code"
|
||||
label="Fiat code" class="q-mb-md" dense outlined></q-input>
|
||||
<q-toggle v-model="editMachineDialog.data.is_active"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue