Problem
src/pages/History.tsx's HistoryChart builds its day-bucket keys using only month and day, with no year component:
const days: Record<string, { sent: number; received: number; fees: number }> = {}
const now = Date.now()
for (let i = 29; i >= 0; i--) {
const d = new Date(now - i * 86_400_000)
const key = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
days[key] = { sent: 0, received: 0, fees: 0 }
}
transactions.forEach((tx) => {
const key = new Date(tx.createdAt).toLocaleDateString('en-US', { month: 'short', 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
}
})
key is something like "Jul 15" — no year. The initial 30-day window is built from now, so in practice all 30 generated keys fall within at most two consecutive calendar years, but a transaction whose createdAt happens to fall on the same month/day in a different year than intended (this becomes especially relevant once useRecentTransactions(100)'s underlying data spans more than a year, or simply near a year boundary — e.g. the window spans late December into early January, and any historical quirk in createdAt ordering/pagination could reintroduce an out-of-window date) would be silently summed into the wrong day's bucket via the if (days[key]) truthy check, since "Dec 31" from last year and "Dec 31" from this year produce the identical string key.
Why it matters
- This is a silent-data-corruption bug in a financial summary chart specifically: two transactions a full year apart, on the same calendar day, get merged into a single bar with no indication anything went wrong (no error, no warning —
if (days[key]) just quietly succeeds for the wrong reason). For a chart whose entire purpose is showing "volume sent and received over time," conflating dates a year apart is a materially misleading result for any user who happens to look at data spanning a year boundary.
- Combined with the separately-filed
fetchTransactionsFromHorizon hardcoded-amount: '0' bug in this batch, this chart currently always renders all-zero bars regardless of this bucketing bug — but the two are independent defects that would both need fixing before the chart is trustworthy; fixing only the hardcoded-amount bug would surface this year-collision bug as newly-visible incorrect data.
Reproduction
- Construct a
transactions fixture with two entries: one dated 2025-07-15T00:00:00Z and one dated 2026-07-15T00:00:00Z, both within a useRecentTransactions(100)-sized fetch. Feed them through HistoryChart's bucketing logic and observe both are summed into the single "Jul 15" key rather than being treated as distinct days (the intended 30-day window would only actually contain one of them, but the if (days[key]) check has no way to know that — it can't distinguish "this key belongs to the current window" from "this key happens to string-match a key in the window from a different year").
Suggested fix
- Include the year in the bucket key (e.g.
d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) for both the window-generation loop and the transaction-bucketing loop), or better, key buckets by a normalized YYYY-MM-DD string rather than a locale-formatted display string, and only compute the locale-formatted label separately for the chart's X-axis tick display.
Edge cases
- Timezone handling:
toLocaleDateString uses the browser's local timezone by default, so a transaction timestamped just before/after local midnight could already land in a different day's bucket than a UTC-based reading of the same instant — worth deciding (and documenting) whether bucketing should be local-time or UTC-based, independent of the year-collision fix.
Testing strategy
- Unit test the bucketing logic in isolation (extracting it from the component if needed) with fixture transactions spanning a year boundary, asserting they land in visually and numerically distinct buckets rather than being summed together.
- Combine with a fixed
fetchTransactionsFromHorizon (per the separate issue in this batch) in an integration-style test to confirm the chart shows genuinely distinct, non-zero, correctly-dated bars end to end.
Related issues in this batch
Directly compounds with the fetchTransactionsFromHorizon hardcoded-amount/counterparty issue filed in this batch — both currently mask each other (the chart is already wrong for one reason, so this bucketing bug isn't yet visible), and both need fixing together for the History page's chart to be trustworthy.
Problem
src/pages/History.tsx'sHistoryChartbuilds its day-bucket keys using only month and day, with no year component:keyis something like"Jul 15"— no year. The initial 30-day window is built fromnow, so in practice all 30 generated keys fall within at most two consecutive calendar years, but a transaction whosecreatedAthappens to fall on the same month/day in a different year than intended (this becomes especially relevant onceuseRecentTransactions(100)'s underlying data spans more than a year, or simply near a year boundary — e.g. the window spans late December into early January, and any historical quirk increatedAtordering/pagination could reintroduce an out-of-window date) would be silently summed into the wrong day's bucket via theif (days[key])truthy check, since"Dec 31"from last year and"Dec 31"from this year produce the identical string key.Why it matters
if (days[key])just quietly succeeds for the wrong reason). For a chart whose entire purpose is showing "volume sent and received over time," conflating dates a year apart is a materially misleading result for any user who happens to look at data spanning a year boundary.fetchTransactionsFromHorizonhardcoded-amount: '0'bug in this batch, this chart currently always renders all-zero bars regardless of this bucketing bug — but the two are independent defects that would both need fixing before the chart is trustworthy; fixing only the hardcoded-amount bug would surface this year-collision bug as newly-visible incorrect data.Reproduction
transactionsfixture with two entries: one dated2025-07-15T00:00:00Zand one dated2026-07-15T00:00:00Z, both within auseRecentTransactions(100)-sized fetch. Feed them throughHistoryChart's bucketing logic and observe both are summed into the single"Jul 15"key rather than being treated as distinct days (the intended 30-day window would only actually contain one of them, but theif (days[key])check has no way to know that — it can't distinguish "this key belongs to the current window" from "this key happens to string-match a key in the window from a different year").Suggested fix
d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })for both the window-generation loop and the transaction-bucketing loop), or better, key buckets by a normalizedYYYY-MM-DDstring rather than a locale-formatted display string, and only compute the locale-formatted label separately for the chart's X-axis tick display.Edge cases
toLocaleDateStringuses the browser's local timezone by default, so a transaction timestamped just before/after local midnight could already land in a different day's bucket than a UTC-based reading of the same instant — worth deciding (and documenting) whether bucketing should be local-time or UTC-based, independent of the year-collision fix.Testing strategy
fetchTransactionsFromHorizon(per the separate issue in this batch) in an integration-style test to confirm the chart shows genuinely distinct, non-zero, correctly-dated bars end to end.Related issues in this batch
Directly compounds with the
fetchTransactionsFromHorizonhardcoded-amount/counterpartyissue filed in this batch — both currently mask each other (the chart is already wrong for one reason, so this bucketing bug isn't yet visible), and both need fixing together for the History page's chart to be trustworthy.