Skip to content

activity: record the fee each wallet transaction paid (part of #463) - #632

Merged
2 commits merged into
release/v0.10.6from
fix/gh-activity-fee-value
Oct 12, 2026
Merged

2 commits merged into
release/v0.10.6from
fix/gh-activity-fee-value

Conversation

@derek-flossman

Copy link
Copy Markdown
Collaborator

Part of #463 (the fee-paid half; value at submission needs a decision, see below). Stacked on #598. The value at submission is left open (see Open decision).

For @derek-flossman: no user-facing strings change. One decision: where the value at submission comes from (see Open decision). The expanded-row UI that shows these fields is #480, which builds on #450 and is not touched here.

GitHub #463: the fee paid

Problem

The activity row's expansion has no fee because the log never stored one. Reconciliation reads starknet_getTransactionStatus, which reports finality only. The fee is in the receipt's actual_fee.

The issue says either field needs a vault schema bump. That isn't so: activity lives in plain storage.local under bramble:activity, not in the vault, and its rows already gain optional fields without a migration (as #598 did with direction).

Fix

  • New RPC read (packages/platform/starknet/src/rpc/transaction-fee.ts): getTransactionFees reads receipts with the same bounded fan-out and failover as status reads. readReceiptFee trusts only the RPC 0.7+ shape { amount, unit }, with a canonical in-field amount and a unit of FRI or WEI. A bare felt has no unit and is dropped, not guessed.
  • New optional field feePaid on ActivityEntry, validated by isTransactionFee. Stored rows from before it still load. record() callers cannot set it; only reconciliation writes it.
  • Reconciliation (tx-reconcile.ts) reads the fee once, for a row this wallet submitted (origin: "wallet"), after the chain reports an outcome that has a receipt (accepted or reverted).
    • Only wallet rows: a receipt's fee is paid by the transaction's sender, so on a discovered incoming transfer it would be someone else's fee.
    • A recorded fee is final: it is never read again or replaced.
    • A failed or unusable read stores nothing. The row is read again while it is observed, and after that by a slow-pass backfill: the 15-minute alarm and a user refresh read up to eight wallet rows per pass that have an outcome but no fee, inside the 7-day observation window. Without it, a reverted or L1-final row whose one read failed would lose its fee for good.
  • Wiring: getTransactionFees is an optional RuntimeRpc member, wired in the worker's default RPC.

Cost: one receipt read per wallet transaction on success. A receipt that stays unreadable is retried at most once per slow pass for 7 days.

Tests

  • packages/platform/starknet/tests/rpc/transaction-fee.test.ts (new): the accepted shapes, eleven rejected shapes (bare felt, unknown unit, negative, out-of-field, non-string…), the stored-fee validator, a hash with no fee left out, and failover on a transport error.
  • packages/features/transactions/tests/activity/activity-fee-paid.test.ts (new): rows without the field still load, valid fees load, malformed ones fail the row.
  • tests/background/transactions/tx-reconcile-fee.test.ts (new), through the real reconciler and in-memory log:
    • recorded for an accepted and for a reverted wallet send;
    • not read while unresolved;
    • never read for a discovered row;
    • a failed read is retried; an empty answer stores nothing; no reader means no fee;
    • read once, never replaced;
    • backfill: a reverted row whose first read failed gets its fee on a later slow pass; an L1-final row gets it on a user refresh; fast passes never backfill; rows past the window or not submitted by the wallet are skipped; at most eight per pass;
    • the worker's RPC wiring provides the reader.

Open decision: the value at submission

Not recorded here. The background holds no price for most submissions: prices live in the side panel, per display currency. Options:

  1. Have the panel pass the value it showed on the review screen with the submission, stored as { amount, currency }. New transactions only; the background must treat it as a display figure, never an amount.
  2. A price-at-timestamp source (new dependency or endpoint).

Recommended: option 1, in its own PR together with #480's display.

Residuals

  • Transactions submitted before this change, or older than the 7-day window, never get a fee.
  • withRpcFallback returns the first node's answer, so a receipt lagging on that node waits for a later pass.
  • The CSV export has no fee column; feat(activity): rebuild the CSV export columns and summary #573 owns the CSV columns.

Mutation checks

Every revert failed at least one test:

Revert Failing tests
origin filter removed 1
read before an outcome with a receipt 1
re-read when a fee is already recorded 1
fee never merged into the outcome 3
a later fee replaces the recorded one 1
validator does not know feePaid 4
receipt amount not field-checked 1
stored amount not field-checked 1
default RPC wiring omits the reader 1
no backfill on the slow alarm 2
no backfill on a user refresh 1
backfill on fast passes 1
no window bound 1
no per-pass limit 1

Checks run locally (Linux, Bun 1.3.3)

Check Result
bun run check pass, 2,530 tests
bun run check:coverage pass: functions 95.2%, lines 95.7%
bun run check:audit pass
bun run build pass

Review

DeepSeek (deepseek-v4-pro-0813, about $0.014) found that a reverted or L1-final row is never observed again, so one failed read lost its fee. Fixed by the backfill in the second commit, with tests. Its point about unknown keys on the receipt's fee object doesn't apply: readReceiptFee builds a fresh { amount, unit }. Its other points are informational and listed as residuals.

🤖 Generated with Claude Code

derek-flossman and others added 2 commits October 12, 2026 07:14
The activity row's expansion has no fee because the log never stored
one: reconciliation reads getTransactionStatus, which carries finality
only (GitHub 463). The receipt carries the true fee, so reconciliation
now reads it once, for a row this wallet submitted, after the chain
reports an outcome that has a receipt (accepted or reverted), and
stores it as the optional feePaid field.

- Only origin "wallet" rows qualify. A receipt's fee is paid by the
  transaction's sender; on a discovered incoming transfer it would be
  someone else's fee.
- A recorded fee is final: it is never read again or replaced.
- A missing receipt, an unusable fee (no unit, out-of-field amount) or
  a failed read stores nothing and is retried on a later pass while the
  transaction is still observed. The reviewed estimate is never stored
  as a fee paid.
- The field is optional and validated, so stored rows from before it
  load unchanged. Activity is plain storage, not the vault, so no vault
  schema bump is needed. Callers cannot set it when recording a row.

The cost is one receipt read per wallet transaction, once, not one per
poll. The value at submission is not recorded here: the background
holds no price for most submissions, so it needs a decision about where
that price comes from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that a reverted or L1-final row is never observed again,
so if the one fee read in the pass that reached that outcome failed,
the fee was lost for good. Slow passes (the 15-minute alarm and a
user refresh) now also read the fee for wallet rows that have a
receipt-bearing outcome, no fee, and are inside the 7-day observation
window, at most eight per pass. A backfill row writes only when its
fee is found; fast passes never backfill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from fix/audit-activity-store to release/v0.10.6 October 12, 2026 13:10
@derek-flossman derek-flossman closed this pull request by merging all changes into release/v0.10.6 in 7eb9028 Oct 12, 2026
@derek-flossman
derek-flossman deleted the fix/gh-activity-fee-value branch October 12, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant