Millisatoshi audit: 1000x display error in market checkout, plus two conversion/unit defects #172
Labels
No labels
app:activities
app:chat
app:chatelet
app:events
app:forum
app:libra
app:market
app:restaurant
app:tasks
app:wallet
app:webapp
bug
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiolabs/webapp#172
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Findings from a satoshi/millisatoshi audit across the tree, checked against the LNbits source and verified against a live instance. Each item below was reproduced, not inferred.
1. Market checkout shows a 1000x inflated amount
src/modules/market/components/PaymentDisplay.vue:17renders:invoiceisorder.lightningInvoice, stored raw from the LNbits create-invoice response atsrc/modules/market/stores/market.ts:520, and thatamountis millisats.currencydefaults to'sat'at line 206.Reproduced live:
The wallet module divides the same field by 1000 (
WalletService.ts:230). This one does not.The bolt11 and QR are correct, so the customer still pays the right amount. The number next to them is simply wrong by 1000x, which on a payment screen is alarming. Fix is
Math.floor(invoice.amount / 1000), or better, reuse one of the existing msat formatters.2.
useExpenseDraftsnever gets a BTC pricesrc/accounting-app/composables/useExpenseDrafts.ts:137reads:from
POST /api/v1/conversion. That endpoint keys its response by currency code and never returnsamountorresult, so the value is always undefined:Same root cause as #165, which fixed this for
usePriceConversion. This site was missed because it calls the API directly instead of going through that composable. Best fix is to route it throughusePriceConversionso there is one extraction strategy.3. Events payment provider silently drops the currency
src/modules/events/services/LnbitsPaymentProvider.ts:28-33sendsamountwith nounitfield, whilePaymentProviderInterface.ts:9-12documents the parameter as "Amount in the specified currency" alongside acurrencyfield that is never forwarded. LNbits defaults to sats, so a fiat-denominated amount would be created as that many satoshis.Latent rather than live: current callers pass sats. Worth either forwarding
currencyasunit(the mechanism #166 uses in the wallet) or narrowing the interface docs to say sats only.4. Lower priority consistency items
src/modules/market/services/paymentMonitor.ts:77,221propagate the msatinvoice.amountintoPaymentUpdate.amount, landing atmarket.ts:571in a store where every other amount is stall-currency sats.lib/utils/formatting.ts:32,lib/utils/currency.ts:67,CurrencyDisplay.vue:40,WalletPage.vue:196), plus two different exported functions both namedformatWalletBalancewith identical signatures. All agree on the unit today; it is an import-site hazard rather than a bug.src/modules/wallet/components/SendDialog.vue:35-36declaresminSendable/maxSendablein msats but never compares them to the satamount. Dead today; would be a 1000x bug the moment LNURL min/max enforcement is added.restaurant/views/SettingsPage.vue:99stores acurrencyDisplay: 'msat'preference that no formatter reads.Checked and correct
Recording these so nobody re-investigates:
GET /api/v1/walletreturnsbalancein millisats, while the WebSocket'swallet_balanceis in satoshis. The LNbits REST handler reads thebalance_msatfield and the WebSocket handler reads thebalanceproperty, which floor-divides by 1000. Our WebSocket path multiplies by 1000 and our polling path does not, which is correct for each. Verified live at a ratio of exactly 1000.amountandfeeon payments are signed millisats everywhere, and all three of our fee reads applyMath.abs. This matters because LNbits signsfeenegative for external sends but positive for internal payments.amount + fee, which would be wrong on internal payments./payments/stats/dailyis sats while/payments/stats/walletsis millisats;/payments/historymixes signed and unsigned in one object).amount_sat,price_fiat,quote_sat) and never using millisats.