feat(explorer): show network costs and protocol fee breakdown on order details - #7588
feat(explorer): show network costs and protocol fee breakdown on order details#7588jmg-duarte wants to merge 38 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe Explorer now models protocol fees, aggregates them from executed trades, fetches them across paginated order trades, and displays network and protocol-fee breakdowns behind a feature flag. Legacy fee rendering remains as a fallback. ChangesProtocol fee extraction and display
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying explorer-dev with
|
| Latest commit: |
f051cfc
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e6f10d01.explorer-dev-dxz.pages.dev |
| Branch Preview URL: | https://jmgd-ucp.explorer-dev-dxz.pages.dev |
Deploying swap-dev with
|
| Latest commit: |
f051cfc
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e778039a.swap-dev-5u6.pages.dev |
| Branch Preview URL: | https://jmgd-ucp.swap-dev-5u6.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/explorer/src/test/utils/operator/orderFees.test.ts (1)
81-89: ⚡ Quick winAdd a mixed-checksum aggregation regression test.
Current coverage checks case-insensitive comparison, but not aggregation across multiple trades where the same token appears in different casing. Add one test with two trades (
FEE_TOKENandFEE_TOKEN.toLowerCase()) and assert single-token aggregation without warning.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/explorer/src/test/utils/operator/orderFees.test.ts` around lines 81 - 89, Add a regression test in orderFees.test.ts that verifies aggregation across mixed-checksum token strings: create two trades using makeTrade — one with token FEE_TOKEN and one with FEE_TOKEN.toLowerCase() — pass them to getFees alongside an order from makeOrder with executedFeeToken set to a different-cased FEE_TOKEN and totalFee large enough to split protocol/network fees, then assert the returned protocolFees equals the sum of both trade amounts (as a single token aggregation) and networkCosts equals the expected remainder; also assert no warning was emitted (e.g., result.warnings is empty or console.warn was not called) to ensure the same-token branch aggregates case-insensitively across multiple trades.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/explorer/src/api/operator/types.ts`:
- Around line 104-108: Change the protocolFeeTokenAddress property in the
OperatorTrade type from string to AddressKey (replace protocolFeeTokenAddress?:
string with protocolFeeTokenAddress?: AddressKey) and update all callers; then
replace any address comparisons that use toLowerCase() in the sameDenomination
logic with the address helpers — in apps/explorer/src/utils/operator.ts and
apps/explorer/src/components/orders/GasFeeDisplay/index.tsx use
areAddressesEqual(a,b) or normalize using getAddressKey(addr) before comparing
instead of addr.toLowerCase(), ensuring you import AddressKey, areAddressesEqual
and getAddressKey from the existing address utility module.
In `@apps/explorer/src/components/orders/GasFeeDisplay/index.tsx`:
- Around line 67-69: Replace direct .toLowerCase() comparisons with the cow-sdk
address helper: update the sameDenomination computation in GasFeeDisplay to call
areAddressesEqual(executedFeeToken, protocolFeeTokenAddress) instead of
comparing lowered strings; change formatFee and resolveToken signatures to
accept an AddressKey parameter type and inside each use
areAddressesEqual(tokenAddress, addressKey) (or similar) to determine equality
rather than calling toLowerCase() on addresses, and update any internal logic
that relied on string lowercasing to use the areAddressesEqual helper for all
address comparisons.
In `@apps/explorer/src/utils/operator.ts`:
- Around line 461-468: The fees grouping currently keys feesByToken by raw token
strings causing checksum-casing splits; change feesByToken to Map<AddressKey,
BigNumber> and when iterating executedProtocolFees use getAddressKey(token) as
the key (while still validating token exists) so amounts aggregate under a
normalized AddressKey (refer to feesByToken and the trades.forEach block). Also
replace the string lower-case comparison in sameDenomination with a call to
areAddressesEqual(tokenA, tokenB) per the address-comparison guideline to ensure
correct equality checks across checksum variants.
---
Nitpick comments:
In `@apps/explorer/src/test/utils/operator/orderFees.test.ts`:
- Around line 81-89: Add a regression test in orderFees.test.ts that verifies
aggregation across mixed-checksum token strings: create two trades using
makeTrade — one with token FEE_TOKEN and one with FEE_TOKEN.toLowerCase() — pass
them to getFees alongside an order from makeOrder with executedFeeToken set to a
different-cased FEE_TOKEN and totalFee large enough to split protocol/network
fees, then assert the returned protocolFees equals the sum of both trade amounts
(as a single token aggregation) and networkCosts equals the expected remainder;
also assert no warning was emitted (e.g., result.warnings is empty or
console.warn was not called) to ensure the same-token branch aggregates
case-insensitively across multiple trades.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 834ec6c8-a987-4d6c-a53b-f60aacd95af2
📒 Files selected for processing (9)
apps/explorer/src/api/operator/types.tsapps/explorer/src/components/AppDataRowContent/AppDataRowContent.tsxapps/explorer/src/components/common/ShowMoreButton.tsxapps/explorer/src/components/orders/GasFeeDisplay/GasFeeDisplay.stories.tsxapps/explorer/src/components/orders/GasFeeDisplay/index.tsxapps/explorer/src/components/orders/NumbersBreakdown/index.tsxapps/explorer/src/components/orders/OrderDetails/index.tsxapps/explorer/src/test/utils/operator/orderFees.test.tsapps/explorer/src/utils/operator.ts
|
@jmg-duarte can you take a look at the failing unittests? https://github.com/cowprotocol/cowswap/actions/runs/26631936508/job/78482889272?pr=7588 |
|
Additionally, something is weird. Checking a random order, I see this:
The network costs token is not being decoded for some reason. In this case, the token is WETH. However, it's also an edge case as it's an ethflow order, so the sell token should actually be displayed as ETH. |
|
I was expecting a change towards only displaying protocol fees. We will not be able to reconstruct network fees. (I am assuming that |
fairlighteth
left a comment
There was a problem hiding this comment.
⚠️ AI Review (Codex GPT-5, worked 4m): fee breakdown uses only the current fills page
Finding: Aggregate fee breakdown from all order trades, not the paginated fills table page
- Location:
apps/explorer/src/components/orders/OrderDetails/index.tsx:160 enrichOrderFromTradescallsgetFees(order, trades), buttradescomes fromuseOrderTrades(order, tableState.pageOffset, tableState.pageSize), which fetcheslimit + 1records and then slices back to the current page.- For a partially fillable order with more than 10 fills, the overview breakdown only sums the visible page. Page 1 omits later fills; after paging in the Fills tab, returning to Overview can show different fee/network-cost values for the same order.
- Impact: order-level costs become undercounted and page-dependent.
Suggested fix
- Use a complete trade set or dedicated aggregate for the order-level fee breakdown, or only render the breakdown when all trades needed for the aggregate have been loaded.
- Add coverage for an order with more fills than
RESULTS_PER_PAGEsoprotocolFeesincludes all fills and does not change withpageOffset.
Review scope and related context
This is separate from the existing review comments, which already cover:
- Address typing/comparison: resolved with
AddressKey,getAddressKey, andareAddressesEqual. - EthFlow WETH/ETH fee-token formatting: already reported by @alfetopito.
- Whether network fees should be displayed at all: already raised by @fhenneke.
- Mixed-checksum aggregation coverage: already suggested by CodeRabbit.
🤖 Prompt for AI agents
Verify this finding against current code. Fix only if still valid, keep the change minimal, and validate with targeted tests.
Context:
- apps/explorer/src/components/orders/OrderDetails/index.tsx:160 calls getFees(order, trades).
- apps/explorer/src/explorer/components/OrderWidget/index.tsx:36 passes useOrderTrades(order, tableState.pageOffset, tableState.pageSize).
- apps/explorer/src/hooks/useOperatorTrades.ts:69 fetches limit + 1 and lines 119-122 slice back to the current page.
- Fee breakdown should be order-level, not dependent on the currently selected fills page.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/explorer/src/components/orders/GasFeeDisplay/index.tsx (1)
105-123: 💤 Low valueMinor: Redundant
getAddressKeycall on already-normalized address.Line 122 calls
getAddressKey(address)butaddressis already anAddressKey(fromfee.tokenAddress, which was normalized viagetAddressKey(token)inoperator.tsline 482). SincefeeTokensByKeyis also keyed bygetAddressKey(...)results, you can usefeeTokensByKey.get(address)directly.♻️ Simplify lookup
- return feeTokensByKey.get(getAddressKey(address)) || undefined + return feeTokensByKey.get(address) || undefined🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/explorer/src/components/orders/GasFeeDisplay/index.tsx` around lines 105 - 123, In resolveToken, avoid the redundant normalization by using the already-normalized AddressKey directly when looking up feeTokensByKey: replace the final lookup that uses feeTokensByKey.get(getAddressKey(address)) with feeTokensByKey.get(address). This touches the resolveToken function and the feeTokensByKey map usage; no other behavior changes needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/explorer/src/components/orders/GasFeeDisplay/index.tsx`:
- Around line 105-123: In resolveToken, avoid the redundant normalization by
using the already-normalized AddressKey directly when looking up feeTokensByKey:
replace the final lookup that uses feeTokensByKey.get(getAddressKey(address))
with feeTokensByKey.get(address). This touches the resolveToken function and the
feeTokensByKey map usage; no other behavior changes needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 0cdd163f-1d15-40e1-b367-37dab2ea2e25
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (28)
apps/cowswap-frontend/package.jsonapps/explorer/package.jsonapps/explorer/src/api/operator/types.tsapps/explorer/src/components/orders/DetailsTable/detailsTableTooltips.tsxapps/explorer/src/components/orders/GasFeeDisplay/GasFeeDisplay.stories.tsxapps/explorer/src/components/orders/GasFeeDisplay/index.tsxapps/explorer/src/components/orders/OrderDetails/index.tsxapps/explorer/src/test/data/operator.tsapps/explorer/src/test/utils/operator/orderFees.test.tsapps/explorer/src/utils/operator.tsapps/sdk-tools/package.jsonapps/widget-configurator/package.jsonlibs/analytics/package.jsonlibs/balances-and-allowances/package.jsonlibs/common-const/package.jsonlibs/common-hooks/package.jsonlibs/common-utils/package.jsonlibs/core/package.jsonlibs/currency/package.jsonlibs/ens/package.jsonlibs/events/package.jsonlibs/multicall/package.jsonlibs/permit-utils/package.jsonlibs/tokens/package.jsonlibs/types/package.jsonlibs/ui/package.jsonlibs/wallet/package.jsonlibs/widget-lib/package.json
✅ Files skipped from review due to trivial changes (18)
- libs/analytics/package.json
- libs/ui/package.json
- libs/widget-lib/package.json
- libs/wallet/package.json
- libs/balances-and-allowances/package.json
- libs/permit-utils/package.json
- libs/common-utils/package.json
- libs/multicall/package.json
- libs/currency/package.json
- libs/common-const/package.json
- libs/core/package.json
- libs/common-hooks/package.json
- apps/widget-configurator/package.json
- libs/events/package.json
- libs/types/package.json
- apps/sdk-tools/package.json
- libs/ens/package.json
- apps/explorer/package.json
The breakdown is still being built out, so ship it disabled and enable it per environment through `isExplorerFeeDisplayEnabled`. With the flag off the order details page behaves as it did before the feature: the legacy combined executed fee, its original label, tooltip and inline layout, and none of the extra requests. The flag is read once, in a named hook over the shared `useFeatureFlags`, and gates two seams: `CostAndFeesItem` for what renders, and `OrderWidget` for whether the all-trades fetch runs at all. `GasFeeDisplay` stays flag-agnostic and takes `showBreakdown`, so Storybook and tests can drive both states. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut the commentary down to what the code cannot say for itself, and drop what would go stale on the next edit: an arithmetic aside tied to two constants, a hardcoded route path, references to a test file by path and to another test by name, and a restatement of the fee aggregation key that gave that invariant a second home to drift from. Two were not merely verbose but wrong. `OrderWidget` claimed the fee error was suppressed because it shared a root cause with the trades error, which the distinct error messages contradict; the real reason is to avoid a second banner. And `ProtocolFee.tokenAddress` was documented as the surplus-side token, an unverified claim in a place where it would be taken on trust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
apps/explorer/src/test/components/costsAndFeesFeatureFlag.test.tsx (1)
1-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winImport
ReactNodedirectly.Replace
React.ReactNodewithReactNodeimported fromreactinapps/explorer/src/test/components/costsAndFeesFeatureFlag.test.tsx.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/explorer/src/test/components/costsAndFeesFeatureFlag.test.tsx` around lines 1 - 33, Import ReactNode directly from react in the test file and replace the React.ReactNode type references in the mocked DetailRow definition with ReactNode, preserving the existing component behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/explorer/src/components/orders/DetailsTable/items/CostAndFeesItem.tsx`:
- Around line 15-24: Update showBreakdown in CostAndFeesItem to require both the
fee display feature flag and a positive order.gasCost value. Use this gated
value for the label, tooltipText, stack, and GasFeeDisplay props so missing or
zero gas costs retain the legacy presentation.
In `@apps/explorer/src/components/orders/GasFeeDisplay/index.tsx`:
- Line 157: Update the raw-address fallback in the token display logic to call
shortenAddress(tokenAddress) instead of abbreviateString(tokenAddress, 6, 4).
Ensure shortenAddress is imported from `@cowprotocol/common-utils` and remove the
obsolete abbreviation usage for this fallback.
- Around line 100-107: Update the totals useMemo in GasFeeDisplay to aggregate
wrapped-native fee amounts under the native token key while preserving original
token addresses for detail rows. In
apps/explorer/src/components/orders/GasFeeDisplay/index.tsx lines 100-107, apply
the wrapped-native-to-native key mapping during aggregation. In
apps/explorer/src/test/components/costsAndFeesBreakdown.test.tsx lines 85-89,
assert a single native headline total and retain WETH in the expanded fee rows.
In `@apps/explorer/src/hooks/useOperatorTrades.ts`:
- Line 154: Update the onError callback in useOperatorTrades to type its
parameter as unknown, call normalizeError once to create an error value, and
pass that normalized value to console.error alongside the existing context and
message.
In `@apps/explorer/src/test/hooks/useOperatorTrades.test.tsx`:
- Around line 112-113: Replace React.ReactNode annotations with the directly
imported ReactNode type at
apps/explorer/src/test/hooks/useOperatorTrades.test.tsx:112-113,
apps/explorer/src/components/orders/GasFeeDisplay/index.tsx:40, 51-59, 148-156,
and 162-163, and
apps/explorer/src/test/components/costsAndFeesBreakdown.test.tsx:55; add or
reuse the named ReactNode imports in each file without changing runtime imports
or behavior.
---
Nitpick comments:
In `@apps/explorer/src/test/components/costsAndFeesFeatureFlag.test.tsx`:
- Around line 1-33: Import ReactNode directly from react in the test file and
replace the React.ReactNode type references in the mocked DetailRow definition
with ReactNode, preserving the existing component behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e5ec8c2d-3e27-408b-bfbd-2d8b2543b04a
📒 Files selected for processing (19)
apps/explorer/src/api/operator/types.tsapps/explorer/src/components/AppDataRowContent/AppDataRowContent.tsxapps/explorer/src/components/common/ShowMoreButton.tsxapps/explorer/src/components/orders/DetailsTable/detailsTableTooltips.tsxapps/explorer/src/components/orders/DetailsTable/items/CostAndFeesItem.tsxapps/explorer/src/components/orders/GasFeeDisplay/GasFeeDisplay.stories.tsxapps/explorer/src/components/orders/GasFeeDisplay/index.tsxapps/explorer/src/components/orders/NumbersBreakdown/index.tsxapps/explorer/src/components/orders/OrderDetails/index.tsxapps/explorer/src/components/orders/OrderSurplusDisplay/index.tsxapps/explorer/src/explorer/components/OrderWidget/index.tsxapps/explorer/src/hooks/useFeeDisplayFeatureFlag.tsapps/explorer/src/hooks/useOperatorTrades.tsapps/explorer/src/test/components/costsAndFeesBreakdown.test.tsxapps/explorer/src/test/components/costsAndFeesFeatureFlag.test.tsxapps/explorer/src/test/data/operator.tsapps/explorer/src/test/hooks/useOperatorTrades.test.tsxapps/explorer/src/test/utils/operator/protocolFees.test.tsapps/explorer/src/utils/operator.ts
💤 Files with no reviewable changes (1)
- apps/explorer/src/components/orders/GasFeeDisplay/GasFeeDisplay.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/explorer/src/components/AppDataRowContent/AppDataRowContent.tsx
- apps/explorer/src/test/data/operator.ts
- Gate the Costs and fees row presentation (label, tooltip, stacked layout) on the same conditions GasFeeDisplay uses, so orders without a usable gas cost or fee list keep the legacy row end to end. - Use shortenAddress for the raw-address fallback instead of slicing. - Normalize the SWR onError rejection value before logging it. - Import React type names directly instead of the React.* prefix form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
elena-zh
left a comment
There was a problem hiding this comment.
Hey @jmg-duarte , could you please check why I don't see any breakdown for my costs and fees for this order?
It is not affected by the feature-flag, and the order was placed using the widget with a partner fee inside:
|
@elena-zh the order was found in prod, which is already tracking but not returning the gas values yet
For example, this one was placed against staging, so it's found and returned by barn: https://explorer-dev-git-jmgd-ucp-cowswap-dev.vercel.app/sepolia/orders/0x84ed78f0be57edfd8a791acb8a773c7c0cec8a9800345e31575f8baa632c9e9309fbad1ea29c36dfe4f8f7baa87c5edf85e0d9f36a7afb73 |
|
@jmg-duarte , sorry! Indeed, my develop was pointing to Prod. I managed to place another order, I see a breakdown now, But could you please clarify why all fees are 'volume' fees now? As far as I remember, we agreed to have:
Other than that, changes LGTM, since CoW Swap UI related changes will be implemented later. |
|
They show as volume because they're applied as volume fees, I didn't recall what we had decided and at this point we're running multiple conversations, sorry about that. Let me fix it and I'll re-request a review |
Move the row building, per-token totals and token lookup out of the GasFeeDisplay hooks into pure functions, and unit test them directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Fills table and the fee breakdown each paged the trades themselves. Both now read one shared SWR entry: the table slices its page from that list, and the breakdown reduces over it. Paging the table client-side is also more correct, since getTrades merges PROD and BARN but offsets each environment separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>



Summary
Replace the single combined "fee" figure on the order details page with a costs & fees breakdown:
gasCost(read offRawOrderuntil the SDK ships it).executedProtocolFees, per token + fee type — position0is the protocol fee, later ones are numbered partner fees.gasCostfall back to the old combined-fee display.Based on #5024
To Test
gasCostSummary by CodeRabbit
New Features
Bug Fixes