Skip to content

fix(server): keep the protocol fee note out of dashboard delta listings - #449

Draft
WiktorStarczewski wants to merge 1 commit into
OpenZeppelin:mainfrom
WiktorStarczewski:wiktor/dashboard-fee-note-exclusion
Draft

fix(server): keep the protocol fee note out of dashboard delta listings#449
WiktorStarczewski wants to merge 1 commit into
OpenZeppelin:mainfrom
WiktorStarczewski:wiktor/dashboard-fee-note-exclusion

Conversation

@WiktorStarczewski

Copy link
Copy Markdown
Collaborator

Once a chain charges a fee, every guarded transaction emits a protocol TX_FEE note — and the dashboard was reading it as a transfer, so a plain note consumption showed up as an outgoing asset movement and noteCounts.output grew by one.

  • Listing projections (assets, noteCounts.output, category) now iterate user_output_notes, which drops the note the protocol appended.
  • The excluded fee is reported separately as fee on the delta entry, not dropped — a transaction commits its own conversion rate, so the amount is bounded only by the vault and is a signal worth seeing.
  • The delta detail view is unchanged and still lists the fee note, so noteCounts.output is deliberately one lower than outputNotes.length there.
  • guardian-operator-client types + README and the three OpenAPI documents move with the schema, per AGENTS.md §8.2.
How the fee note is identified, and where it declines to guess

Provenance is not recoverable from a TransactionSummary: it carries no mark for where the auth procedure's outputs begin, and TxFeeNote::builder is public, so a caller-built TX_FEE note is indistinguishable from the protocol's by script root. Position is the only remaining signal — pay_fee runs at the end of authentication, after the transaction script has created every note it is going to, so the injected note is last.

Erring toward exclusion is the dangerous direction: a TX_FEE note restricts nobody from consuming it, so a caller who could persuade the filter to skip a note of their own would be concealing a bearer instrument. protocol_fee_note_index therefore resolves only the unambiguous case — exactly one TX_FEE note, in last position. Two means a caller built at least one, and nothing is excluded.

Residual, accepted and documented in place: on a chain that charges no fee there is no injected note, so a caller's own TX_FEE note placed last would be hidden. Closing that needs a provenance signal the summary does not carry.

Also worth knowing: num_output_notes_before_auth is not this boundary. guardian.masm consults it only on the rotation path, where it must be zero, and the fee note is inside the signed summary regardless.

Why this is its own PR

Split out of #440 (guarded-account fee payment), which is client-side. There is no coupling in either direction: crates/server does not depend on miden-multisig-client, and this change is a pure consumer of TransactionSummary. It needs nothing #440 produces, and it builds and tests green against published protocol crates, which #440 currently does not.

10 new tests in delta_summary::projection cover the single-note, no-note, two-note and not-last cases plus the reported fee; guardian-server is 906 passing (896 before) and gen-openapi --check docs is clean.

Reviewers: protocol_fee_note_index is the part worth your time — specifically whether "exactly one, in last position" is the right refusal boundary.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 76c577e5-f9b4-4b13-8efe-9895d91fd738


Comment @coderabbitai help to get the list of available commands.

@zeljkoX zeljkoX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - just a few nits.

The operator client currently declares fee, but never parses it. parseDeltaEntry handles note_counts, assets, and counterparty, but not record.fee, so TypeScript consumers will always see fee as undefined. This needs a decode branch and a test.

Should we handle FEE_SPONSORSHIP note the same way?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants