From 7d4cb3d49d53a3f0cf6463a548bc56893921af4a Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 23 Sep 2026 09:54:09 +0200 Subject: [PATCH 1/2] fix(core): read the routing fee from `fee`, not the absent `fee_msat` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #170. LNbits payment payloads carry `fee`, signed millisats like `amount`. There is no `fee_msat` field, so three modules read `undefined`: PaymentService.PaymentResult declared `fee_msat: number` — a field the API never sends — so the compiler vouched for a number that was always undefined, and the lie propagated to every consumer. market/useLightningPayment persisted `feeMsat: undefined` onto paid orders (on a property the Order type does not even declare). events/LnbitsPaymentProvider had `data.fee_msat ?? 0`, whose fallback masked the miss so every payment recorded a zero fee. `payInvoice` now normalizes at the boundary instead of returning the raw payload: it maps `fee` to a positive `feeMsat`. Doing it once there means no call site can pick the wrong field name or forget that outgoing amounts are signed. `PaymentResult.feeMsat` replaces the phantom field, so the type finally matches what callers receive. Verified against a live pay-invoice response, plus a synthetic payment carrying a real routing fee (internal LNbits transfers are fee-free, so the live case cannot exercise a non-zero fee): live response fee present: True fee_msat present: False synthetic -1234 msat old (events) -> 0 fee silently lost old (market) -> undefined new (both) -> 1234 positive magnitude Nothing read these values downstream, so no totals were miscomputed; the fee was simply recorded as missing or zero. --- src/core/services/PaymentService.ts | 23 ++++++++++++++++--- .../events/services/LnbitsPaymentProvider.ts | 4 +++- .../market/composables/useLightningPayment.ts | 4 ++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/core/services/PaymentService.ts b/src/core/services/PaymentService.ts index 0021e46..d6935c5 100644 --- a/src/core/services/PaymentService.ts +++ b/src/core/services/PaymentService.ts @@ -5,7 +5,15 @@ import { toast } from 'vue-sonner' export interface PaymentResult { payment_hash: string - fee_msat: number + /** + * Routing fee in millisats, always positive. + * + * LNbits returns this as `fee`, not `fee_msat`, and signs it negative for + * outgoing payments (like `amount`). This interface previously declared + * `fee_msat: number`, a field the API never sends, so every consumer read + * `undefined` while the compiler vouched for a number. + */ + feeMsat: number preimage: string } @@ -230,7 +238,16 @@ export class PaymentService extends BaseService { throw new Error(errorMessage) } - return await response.json() + // Normalize at the boundary rather than handing the raw LNbits payload + // to callers: `fee` is signed millisats, and consumers all want a + // positive magnitude. Doing it here means no call site can pick the + // wrong field name or forget the sign. + const raw = await response.json() + return { + payment_hash: raw.payment_hash, + feeMsat: Math.abs(Number(raw.fee ?? 0)), + preimage: raw.preimage ?? '' + } } /** @@ -266,7 +283,7 @@ export class PaymentService extends BaseService { this.debug('Payment successful', { paymentHash: paymentResult.payment_hash, - feeMsat: paymentResult.fee_msat + feeMsat: paymentResult.feeMsat }) // Show success notification if enabled diff --git a/src/modules/events/services/LnbitsPaymentProvider.ts b/src/modules/events/services/LnbitsPaymentProvider.ts index 140bad4..76fc968 100644 --- a/src/modules/events/services/LnbitsPaymentProvider.ts +++ b/src/modules/events/services/LnbitsPaymentProvider.ts @@ -84,7 +84,9 @@ export class LnbitsPaymentProvider implements PaymentProvider { const data = await response.json() return { paymentHash: data.payment_hash, - feeMsat: data.fee_msat ?? 0, + // LNbits sends `fee` (signed millisats), never `fee_msat`. Reading the + // latter meant every payment recorded a zero fee. + feeMsat: Math.abs(Number(data.fee ?? 0)), preimage: data.preimage ?? '', } } diff --git a/src/modules/market/composables/useLightningPayment.ts b/src/modules/market/composables/useLightningPayment.ts index 0c1d790..0f4689f 100644 --- a/src/modules/market/composables/useLightningPayment.ts +++ b/src/modules/market/composables/useLightningPayment.ts @@ -42,7 +42,7 @@ export function useLightningPayment() { status: 'paid' as const, paidAt: Math.floor(Date.now() / 1000), paymentHash: paymentResult.payment_hash, - feeMsat: paymentResult.fee_msat, + feeMsat: paymentResult.feeMsat, items: [...order.items], // Convert readonly to mutable shippingZone: order.shippingZone ? { ...order.shippingZone, @@ -88,7 +88,7 @@ export function useLightningPayment() { status: 'paid' as const, paidAt: Math.floor(Date.now() / 1000), paymentHash: result.payment_hash, - feeMsat: result.fee_msat, + feeMsat: result.feeMsat, items: [...order.items], // Convert readonly to mutable shippingZone: order.shippingZone ? { ...order.shippingZone, From 21d4e0e4dfa9443160c4ba56dbea7c0cf2cf947e Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 26 Sep 2026 14:26:29 +0200 Subject: [PATCH 2/2] fix(market): declare feeMsat on Order so the persisted fee is typed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useLightningPayment` writes feeMsat onto the paid order, but `Order` never declared it. `updateOrder` takes `Partial`, and the write goes through an inferred variable, so the excess-property check never fired — the value landed in the store invisible to the type system and unreadable by any consumer. Co-Authored-By: Claude Opus 5 (1M context) --- src/modules/market/types/market.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/market/types/market.ts b/src/modules/market/types/market.ts index 7a3b8ca..ef88fad 100644 --- a/src/modules/market/types/market.ts +++ b/src/modules/market/types/market.ts @@ -129,6 +129,7 @@ export interface Order { originalOrderId?: string lightningInvoice?: any paymentHash?: string + feeMsat?: number // Routing fee paid on the Lightning payment, in millisats paidAt?: number paymentStatus?: 'pending' | 'paid' | 'expired' paid?: boolean // Direct boolean field matching nostrmarket reference