From 0b3f95aa33d349aa88c323fe95403f04928df33e Mon Sep 17 00:00:00 2001 From: ghzhost Date: Wed, 2 Sep 2026 16:00:28 +0000 Subject: [PATCH] fix(stats): compute average fee and total fees only across sent transactions (#58) - QuickStats: average fee is now calculated over sent transactions rather than all transactions, avoiding skew from counterparty-paid fees on received payments - HistorySummary: totalFees sums fees charged on sent transactions only - HistoryChart: daily fee aggregation only adds fees on sent transactions - Add unit tests verifying QuickStats excludes received transaction fees --- src/components/dashboard/QuickStats.test.tsx | 72 ++++++++++++++++++++ src/components/dashboard/QuickStats.tsx | 6 +- src/pages/History.tsx | 14 ++-- 3 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 src/components/dashboard/QuickStats.test.tsx diff --git a/src/components/dashboard/QuickStats.test.tsx b/src/components/dashboard/QuickStats.test.tsx new file mode 100644 index 0000000..06765ca --- /dev/null +++ b/src/components/dashboard/QuickStats.test.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import { QuickStats } from './QuickStats' +import { useRecentTransactions } from '@/hooks/useTransactions' +import { Transaction } from '@/types' + +vi.mock('@/hooks/useTransactions', () => ({ + useRecentTransactions: vi.fn(), +})) + +function makeTx(overrides: Partial = {}): Transaction { + return { + id: 'tx-1', + hash: 'hash-1', + createdAt: new Date().toISOString(), + type: 'payment', + status: 'success', + sourceAccount: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', + destinationAccount: 'GAHK7WOJWVU67MHUCHC5QUODIC2GQUQ32ZPXRKZ43D4ML5I5U2PLM6TV', + amount: '10', + assetCode: 'XLM', + assetIssuer: null, + fee: '100', + ledger: 1000, + direction: 'sent', + counterparty: 'GAHK7WOJWVU67MHUCHC5QUODIC2GQUQ32ZPXRKZ43D4ML5I5U2PLM6TV', + memo: '', + ...overrides, + } +} + +describe('QuickStats', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('computes average fee using only sent transactions, ignoring received transaction fees', () => { + // 1 sent transaction with fee = 100 stroops (0.0000100 XLM) + // 1 received transaction with massive fee = 10_000_000 stroops (1.0 XLM) + const transactions = [ + makeTx({ id: 'tx-1', direction: 'sent', fee: '100', amount: '50' }), + makeTx({ id: 'tx-2', direction: 'received', fee: '10000000', amount: '100' }), + ] + + vi.mocked(useRecentTransactions).mockReturnValue({ + data: { transactions, total: 2 }, + isLoading: false, + } as any) + + render() + + // Expected avg fee = 100 / 1 / 10_000_000 = 0.0000100 + expect(screen.getByText('0.0000100')).toBeInTheDocument() + expect(screen.getByText('Average Fee')).toBeInTheDocument() + }) + + it('handles zero sent transactions gracefully with avgFee = "0"', () => { + const transactions = [ + makeTx({ id: 'tx-1', direction: 'received', fee: '5000', amount: '20' }), + ] + + vi.mocked(useRecentTransactions).mockReturnValue({ + data: { transactions, total: 1 }, + isLoading: false, + } as any) + + render() + + expect(screen.getByText('0')).toBeInTheDocument() + }) +}) diff --git a/src/components/dashboard/QuickStats.tsx b/src/components/dashboard/QuickStats.tsx index 11e033b..cc5a41f 100644 --- a/src/components/dashboard/QuickStats.tsx +++ b/src/components/dashboard/QuickStats.tsx @@ -20,10 +20,10 @@ export function QuickStats() { .reduce((sum, t) => sum + parseFloat(t.amount || '0'), 0) .toFixed(4) - const avgFee = transactions.length + const avgFee = sent.length ? ( - transactions.reduce((s, t) => s + parseFloat(t.fee || '0'), 0) / - transactions.length / + sent.reduce((s, t) => s + parseFloat(t.fee || '0'), 0) / + sent.length / 10_000_000 ).toFixed(7) : '0' diff --git a/src/pages/History.tsx b/src/pages/History.tsx index f11a95b..4251088 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -38,9 +38,12 @@ function HistoryChart() { day: 'numeric', }) if (days[key]) { - if (tx.direction === 'sent') days[key].sent += parseFloat(tx.amount || '0') - else days[key].received += parseFloat(tx.amount || '0') - days[key].fees += parseFloat(tx.fee || '0') / 10_000_000 + if (tx.direction === 'sent') { + days[key].sent += parseFloat(tx.amount || '0') + days[key].fees += parseFloat(tx.fee || '0') / 10_000_000 + } else { + days[key].received += parseFloat(tx.amount || '0') + } } }) @@ -102,9 +105,10 @@ function HistorySummary() { const { data } = useRecentTransactions(50) const txs = data?.transactions ?? [] - const totalSent = txs.filter((t) => t.direction === 'sent').reduce((s, t) => s + parseFloat(t.amount || '0'), 0) + const sentTxs = txs.filter((t) => t.direction === 'sent') + const totalSent = sentTxs.reduce((s, t) => s + parseFloat(t.amount || '0'), 0) const totalReceived = txs.filter((t) => t.direction === 'received').reduce((s, t) => s + parseFloat(t.amount || '0'), 0) - const totalFees = txs.reduce((s, t) => s + parseFloat(t.fee || '0') / 10_000_000, 0) + const totalFees = sentTxs.reduce((s, t) => s + parseFloat(t.fee || '0') / 10_000_000, 0) const successRate = txs.length ? Math.round((txs.filter((t) => t.status === 'success').length / txs.length) * 100) : 100