Skip to content

Commit 5a5dedc

Browse files
authored
UX: Add visual indicators for paused CCIP tokens (capacity ≤ 1) (#2767)
* grey paused tokens * update text * nit
1 parent 09c4fb0 commit 5a5dedc

File tree

5 files changed

+221
-64
lines changed

5 files changed

+221
-64
lines changed

src/components/CCIP/Drawer/LaneDrawer.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Address from "~/components/AddressReact.tsx"
22
import "../Tables/Table.css"
33
import { Environment, LaneConfig, LaneFilter, Version } from "~/config/data/ccip/types.ts"
44
import { getNetwork, getTokenData } from "~/config/data/ccip/data.ts"
5-
import { displayCapacity, determineTokenMechanism } from "~/config/data/ccip/utils.ts"
5+
import { displayCapacity, determineTokenMechanism, isTokenPaused } from "~/config/data/ccip/utils.ts"
66
import { useState } from "react"
77
import LaneDetailsHero from "../ChainHero/LaneDetailsHero.tsx"
88
import { getExplorerAddressUrl, getTokenIconUrl, fallbackTokenIconUrl } from "~/features/utils/index.ts"
@@ -136,11 +136,22 @@ function LaneDrawer({
136136
})
137137
if (!Object.keys(data).length) return null
138138
const logo = getTokenIconUrl(token)
139+
140+
// Check if token is paused
141+
const tokenPaused = isTokenPaused(
142+
data[sourceNetwork.key].decimals,
143+
lane.supportedTokens?.[token]?.rateLimiterConfig?.[
144+
inOutbound === LaneFilter.Inbound ? "in" : "out"
145+
]
146+
)
147+
139148
return (
140-
<tr key={index}>
149+
<tr key={index} className={tokenPaused ? "ccip-table__row--paused" : ""}>
141150
<td>
142151
<a href={`/ccip/directory/${environment}/token/${token}`}>
143-
<div className="ccip-table__network-name">
152+
<div
153+
className={`ccip-table__network-name ${tokenPaused ? "ccip-table__network-name--paused" : ""}`}
154+
>
144155
<img
145156
src={logo}
146157
alt={`${token} logo`}
@@ -151,6 +162,11 @@ function LaneDrawer({
151162
}}
152163
/>
153164
{token}
165+
{tokenPaused && (
166+
<span className="ccip-table__paused-badge" title="Transfers are currently paused">
167+
⏸️
168+
</span>
169+
)}
154170
</div>
155171
</a>
156172
</td>

src/components/CCIP/Drawer/TokenDrawer.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getTokenData,
1515
LaneConfig,
1616
} from "~/config/data/ccip/index.ts"
17+
import { isTokenPaused } from "~/config/data/ccip/utils.ts"
1718
import { useState } from "react"
1819
import { ChainType, ExplorerInfo, SupportedChain } from "~/config/index.ts"
1920
import LaneDrawer from "../Drawer/LaneDrawer.tsx"
@@ -214,11 +215,19 @@ function TokenDrawer({
214215
.map(({ networkDetails, laneData, destinationChain, destinationPoolType }) => {
215216
if (!laneData || !networkDetails) return null
216217

218+
// Check if token is paused on this lane
219+
const tokenPaused = isTokenPaused(
220+
network.tokenDecimals,
221+
destinationLanes[destinationChain].rateLimiterConfig?.[
222+
inOutbound === LaneFilter.Inbound ? "in" : "out"
223+
]
224+
)
225+
217226
return (
218-
<tr key={networkDetails.name}>
227+
<tr key={networkDetails.name} className={tokenPaused ? "ccip-table__row--paused" : ""}>
219228
<td>
220229
<div
221-
className="ccip-table__network-name"
230+
className={`ccip-table__network-name ${tokenPaused ? "ccip-table__network-name--paused" : ""}`}
222231
role="button"
223232
onClick={() => {
224233
drawerContentStore.set(() => (
@@ -239,6 +248,11 @@ function TokenDrawer({
239248
>
240249
<img src={networkDetails?.logo} alt={networkDetails?.name} className="ccip-table__logo" />
241250
{networkDetails?.name}
251+
{tokenPaused && (
252+
<span className="ccip-table__paused-badge" title="Transfers are currently paused">
253+
⏸️
254+
</span>
255+
)}
242256
</div>
243257
</td>
244258
<td>

src/components/CCIP/Tables/Table.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,48 @@
253253
justify-content: center;
254254
}
255255
}
256+
257+
/* Paused token styles */
258+
.ccip-table__row--paused {
259+
opacity: 0.6;
260+
background-color: var(--gray-50);
261+
}
262+
263+
.ccip-table__row--paused:hover {
264+
opacity: 0.8;
265+
background-color: var(--gray-100);
266+
}
267+
268+
.ccip-table__network-name--paused {
269+
color: var(--gray-400) !important;
270+
filter: grayscale(0.7);
271+
}
272+
273+
.ccip-table__network-name--paused img {
274+
filter: grayscale(0.8) opacity(0.7);
275+
}
276+
277+
.ccip-table__paused-badge {
278+
margin-left: var(--space-2x);
279+
font-size: 12px;
280+
opacity: 0.8;
281+
display: inline-flex;
282+
align-items: center;
283+
cursor: help;
284+
}
285+
286+
/* Ensure paused tokens still allow interaction for tooltips */
287+
.ccip-table__row--paused .ccip-table__network-name {
288+
pointer-events: auto;
289+
}
290+
291+
/* Additional styling for capacity and rate cells in paused state */
292+
.ccip-table__row--paused td {
293+
color: var(--gray-400);
294+
}
295+
296+
.ccip-table__row--paused a {
297+
color: var(--gray-400);
298+
text-decoration: none;
299+
pointer-events: none;
300+
}

src/components/CCIP/Tables/TokenChainsTable.tsx

Lines changed: 73 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Address from "~/components/AddressReact.tsx"
22
import "./Table.css"
33
import { drawerContentStore } from "../Drawer/drawerStore.ts"
44
import { Environment, SupportedTokenConfig, tokenPoolDisplay, PoolType } from "~/config/data/ccip/index.ts"
5+
import { areAllLanesPaused } from "~/config/data/ccip/utils.ts"
56
import { ChainType, ExplorerInfo } from "~/config/types.ts"
67
import TableSearchInput from "./TableSearchInput.tsx"
78
import { useState } from "react"
@@ -64,66 +65,79 @@ function TokenChainsTable({ networks, token, lanes, environment }: TableProps) {
6465
<tbody>
6566
{networks
6667
?.filter((network) => network.name.toLowerCase().includes(search.toLowerCase()))
67-
.map((network, index) => (
68-
<tr key={index}>
69-
<td>
70-
<div
71-
className="ccip-table__network-name"
72-
role="button"
73-
onClick={() => {
74-
drawerContentStore.set(() => (
75-
<TokenDrawer
76-
token={token}
77-
network={network}
78-
destinationLanes={lanes[network.key]}
79-
environment={environment}
68+
.map((network, index) => {
69+
// Check if all lanes for this token on this network are paused
70+
const allLanesPaused = areAllLanesPaused(network.tokenDecimals, lanes[network.key] || {})
71+
72+
return (
73+
<tr key={index} className={allLanesPaused ? "ccip-table__row--paused" : ""}>
74+
<td>
75+
<div
76+
className={`ccip-table__network-name ${allLanesPaused ? "ccip-table__network-name--paused" : ""}`}
77+
role="button"
78+
onClick={() => {
79+
drawerContentStore.set(() => (
80+
<TokenDrawer
81+
token={token}
82+
network={network}
83+
destinationLanes={lanes[network.key]}
84+
environment={environment}
85+
/>
86+
))
87+
}}
88+
>
89+
<span className="ccip-table__logoContainer">
90+
<img
91+
src={network.logo}
92+
alt={network.name}
93+
className="ccip-table__logo"
94+
onError={({ currentTarget }) => {
95+
currentTarget.onerror = null // prevents looping
96+
currentTarget.src = fallbackTokenIconUrl
97+
}}
98+
/>
99+
<img
100+
src={network.tokenLogo}
101+
alt={network.tokenId}
102+
className="ccip-table__smallLogo"
103+
onError={({ currentTarget }) => {
104+
currentTarget.onerror = null // prevents looping
105+
currentTarget.src = fallbackTokenIconUrl
106+
}}
80107
/>
81-
))
82-
}}
83-
>
84-
<span className="ccip-table__logoContainer">
85-
<img
86-
src={network.logo}
87-
alt={network.name}
88-
className="ccip-table__logo"
89-
onError={({ currentTarget }) => {
90-
currentTarget.onerror = null // prevents looping
91-
currentTarget.src = fallbackTokenIconUrl
92-
}}
93-
/>
94-
<img
95-
src={network.tokenLogo}
96-
alt={network.tokenId}
97-
className="ccip-table__smallLogo"
98-
onError={({ currentTarget }) => {
99-
currentTarget.onerror = null // prevents looping
100-
currentTarget.src = fallbackTokenIconUrl
101-
}}
102-
/>
103-
</span>
104-
{network.name}
105-
</div>
106-
</td>
107-
<td>{network.tokenName}</td>
108-
<td>{network.tokenSymbol}</td>
109-
<td>{network.tokenDecimals}</td>
110-
<td data-clipboard-type="token">
111-
<Address
112-
contractUrl={getExplorerAddressUrl(network.explorer)(network.tokenAddress)}
113-
address={network.tokenAddress}
114-
endLength={6}
115-
/>
116-
</td>
117-
<td>{tokenPoolDisplay(network.tokenPoolType)}</td>
118-
<td data-clipboard-type="token-pool">
119-
<Address
120-
contractUrl={getExplorerAddressUrl(network.explorer)(network.tokenPoolAddress)}
121-
address={network.tokenPoolAddress}
122-
endLength={6}
123-
/>
124-
</td>
125-
</tr>
126-
))}
108+
</span>
109+
{network.name}
110+
{allLanesPaused && (
111+
<span
112+
className="ccip-table__paused-badge"
113+
title="All transfers from this network are currently paused"
114+
>
115+
⏸️
116+
</span>
117+
)}
118+
</div>
119+
</td>
120+
<td>{network.tokenName}</td>
121+
<td>{network.tokenSymbol}</td>
122+
<td>{network.tokenDecimals}</td>
123+
<td data-clipboard-type="token">
124+
<Address
125+
contractUrl={getExplorerAddressUrl(network.explorer)(network.tokenAddress)}
126+
address={network.tokenAddress}
127+
endLength={6}
128+
/>
129+
</td>
130+
<td>{tokenPoolDisplay(network.tokenPoolType)}</td>
131+
<td data-clipboard-type="token-pool">
132+
<Address
133+
contractUrl={getExplorerAddressUrl(network.explorer)(network.tokenPoolAddress)}
134+
address={network.tokenPoolAddress}
135+
endLength={6}
136+
/>
137+
</td>
138+
</tr>
139+
)
140+
})}
127141
</tbody>
128142
</table>
129143
<div className="ccip-table__notFound">

src/config/data/ccip/utils.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,71 @@ export const displayRate = (capacity: string, rate: string, symbol: string, deci
189189
maxThroughput: `Refills from 0 to ${commify(cleanedCapacity)} ${symbol} in ${displayTime}`,
190190
}
191191
}
192+
193+
// ==============================
194+
// UTILITY FUNCTIONS FOR TOKEN STATUS
195+
// ==============================
196+
197+
/**
198+
* Determines if a token is paused based on its rate limiter configuration
199+
* A token is considered paused if its capacity is <= 1
200+
*
201+
*/
202+
export const isTokenPaused = (decimals = 18, rateLimiterConfig?: RateLimiterConfig): boolean => {
203+
if (!rateLimiterConfig?.isEnabled) {
204+
return false // N/A tokens are not considered paused
205+
}
206+
207+
const capacity = rateLimiterConfig?.capacity || "0"
208+
209+
try {
210+
// Convert to BigInt for precise comparison
211+
const capacityBigInt = BigInt(capacity)
212+
// Calculate threshold: 1 token in smallest units = 10^decimals
213+
const oneTokenInSmallestUnits = BigInt(10) ** BigInt(decimals)
214+
215+
// Direct BigInt comparison - no floating point risks
216+
return capacityBigInt <= oneTokenInSmallestUnits
217+
} catch (error) {
218+
// If capacity is not a valid number, treat as paused for safety
219+
console.warn(`Invalid capacity value for rate limiter: ${capacity}`, error)
220+
return true
221+
}
222+
}
223+
224+
/**
225+
* Determines if all outbound lanes for a token from a specific network are paused
226+
* Used to grey out network rows in the token view when all destination lanes are paused
227+
*
228+
* @example
229+
* // Example: LBTC (8 decimals) on Ink with only one destination lane that has capacity "2"
230+
* const destinationLanes = {
231+
* "ethereum-mainnet-ink-1": {
232+
* rateLimiterConfig: {
233+
* out: {
234+
* capacity: "2",
235+
* isEnabled: true,
236+
* rate: "1"
237+
* }
238+
* }
239+
* }
240+
* }
241+
* areAllLanesPaused(8, destinationLanes) // returns true (2 ≤ 10^8)
242+
*/
243+
export const areAllLanesPaused = (
244+
decimals = 18,
245+
destinationLanes: { [destinationChain: string]: { rateLimiterConfig?: { out?: RateLimiterConfig } } }
246+
): boolean => {
247+
const laneKeys = Object.keys(destinationLanes)
248+
249+
// If no lanes exist, don't consider it paused
250+
if (laneKeys.length === 0) {
251+
return false
252+
}
253+
254+
// Check if ALL outbound lanes are paused
255+
return laneKeys.every((destinationChain) => {
256+
const outboundConfig = destinationLanes[destinationChain]?.rateLimiterConfig?.out
257+
return isTokenPaused(decimals, outboundConfig)
258+
})
259+
}

0 commit comments

Comments
 (0)