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
|
|
@ -82,7 +82,7 @@ window.app = Vue.createApp({
|
|||
columns: [
|
||||
{name: 'machine', label: 'Machine', field: 'machine_id', align: 'left'},
|
||||
{name: 'created_at', label: 'Created', field: 'created_at', align: 'left'},
|
||||
{name: 'gross_sats', label: 'Gross', field: 'gross_sats', align: 'right'},
|
||||
{name: 'wire_sats', label: 'Wire', field: 'wire_sats', align: 'right'},
|
||||
{
|
||||
name: 'error_message',
|
||||
label: 'Error',
|
||||
|
|
@ -100,7 +100,7 @@ window.app = Vue.createApp({
|
|||
superFeeDialog: {
|
||||
show: false,
|
||||
saving: false,
|
||||
data: {super_fee_pct: 0, super_fee_wallet_id: ''}
|
||||
data: {super_fee_fraction: 0, super_fee_wallet_id: ''}
|
||||
},
|
||||
|
||||
// UI configuration -----------------------------------------------
|
||||
|
|
@ -111,12 +111,6 @@ window.app = Vue.createApp({
|
|||
{name: 'machine_npub', label: 'npub', field: 'machine_npub', align: 'left'},
|
||||
{name: 'wallet_id', label: 'Wallet', field: 'wallet_id', align: 'left'},
|
||||
{name: 'fiat_code', label: 'Fiat', field: 'fiat_code', align: 'left'},
|
||||
{
|
||||
name: 'fallback_commission_pct',
|
||||
label: 'Fallback %',
|
||||
field: 'fallback_commission_pct',
|
||||
align: 'right'
|
||||
},
|
||||
{name: 'actions', label: 'Actions', field: 'id', align: 'right'}
|
||||
],
|
||||
pagination: {rowsPerPage: 10, sortBy: 'name'}
|
||||
|
|
@ -166,12 +160,12 @@ window.app = Vue.createApp({
|
|||
columns: [
|
||||
{name: 'status', label: 'Status', field: 'status', align: 'left'},
|
||||
{name: 'created_at', label: 'Time', field: 'created_at', align: 'left'},
|
||||
{name: 'gross_sats', label: 'Gross', field: 'gross_sats', align: 'right'},
|
||||
{name: 'wire_sats', label: 'Wire', field: 'wire_sats', align: 'right'},
|
||||
{name: 'principal_sats', label: 'Principal (→ LPs)', field: 'principal_sats', align: 'right'},
|
||||
{
|
||||
name: 'commission_sats',
|
||||
label: 'Commission',
|
||||
field: 'commission_sats',
|
||||
name: 'fee_sats',
|
||||
label: 'Fee',
|
||||
field: 'fee_sats',
|
||||
align: 'right'
|
||||
},
|
||||
{name: 'fiat_amount', label: 'Fiat', field: 'fiat_amount', align: 'right'},
|
||||
|
|
@ -332,7 +326,7 @@ window.app = Vue.createApp({
|
|||
},
|
||||
commissionSum() {
|
||||
return this.commissionLegs.reduce(
|
||||
(acc, leg) => acc + (Number(leg.pct) || 0), 0
|
||||
(acc, leg) => acc + (Number(leg.fraction) || 0), 0
|
||||
)
|
||||
},
|
||||
commissionSumValid() {
|
||||
|
|
@ -351,7 +345,7 @@ window.app = Vue.createApp({
|
|||
if (idx === this.commissionLegs.length - 1) {
|
||||
sats = remaining
|
||||
} else {
|
||||
sats = Math.round(total * (Number(leg.pct) || 0))
|
||||
sats = Math.round(total * (Number(leg.fraction) || 0))
|
||||
remaining -= sats
|
||||
}
|
||||
out.push({label: leg.label, sats})
|
||||
|
|
@ -531,7 +525,7 @@ window.app = Vue.createApp({
|
|||
// -----------------------------------------------------------------
|
||||
openSuperFeeDialog() {
|
||||
this.superFeeDialog.data = {
|
||||
super_fee_pct: this.superConfig?.super_fee_pct ?? 0,
|
||||
super_fee_fraction: this.superConfig?.super_fee_fraction ?? 0,
|
||||
super_fee_wallet_id: this.superConfig?.super_fee_wallet_id || ''
|
||||
}
|
||||
this.superFeeDialog.show = true
|
||||
|
|
@ -544,7 +538,7 @@ window.app = Vue.createApp({
|
|||
const {data} = await LNbits.api.request(
|
||||
'PUT', SUPER_FEE_PATH, null,
|
||||
{
|
||||
super_fee_pct: Number(d.super_fee_pct),
|
||||
super_fee_fraction: Number(d.super_fee_fraction),
|
||||
super_fee_wallet_id: (d.super_fee_wallet_id || '').trim() || null
|
||||
}
|
||||
)
|
||||
|
|
@ -565,7 +559,7 @@ window.app = Vue.createApp({
|
|||
this._downloadCsv(
|
||||
'machines.csv',
|
||||
['id', 'machine_npub', 'wallet_id', 'name', 'location', 'fiat_code',
|
||||
'is_active', 'fallback_commission_pct', 'created_at'],
|
||||
'is_active', 'created_at'],
|
||||
this.machines
|
||||
)
|
||||
},
|
||||
|
|
@ -687,7 +681,6 @@ window.app = Vue.createApp({
|
|||
location: machine.location || '',
|
||||
wallet_id: machine.wallet_id,
|
||||
fiat_code: machine.fiat_code,
|
||||
fallback_commission_pct: machine.fallback_commission_pct,
|
||||
is_active: machine.is_active
|
||||
}
|
||||
this.editMachineDialog.show = true
|
||||
|
|
@ -706,7 +699,6 @@ window.app = Vue.createApp({
|
|||
location: d.location,
|
||||
wallet_id: d.wallet_id,
|
||||
fiat_code: d.fiat_code,
|
||||
fallback_commission_pct: d.fallback_commission_pct,
|
||||
is_active: d.is_active
|
||||
}
|
||||
)
|
||||
|
|
@ -1118,7 +1110,7 @@ window.app = Vue.createApp({
|
|||
target: leg.target || '',
|
||||
targetKind: this._inferTargetKind(leg.target),
|
||||
label: leg.label || '',
|
||||
pct: Number(leg.pct) || 0
|
||||
fraction: Number(leg.fraction) || 0
|
||||
}))
|
||||
} catch (e) {
|
||||
this.commissionLegs = []
|
||||
|
|
@ -1140,7 +1132,7 @@ window.app = Vue.createApp({
|
|||
target: this.walletOptions[0]?.value || '',
|
||||
targetKind: 'wallet',
|
||||
label: '',
|
||||
pct: 0
|
||||
fraction: 0
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -1157,7 +1149,7 @@ window.app = Vue.createApp({
|
|||
legs: this.commissionLegs.map((leg, idx) => ({
|
||||
target: (leg.target || '').toString().trim(),
|
||||
label: leg.label || null,
|
||||
pct: Number(leg.pct),
|
||||
fraction: Number(leg.fraction),
|
||||
sort_order: idx
|
||||
}))
|
||||
}
|
||||
|
|
@ -1330,8 +1322,7 @@ window.app = Vue.createApp({
|
|||
wallet_id: null,
|
||||
name: '',
|
||||
location: '',
|
||||
fiat_code: 'GTQ',
|
||||
fallback_commission_pct: 0.05
|
||||
fiat_code: 'GTQ'
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1341,8 +1332,7 @@ window.app = Vue.createApp({
|
|||
wallet_id: d.wallet_id,
|
||||
name: (d.name || '').trim() || null,
|
||||
location: (d.location || '').trim() || null,
|
||||
fiat_code: (d.fiat_code || 'GTQ').trim(),
|
||||
fallback_commission_pct: Number(d.fallback_commission_pct ?? 0.05)
|
||||
fiat_code: (d.fiat_code || 'GTQ').trim()
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue