fix(lightning): cash-in double-charged commission (send gross, not net)
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>
This commit is contained in:
parent
75e1187e67
commit
8fbe6df5c3
1 changed files with 12 additions and 2 deletions
|
|
@ -725,7 +725,17 @@ function createATMServices(
|
|||
* over nostr, we trigger dispense.
|
||||
*/
|
||||
generateLnurlWithdraw: async (context: ATMContext): Promise<string> => {
|
||||
console.log('[ATM Service] Generating LNURL-withdraw for', context.satsAmount, 'sats')
|
||||
// GROSS principal (fiat × rate, BEFORE commission). The server derives
|
||||
// fee + NET from this, so we must NOT send the already-fee'd
|
||||
// context.satsAmount — doing so double-applies the commission (client
|
||||
// subtracts it in calculateSats, then the server subtracts it again,
|
||||
// e.g. 12% → 22.6% effective; the customer is short-changed while the
|
||||
// quote/receipt still read 12%). Mirror calculateSats's principal.
|
||||
const grossPrincipalSats = Math.floor((context.fiatCents / 100) * context.exchangeRate)
|
||||
console.log(
|
||||
`[ATM Service] Generating LNURL-withdraw: gross principal=${grossPrincipalSats} sats ` +
|
||||
`(net after ${(context.feeFraction * 100).toFixed(2)}% ≈ ${context.satsAmount})`
|
||||
)
|
||||
|
||||
try {
|
||||
if (context.cashInSessionId) {
|
||||
|
|
@ -738,7 +748,7 @@ function createATMServices(
|
|||
// the amount or extra. We display the returned LNURL (for NET) and
|
||||
// watch link_id for settlement.
|
||||
const link = await lnbits.createWithdraw(lnbitsWalletId, {
|
||||
principal_sats: context.satsAmount,
|
||||
principal_sats: grossPrincipalSats,
|
||||
fiat_amount: context.fiatCents / 100,
|
||||
fiat_code: context.currency,
|
||||
title: `bitSpire Cash-In ${context.cashInSessionId?.slice(0, 8) || 'session'}`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue