Repository navigation
feat(fees): record every fee quote with the policy behind it (W085) - #260
Open
JemimahEkong wants to merge 1 commit into
Open
JemimahEkong wants to merge 1 commit into
JemimahEkong wants to merge 1 commit into
Conversation
GET /fees tells a client what to charge, and the fee then rides on-chain in the user's own transaction. Nothing on this side recorded that it happened, so there was no answer to what had been charged, in which asset, on which day, or under which policy — and a misconfigured percent, floor or cap would overcharge every user until someone read a block explorer. - FeeQuote: one row per asset per quote — amount priced against, fee, the rule that decided it (percent | floor | cap | flat | no_amount), the fee as a percent of the amount, an alarm flag, and the policy id. No IP, user agent or sender address; the only address is the published destination. - FeePolicy: keyed by a hash of the policy, so a change is a new row with its own firstSeenAt and every quote names the policy that priced it. The policy is taken from the entry the client was sent, never re-read from env. - Recording runs after the response and never throws: a ledger outage is logged and counted (veil_fee_quote_record_failures_total), not a failed send. - Alarm: a fee above VEIL_FEE_ALARM_PERCENT (default 10) of its amount flags the row, increments veil_fee_quote_alarms_total and logs a line — raised before the write, so a database outage cannot silence it. - GET /fees/report?from=&to=: per UTC day and asset — quotes, fees quoted, alarms, policies in force, and what the destination actually received per the index (null, with a reason, when the asset is not indexed). Behind VEIL_FEE_REPORT_TOKEN; 503 when unset. Documented as x-internal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvL6Yz2Zfg7spM1F9DXXtz
|
@JemimahEkong Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Owner
|
Rebase needed — #261 landed, consolidating the two Linq HTTP clients into a single What changed, so the rebase is predictable:
Also worth knowing: Push and I will pick it up. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
W085 · Record what the platform fee actually charged
Closes #242
Problem
GET /feestells a client what to charge and where to send it. The fee then rides on-chain in the user's own transaction, and nothing on this side recorded that it happened. So there was no answer to how much had been charged, in which asset, on which day, or under which policy. Because the fee is now policy (percent, floor and cap) rather than a constant, a misconfiguration would overcharge every user until someone read a block explorer.What changed
FeeQuote, one row per asset per quote. Each row holds the network and asset, and the amount priced against (null for clients that send none). It also holds the fee, the rule that decided it (percent | floor | cap | flat | no_amount), the fee as a percentage of the amount, an alarm flag and the policy id.FeePolicy, keyed by a hash of one asset's policy: mode, percent, floor, cap, flat amount, asset and destination. A policy change is a new row with its ownfirstSeenAt, and every quote names the policy that priced it. The policy comes from the exact entry the client was sent, not from re-reading environment variables, so the record cannot disagree with what the user was told.veil_fee_quote_record_failures_total; it never becomes a failed send for someone mid-payment. Each policy is written once per process, withskipDuplicatescovering races between instances.VEIL_FEE_ALARM_PERCENTof its amount (default 10, the rate the fee code's own comments call stark) does three things: it flags the row, incrementsveil_fee_quote_alarms_total{network,asset}and logs a line. The alarm is raised before the database write, so an outage cannot silence an overcharge.GET /fees/report?from=&to=(UTC dates, inclusive, at most 92 days). Per day and asset it returns the quote count, total fees quoted, alarm count and the policy ids, plus the full policies withfirstSeenAt.collectedisnullwith a note, because zero would be false.VEIL_FEE_REPORT_TOKEN, compared in constant time. It answers 503 when the token is unset, so it is never served open by accident.x-internal, andopenapi.jsonis regenerated..env.exampleanddocker-compose.ymlgain the two new variables. The existing compose guard test requires both.Nothing recorded identifies a person
No IP address, user agent or sender address is stored. The only address anywhere in the ledger is the fee destination, which
/feesalready publishes. One test pins the exact column set of a quote row. Another sends a request carrying an address, a user agent andX-Forwarded-For, and asserts none of them reaches the database.Acceptance criteria
/feesquote that returns a fee is recorded with the policy that produced it. Tested end to end through the route, field for field against what the client was sent.VEIL_FEE_PERCENTproduces new policy rows, and the quotes before and after name different policies.Validation
npm test: 763 of 764 passing. The single failure is the pre-existingopenapiCoveragetest (get /feesundocumented), which is W082's job and is unchanged by this PR. The new/fees/reportroute is documented, so it does not add to that list.src/__tests__/feeLedger.test.ts.npm run typecheck,npx tsc --noEmitandnpm run buildall pass.npx prisma validatepasses.mintevents, cut the day boundary at exactly midnight UTC, and sum a maximum-size i128 amount without overflow. Prisma's own engines couldn't be downloaded in my environment, so this exercised the SQL, not the Prisma client.