fix(lightning): cash-in commission charged twice (send gross principal, not net) #81
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/cashin-double-fee"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Buy Bitcoin short-changes the customer: on the batm3, a $5 buy at 1564 sats/USD with a 12% commission paid out 6,056 sats instead of 6,882 — an effective ~22.6%. The commission is applied twice.
Root cause
state-machinecalculateSatsalready subtracts the fee:7820 gross → 938 fee → 6882 net, stored incontext.satsAmount. Butservices/lightning.tsgenerateLnurlWithdrawthen passed that already-net value asprincipal_satsto the server'screate_withdraw, which itself derives fee + net from the principal — so it subtracted 12% a second time. Straight from the machine's log:This directly contradicts the function's own documented contract:
The quote, the recorded transaction (
sats=6882, fee_sats=938, fee_fraction=0.12), and the on-screen commission (12%) all read a single fee — only the delivered LNURL-withdraw amount was double-charged, so it's invisible on the receipt.The exchange rate was never wrong (1564 sats/USD ≈ $63,934/BTC, matches market).
Fix
Send the gross principal (
fiat × rate, before commission) tocreate_withdraw, so the server applies the fee exactly once:One-line change in
generateLnurlWithdraw(context.satsAmount→ gross principal), plus a clarifying log.Verification
vue-tsc --noEmitclean.floor(5×1564)=7820,floor(7820×0.12)=938, net6882.create_withdraw: principal=7820 … net=6882and the wallet receives 6882.Follow-up (not this PR)
Consider making the server's
net_satsthe single source of truth for the displayed/recorded amount, so a divergence between the ATM's fee config and the server's can't silently mis-quote (today they agree at 12%, so quote == delivered after this fix).🤖 Generated with Claude Code
Buy Bitcoin short-changed the customer: a $5 buy at 1564 sats/USD with a 12% commission paid out 6056 sats instead of 6882 — an effective ~22.6%. The commission was applied twice. `calculateSats` already subtracts the fee (7820 gross → 6882 net) into `context.satsAmount`. But `generateLnurlWithdraw` then passed that already-net value as `principal_sats` to the server's create_withdraw, which derives fee + net from the principal and subtracted 12% AGAIN: [ATM Service] create_withdraw: principal=6882 fee=826 net=6056 This contradicted the function's own contract ("the ATM sends only the hardware-attested gross principal; the operator side derives fee + NET"). The quote, the recorded transaction (sats=6882, fee_fraction=0.12), and the on-screen commission (12%) all read a single fee — only the delivered LNURL-withdraw amount was double-charged. Fix: send the GROSS principal (fiat × rate, before commission), so the server applies the fee exactly once. Now 7820 → server 12% → net 6882, matching the quote/receipt. Exchange rate itself was always correct. Verified: vue-tsc typechecks; math checks (gross=7820 fee=938 net=6882). Hardware retest (one $5 buy → 6882) recommended before relying in prod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>