feat(v2): principal-based fee split — fixes super under-payment (#38 3/5)
Replaces the broken fraction-of-fee math with fraction-of-principal, direction-aware. Pre-#38: super_fee_fraction was interpreted as `round(fee_sats * super_fraction)`, paying super ~13× below intent on every cashout since the bitspire wire-shape landed. Post-#38: super and operator shares are computed independently against principal using the per-direction fractions from SuperConfig + Machine. Per workspace CLAUDE.md "Backwards-compatibility on pre-public-launch code" (v2-bitspire hasn't shipped to users), no compat shims: - calculations.py: delete `split_two_stage_commission` (legacy fraction-of-fee). Keep `split_principal_based` as the sole split fn. - migrations.py m009: extend to also DROP the deprecated `super_fee_fraction` column after backfilling its value into the new directional fields. - models.py: drop `super_fee_fraction` from SuperConfig + UpdateSuperConfigData entirely. - bitspire.py parse_settlement: new signature takes `super_config: SuperConfig` instead of `super_fee_fraction: float`. Resolves directional fractions from super_config + machine by tx_type, then computes via split_principal_based. Raises SettlementInvariantError on unknown tx_type. - tasks.py: pass `super_config` through to parse_settlement; assert non-None (m001 inserts the singleton at install time — None is an impossible state). - partial-dispense ratio path in distribution.py is unchanged — still uses `settlement.platform_fee_sats / settlement.fee_sats` from the landed row, which is the right invariant (lock at landing) and independent of the per-direction config. Tests: - Rename `test_two_stage_split.py` → `test_operator_split_legs.py`. Drop the legacy-function test classes. Keep TestAllocateOperatorSplitLegs (still-production fn) and TestPartialDispenseSplitRatio (inline ratio math in distribution.py). - New `test_principal_based_fees.py`: pure-math tests for `split_principal_based` (six cases including a direct regression test pinning the pre-#38 bug at 240→3000 sats per 100k principal at 3% super), plus parse_settlement directional dispatch tests (cash-in routes through cash-in fractions; cash-out through cash-out; unknown tx_type raises; zero-zero free-charge ATM; cross- direction guard). Migration verified end-to-end via container restart: super_config columns post-m009 = id/super_fee_wallet_id/updated_at/ super_cash_in_fee_fraction/super_cash_out_fee_fraction (no super_fee_fraction). dca_machines + dca_settlements gained the expected new columns. 156/156 tests green. Refs: aiolabs/satmachineadmin#37 (parent), #38 (this layer). Closes the load-bearing super under-payment bug standalone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
4cd0041923
commit
1babdfbf06
8 changed files with 522 additions and 275 deletions
|
|
@ -682,9 +682,11 @@ async def m009_split_fee_fractions_by_direction(db):
|
|||
lnbits advisory; option A locked).
|
||||
|
||||
Idempotency via column-probe pattern (same shape as m006's rename
|
||||
sweep). The existing `super_config.super_fee_fraction` column is
|
||||
NOT dropped here — deprecated, removed in a follow-up release after
|
||||
callers migrate to the directional fields.
|
||||
sweep). The deprecated `super_config.super_fee_fraction` singleton
|
||||
is backfilled into the new directional fields, then dropped in the
|
||||
same migration — strict-from-the-start per workspace CLAUDE.md
|
||||
"Backwards-compatibility on pre-public-launch code" (v2-bitspire
|
||||
hasn't shipped to public users).
|
||||
"""
|
||||
additions = [
|
||||
("super_config", "super_cash_in_fee_fraction", "DECIMAL(10,4) NOT NULL DEFAULT 0.0000"),
|
||||
|
|
@ -704,17 +706,32 @@ async def m009_split_fee_fractions_by_direction(db):
|
|||
f"ALTER TABLE satoshimachine.{table} ADD COLUMN {col} {coltype}"
|
||||
)
|
||||
|
||||
# Backfill super-config directional fractions from the legacy singleton
|
||||
# so the live deployment's super_fee_fraction setting carries forward.
|
||||
# Guarded WHERE clause: only fire when both new fields are still at
|
||||
# their DEFAULT 0 (i.e., this is a first migrate-up, not a repeat).
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE satoshimachine.super_config
|
||||
SET super_cash_in_fee_fraction = super_fee_fraction,
|
||||
super_cash_out_fee_fraction = super_fee_fraction
|
||||
WHERE super_cash_in_fee_fraction = 0
|
||||
AND super_cash_out_fee_fraction = 0
|
||||
AND super_fee_fraction > 0
|
||||
"""
|
||||
)
|
||||
# Backfill + drop the legacy singleton, gated on the column still
|
||||
# existing. Once dropped, a re-run of this migration skips both
|
||||
# steps cleanly.
|
||||
try:
|
||||
await db.fetchone(
|
||||
"SELECT super_fee_fraction FROM satoshimachine.super_config LIMIT 1"
|
||||
)
|
||||
legacy_present = True
|
||||
except Exception:
|
||||
legacy_present = False
|
||||
|
||||
if legacy_present:
|
||||
# Carry the live deployment's super_fee_fraction setting forward
|
||||
# into both directional fields, but only when the operator hasn't
|
||||
# already explicitly set per-direction values (i.e., both are
|
||||
# still at DEFAULT 0).
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE satoshimachine.super_config
|
||||
SET super_cash_in_fee_fraction = super_fee_fraction,
|
||||
super_cash_out_fee_fraction = super_fee_fraction
|
||||
WHERE super_cash_in_fee_fraction = 0
|
||||
AND super_cash_out_fee_fraction = 0
|
||||
AND super_fee_fraction > 0
|
||||
"""
|
||||
)
|
||||
await db.execute(
|
||||
"ALTER TABLE satoshimachine.super_config DROP COLUMN super_fee_fraction"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue