Skip to content

Add search for transferable tokens table #1969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Environment,
Version,
PoolType,
LaneConfig,
type LaneConfig,
determineTokenMechanism,
TokenMechanism,
} from "@config/data/ccip/"
Expand All @@ -14,6 +14,7 @@ import BigNumber from "bignumber.js"
import { utils } from "ethers"
import { getExplorer, getExplorerAddressUrl } from "@features/utils"
import { Tooltip } from "@features/common/Tooltip"
import TokenSearch from "./TokenSearch"

type ConfigProps = {
laneConfig: LaneConfig
Expand Down Expand Up @@ -242,89 +243,7 @@ if (supportedTokens) {
<p>
<strong>Transferable tokens</strong>
</p>
<div style="overflow-x: auto;width: 100%">
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Token&nbsp;Address</th>
<th>Decimals</th>
<th>
<Tooltip
tip="Token pool mechanism: Lock & Mint, Burn & Mint, Lock & Unlock, Burn & Unlock"
label="Mechanism"
style={{ marginTop: "0", minWidth: "7em", maxWidth: "10em" }}
labelStyle={{ fontWeight: "bold", whiteSpace: "nowrap" }}
client:load
/>
</th>
<th>
<Tooltip
tip="Maximum amount per transaction"
label="Rate Limit Capacity"
style={{ marginTop: "0", minWidth: "11em", maxWidth: "15em" }}
labelStyle={{ fontWeight: "bold", whiteSpace: "nowrap" }}
client:load
/>
</th>
<th>
<Tooltip
tip="Rate at which available capacity is replenished"
label="Rate Limit Refill Rate"
style={{ marginTop: "0", minWidth: "12em", maxWidth: "15em" }}
labelStyle={{ fontWeight: "bold", whiteSpace: "nowrap" }}
client:load
/>
</th>
</tr>
</thead>
<tbody>
{tokensWithExtraInfo.map((tokenWithExtraInfo) => {
return (
<tr>
<td style={{ whiteSpace: "nowrap" }}>{tokenWithExtraInfo.token}</td>
<td style={{ whiteSpace: "nowrap" }}>
<Address
contractUrl={getExplorerAddressUrl(explorerUrl)(tokenWithExtraInfo.address)}
endLength={4}
/>
</td>
<td style={{ whiteSpace: "nowrap" }}>{tokenWithExtraInfo.decimals}</td>
<td style={{ whiteSpace: "nowrap" }}>{tokenWithExtraInfo.poolMechanism}</td>
<td style={{ whiteSpace: "nowrap" }}>
{tokenWithExtraInfo.rateLimiterConfig?.isEnabled
? display(tokenWithExtraInfo.rateLimiterConfig.capacity, tokenWithExtraInfo.decimals) +
" " +
tokenWithExtraInfo.token
: "N/A"}
</td>
<td>
{tokenWithExtraInfo.rateLimiterConfig?.isEnabled
? (() => {
const { rateSecond, maxThroughput } = displayRate(
tokenWithExtraInfo.rateLimiterConfig.capacity,
tokenWithExtraInfo.rateLimiterConfig.rate,
tokenWithExtraInfo.token,
tokenWithExtraInfo.decimals
)

return (
<Tooltip
tip={maxThroughput}
label={rateSecond}
style={{ marginTop: "0", whiteSpace: "nowrap", minWidth: "15em", maxWidth: "15em" }}
client:load
/>
)
})()
: "N/A"}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
<TokenSearch tokens={tokensWithExtraInfo} sourceChain={sourceChain} client:load />
</>
</>
) : (
Expand Down
215 changes: 215 additions & 0 deletions src/features/ccip/components/supported-networks/TokenSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/** @jsxImportSource preact */
import { useState } from "preact/hooks"
import { h, FunctionComponent } from "preact"
import BigNumber from "bignumber.js"
import { utils } from "ethers"
import { SupportedChain } from "@config"
import { getExplorer, getExplorerAddressUrl } from "@features/utils"
import Address from "@components/Address"
import { SimplePreactTooltip } from "@features/common/Tooltip"

interface TokenExtraInfo {
token: string
address: string
rateLimiterConfig: {
capacity: string
isEnabled: boolean
rate: string
}
decimals: number
poolMechanism?: string
}

interface TokenSearchProps {
tokens: TokenExtraInfo[]
sourceChain: SupportedChain
}

const normalizeNumber = (bigNum: BigNumber, decimals = 18) => {
const divisor = new BigNumber(10).pow(decimals)
const normalized = bigNum.dividedBy(divisor)

return normalized.toNumber()
}

const display = (bigNum: string, decimals = 18) => {
const numberWithoutDecimals = normalizeNumber(new BigNumber(bigNum), decimals).toString()
return utils.commify(numberWithoutDecimals)
}

const formatTime = (seconds: number) => {
const minute = 60
const hour = 3600 // 60*60

if (seconds < minute) {
return `${seconds} second${seconds > 1 ? "s" : ""}`
} else if (seconds < hour && hour - seconds > 300) {
// if the difference less than 5 minutes(300 seconds), round to hours
const minutes = Math.round(seconds / minute)
return `${minutes} minute${minutes > 1 ? "s" : ""}`
} else {
let hours = Math.floor(seconds / hour)
const remainingSeconds = seconds % hour

// Determine the nearest 5-minute interval
let minutes = Math.round(remainingSeconds / minute / 5) * 5

// Round up to the next hour if minutes are 60
if (minutes === 60) {
hours += 1
minutes = 0
}

return `${hours}${
minutes > 0
? ` hour${hours > 1 ? "s" : ""} and ${minutes} minute${minutes > 1 ? "s" : ""}`
: ` hour${hours > 1 ? "s" : ""}`
}`
}
}

const displayRate = (capacity: string, rate: string, symbol: string, decimals = 18) => {
const capacityNormalized = normalizeNumber(new BigNumber(capacity), decimals) // normalize capacity
const rateNormalized = normalizeNumber(new BigNumber(rate), decimals) // normalize capacity

const totalRefillTime = capacityNormalized / rateNormalized // in seconds
const displayTime = `${formatTime(totalRefillTime)}`

return {
rateSecond: `${utils.commify(rateNormalized)} ${symbol}/second`,
maxThroughput: `Refills from 0 to ${utils.commify(capacityNormalized)} ${symbol} in ${displayTime}`,
}
}

const TokenSearch: FunctionComponent<TokenSearchProps> = ({ tokens, sourceChain }) => {
const [searchTerm, setSearchTerm] = useState("")
const [filteredTokens, setFilteredTokens] = useState(tokens)

const explorerUrl = getExplorer(sourceChain)

if (!explorerUrl) throw Error(`Explorer url not found for ${sourceChain}`)

const handleInput = (event: h.JSX.TargetedEvent<HTMLInputElement>) => {
const newSearchTerm = event.currentTarget.value.toLowerCase()
setSearchTerm(newSearchTerm)
const newFilteredTokens = tokens.filter((token) => token.token.toLowerCase().includes(newSearchTerm))
setFilteredTokens(newFilteredTokens)
}

return (
<>
<input
type="text"
placeholder="Search tokens..."
value={searchTerm}
onInput={handleInput}
style={{
height: "42px",
border: "var(--border-width-primary) solid var(--color-border-primary)",
backgroundImage:
"url(https://smartcontract.imgix.net/icons/searchIcon_blue_noborder.svg?auto=compress%2Cformat)",
backgroundRepeat: "no-repeat",
backgroundPosition: "left var(--space-2x) top 50%",
paddingLeft: "var(--space-8x)",
backgroundSize: "16px",
maxWidth: "250px",
}}
/>

<div style="overflow: auto; width: 100%; max-height: 350px;">
<table style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background: white; z-index: 10;">
<tr>
<th>Symbol</th>
<th>Token Address</th>
<th>Decimals</th>
<th>
<div style="position: relative;">
<SimplePreactTooltip
label="Mechanism"
tip="Token pool mechanism: Lock & Mint, Burn & Mint, Lock & Unlock, Burn & Unlock"
labelStyle={{ fontWeight: "bold", whiteSpace: "nowrap" }}
tooltipStyle={{ marginTop: "8px", minWidth: "200px" }}
/>
</div>
</th>
<th>
<div style="position: relative;">
<SimplePreactTooltip
label="Rate Limit Capacity"
tip="Maximum amount per transaction"
labelStyle={{ fontWeight: "bold", whiteSpace: "nowrap" }}
tooltipStyle={{ marginTop: "8px", minWidth: "200px" }}
/>
</div>
</th>
<th>
<div style="position: relative;">
<SimplePreactTooltip
label="Rate Limit Refill Rate"
tip="Rate at which available capacity is replenished"
labelStyle={{ fontWeight: "bold", whiteSpace: "nowrap" }}
tooltipStyle={{ marginTop: "8px", minWidth: "200px" }}
/>
</div>
</th>
</tr>
</thead>
<tbody>
{filteredTokens.length > 0 ? (
filteredTokens.map((token) => (
<tr>
<td style={{ whiteSpace: "nowrap" }}>{token.token}</td>
<td style={{ whiteSpace: "nowrap" }}>
<Address
address={token.address}
contractUrl={getExplorerAddressUrl(explorerUrl)(token.address)}
endLength={4}
/>
</td>
<td style={{ whiteSpace: "nowrap" }}>{token.decimals}</td>
<td style={{ whiteSpace: "nowrap" }}>{token.poolMechanism}</td>
<td style={{ whiteSpace: "nowrap" }}>
{token.rateLimiterConfig?.isEnabled
? display(token.rateLimiterConfig.capacity, token.decimals) + " " + token.token
: "N/A"}
</td>
<td>
{token.rateLimiterConfig?.isEnabled
? (() => {
const { rateSecond, maxThroughput } = displayRate(
token.rateLimiterConfig.capacity,
token.rateLimiterConfig.rate,
token.token,
token.decimals
)
return (
<div style={{ position: "relative" }}>
<SimplePreactTooltip
label={rateSecond}
tip={maxThroughput}
labelStyle={{ fontWeight: "normal", whiteSpace: "nowrap" }}
tooltipStyle={{ marginTop: "8px", minWidth: "200px", bottom: "110%" }}
/>
</div>
)
})()
: "N/A"}
</td>
</tr>
))
) : (
<tr>
<td colSpan={6} style={{ textAlign: "center" }}>
No token found
</td>
</tr>
)}
</tbody>
</table>
</div>
</>
)
}

export default TokenSearch
55 changes: 55 additions & 0 deletions src/features/common/Tooltip/SimplePreactTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/** @jsxImportSource preact */
import { useState } from "preact/hooks"

export const SimplePreactTooltip = ({
label,
tip,
imgURL = "https://smartcontract.imgix.net/icons/info.svg?auto=compress%2Cformat",
labelStyle = {},
tooltipStyle = {},
}) => {
const [isVisible, setIsVisible] = useState(false)

const containerStyle = {
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "help",
...labelStyle,
}

const iconStyle = {
width: "0.8em",
height: "0.8em",
marginLeft: "2px",
marginRight: "4px",
}

const defaultTooltipStyle = {
position: "absolute",
backgroundColor: "white",
color: "var(--color-text-secondary)",
padding: "8px 12px",
borderRadius: "4px",
right: "20%",
whiteSpace: "normal",
display: isVisible ? "block" : "none",
fontSize: "12px",
fontWeight: "normal",
maxWidth: "200px",
textAlign: "center",
boxShadow: "0 2px 4px rgba(0,0,0,0.2)",
zIndex: "1000",
...tooltipStyle,
}

return (
<div style={{ position: "relative" }}>
<div style={containerStyle} onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => setIsVisible(false)}>
{label}
<img src={imgURL} alt="Info" style={iconStyle} />
</div>
{isVisible && <div style={defaultTooltipStyle}>{tip}</div>}
</div>
)
}
1 change: 1 addition & 0 deletions src/features/common/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./Tooltip"
export * from "./SimplePreactTooltip"