Skip to content

Commit 8fbb975

Browse files
kaze-cowclaudefairlightethelena-zhDanziger
authored
feat (explorer): add wrapper designation to order details (#7404)
* feat (explorer): add wrapper designation to order details Renders a "Wrappers" row in the verbose order details table for orders whose appData references known wrapper contracts. Wrapper-specific UI is code-split and loaded on demand; unknown addresses fall back to fetching the contract name() on-chain. Supported wrappers: - OrderFlowFactory (bridge & swap) - Euler EVC – Open Position, Close Position, Collateral Swap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * remove references to orderflow shouldn't be in this pr * fix build error * feat: improve explorer wrapper details display and code quality - Add tooltip for unknown wrappers explaining missing details - Lift contract name lookup into WrapperEntry header - Key wrapper rows by address+data to avoid React reconciliation collisions - Use getAddressKey() from cow-sdk for registry lookup - Remove non-null assertions in _vaultLookup.ts with proper SWR guards - Fix getPublicClient with explicit return type and early-return pattern - Fix import ordering across touched files - Extract *View sub-components to reduce per-function complexity - Delete unused unknown.tsx wrapper component Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: split eulerEvc.tsx into per-wrapper files Move each wrapper component into its own file under eulerEvc/ with shared styled components and helpers extracted to common.tsx. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: move Euler vault hooks to hooks/euler/ Split _vaultLookup.ts into dedicated files under hooks/euler/: abis.ts, client.ts, and one file per hook. Dropped three hooks that had no callers (useVaultBalance, useVaultDebt, useNativePrice). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: use AddressKey type, shortenAddress, direct React imports, multicall - Type address parameters as AddressKey from @cowprotocol/cow-sdk - Replace manual address slicing with shortenAddress from @cowprotocol/common-utils - Import ComponentType/ReactElement directly instead of via React. prefix - Batch ERC20 symbol/name/decimals reads in useVaultAsset into a single multicall Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * add import for detail row/tooltips * use jotai for state management, and remove unnecessary orderflow file (should be separate PR) * lint fix and resolve deprecated dependency * fix design issues as suggested * fix word wrap * try to fix pnpm lock * docs(explorer): add UI and code conventions to AGENTS.md (#7459) * docs(explorer): add UI and code conventions to AGENTS.md Standards identified through review feedback on #7404: - Link display: ↗ suffix rules, parenthesis placement, flex gap pitfall - Token display: TokenImg icon placement, orange link colour via Color.explorer_textActive - Text wrapping: word-break override inside RenderedData - State caching: use jotai/jotai-family instead of module-level Map - Address utilities: AddressKey type, shortenAddress, getAddressKey for lookups - React imports: named imports over React.X prefix - Contract reads: batch with multicall instead of separate readContract calls - Hook organisation: one hook per file under a domain directory - Component shape: *View sub-component pattern for lint compliance - Pre-commit checklist: lint and dead-code removal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * try to fix pnpm lock --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: re-organize Euler wrapper-realted components and update layout to work on narrow vieports * fix: rename Euler-related files * fix: rename Euler-related files * refactor: fix typo in function name * fix: remove jotai-family and revert dependency changes * feat: introduce FiniteMap and use it for Euler's getPublicClient cache * fix: narrow down deps of useEffect in OrderWrapperDetailsItem * fix: fix FiniteMap eviction logic when accessing missing keys * fix: add wrapper to TokenLink --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: fairlight <31534717+fairlighteth@users.noreply.github.com> Co-authored-by: Elena <70885163+elena-zh@users.noreply.github.com> Co-authored-by: Dani Gámez Franco <gmzcodes@outlook.com>
1 parent fd287c8 commit 8fbb975

33 files changed

Lines changed: 1244 additions & 9 deletions

apps/explorer/AGENTS.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
author: agents
33
status: normative
4-
last_reviewed: 2026-03-05
4+
last_reviewed: 2026-05-06
55
---
66

77
# explorer AGENTS.md
88

99
Root rules: [`../../AGENTS.md`](../../AGENTS.md) (global safety, workflow, and verification baseline).
10-
This file: explorer app-specific commands only.
10+
This file: explorer app-specific commands and UI conventions.
1111

1212
## App commands
1313
- Start dev server: `pnpm start:explorer`
@@ -16,3 +16,49 @@ This file: explorer app-specific commands only.
1616
- Lint: `pnpx nx run explorer:lint`
1717
- Test: `pnpx nx run explorer:test`
1818
- Cosmos: `pnpm start:cosmos:explorer`
19+
20+
## Link and address display conventions
21+
22+
- `MUST` append `` to link text for external links (block explorer, etherscan). Do this for address links and contract links.
23+
- `MUST NOT` append `` to token symbol links — token names are never suffixed with the external link indicator.
24+
- `MUST` render parentheses around address links as plain text nodes outside the `<a>` tag, not inside it. E.g. `(<a href={url}>{shortAddress}↗</a>)`.
25+
- `MUST` wrap `(link)` constructs in a single `<span>` when inside a flex container, otherwise `gap` inserts space between the parens and the link text.
26+
- `MUST` use `Color.explorer_textActive` (orange) explicitly for link `color` inside styled components. Do not use `color: inherit` on `a` elements inside styled components — it will inherit the surrounding prose color (grey) instead of the global orange.
27+
28+
## Token display
29+
30+
- `MUST` show a `TokenImg` icon before a linked token symbol. Size the icon to match the surrounding font-size context (e.g. `width: 2rem; height: 2rem` when `font-size: 2rem`).
31+
- Styled containers wrapping token links `MUST` use `display: flex; align-items: center` so the icon and text sit on the same baseline.
32+
33+
## Text wrapping inside RenderedData
34+
35+
- `RenderedData` sets `word-break: break-all`. Any child component that contains prose text `MUST` override this with `word-break: normal` to get word-level wrapping instead of character-level wrapping.
36+
37+
## State caching outside React
38+
39+
- Use `jotai` or `jotai-family` to handle any outside-react caching.
40+
41+
## Address utilities
42+
43+
- `MUST` type address parameters as `AddressKey` from `@cowprotocol/cow-sdk`, not plain `string`.
44+
- `MUST` use `shortenAddress` from `@cowprotocol/common-utils` to abbreviate addresses. `MUST NOT` manually slice (`address.slice(0, 6)…address.slice(-4)`).
45+
- `MUST` use `getAddressKey()` from `@cowprotocol/cow-sdk` when looking up addresses in registries or maps (e.g. `WRAPPERS_BY_ADDRESS`). `MUST NOT` use `address.toLowerCase()` for this purpose (root rule, repeated here for emphasis).
46+
47+
## React imports
48+
49+
- `MUST` import React types directly by name (`ComponentType`, `ReactElement`, `ReactNode`, etc.). `MUST NOT` use `React.ComponentType` / `React.ReactElement` prefix form — it couples the import to the default namespace unnecessarily.
50+
51+
## Contract reads
52+
53+
- `MUST` batch multiple reads from the same contract into a single `multicall` call rather than issuing separate `readContract` calls. For example, fetching `symbol`, `name`, and `decimals` for an ERC-20 should be one `multicall`.
54+
55+
## Hook and component organisation
56+
57+
- `MUST` put each hook in its own file under a domain directory (e.g. `hooks/euler/useVaultAsset.ts`). `MUST NOT` bundle unrelated hooks in one file.
58+
- Wrapper components that render decoded data `SHOULD` extract a `*View` sub-component with all props passed explicitly and no hooks, to satisfy lint complexity limits.
59+
60+
## Before committing
61+
62+
- `SHOULD` run `pnpm nx run explorer:lint` and fix all violations.
63+
- `SHOULD` delete any exports, hooks, or components with no callers introduced or left behind by the change.
64+

apps/explorer/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@
2222
},
2323
"dependencies": {
2424
"@apollo/client": "3.1.5",
25-
"@cowprotocol/cow-sdk": "9.0.2",
26-
"@cowprotocol/sdk-bridging": "4.0.2",
27-
"@cowprotocol/sdk-ethers-v5-adapter": "0.4.4",
28-
"@cowprotocol/sdk-subgraph": "1.0.7",
29-
"@cowprotocol/sdk-contracts-ts": "3.0.1",
30-
"@cowprotocol/sdk-viem-adapter": "0.3.18",
3125
"@cowprotocol/analytics": "workspace:*",
3226
"@cowprotocol/common-const": "workspace:*",
3327
"@cowprotocol/common-hooks": "workspace:*",
3428
"@cowprotocol/common-utils": "workspace:*",
3529
"@cowprotocol/core": "workspace:*",
30+
"@cowprotocol/cow-sdk": "9.0.2",
3631
"@cowprotocol/hook-dapp-lib": "workspace:*",
32+
"@cowprotocol/sdk-bridging": "4.0.2",
33+
"@cowprotocol/sdk-contracts-ts": "3.0.1",
34+
"@cowprotocol/sdk-ethers-v5-adapter": "0.4.4",
35+
"@cowprotocol/sdk-subgraph": "1.0.7",
36+
"@cowprotocol/sdk-viem-adapter": "0.3.18",
3737
"@cowprotocol/types": "workspace:*",
3838
"@cowprotocol/ui": "workspace:*",
3939
"@fortawesome/fontawesome-svg-core": "^6.7.1",

apps/explorer/src/components/orders/DetailsTable/VerboseDetails.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import { OrderSurplusItem } from './items/OrderSurplusItem'
1212
import { SolvedByItem } from './items/SolvedByItem'
1313

1414
import { Order } from '../../../api/operator'
15+
import { DetailRow } from '../../../components/common/DetailRow'
16+
import { DetailsTableTooltips } from '../../../components/orders/DetailsTable/detailsTableTooltips'
1517
import { OrderSolverInfo } from '../../../hooks/useOrderSolver'
1618
import { OrderHooksDetails } from '../OrderHooksDetails'
1719
import { OrderPriceDisplayProps } from '../OrderPriceDisplay'
20+
import { OrderWrapperDetails } from '../OrderWrapperDetails/OrderWrapperDetails.component'
1821

1922
interface VerboseDetailsProps {
2023
order: Order
@@ -88,7 +91,13 @@ export function VerboseDetails({
8891
viewFills={viewFills}
8992
/>
9093
)}
91-
94+
<OrderWrapperDetails fullAppData={fullAppData ?? undefined} order={order}>
95+
{(content) => (
96+
<DetailRow label="Wrappers" tooltipText={DetailsTableTooltips.wrappers}>
97+
{content}
98+
</DetailRow>
99+
)}
100+
</OrderWrapperDetails>
92101
<OrderHooksDetails appData={appData} fullAppData={fullAppData ?? undefined}>
93102
{(content) => <HooksItem>{content}</HooksItem>}
94103
</OrderHooksDetails>

apps/explorer/src/components/orders/DetailsTable/detailsTableTooltips.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const DetailsTableTooltips = {
1919
'The AppData hash for this order. It can denote encoded metadata with info on the app, environment and more, although not all interfaces follow the same pattern. Show more will try to decode that information.',
2020
status: 'The order status is either Signing, Open, Filled, Partially filled, Expired or Cancelled.',
2121
solvedBy: 'The solver that won and executed the settlement for this order.',
22+
wrappers: 'Wrappers are smart contracts that execute as part of this order, as specified in the appData.',
2223
hooks: 'Hooks are interactions before/after order execution.',
2324
submission:
2425
'The date and time at which the order was submitted. The timezone is based on the browser locale settings.',
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ReactElement } from 'react'
2+
3+
import { OrderWrapperDetailsItem } from 'components/orders/OrderWrapperDetails/item/OrderWrapperDetailsItem.component'
4+
5+
import { OrderCtxProvider } from './OrderWrapperDetails.provider'
6+
import * as styledEl from './OrderWrapperDetails.styled'
7+
8+
import { Order } from '../../../api/operator'
9+
import { getOrderWrappers } from '../../../utils/getOrderWrappers'
10+
11+
interface OrderWrapperDetailsProps {
12+
fullAppData: string | undefined
13+
order?: Order
14+
children: (content: ReactElement) => ReactElement
15+
}
16+
17+
export function OrderWrapperDetails({ fullAppData, order, children }: OrderWrapperDetailsProps): ReactElement | null {
18+
const wrappers = getOrderWrappers(fullAppData)
19+
20+
if (wrappers.length === 0) return null
21+
22+
return (
23+
<OrderCtxProvider order={order ?? null}>
24+
{children(
25+
<styledEl.WrapperList>
26+
{wrappers.map((wrapper) => (
27+
<OrderWrapperDetailsItem key={`${wrapper.address}-${wrapper.data ?? ''}`} wrapper={wrapper} />
28+
))}
29+
</styledEl.WrapperList>,
30+
)}
31+
</OrderCtxProvider>
32+
)
33+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createContext, PropsWithChildren, useContext, ReactNode } from 'react'
2+
3+
import { Order } from '../../../api/operator'
4+
5+
export const OrderCtx = createContext<Order | null>(null)
6+
7+
export function useOrderContext(): Order | null {
8+
return useContext(OrderCtx)
9+
}
10+
11+
export interface OrderCtxProviderProps extends PropsWithChildren {
12+
order: Order | null
13+
}
14+
15+
export function OrderCtxProvider({ order, children }: OrderCtxProviderProps): ReactNode {
16+
return <OrderCtx.Provider value={order}>{children}</OrderCtx.Provider>
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Media } from '@cowprotocol/ui'
2+
3+
import styled from 'styled-components/macro'
4+
5+
export const WrapperList = styled.ul`
6+
margin: 0;
7+
padding: 0;
8+
list-style: none;
9+
display: flex;
10+
flex-flow: column wrap;
11+
gap: 24px;
12+
min-width: 420px;
13+
14+
${Media.upToExtraSmall()} {
15+
min-width: 100%;
16+
}
17+
`
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ComponentType, ReactElement, Suspense, useEffect, useState } from 'react'
2+
3+
import { getBlockExplorerUrl, shortenAddress } from '@cowprotocol/common-utils'
4+
import { getAddressKey, SupportedChainId } from '@cowprotocol/cow-sdk'
5+
6+
import * as styledEl from './OrderWrapperDetailsItem.styled'
7+
8+
import { useContractName } from '../../../../hooks/euler'
9+
import { usePopperDefault } from '../../../../hooks/usePopper'
10+
import { useNetworkId } from '../../../../state/network/hooks'
11+
import { ResolvedWrapper } from '../../../../utils/getOrderWrappers'
12+
import { WRAPPERS_BY_ADDRESS } from '../../../../utils/wrappers/wrapperRegistry.constants'
13+
import { Tooltip } from '../../../Tooltip'
14+
15+
const UNKNOWN_WRAPPER_TOOLTIP =
16+
'Explorer does not recognize this wrapper address yet, so it cannot decode wrapper-specific details. This may be a custom wrapper or a supported wrapper missing from the registry.'
17+
18+
export function OrderWrapperDetailsItem({ wrapper }: { wrapper: ResolvedWrapper }): ReactElement {
19+
const [Component, setComponent] = useState<ComponentType<{ data: string }> | null>(null)
20+
const { info, address, data, loadComponent, isOmittable } = wrapper
21+
const chainId = useNetworkId() as SupportedChainId | null
22+
const isUnknown = !WRAPPERS_BY_ADDRESS[getAddressKey(address)]
23+
const contractName = useContractName(isUnknown ? address : '')
24+
const { tooltipProps, targetProps } = usePopperDefault<HTMLLIElement>('top')
25+
26+
useEffect(() => {
27+
if (!data || !loadComponent) return
28+
loadComponent().then((Comp) => setComponent(() => Comp))
29+
}, [data, loadComponent])
30+
31+
const addressUrl = chainId ? getBlockExplorerUrl(chainId, 'contract', address) : undefined
32+
const shortAddress = shortenAddress(address)
33+
const displayName = isUnknown ? (contractName ?? shortAddress) : info.name
34+
35+
return (
36+
<>
37+
<styledEl.WrapperItem {...(isUnknown ? targetProps : {})}>
38+
<styledEl.WrapperHeader>
39+
{info.image && <img src={info.image} alt={info.name} />}
40+
<strong>{displayName}</strong>
41+
{addressUrl ? (
42+
<span>
43+
(
44+
<a href={addressUrl} target="_blank" rel="noopener noreferrer" title={address}>
45+
{shortAddress}
46+
</a>
47+
)
48+
</span>
49+
) : (
50+
<span title={address}>({shortAddress})</span>
51+
)}
52+
{isOmittable && <em>(omittable)</em>}
53+
</styledEl.WrapperHeader>
54+
{Component && data && (
55+
<styledEl.RenderedData>
56+
<Suspense fallback={<span>Loading…</span>}>
57+
<Component data={data} />
58+
</Suspense>
59+
</styledEl.RenderedData>
60+
)}
61+
</styledEl.WrapperItem>
62+
{isUnknown && <Tooltip {...tooltipProps}>{UNKNOWN_WRAPPER_TOOLTIP}</Tooltip>}
63+
</>
64+
)
65+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Color } from '@cowprotocol/ui'
2+
3+
import styled from 'styled-components/macro'
4+
5+
export const WrapperItem = styled.li`
6+
display: flex;
7+
flex-flow: column wrap;
8+
gap: 12px;
9+
`
10+
11+
export const WrapperHeader = styled.p`
12+
margin: 0;
13+
display: flex;
14+
align-items: center;
15+
gap: 0.5rem;
16+
flex-wrap: wrap;
17+
18+
> img {
19+
width: 2rem;
20+
height: 2rem;
21+
}
22+
`
23+
24+
export const RenderedData = styled.div`
25+
border: 1px solid ${Color.explorer_tableRowBorder};
26+
border-radius: 0.5rem;
27+
background: ${Color.explorer_tableRowBorder};
28+
overflow: auto;
29+
word-break: break-all;
30+
line-height: 1.5;
31+
32+
table {
33+
border-collapse: collapse;
34+
width: 100%;
35+
}
36+
37+
td {
38+
padding: 0.2rem 1rem 0.2rem 0;
39+
vertical-align: top;
40+
41+
&:first-child {
42+
font-weight: 500;
43+
white-space: nowrap;
44+
}
45+
}
46+
47+
code {
48+
font-size: 1.2rem;
49+
}
50+
`
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const ERC4626_ABI = [
2+
{ name: 'asset', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
3+
{
4+
name: 'convertToAssets',
5+
type: 'function',
6+
stateMutability: 'view',
7+
inputs: [{ name: 'shares', type: 'uint256' }],
8+
outputs: [{ type: 'uint256' }],
9+
},
10+
] as const
11+
12+
export const ERC20_ABI = [
13+
{ name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
14+
{ name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
15+
{ name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] },
16+
] as const

0 commit comments

Comments
 (0)