Skip to content

in the Payment details section the user should see the fees #244

@22JohnGalt

Description

@22JohnGalt

Description

When viewing the details of a completed payment, the transaction fee (routing fee for Lightning, network fee for on-chain) is not shown. Users have no way to know how much they paid in fees for a given transaction.

Current State

The activities table has no fee_sats column. The Wallet.sendPayment interface returns only a string (preimage or payment ID). Fee data is either discarded at the point of execution or never retrieved, so it is never stored or displayed.

The activity detail screen (app/activity/[id]/index.tsx) renders amount and converted amount, but has no fee row.

Fee Data Availability by Wallet Type

Breez (Lightning / Spark)

Fee information is already available in BreezService.sendPayment — it is just discarded today.

prepareSendPayment is called before every payment and its response (prepareResponse) contains the fee breakdown on the paymentMethod field:

// services/BreezService.ts — inside sendPayment()
const prepareResponse = await this.client.prepareSendPayment({ ... });

// For Bolt11 invoices (already instanceof-checked):
if (prepareResponse.paymentMethod instanceof SendPaymentMethod.Bolt11Invoice) {
  // lightningFeeSats  — routing fee paid over Lightning (bigint)
  // sparkTransferFeeSats — fee if paid via Spark transfer instead (bigint | undefined)
}

Both lightningFeeSats and sparkTransferFeeSats are known before the payment is sent (prepare step), so no extra SDK call is needed. The actual method used is determined at send time; the correct fee is whichever path the SDK took.

Additionally, getPaymentById is already implemented in BreezService and could be used post-payment to retrieve the final confirmed fee from the Payment object if needed.

NWC (Nostr Wallet Connect)

NIP-47 defines a fees_paid field (in millisats) on the pay_invoice result object:

{
  "result_type": "pay_invoice",
  "result": {
    "preimage": "...",
    "fees_paid": 1000
  }
}

NwcService.sendPayment currently calls this.client.payInvoice(paymentRequest) and returns only the preimage as a string. Whether fees_paid is surfaced depends on the NWC client library in use — this needs to be verified. If not directly available, a follow-up getPayment by preimage/hash may be required.

Note: for NWC, fee availability depends entirely on the connected wallet's implementation. Some wallets omit fees_paid even if the field is in the spec. A null/undefined fee should be treated gracefully (display "N/A" rather than crashing or showing 0).

Implementation Plan

1. DB Migration — add fee_sats column

ALTER TABLE activities ADD COLUMN fee_sats INTEGER;

Nullable — existing rows and wallets that can't return fees will be NULL.

2. Wallet interface — extend sendPayment return type

// models/WalletType.ts
sendPayment: (paymentRequest: string, amountSats: bigint) => Promise<{ preimage: string; feeSats?: number }>;

3. BreezService.sendPayment — capture fee from prepare response

// Extract fee from prepareResponse before calling sendPayment
let feeSats: number | undefined;
if (prepareResponse.paymentMethod instanceof SendPaymentMethod.Bolt11Invoice) {
  feeSats = Number(prepareResponse.paymentMethod.lightningFeeSats);
}
const response = await this.client.sendPayment({ prepareResponse, options: sendOptions });
return { preimage: response.payment.id, feeSats };

4. NwcService.sendPayment — extract fees_paid if available

const result = await this.client.payInvoice(paymentRequest);
// result may be a string (preimage) or object depending on library version
const preimage = typeof result === 'string' ? result : result.preimage;
const feeSats = typeof result === 'object' && result.fees_paid != null
  ? Math.ceil(result.fees_paid / 1000)  // msats → sats
  : undefined;
return { preimage, feeSats };

5. PayInvoiceTask — propagate fee through task chain

Update return type and pass feeSats up to StartPaymentTask.

6. SaveActivityArgs / SaveActivityTask / DatabaseService

Add optional fee_sats?: number | null field to SaveActivityArgs and persist it.

7. Activity detail UI

Add a new ActivityDetailRow for the fee in the "Transaction Details" section, shown only for payment activities and only when fee_sats is non-null:

{isPayment && activity.fee_sats != null && (
  <ActivityDetailRow
    icon={<Percent size={18} color={secondaryTextColor} />}
    label="Network Fee"
    value={formatActivityAmount(activity.fee_sats, 'SATS')}
    isLast={...}
  />
)}

Scope Notes

  • Fees are only relevant for outgoing payments (ActivityType.Pay). Incoming Lightning payments have no fee for the recipient. This feature can be scoped to type = 'pay' only.
  • For Breez, the fee shown is the pre-payment estimate from the prepare step. The actual fee may differ slightly in edge cases (e.g. if the route changes). A prefix can be added if desired, though in practice for Lightning the prepared fee is what gets charged.
  • For NWC, a null fee should be shown as "N/A" — do not display 0 when data is unavailable, as that would be misleading.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions