Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d3e2968
feat (explorer): add wrapper designation to order details
kaze-cow Apr 27, 2026
7c5fc42
remove references to orderflow
kaze-cow Apr 27, 2026
37ae0e9
fix build error
kaze-cow Apr 27, 2026
014593d
Merge branch 'develop' into explorer-wrapper-designation
fairlighteth Apr 27, 2026
a7134c2
Merge branch 'develop' into explorer-wrapper-designation
elena-zh Apr 28, 2026
b9c2b73
feat: improve explorer wrapper details display and code quality
kaze-cow May 6, 2026
5d6a348
refactor: split eulerEvc.tsx into per-wrapper files
kaze-cow May 6, 2026
5ae0400
refactor: move Euler vault hooks to hooks/euler/
kaze-cow May 6, 2026
43e4b07
refactor: use AddressKey type, shortenAddress, direct React imports, …
kaze-cow May 6, 2026
a9eb4df
Merge branch 'develop' into explorer-wrapper-designation
kaze-cow May 6, 2026
46977ee
add import for detail row/tooltips
kaze-cow May 6, 2026
743c3db
Merge branch 'develop' into explorer-wrapper-designation
elena-zh May 6, 2026
62109a8
use jotai for state management, and remove unnecessary orderflow file…
kaze-cow May 6, 2026
cccc73d
Merge branch 'explorer-wrapper-designation' of https://github.com/cow…
kaze-cow May 6, 2026
aff025c
lint fix and resolve deprecated dependency
kaze-cow May 6, 2026
1c730ce
fix design issues as suggested
kaze-cow May 6, 2026
577b0dc
fix word wrap
kaze-cow May 6, 2026
aca39df
Merge branch 'develop' into explorer-wrapper-designation
kaze-cow May 6, 2026
0e20fbf
try to fix pnpm lock
kaze-cow May 6, 2026
b53c40c
Merge branch 'explorer-wrapper-designation' of https://github.com/cow…
kaze-cow May 6, 2026
cafa54c
Merge branch 'develop' into explorer-wrapper-designation
elena-zh May 6, 2026
9c61fc6
docs(explorer): add UI and code conventions to AGENTS.md (#7459)
kaze-cow May 7, 2026
06e3e68
Merge branch 'develop' into explorer-wrapper-designation
Danziger May 8, 2026
9fa04c7
fix: re-organize Euler wrapper-realted components and update layout t…
Danziger May 8, 2026
b1fdb7c
fix: rename Euler-related files
Danziger May 8, 2026
551a014
fix: rename Euler-related files
Danziger May 8, 2026
d463bca
refactor: fix typo in function name
Danziger May 8, 2026
cf5942b
fix: remove jotai-family and revert dependency changes
Danziger May 8, 2026
7260930
feat: introduce FiniteMap and use it for Euler's getPublicClient cache
Danziger May 8, 2026
1d87ea3
fix: narrow down deps of useEffect in OrderWrapperDetailsItem
Danziger May 8, 2026
745e188
fix: fix FiniteMap eviction logic when accessing missing keys
Danziger May 8, 2026
f6dc090
fix: add wrapper to TokenLink
Danziger May 8, 2026
a50bd81
Merge branch 'develop' into explorer-wrapper-designation
fairlighteth May 11, 2026
60cfe85
Merge branch 'develop' into explorer-wrapper-designation
fairlighteth May 11, 2026
787c8bc
Merge branch 'develop' into explorer-wrapper-designation
fairlighteth May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions apps/explorer/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
author: agents
status: normative
last_reviewed: 2026-03-05
last_reviewed: 2026-05-06
---

# explorer AGENTS.md

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

## App commands
- Start dev server: `pnpm start:explorer`
Expand All @@ -16,3 +16,49 @@ This file: explorer app-specific commands only.
- Lint: `pnpx nx run explorer:lint`
- Test: `pnpx nx run explorer:test`
- Cosmos: `pnpm start:cosmos:explorer`

## Link and address display conventions

- `MUST` append `↗` to link text for external links (block explorer, etherscan). Do this for address links and contract links.
- `MUST NOT` append `↗` to token symbol links — token names are never suffixed with the external link indicator.
- `MUST` render parentheses around address links as plain text nodes outside the `<a>` tag, not inside it. E.g. `(<a href={url}>{shortAddress}↗</a>)`.
- `MUST` wrap `(link)` constructs in a single `<span>` when inside a flex container, otherwise `gap` inserts space between the parens and the link text.
- `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.

## Token display

- `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`).
- Styled containers wrapping token links `MUST` use `display: flex; align-items: center` so the icon and text sit on the same baseline.

## Text wrapping inside RenderedData

- `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.

## State caching outside React

- Use `jotai` or `jotai-family` to handle any outside-react caching.

## Address utilities

- `MUST` type address parameters as `AddressKey` from `@cowprotocol/cow-sdk`, not plain `string`.
- `MUST` use `shortenAddress` from `@cowprotocol/common-utils` to abbreviate addresses. `MUST NOT` manually slice (`address.slice(0, 6)…address.slice(-4)`).
- `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).

## React imports

- `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.

## Contract reads

- `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`.

## Hook and component organisation

- `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.
- 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.

## Before committing

- `SHOULD` run `pnpm nx run explorer:lint` and fix all violations.
- `SHOULD` delete any exports, hooks, or components with no callers introduced or left behind by the change.

12 changes: 6 additions & 6 deletions apps/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@
},
"dependencies": {
"@apollo/client": "3.1.5",
"@cowprotocol/cow-sdk": "9.0.2",
"@cowprotocol/sdk-bridging": "4.0.2",
"@cowprotocol/sdk-ethers-v5-adapter": "0.4.4",
"@cowprotocol/sdk-subgraph": "1.0.7",
"@cowprotocol/sdk-contracts-ts": "3.0.1",
"@cowprotocol/sdk-viem-adapter": "0.3.18",
"@cowprotocol/analytics": "workspace:*",
"@cowprotocol/common-const": "workspace:*",
"@cowprotocol/common-hooks": "workspace:*",
"@cowprotocol/common-utils": "workspace:*",
"@cowprotocol/core": "workspace:*",
"@cowprotocol/cow-sdk": "9.0.2",
"@cowprotocol/hook-dapp-lib": "workspace:*",
"@cowprotocol/sdk-bridging": "4.0.2",
"@cowprotocol/sdk-contracts-ts": "3.0.1",
"@cowprotocol/sdk-ethers-v5-adapter": "0.4.4",
"@cowprotocol/sdk-subgraph": "1.0.7",
"@cowprotocol/sdk-viem-adapter": "0.3.18",
"@cowprotocol/types": "workspace:*",
"@cowprotocol/ui": "workspace:*",
"@fortawesome/fontawesome-svg-core": "^6.7.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import { OrderSurplusItem } from './items/OrderSurplusItem'
import { SolvedByItem } from './items/SolvedByItem'

import { Order } from '../../../api/operator'
import { DetailRow } from '../../../components/common/DetailRow'
import { DetailsTableTooltips } from '../../../components/orders/DetailsTable/detailsTableTooltips'
import { OrderSolverInfo } from '../../../hooks/useOrderSolver'
import { OrderHooksDetails } from '../OrderHooksDetails'
import { OrderPriceDisplayProps } from '../OrderPriceDisplay'
import { OrderWrapperDetails } from '../OrderWrapperDetails/OrderWrapperDetails.component'

interface VerboseDetailsProps {
order: Order
Expand Down Expand Up @@ -88,7 +91,13 @@ export function VerboseDetails({
viewFills={viewFills}
/>
)}

<OrderWrapperDetails fullAppData={fullAppData ?? undefined} order={order}>
{(content) => (
<DetailRow label="Wrappers" tooltipText={DetailsTableTooltips.wrappers}>
{content}
</DetailRow>
)}
</OrderWrapperDetails>
<OrderHooksDetails appData={appData} fullAppData={fullAppData ?? undefined}>
{(content) => <HooksItem>{content}</HooksItem>}
</OrderHooksDetails>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const DetailsTableTooltips = {
'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.',
status: 'The order status is either Signing, Open, Filled, Partially filled, Expired or Cancelled.',
solvedBy: 'The solver that won and executed the settlement for this order.',
wrappers: 'Wrappers are smart contracts that execute as part of this order, as specified in the appData.',
hooks: 'Hooks are interactions before/after order execution.',
submission:
'The date and time at which the order was submitted. The timezone is based on the browser locale settings.',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ReactElement } from 'react'

import { OrderWrapperDetailsItem } from 'components/orders/OrderWrapperDetails/item/OrderWrapperDetailsItem.component'

import { OrderCtxProvider } from './OrderWrapperDetails.provider'
import * as styledEl from './OrderWrapperDetails.styled'

import { Order } from '../../../api/operator'
import { getOrderWrappers } from '../../../utils/getOrderWrappers'

interface OrderWrapperDetailsProps {
fullAppData: string | undefined
order?: Order
children: (content: ReactElement) => ReactElement
}

export function OrderWrapperDetails({ fullAppData, order, children }: OrderWrapperDetailsProps): ReactElement | null {
const wrappers = getOrderWrappers(fullAppData)

if (wrappers.length === 0) return null

return (
<OrderCtxProvider order={order ?? null}>
{children(
<styledEl.WrapperList>
{wrappers.map((wrapper) => (
<OrderWrapperDetailsItem key={`${wrapper.address}-${wrapper.data ?? ''}`} wrapper={wrapper} />
))}
</styledEl.WrapperList>,
)}
</OrderCtxProvider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createContext, PropsWithChildren, useContext, ReactNode } from 'react'

import { Order } from '../../../api/operator'

export const OrderCtx = createContext<Order | null>(null)

export function useOrderContext(): Order | null {
return useContext(OrderCtx)
}

export interface OrderCtxProviderProps extends PropsWithChildren {
order: Order | null
}

export function OrderCtxProvider({ order, children }: OrderCtxProviderProps): ReactNode {
return <OrderCtx.Provider value={order}>{children}</OrderCtx.Provider>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Media } from '@cowprotocol/ui'

import styled from 'styled-components/macro'

export const WrapperList = styled.ul`
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-flow: column wrap;
gap: 24px;
min-width: 420px;

${Media.upToExtraSmall()} {
min-width: 100%;
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ComponentType, ReactElement, Suspense, useEffect, useState } from 'react'

import { getBlockExplorerUrl, shortenAddress } from '@cowprotocol/common-utils'
import { getAddressKey, SupportedChainId } from '@cowprotocol/cow-sdk'

import * as styledEl from './OrderWrapperDetailsItem.styled'

import { useContractName } from '../../../../hooks/euler'
import { usePopperDefault } from '../../../../hooks/usePopper'
import { useNetworkId } from '../../../../state/network/hooks'
import { ResolvedWrapper } from '../../../../utils/getOrderWrappers'
import { WRAPPERS_BY_ADDRESS } from '../../../../utils/wrappers/wrapperRegistry.constants'
import { Tooltip } from '../../../Tooltip'

const UNKNOWN_WRAPPER_TOOLTIP =
'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.'

export function OrderWrapperDetailsItem({ wrapper }: { wrapper: ResolvedWrapper }): ReactElement {
const [Component, setComponent] = useState<ComponentType<{ data: string }> | null>(null)
const { info, address, data, loadComponent, isOmittable } = wrapper
const chainId = useNetworkId() as SupportedChainId | null
const isUnknown = !WRAPPERS_BY_ADDRESS[getAddressKey(address)]
const contractName = useContractName(isUnknown ? address : '')
const { tooltipProps, targetProps } = usePopperDefault<HTMLLIElement>('top')

useEffect(() => {
if (!data || !loadComponent) return
loadComponent().then((Comp) => setComponent(() => Comp))
}, [data, loadComponent])

const addressUrl = chainId ? getBlockExplorerUrl(chainId, 'contract', address) : undefined
const shortAddress = shortenAddress(address)
const displayName = isUnknown ? (contractName ?? shortAddress) : info.name

return (
<>
<styledEl.WrapperItem {...(isUnknown ? targetProps : {})}>
<styledEl.WrapperHeader>
{info.image && <img src={info.image} alt={info.name} />}
<strong>{displayName}</strong>
{addressUrl ? (
<span>
(
<a href={addressUrl} target="_blank" rel="noopener noreferrer" title={address}>
{shortAddress}↗
</a>
)
</span>
) : (
<span title={address}>({shortAddress})</span>
)}
{isOmittable && <em>(omittable)</em>}
</styledEl.WrapperHeader>
{Component && data && (
<styledEl.RenderedData>
<Suspense fallback={<span>Loading…</span>}>
<Component data={data} />
</Suspense>
</styledEl.RenderedData>
)}
</styledEl.WrapperItem>
{isUnknown && <Tooltip {...tooltipProps}>{UNKNOWN_WRAPPER_TOOLTIP}</Tooltip>}
</>
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OrderWrapperDetails.styled.tsx

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Color } from '@cowprotocol/ui'

import styled from 'styled-components/macro'

export const WrapperItem = styled.li`
display: flex;
flex-flow: column wrap;
gap: 12px;
`

export const WrapperHeader = styled.p`
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;

> img {
width: 2rem;
height: 2rem;
}
`

export const RenderedData = styled.div`
border: 1px solid ${Color.explorer_tableRowBorder};
border-radius: 0.5rem;
background: ${Color.explorer_tableRowBorder};
overflow: auto;
word-break: break-all;
line-height: 1.5;

table {
border-collapse: collapse;
width: 100%;
}

td {
padding: 0.2rem 1rem 0.2rem 0;
vertical-align: top;

&:first-child {
font-weight: 500;
white-space: nowrap;
}
}

code {
font-size: 1.2rem;
}
`
16 changes: 16 additions & 0 deletions apps/explorer/src/hooks/euler/abis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const ERC4626_ABI = [
{ name: 'asset', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
{
name: 'convertToAssets',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'shares', type: 'uint256' }],
outputs: [{ type: 'uint256' }],
},
] as const

export const ERC20_ABI = [
{ name: 'symbol', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
{ name: 'name', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'string' }] },
{ name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] },
] as const
22 changes: 22 additions & 0 deletions apps/explorer/src/hooks/euler/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RPC_URLS, VIEM_CHAINS } from '@cowprotocol/common-const'
import { FiniteMap } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'

import { createPublicClient, http } from 'viem'

type PublicClient = ReturnType<typeof createPublicClient>

const publicClientsCache = new FiniteMap<SupportedChainId, PublicClient>(16)

export function getPublicClient(chainId: SupportedChainId): PublicClient {
const cached = publicClientsCache.get(chainId)
if (cached) return cached

const client = createPublicClient({
chain: VIEM_CHAINS[chainId],
transport: http(RPC_URLS[chainId]),
})

publicClientsCache.set(chainId, client)
return client
}
4 changes: 4 additions & 0 deletions apps/explorer/src/hooks/euler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type { VaultAsset } from './useVaultAsset'
export { useVaultAsset } from './useVaultAsset'
export { useConvertToAssets } from './useConvertToAssets'
export { useContractName } from './useContractName'
27 changes: 27 additions & 0 deletions apps/explorer/src/hooks/euler/useContractName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AddressKey, SupportedChainId } from '@cowprotocol/cow-sdk'

import useSWR from 'swr'

import { ERC20_ABI } from './abis'
import { getPublicClient } from './client'

import { useNetworkId } from '../../state/network/hooks'

export function useContractName(address: AddressKey): string | undefined {
const chainId = useNetworkId() as SupportedChainId | null

const { data } = useSWR<string>(
chainId && address ? `contract-name:${chainId}:${address}` : null,
() => {
if (!chainId || !address) throw new Error('missing chainId or address')
return getPublicClient(chainId).readContract({
address: address as `0x${string}`,
abi: ERC20_ABI,
functionName: 'name',
}) as Promise<string>
},
{ onError: () => undefined },
)

return data
}
Loading
Loading