From 4cc5309b006a9a8754879f05162ec9604f137d23 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 19 Sep 2025 17:00:09 +0200 Subject: [PATCH 001/122] initial port Signed-off-by: Gerhard Steenkamp --- src/Routes.tsx | 5 +- src/assets/chain-logos/all-swap-chain.png | Bin 0 -> 10258 bytes src/assets/icons/arrows-cross.svg | 17 + src/assets/icons/product.svg | 6 + src/assets/icons/search.svg | 7 + src/assets/mask/token-mask-corner.svg | 3 + src/assets/mask/token-mask.svg | 3 + src/hooks/useAvailableCrosschainRoutes.ts | 38 + src/hooks/useEnrichedCrosschainBalances.ts | 60 ++ src/hooks/useTokenBalancesOnChain.ts | 68 ++ .../components/BalanceSelector.tsx | 122 +++ .../components/ChainTokenSelector/Modal.tsx | 477 +++++++++++ .../ChainTokenSelector/Searchbar.tsx | 79 ++ .../ChainTokenSelector/SelectorButton.tsx | 210 +++++ .../components/ConfirmationButton.tsx | 799 ++++++++++++++++++ .../SwapAndBridge/components/InputForm.tsx | 310 +++++++ src/views/SwapAndBridge/hooks/useSwapQuote.ts | 101 +++ src/views/SwapAndBridge/index.tsx | 109 +++ 18 files changed, 2413 insertions(+), 1 deletion(-) create mode 100644 src/assets/chain-logos/all-swap-chain.png create mode 100644 src/assets/icons/arrows-cross.svg create mode 100644 src/assets/icons/product.svg create mode 100644 src/assets/icons/search.svg create mode 100644 src/assets/mask/token-mask-corner.svg create mode 100644 src/assets/mask/token-mask.svg create mode 100644 src/hooks/useAvailableCrosschainRoutes.ts create mode 100644 src/hooks/useEnrichedCrosschainBalances.ts create mode 100644 src/hooks/useTokenBalancesOnChain.ts create mode 100644 src/views/SwapAndBridge/components/BalanceSelector.tsx create mode 100644 src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx create mode 100644 src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx create mode 100644 src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx create mode 100644 src/views/SwapAndBridge/components/ConfirmationButton.tsx create mode 100644 src/views/SwapAndBridge/components/InputForm.tsx create mode 100644 src/views/SwapAndBridge/hooks/useSwapQuote.ts create mode 100644 src/views/SwapAndBridge/index.tsx diff --git a/src/Routes.tsx b/src/Routes.tsx index 911bfadb3..a7943530a 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -50,6 +50,9 @@ const Transactions = lazyWithRetry( const Staking = lazyWithRetry( () => import(/* webpackChunkName: "RewardStaking" */ "./views/Staking") ); +const SwapAndBridge = lazyWithRetry( + () => import(/* webpackChunkName: "RewardStaking" */ "./views/SwapAndBridge") +); const DepositStatus = lazyWithRetry(() => import("./views/DepositStatus")); function useRoutes() { @@ -137,7 +140,7 @@ const Routes: React.FC = () => { } }} /> - + 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 Y|9n+SEyE|LPXGV_07*qoM6N<$f) + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/product.svg b/src/assets/icons/product.svg new file mode 100644 index 000000000..a370c74fb --- /dev/null +++ b/src/assets/icons/product.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 000000000..908f1c03d --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/assets/mask/token-mask-corner.svg b/src/assets/mask/token-mask-corner.svg new file mode 100644 index 000000000..1b89f0b41 --- /dev/null +++ b/src/assets/mask/token-mask-corner.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mask/token-mask.svg b/src/assets/mask/token-mask.svg new file mode 100644 index 000000000..b968415b0 --- /dev/null +++ b/src/assets/mask/token-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts new file mode 100644 index 000000000..ca6eddbc8 --- /dev/null +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -0,0 +1,38 @@ +import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; +import { useQuery } from "@tanstack/react-query"; + +export type LifiToken = { + chainId: number; + address: string; + symbol: string; + name: string; + decimals: number; + priceUSD: string; + coinKey: string; + logoURI: string; +}; + +// TODO: Currently stubbed and will need to be added to the swap API +export default function useAvailableCrosschainRoutes() { + return useQuery({ + queryKey: ["availableCrosschainRoutes"], + queryFn: async () => { + const result = await fetch( + "https://li.quest/v1/tokens?chainTypes=EVM&minPriceUSD=0.001" + ); + const data = (await result.json()) as { + tokens: Record>; + }; + + return Object.entries(data.tokens).reduce( + (acc, [chainId, tokens]) => { + if (Object.values(MAINNET_CHAIN_IDs).includes(Number(chainId))) { + acc[Number(chainId)] = tokens; + } + return acc; + }, + {} as Record> + ); + }, + }); +} diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts new file mode 100644 index 000000000..4b07e0c29 --- /dev/null +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -0,0 +1,60 @@ +import { useMemo } from "react"; +import useAvailableCrosschainRoutes, { + LifiToken, +} from "./useAvailableCrosschainRoutes"; +import useTokenBalancesOnChain from "./useTokenBalancesOnChain"; +import { compareAddressesSimple } from "utils"; +import { BigNumber, utils } from "ethers"; + +export default function useEnrichedCrosschainBalances() { + const tokenBalances = useTokenBalancesOnChain(); + const availableCrosschainRoutes = useAvailableCrosschainRoutes(); + + return useMemo(() => { + if (availableCrosschainRoutes.isLoading) { + return {}; + } + const chains = Object.keys(availableCrosschainRoutes.data || {}); + + return chains.reduce( + (acc, chainId) => { + const balancesForChain = tokenBalances.find( + (t) => t.isSuccess && t.data.chainId === Number(chainId) + ); + + const tokens = availableCrosschainRoutes.data![Number(chainId)]; + const enrichedTokens = tokens + .map((t) => { + const balance = balancesForChain?.data?.balances.find((b) => + compareAddressesSimple(b.address, t.address) + ); + return { + ...t, + balance: balance?.balance ?? BigNumber.from(0), + balanceUsd: + balance?.balance && t + ? Number(utils.formatUnits(balance.balance, t.decimals)) * + Number(t.priceUSD) + : 0, + }; + }) + // Filter out tokens that don't have a logoURI + .filter((t) => t.logoURI !== undefined); + + // Sort high to low balanceUsd + const orderedEnrichedTokens = enrichedTokens.sort( + (a, b) => b.balanceUsd - a.balanceUsd + ); + + return { + ...acc, + [Number(chainId)]: orderedEnrichedTokens, + }; + }, + {} as Record< + number, + Array + > + ); + }, [availableCrosschainRoutes, tokenBalances]); +} diff --git a/src/hooks/useTokenBalancesOnChain.ts b/src/hooks/useTokenBalancesOnChain.ts new file mode 100644 index 000000000..09ad766e8 --- /dev/null +++ b/src/hooks/useTokenBalancesOnChain.ts @@ -0,0 +1,68 @@ +import { useConnection } from "./useConnection"; +import { CHAIN_IDs, MAINNET_CHAIN_IDs } from "@across-protocol/constants"; +import { useQueries } from "@tanstack/react-query"; +import { BigNumber } from "ethers"; + +const CHAIN_TO_ALCHEMY = { + [CHAIN_IDs.MAINNET]: "eth-mainnet", + [CHAIN_IDs.OPTIMISM]: "opt-mainnet", + [CHAIN_IDs.POLYGON]: "polygon-mainnet", + [CHAIN_IDs.BASE]: "base-mainnet", + [CHAIN_IDs.LINEA]: "linea-mainnet", + [CHAIN_IDs.ARBITRUM]: "arb-mainnet", +}; + +// TODO: delete this, move to serverless /batch-account-balance +const getAlchemyRpcUrl = (chainId: number) => { + const chain = CHAIN_TO_ALCHEMY[chainId]; + return `https://${chain}.g.alchemy.com/v2/${process.env.REACT_APP_ALCHEMY_KEY}`; +}; + +export default function useTokenBalancesOnChain() { + const { account } = useConnection(); + const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs) + .sort((a, b) => a - b) + .filter((chainId) => !!CHAIN_TO_ALCHEMY[chainId]); + + return useQueries({ + queries: chainIdsAvailable.map((chainId) => ({ + queryKey: ["tokenBalancesOnChain", chainId], + enabled: account !== undefined, + queryFn: async () => { + const rpcUrl = getAlchemyRpcUrl(chainId); + + const balances = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "alchemy_getTokenBalances", + params: [account], + }), + }); + + const data = await balances.json(); + + return { + chainId, + balances: ( + data.result.tokenBalances as { + contractAddress: string; + tokenBalance: string; + }[] + ) + .filter( + (t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0) + ) + .map((t) => ({ + address: t.contractAddress, + balance: BigNumber.from(t.tokenBalance), + })), + }; + }, + })), + }); +} diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx new file mode 100644 index 000000000..89ebfc280 --- /dev/null +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -0,0 +1,122 @@ +import { motion, AnimatePresence } from "framer-motion"; +import { useState } from "react"; +import { BigNumber } from "ethers"; +import styled from "@emotion/styled"; +import { COLORS, formatUnitsWithMaxFractions } from "utils"; + +type BalanceSelectorProps = { + balance: BigNumber; + decimals: number; + setAmount: (amount: BigNumber | null) => void; + disableHover?: boolean; +}; + +export default function BalanceSelector({ + balance, + decimals, + setAmount, + disableHover, +}: BalanceSelectorProps) { + const [isHovered, setIsHovered] = useState(false); + if (!balance || balance.lte(0)) return null; + const percentages = ["25%", "50%", "75%", "MAX"]; + + const handlePillClick = (percentage: string) => { + if (percentage === "MAX") { + setAmount(balance); + } else { + const percent = parseInt(percentage) / 100; + const amount = balance.mul(Math.floor(percent * 10000)).div(10000); + setAmount(amount); + } + }; + + const formattedBalance = formatUnitsWithMaxFractions(balance, decimals); + + return ( + !disableHover && setIsHovered(true)} + onMouseLeave={() => !disableHover && setIsHovered(false)} + > + + + {isHovered && + percentages.map((percentage, index) => ( + handlePillClick(percentage)} + > + {percentage} + + ))} + + + Balance: {formattedBalance} + + ); +} + +const BalanceWrapper = styled.div` + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +`; + +const BalanceText = styled.span` + color: ${() => COLORS.aqua}; + font-size: 14px; + font-weight: 400; + line-height: 130%; +`; + +const PillsContainer = styled.div` + display: flex; + align-items: center; + gap: 4px; + + .pill { + display: flex; + height: 20px; + padding: 0 8px; + border-radius: 10px; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + background-color: rgba(224, 243, 255, 0.05); + color: rgba(224, 243, 255, 0.5); + cursor: pointer; + user-select: none; + } +`; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx new file mode 100644 index 000000000..f884119dd --- /dev/null +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -0,0 +1,477 @@ +import Modal from "components/Modal"; +import { EnrichedTokenSelect } from "./SelectorButton"; +import styled from "@emotion/styled"; +import Searchbar from "./Searchbar"; +import TokenMask from "assets/mask/token-mask-corner.svg"; +import useAvailableCrosschainRoutes, { + LifiToken, +} from "hooks/useAvailableCrosschainRoutes"; +import { + COLORS, + formatUnitsWithMaxFractions, + formatUSD, + getChainInfo, + parseUnits, +} from "utils"; +import { useMemo, useState } from "react"; +import { ReactComponent as CheckmarkCircle } from "assets/icons/checkmark-circle.svg"; +import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; +import useEnrichedCrosschainBalances from "hooks/useEnrichedCrosschainBalances"; +import { BigNumber } from "ethers"; + +type Props = { + onSelect: (token: EnrichedTokenSelect) => void; + isOriginToken: boolean; + + displayModal: boolean; + setDisplayModal: (displayModal: boolean) => void; +}; + +export default function ChainTokenSelectorModal({ + isOriginToken, + displayModal, + setDisplayModal, + onSelect, +}: Props) { + const balances = useEnrichedCrosschainBalances(); + + const crossChainRoutes = useAvailableCrosschainRoutes(); + + const [selectedChain, setSelectedChain] = useState(null); + + const [tokenSearch, setTokenSearch] = useState(""); + const [chainSearch, setChainSearch] = useState(""); + + const displayedTokens = useMemo(() => { + let tokens = selectedChain ? (balances[selectedChain] ?? []) : []; + + if (tokens.length === 0 && selectedChain === null) { + tokens = Object.values(balances).flatMap((t) => t); + } + // Return ordering top 100 tokens ordering highest balanceUsd to lowest (fallback alphabetical) + const sortedTokens = tokens.slice(0, 100).sort((a, b) => { + if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { + return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); + } + return b.balanceUsd - a.balanceUsd; + }); + + return sortedTokens.filter((t) => { + if (tokenSearch === "") { + return true; + } + const keywords = [ + t.symbol.toLowerCase().replaceAll(" ", ""), + t.name.toLowerCase().replaceAll(" ", ""), + t.address.toLowerCase().replaceAll(" ", ""), + ]; + return keywords.some((keyword) => + keyword.includes(tokenSearch.toLowerCase().replaceAll(" ", "")) + ); + }); + }, [selectedChain, balances, tokenSearch]); + + const displayedChains = useMemo(() => { + return Object.fromEntries( + Object.entries(crossChainRoutes.data || {}).filter(([chainId]) => { + if ([288].includes(Number(chainId))) { + return false; + } + + const keywords = [ + String(chainId), + getChainInfo(Number(chainId)).name.toLowerCase().replace(" ", ""), + ]; + return keywords.some((keyword) => + keyword.toLowerCase().includes(chainSearch.toLowerCase()) + ); + }) + ); + }, [chainSearch, crossChainRoutes.data]); + + return ( + Select {isOriginToken ? "Origin" : "Destination"} Token + } + isOpen={displayModal} + padding="thin" + exitModalHandler={() => setDisplayModal(false)} + exitOnOutsideClick + width={720} + height={800} + > + + + + + + + setSelectedChain(null)} + /> + {Object.entries(displayedChains).map(([chainId]) => ( + setSelectedChain(Number(chainId))} + /> + ))} + + + + + + + + + {displayedTokens.map((token) => ( + { + onSelect({ + chainId: token.chainId, + symbolUri: token.logoURI, + symbol: token.symbol, + address: token.address, + balance: token.balance, + priceUsd: parseUnits(token.priceUSD, 18), + decimals: token.decimals, + }); + setDisplayModal(false); + }} + /> + ))} + + + + + ); +} + +const ChainEntry = ({ + chainId, + isSelected, + onClick, +}: { + chainId: number | null; + isSelected: boolean; + onClick: () => void; +}) => { + const chainInfo = chainId + ? getChainInfo(chainId) + : { + logoURI: AllChainsIcon, + name: "All", + }; + return ( + + + {chainInfo.name} + {isSelected && } + + ); +}; + +const TokenEntry = ({ + token, + isSelected, + onClick, +}: { + token: LifiToken & { balanceUsd: number; balance: BigNumber }; + isSelected: boolean; + onClick: () => void; +}) => { + const hasBalance = token.balance.gt(0) && token.balanceUsd > 0.01; + return ( + + + + {token.name} + {token.symbol} + + {hasBalance && ( + + + {formatUnitsWithMaxFractions( + token.balance.toBigInt(), + token.decimals + )} + + + ${formatUSD(parseUnits(token.balanceUsd.toString(), 18))} + + + )} + + ); +}; + +const TokenItemImage = ({ token }: { token: LifiToken }) => { + return ( + + + + + ); +}; + +const TokenItemImageWrapper = styled.div` + width: 32px; + height: 32px; + + flex-shrink: 0; + + position: relative; +`; + +const TokenItemTokenImage = styled.img` + width: 100%; + height: 100%; + + top: 0; + left: 0; + + position: absolute; + + mask-image: url(${TokenMask}); + mask-size: 100% 100%; + mask-repeat: no-repeat; + mask-position: center; +`; + +const TokenItemChainImage = styled.img` + width: 12px; + height: 12px; + + position: absolute; + + bottom: 0; + right: 0; +`; + +const InnerWrapper = styled.div` + width: 100%; + height: 100%; + + display: flex; + flex-direction: row; + gap: 12px; +`; + +const VerticalDivider = styled.div` + width: 1px; + + height: 400px; + + margin: -16px 0; + + background-color: #3f4247; + + flex-shrink: 0; +`; + +const Title = styled.div` + overflow: hidden; + color: var(--Base-bright-gray, #e0f3ff); + + font-family: Barlow; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 26px */ +`; + +const ChainWrapper = styled.div` + width: calc(33% - 0.5px); + height: 100%; + + display: flex; + flex-direction: column; + gap: 16px; +`; + +const TokenWrapper = styled.div` + width: calc(67% - 0.5px); + height: 100%; + + display: flex; + flex-direction: column; + gap: 8px; +`; + +const SearchWrapper = styled.div` + padding: 0px 8px; +`; + +const ListWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + + overflow-y: scroll; + max-height: 300px; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); + } + + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.1) transparent; +`; + +const EntryItem = styled.div<{ isSelected: boolean }>` + display: flex; + flex-direction: row; + justify-content: space-between; + + width: 100%; + flex-shrink: 0; + + align-items: center; + + padding: 8px; + height: 48px; + gap: 8px; + + border-radius: 8px; + background: ${({ isSelected }) => + isSelected ? COLORS["aqua-5"] : "transparent"}; + + cursor: pointer; + + transition: background 0.2s ease-in-out; + + &:hover { + background: ${({ isSelected }) => + isSelected ? COLORS["aqua-15"] : COLORS["grey-400-15"]}; + } +`; + +const ChainItemImage = styled.img` + width: 32px; + height: 32px; + + flex-shrink: 0; +`; + +const ChainItemName = styled.div` + overflow: hidden; + color: var(--Base-bright-gray, #e0f3ff); + text-overflow: ellipsis; + /* Body/Medium */ + font-family: Barlow; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 20.8px */ + + width: 100%; +`; + +const ChainItemCheckmark = styled(CheckmarkCircle)` + width: 20px; + height: 20px; +`; + +const TokenNameSymbolWrapper = styled.div` + display: flex; + flex-direction: row; + gap: 4px; + + width: 100%; + + align-items: center; + justify-content: start; +`; + +const TokenName = styled.div` + overflow: hidden; + color: var(--Base-bright-gray, #e0f3ff); + /* Body/Medium */ + font-family: Barlow; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 20.8px */ + + max-width: 20ch; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const TokenSymbol = styled.div` + overflow: hidden; + color: var(--Base-bright-gray, #e0f3ff); + text-overflow: ellipsis; + /* Body/X Small */ + font-family: Barlow; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 15.6px */ + + opacity: 0.5; + + text-transform: uppercase; +`; + +const TokenBalanceStack = styled.div` + display: flex; + flex-direction: column; + + align-items: flex-end; + + gap: 4px; +`; + +const TokenBalance = styled.div` + color: var(--Base-bright-gray, #e0f3ff); + /* Body/Small */ + font-family: Barlow; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 18.2px */ +`; + +const TokenBalanceUsd = styled.div` + color: var(--Base-bright-gray, #e0f3ff); + /* Body/X Small */ + font-family: Barlow; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 15.6px */ + opacity: 0.5; +`; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx new file mode 100644 index 000000000..9b2603699 --- /dev/null +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -0,0 +1,79 @@ +import styled from "@emotion/styled"; +import { ReactComponent as SearchIcon } from "assets/icons/search.svg"; +import { ReactComponent as ProductIcon } from "assets/icons/product.svg"; + +type Props = { + searchTopic: string; + search: string; + setSearch: (search: string) => void; +}; + +export default function Searchbar({ searchTopic, search, setSearch }: Props) { + return ( + + + setSearch(e.target.value)} + /> + {search ? setSearch("")} /> :
} + + ); +} + +const Wrapper = styled.div` + display: flex; + height: 44px; + padding: 0px 12px; + align-items: center; + gap: 8px; + + flex-direction: row; + justify-content: space-between; + + border-radius: 8px; + background: rgba(224, 243, 255, 0.05); + + width: 100%; +`; + +const StyledSearchIcon = styled(SearchIcon)` + width: 20px; + height: 20px; + + flex-grow: 0; +`; + +const StyledProductIcon = styled(ProductIcon)` + width: 16px; + height: 16px; + + cursor: pointer; + + flex-grow: 0; +`; + +const Input = styled.input` + overflow: hidden; + color: var(--Base-bright-gray, #e0f3ff); + text-overflow: ellipsis; + + &::placeholder { + color: #e0f3ff4d; + } + + background: transparent; + + border: none; + outline: none; + + width: 100%; + + /* Body/Medium */ + font-family: Barlow; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 20.8px */ +`; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx new file mode 100644 index 000000000..1909ce16f --- /dev/null +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -0,0 +1,210 @@ +import styled from "@emotion/styled"; +import { BigNumber } from "ethers"; +import { useCallback, useEffect, useState } from "react"; +import { COLORS, getChainInfo } from "utils"; +import TokenMask from "assets/mask/token-mask.svg"; +import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; +import ChainTokenSelectorModal from "./Modal"; + +export type TokenSelect = { + chainId: number; + symbolUri: string; + symbol: string; + address: string; +}; + +export type EnrichedTokenSelect = TokenSelect & { + priceUsd: BigNumber; + balance: BigNumber; + decimals: number; +}; + +type Props = { + selectedToken: EnrichedTokenSelect | null; + onSelect?: (token: EnrichedTokenSelect) => void; + isOriginToken: boolean; + marginBottom?: string; +}; + +export default function SelectorButton({ + onSelect, + selectedToken, + isOriginToken, + marginBottom, +}: Props) { + const [displayModal, setDisplayModal] = useState(false); + + useEffect(() => { + if (selectedToken) { + onSelect?.(selectedToken); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedToken]); + + const setSelectedToken = useCallback( + (token: EnrichedTokenSelect) => { + onSelect?.(token); + setDisplayModal(false); + }, + [onSelect] + ); + + if (!selectedToken) { + return ( + <> + setDisplayModal(true)} + marginBottom={marginBottom} + > + + Select a token + + + + + + + + + ); + } + + const chain = getChainInfo(selectedToken.chainId); + + return ( + <> + setDisplayModal(true)} + marginBottom={marginBottom} + > + + + + + + {selectedToken.symbol} + {chain.name} + + + + + + + + + ); +} + +const Wrapper = styled.div<{ marginBottom?: string }>` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: ${({ marginBottom }) => marginBottom || "0"}; + + border-radius: 8px; + border: 1px solid #3f4247; + background: #e0f3ff0d; + padding: 8px 12px; + + height: 64px; + + gap: 12px; + width: 184px; + + cursor: pointer; +`; + +const SelectWrapper = styled(Wrapper)` + height: 48px; +`; + +const VerticalDivider = styled.div` + width: 1px; + height: calc(100% + 16px); + + margin-top: -8px; + + background: #3f4247; +`; + +const ChevronStack = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const NamesStack = styled.div` + display: flex; + flex-direction: column; + gap: 6px; + + height: 100%; + + flex-grow: 1; + + justify-content: center; + align-items: flex-start; +`; + +const TokenName = styled.div` + font-size: 16px; + line-height: 16px; + + font-weight: 600; + color: #e0f3ff; +`; + +const SelectTokenName = styled(TokenName)` + color: ${COLORS["aqua"]}; +`; + +const ChainName = styled.div` + font-size: 12px; + line-height: 12px; + font-weight: 400; + color: #e0f3ff; +`; + +const TokenStack = styled.div` + width: 32px; + height: 48px; + position: relative; + + flex-grow: 0; +`; + +const TokenImg = styled.img` + position: absolute; + top: 0; + left: 0; + width: 32px; + height: 32px; + z-index: 1; + + mask: url(${TokenMask}) no-repeat center center; +`; + +const ChainImg = styled.img` + position: absolute; + bottom: 0; + left: 4.5px; + width: 24px; + height: 24px; + z-index: 2; +`; + +const ChevronDown = styled(ChevronDownIcon)` + height: 16px; + width: 16px; +`; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx new file mode 100644 index 000000000..2e5a4e69d --- /dev/null +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -0,0 +1,799 @@ +"use client"; +import { ButtonHTMLAttributes } from "react"; +import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; +import { ReactComponent as LoadingIcon } from "assets/icons/loading.svg"; +import React from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { BigNumber } from "ethers"; +import { COLORS } from "utils"; +import { useConnection, useIsWrongNetwork } from "hooks"; +import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; +import styled from "@emotion/styled"; + +type SwapQuoteResponse = { + checks: object; + steps: object; + refundToken: object; + inputAmount: string; + expectedOutputAmount: string; + minOutputAmount: string; + expectedFillTime: number; + swapTx: object; +}; + +export type BridgeButtonState = + | "notConnected" + | "awaitingTokenSelection" + | "awaitingAmountInput" + | "readyToConfirm" + | "submitting" + | "wrongNetwork"; + +interface ConfirmationButtonProps + extends ButtonHTMLAttributes { + inputToken: EnrichedTokenSelect | null; + outputToken: EnrichedTokenSelect | null; + amount: BigNumber | null; + swapQuote: SwapQuoteResponse | null; + isQuoteLoading: boolean; + onConfirm?: () => void; +} + +const stateLabels: Record = { + notConnected: "Connect Wallet", + awaitingTokenSelection: "Select Token", + awaitingAmountInput: "Input Amount", + readyToConfirm: "Confirm Swap", + submitting: "Submitting...", + wrongNetwork: "Switch Network", +}; + +// Expandable label section component +const ExpandableLabelSection: React.FC< + React.PropsWithChildren<{ + fee: string; + time: string; + expanded: boolean; + onToggle: () => void; + visible: boolean; + }> +> = ({ fee, time, expanded, onToggle, visible, children }) => { + return ( + + {visible && ( + + + + + + + Fast & Secure + + + + + + + + + {fee} + + + + + + + + + {time} + + + + + + {expanded && children} + + + )} + + ); +}; + +// Core button component, used by all states +const ButtonCore: React.FC< + ConfirmationButtonProps & { + label: string; + loading?: boolean; + aqua?: boolean; + state: BridgeButtonState; + fullHeight?: boolean; + } +> = ({ + label, + loading, + disabled, + aqua, + state, + onConfirm, + onClick, + fullHeight, +}) => ( + + + + + {loading && } + {!loading && label} + + + + +); + +export const ConfirmationButton: React.FC = ({ + inputToken, + outputToken, + amount, + swapQuote, + isQuoteLoading, + onConfirm, + ...props +}) => { + const { account, connect } = useConnection(); + const [expanded, setExpanded] = React.useState(false); + const [isSubmitting, setIsSubmitting] = React.useState(false); + + const { isWrongNetworkHandler, isWrongNetwork } = useIsWrongNetwork( + inputToken?.chainId + ); + + // Determine the current state + const getButtonState = (): BridgeButtonState => { + if (isSubmitting) return "submitting"; + if (!account) return "notConnected"; + if (!inputToken || !outputToken) return "awaitingTokenSelection"; + if (!amount || amount.lte(0)) return "awaitingAmountInput"; + if (isWrongNetwork) return "wrongNetwork"; + return "readyToConfirm"; + }; + + const state = getButtonState(); + + // Calculate display values from swapQuote + const displayValues = React.useMemo(() => { + if (!swapQuote || !inputToken || !outputToken) { + return { + fee: "$0.05", + time: "~2 min", + bridgeFee: "$0.01", + destinationGasFee: "$0", + extraFee: "$0.04", + route: "Across V4", + estimatedTime: "~2 secs", + netFee: "$0.05", + }; + } + + // Calculate fees based on swapQuote data + // This is a placeholder - you'd calculate actual fees from the quote + const bridgeFee = "$0.01"; + const destinationGasFee = "$0"; + const extraFee = "$0.04"; + const netFee = "$0.05"; + + // Format time from expectedFillTime (in seconds) + const timeInMinutes = Math.ceil(swapQuote.expectedFillTime / 60); + const time = timeInMinutes < 1 ? "~30 sec" : `~${timeInMinutes} min`; + + return { + fee: netFee, + time, + bridgeFee, + destinationGasFee, + extraFee, + route: "Across V4", + estimatedTime: timeInMinutes < 1 ? "~30 secs" : `~${timeInMinutes} mins`, + netFee, + }; + }, [swapQuote, inputToken, outputToken]); + + // Handle confirmation + const handleConfirm = async () => { + if (!onConfirm) return; + + setIsSubmitting(true); + try { + onConfirm(); + } catch (error) { + console.error("Confirmation failed:", error); + } finally { + setIsSubmitting(false); + } + }; + + // Compute target height based on state and expansion + let targetHeight = 88; + if (state === "readyToConfirm") { + targetHeight = expanded ? 300 : 128; + } + + // Render state-specific content + let content: React.ReactNode = null; + switch (state) { + case "readyToConfirm": + content = ( + <> + + setExpanded((e) => !e)} + visible={true} + > + {expanded ? ( + + + + + + + + + Route + + + + {displayValues.route} + + + + + + + + + + + + Est. Time + + {displayValues.estimatedTime} + + + + + + + + + + + Net Fee + + + + + + + + + {displayValues.netFee} + + + + Bridge Fee + + {displayValues.bridgeFee} + + + + Destination Gas Fee + + {displayValues.destinationGasFee} + + + + Extra Fee + + {displayValues.extraFee} + + + + + ) : null} + + + + + + + ); + break; + case "notConnected": + content = ( + connect()} + /> + ); + break; + case "wrongNetwork": + content = ( + isWrongNetworkHandler()} + /> + ); + break; + case "awaitingTokenSelection": + content = ( + + ); + break; + case "awaitingAmountInput": + content = ( + + ); + break; + case "submitting": + content = ( + + ); + break; + default: + content = null; + } + + return ( + + {content} + + ); +}; + +// Styled components +const Container = styled(motion.div)<{ state: BridgeButtonState }>` + background: rgba(108, 249, 216, 0.1); + border-radius: 24px; + display: flex; + flex-direction: column; + padding: ${({ state }) => + state === "readyToConfirm" || state === "submitting" + ? "4px 12px 12px 12px" + : "0"}; + width: 100%; + overflow: hidden; + gap: ${({ state }) => (state === "readyToConfirm" ? "8px" : "0")}; +`; + +const ExpandableLabelButton = styled.button` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px; + background: transparent; + border: none; + cursor: pointer; + user-select: none; +`; + +const ExpandableLabelLeft = styled.span` + color: ${COLORS.aqua}; + font-weight: 600; + font-size: 14px; + flex: 1; + text-align: left; + display: flex; + align-items: center; + gap: 8px; +`; + +const ShieldIcon = styled.svg` + width: 16px; + height: 16px; +`; + +const FastSecureText = styled.span` + color: ${COLORS.aqua}; +`; + +const ExpandableLabelRight = styled.div` + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #e0f3ff; +`; + +const FeeTimeItem = styled.span` + display: flex; + align-items: center; + gap: 4px; +`; + +const GasIcon = styled.svg` + width: 16px; + height: 16px; +`; + +const TimeIcon = styled.svg` + width: 16px; + height: 16px; +`; + +const Divider = styled.span` + margin: 0 8px; + height: 16px; + width: 1px; + background: rgba(224, 243, 255, 0.5); +`; + +const StyledChevronDown = styled(ChevronDownIcon)<{ expanded: boolean }>` + width: 20px; + height: 20px; + margin-left: 12px; + transition: transform 0.3s ease; + cursor: pointer; + color: #e0f3ff; + transform: ${({ expanded }) => + expanded ? "rotate(180deg)" : "rotate(0deg)"}; +`; + +const ExpandableContent = styled.div<{ expanded: boolean }>` + overflow: hidden; + transition: all 0.3s ease; + max-height: ${({ expanded }) => (expanded ? "160px" : "0")}; + margin-top: ${({ expanded }) => (expanded ? "8px" : "0")}; +`; + +const StyledButton = styled.button<{ + aqua?: boolean; + loading?: boolean; + fullHeight?: boolean; +}>` + width: 100%; + height: ${({ fullHeight }) => (fullHeight ? "100%" : "64px")}; + border-radius: 12px; + font-weight: 600; + font-size: 16px; + transition: all 0.3s ease; + border: none; + cursor: pointer; + + background: ${({ aqua }) => (aqua ? "transparent" : COLORS.aqua)}; + color: ${({ aqua }) => (aqua ? COLORS.aqua : "#000000")}; + + &:hover { + ${({ aqua }) => + aqua + ? `background: rgba(108, 249, 216, 0.1);` + : ` + box-shadow: 0 0 16px 0 ${COLORS.aqua}; + background: ${COLORS.aqua}; + `} + } + + &:focus { + ${({ aqua }) => !aqua && `box-shadow: 0 0 16px 0 ${COLORS.aqua};`} + } + + &:disabled { + ${({ loading }) => loading && "opacity: 0.6; cursor: wait;"} + } +`; + +const ButtonContent = styled.span` + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +`; + +const StyledLoadingIcon = styled(LoadingIcon)<{ aqua?: boolean }>` + width: 16px; + height: 16px; + animation: spin 1s linear infinite; + color: ${({ aqua }) => (aqua ? COLORS.aqua : "#000000")}; + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +`; + +const ButtonContainer = styled.div<{ expanded: boolean }>` + margin-top: ${({ expanded }) => (expanded ? "24px" : "0")}; +`; + +const ExpandedDetails = styled.div` + color: #e0f3ff; + font-size: 14px; + width: 100%; + padding: 8px 16px 24px; +`; + +const DetailRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +`; + +const DetailLeft = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const DetailRight = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const RouteIcon = styled.svg` + width: 20px; + height: 20px; +`; + +const InfoIcon = styled.svg` + width: 20px; + height: 20px; +`; + +const RouteDot = styled.span` + display: inline-block; + width: 20px; + height: 20px; + background: ${COLORS.aqua}; + border-radius: 50%; + opacity: 0.8; +`; + +const FeeBreakdown = styled.div` + padding-left: 24px; + border-left: 1px solid rgba(224, 243, 255, 0.1); + margin-left: 8px; +`; + +const FeeBreakdownRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +`; + +const FeeBreakdownLabel = styled.span` + color: rgba(224, 243, 255, 0.7); +`; + +const FeeBreakdownValue = styled.span` + color: #e0f3ff; +`; + +export default ConfirmationButton; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx new file mode 100644 index 000000000..18e9b501b --- /dev/null +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -0,0 +1,310 @@ +import { COLORS, formatUnitsWithMaxFractions, formatUSD } from "utils"; +import SelectorButton, { + EnrichedTokenSelect, +} from "./ChainTokenSelector/SelectorButton"; +import BalanceSelector from "./BalanceSelector"; +import styled from "@emotion/styled"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { BigNumber, utils } from "ethers"; +import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; + +export const InputForm = ({ + inputToken, + outputToken, + setInputToken, + setOutputToken, + setAmount, + isAmountOrigin, + setIsAmountOrigin, + isQuoteLoading, + expectedOutputAmount, + expectedInputAmount, +}: { + inputToken: EnrichedTokenSelect | null; + setInputToken: (token: EnrichedTokenSelect | null) => void; + + outputToken: EnrichedTokenSelect | null; + setOutputToken: (token: EnrichedTokenSelect | null) => void; + + isQuoteLoading: boolean; + expectedOutputAmount: string | undefined; + expectedInputAmount: string | undefined; + + setAmount: (amount: BigNumber | null) => void; + + isAmountOrigin: boolean; + setIsAmountOrigin: (isAmountOrigin: boolean) => void; +}) => { + const quickSwap = useCallback(() => { + const origin = inputToken; + const destination = outputToken; + + setOutputToken(origin); + setInputToken(destination); + + setAmount(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputToken, outputToken]); + + return ( + + { + setAmount(amount); + setIsAmountOrigin(true); + }} + isOrigin={true} + expectedAmount={expectedInputAmount} + shouldUpdate={!isAmountOrigin} + isUpdateLoading={isQuoteLoading} + /> + + + + + + { + setAmount(amount); + setIsAmountOrigin(false); + }} + isOrigin={false} + expectedAmount={expectedOutputAmount} + shouldUpdate={isAmountOrigin} + isUpdateLoading={isQuoteLoading} + /> + + ); +}; + +const TokenInput = ({ + setToken, + token, + setAmount, + isOrigin, + expectedAmount, + shouldUpdate, + isUpdateLoading, +}: { + setToken: (token: EnrichedTokenSelect) => void; + token: EnrichedTokenSelect | null; + setAmount: (amount: BigNumber | null) => void; + isOrigin: boolean; + expectedAmount: string | undefined; + shouldUpdate: boolean; + isUpdateLoading: boolean; +}) => { + const [amountString, setAmountString] = useState(""); + const [justTyped, setJustTyped] = useState(false); + + // Handle user input changes + useEffect(() => { + if (!justTyped) { + return; + } + setJustTyped(false); + try { + setAmount(utils.parseUnits(amountString, token!.decimals)); + } catch (e) { + setAmount(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amountString]); + + // Reset amount when token changes + useEffect(() => { + if (token) { + setAmountString(""); + } + }, [token]); + + // Handle quote updates - only update the field that should receive the quote + useEffect(() => { + if (shouldUpdate && isUpdateLoading) { + setAmountString(""); + } + + if (expectedAmount && token && shouldUpdate) { + setAmountString( + formatUnitsWithMaxFractions(expectedAmount, token.decimals) + ); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [expectedAmount, isUpdateLoading]); + + const estimatedUsdAmount = useMemo(() => { + try { + const amount = utils.parseUnits(amountString, token!.decimals); + if (!token) { + return null; + } + const priceAsNumeric = Number(utils.formatUnits(token.priceUsd, 18)); + const amountAsNumeric = Number(utils.formatUnits(amount, token.decimals)); + const estimatedUsdAmountNumeric = amountAsNumeric * priceAsNumeric; + const estimatedUsdAmount = utils.parseUnits( + estimatedUsdAmountNumeric.toString(), + 18 + ); + return formatUSD(estimatedUsdAmount); + } catch (e) { + return null; + } + }, [amountString, token]); + + return ( + + + + {isOrigin ? "Sell" : "Buy"} + + { + setJustTyped(true); + setAmountString(e.target.value); + }} + disabled={shouldUpdate && isUpdateLoading} + /> + + {estimatedUsdAmount && <>Value: ~${estimatedUsdAmount}} + + + + {token && ( + + { + if (amount) { + setAmount(amount); + setAmountString( + formatUnitsWithMaxFractions(amount, token.decimals) + ); + } + }} + /> + + )} + + ); +}; + +const TokenAmountStack = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; + + height: 100%; +`; + +const TokenAmountInputTitle = styled.div` + color: ${() => COLORS.aqua}; + font-size: 16px; + font-weight: 400; + line-height: 130%; +`; + +const TokenAmountInput = styled.input` + color: #e0f3ff; + font-family: Barlow; + font-size: 48px; + font-weight: 300; + line-height: 120%; + letter-spacing: -1.92px; + + outline: none; + border: none; + background: transparent; + + flex-shrink: 0; + + &:focus { + font-size: 48px; + } +`; + +const TokenAmountInputEstimatedUsd = styled.div` + color: #e0f3ff; + font-family: Barlow; + font-size: 14px; + font-weight: 400; + line-height: 130%; +`; + +const TokenInputWrapper = styled.div` + display: flex; + height: 132px; + padding: 16px; + justify-content: space-between; + align-items: center; + align-self: stretch; + border-radius: 12px; + border: 1px solid rgba(224, 243, 255, 0.05); + background: #2d2e32; + position: relative; +`; + +const BalanceSelectorWrapper = styled.div` + position: absolute; + bottom: 16px; + right: 16px; +`; + +const Wrapper = styled.div` + position: relative; + + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 12px; + align-self: stretch; + padding: 12px; + border-radius: 24px; + border: 1px solid rgba(224, 243, 255, 0.05); + background: #34353b; + box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); +`; + +const QuickSwapButton = styled.button` + display: flex; + width: 48px; + height: 32px; + + padding: 0px 16px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 32px; + border: 1px solid #4c4e57; + background: #34353b; + box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); + cursor: pointer; + + & * { + flex-shrink: 0; + } +`; + +const QuickSwapButtonWrapper = styled.div` + position: absolute; + left: calc(50% - 24px); + top: calc(50% - 16px); + + z-index: 4; +`; diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts new file mode 100644 index 000000000..c4ea54b84 --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -0,0 +1,101 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { BigNumber } from "ethers"; +import { useConnection } from "hooks"; +import { vercelApiBaseUrl } from "utils"; + +type TokenParam = { + address: string; + chainId: number; +}; + +type SwapQuoteParams = { + origin: TokenParam | null; + destination: TokenParam | null; + amount: BigNumber | null; + isInputAmount: boolean; + recipient?: string; + integratorId?: string; + refundAddress?: string; + refundOnOrigin?: boolean; + slippageTolerance?: number; +}; + +type SwapTransaction = { + simulationSuccess: boolean; + chainId: number; + to: string; + data: string; + maxFeePerGas: string; + maxPriorityFeePerGas: string; +}; + +type SwapQuoteResponse = { + checks: object; + steps: object; + refundToken: object; + inputAmount: string; + expectedOutputAmount: string; + minOutputAmount: string; + expectedFillTime: number; + swapTx: SwapTransaction; + approvalTxns: { + chainId: number; + data: string; + to: string; + }[]; +}; + +const useSwapQuote = ({ + origin, + destination, + amount, + isInputAmount, + recipient, + integratorId, + refundAddress, + refundOnOrigin = true, + slippageTolerance = 1, +}: SwapQuoteParams) => { + const { account: depositor } = useConnection(); + const { data, isLoading, error } = useQuery({ + queryKey: [ + "swap-quote", + origin, + destination, + amount, + isInputAmount, + depositor, + recipient, + ], + queryFn: async (): Promise => { + const { data } = await axios.get( + `${vercelApiBaseUrl}/api/swap/approval`, + { + params: { + tradeType: isInputAmount ? "exactInput" : "exactOutput", + inputToken: origin?.address, + outputToken: destination?.address, + originChainId: origin?.chainId, + destinationChainId: destination?.chainId, + depositor, + recipient: recipient || depositor, + ...(integratorId && { integratorId }), + ...(refundAddress && { refundAddress }), + amount: amount?.toString(), + refundOnOrigin, + slippageTolerance, + }, + } + ); + return data; + }, + enabled: + !!origin?.address && !!destination?.address && !!amount && !!depositor, + refetchInterval: 2_000, + }); + + return { data, isLoading, error }; +}; + +export default useSwapQuote; diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx new file mode 100644 index 000000000..cb7b05434 --- /dev/null +++ b/src/views/SwapAndBridge/index.tsx @@ -0,0 +1,109 @@ +import { LayoutV2 } from "components"; +import { EnrichedTokenSelect } from "./components/ChainTokenSelector/SelectorButton"; +import styled from "@emotion/styled"; +import { useCallback, useState } from "react"; +import { InputForm } from "./components/InputForm"; +import { BigNumber } from "ethers"; +import ConfirmationButton from "./components/ConfirmationButton"; +import useSwapQuote from "./hooks/useSwapQuote"; +import { useConnection } from "hooks"; +import { useHistory } from "react-router-dom"; + +export default function SwapAndBridge() { + const [inputToken, setInputToken] = useState( + null + ); + const [outputToken, setOutputToken] = useState( + null + ); + const [amount, setAmount] = useState(null); + const [isAmountOrigin, setIsAmountOrigin] = useState(true); + const { signer } = useConnection(); + const history = useHistory(); + + const { data: swapData, isLoading: isUpdateLoading } = useSwapQuote({ + origin: inputToken + ? { + address: inputToken.address, + chainId: inputToken.chainId, + } + : null, + destination: outputToken + ? { + address: outputToken.address, + chainId: outputToken.chainId, + } + : null, + amount: amount, + isInputAmount: isAmountOrigin, + }); + + // Handle confirmation (placeholder for now) + const handleConfirm = useCallback(async () => { + if (!swapData || !signer) { + return; + } + + if (swapData.approvalTxns?.length > 0) { + for (const approvalTxn of swapData.approvalTxns) { + await signer.sendTransaction({ + data: approvalTxn.data, + to: approvalTxn.to, + chainId: approvalTxn.chainId, + }); + } + } + + const tx = await signer.sendTransaction({ + data: swapData.swapTx.data, + to: swapData.swapTx.to, + chainId: swapData.swapTx.chainId, + }); + + history.push( + `/bridge/${tx.hash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [swapData, signer]); + + return ( + + + + + + + ); +} + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + + gap: 16px; + + align-items: center; + justify-content: center; + + width: 100%; + + padding-top: 64px; +`; From 11d6044d3d4a4304a88bbee3fb1336d93a8adc72 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 22 Sep 2025 13:56:36 +0200 Subject: [PATCH 002/122] add chains and tokens fetchers Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 46 +++++++++++++++----- src/utils/serverless-api/mocked/index.ts | 4 ++ src/utils/serverless-api/prod/index.ts | 4 ++ src/utils/serverless-api/prod/swap-chains.ts | 12 +++++ src/utils/serverless-api/prod/swap-tokens.ts | 21 +++++++++ src/utils/serverless-api/types.ts | 22 ++++++++++ 6 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 src/utils/serverless-api/prod/swap-chains.ts create mode 100644 src/utils/serverless-api/prod/swap-tokens.ts diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index ca6eddbc8..1ced8551d 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -1,5 +1,7 @@ import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; import { useQuery } from "@tanstack/react-query"; +import getApiEndpoint from "utils/serverless-api"; +import { SwapChain, SwapToken } from "utils/serverless-api/types"; export type LifiToken = { chainId: number; @@ -17,22 +19,46 @@ export default function useAvailableCrosschainRoutes() { return useQuery({ queryKey: ["availableCrosschainRoutes"], queryFn: async () => { - const result = await fetch( - "https://li.quest/v1/tokens?chainTypes=EVM&minPriceUSD=0.001" - ); - const data = (await result.json()) as { - tokens: Record>; - }; + const api = getApiEndpoint(); + const [chains, tokens] = await Promise.all([ + api.swapChains(), + api.swapTokens(), + ]); + + const allowedChainIds = new Set(Object.values(MAINNET_CHAIN_IDs)); - return Object.entries(data.tokens).reduce( - (acc, [chainId, tokens]) => { - if (Object.values(MAINNET_CHAIN_IDs).includes(Number(chainId))) { - acc[Number(chainId)] = tokens; + const tokenByChain = (tokens as SwapToken[]).reduce( + (acc, token) => { + if (!allowedChainIds.has(token.chainId)) { + return acc; + } + const mapped: LifiToken = { + chainId: token.chainId, + address: token.address, + name: token.name, + symbol: token.symbol, + decimals: token.decimals, + logoURI: token.logoUrl || "", + priceUSD: token.priceUsd || "0", + coinKey: token.symbol, + }; + if (!acc[token.chainId]) { + acc[token.chainId] = []; } + acc[token.chainId].push(mapped); return acc; }, {} as Record> ); + + // Ensure chains with no tokens are present as empty arrays + (chains as SwapChain[]).forEach((c) => { + if (allowedChainIds.has(c.chainId) && !tokenByChain[c.chainId]) { + tokenByChain[c.chainId] = []; + } + }); + + return tokenByChain; }, }); } diff --git a/src/utils/serverless-api/mocked/index.ts b/src/utils/serverless-api/mocked/index.ts index e7aec8384..6f7ef7496 100644 --- a/src/utils/serverless-api/mocked/index.ts +++ b/src/utils/serverless-api/mocked/index.ts @@ -11,6 +11,8 @@ import { poolsApiCall } from "./pools.mocked"; import { swapQuoteApiCall } from "./swap-quote"; import { poolsUserApiCall } from "./pools-user.mocked"; import { swapApprovalApiCall } from "../prod/swap-approval"; +import { swapChainsApiCall } from "../prod/swap-chains"; +import { swapTokensApiCall } from "../prod/swap-tokens"; export const mockedEndpoints: ServerlessAPIEndpoints = { coingecko: coingeckoMockedApiCall, @@ -29,4 +31,6 @@ export const mockedEndpoints: ServerlessAPIEndpoints = { poolsUser: poolsUserApiCall, swapQuote: swapQuoteApiCall, swapApproval: swapApprovalApiCall, + swapChains: swapChainsApiCall, + swapTokens: swapTokensApiCall, }; diff --git a/src/utils/serverless-api/prod/index.ts b/src/utils/serverless-api/prod/index.ts index c885b478e..58926c9aa 100644 --- a/src/utils/serverless-api/prod/index.ts +++ b/src/utils/serverless-api/prod/index.ts @@ -11,6 +11,8 @@ import { poolsApiCall } from "./pools"; import { swapQuoteApiCall } from "./swap-quote"; import { poolsUserApiCall } from "./pools-user"; import { swapApprovalApiCall } from "./swap-approval"; +import { swapChainsApiCall } from "./swap-chains"; +import { swapTokensApiCall } from "./swap-tokens"; export const prodEndpoints: ServerlessAPIEndpoints = { coingecko: coingeckoApiCall, suggestedFees: suggestedFeesApiCall, @@ -28,4 +30,6 @@ export const prodEndpoints: ServerlessAPIEndpoints = { poolsUser: poolsUserApiCall, swapQuote: swapQuoteApiCall, swapApproval: swapApprovalApiCall, + swapChains: swapChainsApiCall, + swapTokens: swapTokensApiCall, }; diff --git a/src/utils/serverless-api/prod/swap-chains.ts b/src/utils/serverless-api/prod/swap-chains.ts new file mode 100644 index 000000000..daccf8ea6 --- /dev/null +++ b/src/utils/serverless-api/prod/swap-chains.ts @@ -0,0 +1,12 @@ +import axios from "axios"; +import { vercelApiBaseUrl } from "utils"; +import { SwapChain } from "../types"; + +export type SwapChainsApiCall = typeof swapChainsApiCall; + +export async function swapChainsApiCall(): Promise { + const response = await axios.get( + `${vercelApiBaseUrl}/api/swap/chains` + ); + return response.data; +} diff --git a/src/utils/serverless-api/prod/swap-tokens.ts b/src/utils/serverless-api/prod/swap-tokens.ts new file mode 100644 index 000000000..e323b1604 --- /dev/null +++ b/src/utils/serverless-api/prod/swap-tokens.ts @@ -0,0 +1,21 @@ +import axios from "axios"; +import { vercelApiBaseUrl } from "utils"; +import { SwapToken } from "../types"; + +export type SwapTokensApiCall = typeof swapTokensApiCall; + +export type SwapTokensQuery = { + chainId?: number | number[]; +}; + +export async function swapTokensApiCall( + query?: SwapTokensQuery +): Promise { + const response = await axios.get( + `${vercelApiBaseUrl}/api/swap/tokens`, + { + params: query, + } + ); + return response.data; +} diff --git a/src/utils/serverless-api/types.ts b/src/utils/serverless-api/types.ts index ec196ea61..706376018 100644 --- a/src/utils/serverless-api/types.ts +++ b/src/utils/serverless-api/types.ts @@ -6,6 +6,8 @@ import { PoolsApiCall } from "./prod/pools"; import { SwapQuoteApiCall } from "./prod/swap-quote"; import { PoolsUserApiCall } from "./prod/pools-user"; import { SwapApprovalApiCall } from "./prod/swap-approval"; +import { SwapChainsApiCall } from "./prod/swap-chains"; +import { SwapTokensApiCall } from "./prod/swap-tokens"; export type ServerlessAPIEndpoints = { coingecko: CoingeckoApiCall; @@ -24,6 +26,8 @@ export type ServerlessAPIEndpoints = { poolsUser: PoolsUserApiCall; swapQuote: SwapQuoteApiCall; swapApproval: SwapApprovalApiCall; + swapChains: SwapChainsApiCall; + swapTokens: SwapTokensApiCall; }; export type RewardsApiFunction = @@ -111,3 +115,21 @@ export type BridgeLimitFunction = ( fromChainId: string | ChainId, toChainId: string | ChainId ) => Promise; + +export type SwapChain = { + chainId: number; + name: string; + publicRpcUrl: string; + explorerUrl: string; + logoUrl: string; +}; + +export type SwapToken = { + chainId: number; + address: string; + name: string; + symbol: string; + decimals: number; + logoUrl?: string; + priceUsd: string | null; +}; From 0aa37c72835e98a90aade7c769c6acb121e90ac8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 22 Sep 2025 16:10:30 +0200 Subject: [PATCH 003/122] update frontend swap api client Signed-off-by: Gerhard Steenkamp --- src/utils/serverless-api/prod/swap-approval.ts | 17 +++++++++++++++-- .../components/ChainTokenSelector/Modal.tsx | 1 + src/views/SwapAndBridge/hooks/useSwapQuote.ts | 4 +++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/utils/serverless-api/prod/swap-approval.ts b/src/utils/serverless-api/prod/swap-approval.ts index f23332e66..17714424f 100644 --- a/src/utils/serverless-api/prod/swap-approval.ts +++ b/src/utils/serverless-api/prod/swap-approval.ts @@ -30,6 +30,7 @@ export type SwapApprovalApiResponse = { }; }; approvalTxns: { + chainId: number; to: string; data: string; }[]; @@ -41,6 +42,10 @@ export type SwapApprovalApiResponse = { outputAmount: string; minOutputAmount: string; maxInputAmount: string; + swapProvider: { + name: string; + sources: string[]; + }; }; bridge: { inputAmount: string; @@ -73,6 +78,10 @@ export type SwapApprovalApiResponse = { maxInputAmount: string; outputAmount: string; minOutputAmount: string; + swapProvider: { + name: string; + sources: string[]; + }; }; }; refundToken: SwapApiToken; @@ -85,7 +94,7 @@ export type SwapApprovalApiResponse = { chainId: number; to: string; data: string; - value: string; + value?: string; gas?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; @@ -146,6 +155,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { maxInputAmount: BigNumber.from( result.steps.originSwap.maxInputAmount ), + swapProvider: result.steps.originSwap.swapProvider, } : undefined, bridge: { @@ -190,6 +200,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { minOutputAmount: BigNumber.from( result.steps.destinationSwap.minOutputAmount ), + swapProvider: result.steps.destinationSwap.swapProvider, } : undefined, }, @@ -203,7 +214,9 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { chainId: result.swapTx.chainId, to: result.swapTx.to, data: result.swapTx.data, - value: BigNumber.from(result.swapTx.value || "0"), + value: result.swapTx.value + ? BigNumber.from(result.swapTx.value) + : undefined, gas: result.swapTx.gas ? BigNumber.from(result.swapTx.gas) : undefined, maxFeePerGas: result.swapTx.maxFeePerGas ? BigNumber.from(result.swapTx.maxFeePerGas) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index f884119dd..e0a900a12 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -100,6 +100,7 @@ export default function ChainTokenSelectorModal({ exitOnOutsideClick width={720} height={800} + titleBorder > diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index c4ea54b84..deb6bc1fc 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -26,6 +26,8 @@ type SwapTransaction = { chainId: number; to: string; data: string; + value?: string; + gas?: string; maxFeePerGas: string; maxPriorityFeePerGas: string; }; @@ -73,7 +75,7 @@ const useSwapQuote = ({ `${vercelApiBaseUrl}/api/swap/approval`, { params: { - tradeType: isInputAmount ? "exactInput" : "exactOutput", + tradeType: isInputAmount ? "exactInput" : "minOutput", inputToken: origin?.address, outputToken: destination?.address, originChainId: origin?.chainId, From 185ada7f2452eee83a6b54016d2d13e91b9a2dd8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 26 Sep 2025 18:23:40 +0200 Subject: [PATCH 004/122] add base strategies fro swap and bridge Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 157 ++++++++++++++++++ .../hooks/useSwapApprovalAction/factory.ts | 35 ++++ .../hooks/useSwapApprovalAction/index.ts | 28 ++++ .../strategies/abstract.ts | 20 +++ .../useSwapApprovalAction/strategies/evm.ts | 59 +++++++ .../useSwapApprovalAction/strategies/svm.ts | 37 +++++ .../useSwapApprovalAction/strategies/types.ts | 32 ++++ 7 files changed, 368 insertions(+) create mode 100644 src/views/SwapAndBridge/hooks/useSwapAndBridge.ts create mode 100644 src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts create mode 100644 src/views/SwapAndBridge/hooks/useSwapApprovalAction/index.ts create mode 100644 src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts create mode 100644 src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts create mode 100644 src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts create mode 100644 src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts new file mode 100644 index 000000000..d64a0edc9 --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -0,0 +1,157 @@ +import { useCallback, useMemo, useState } from "react"; +import { BigNumber } from "ethers"; +import axios from "axios"; + +import { AmountInputError } from "../../Bridge/utils"; +import useSwapQuote from "./useSwapQuote"; +import { EnrichedTokenSelect } from "../components/ChainTokenSelector/SelectorButton"; +import { + useSwapApprovalAction, + SwapApprovalData, +} from "./useSwapApprovalAction"; + +export type UseSwapAndBridgeReturn = { + inputToken: EnrichedTokenSelect | null; + outputToken: EnrichedTokenSelect | null; + setInputToken: (t: EnrichedTokenSelect | null) => void; + setOutputToken: (t: EnrichedTokenSelect | null) => void; + quickSwap: () => void; + + amount: BigNumber | null; + setAmount: (a: BigNumber | null) => void; + isAmountOrigin: boolean; + setIsAmountOrigin: (v: boolean) => void; + + swapQuote: ReturnType["data"]; + isQuoteLoading: boolean; + expectedInputAmount?: string; + expectedOutputAmount?: string; + + validationError?: AmountInputError; + validationWarning?: AmountInputError; + + isConnected: boolean; + isWrongNetwork: boolean; + isSubmitting: boolean; + buttonDisabled: boolean; + onConfirm: () => Promise; +}; + +export function useSwapAndBridge(): UseSwapAndBridgeReturn { + const [inputToken, setInputToken] = useState( + null + ); + const [outputToken, setOutputToken] = useState( + null + ); + const [amount, setAmount] = useState(null); + const [isAmountOrigin, setIsAmountOrigin] = useState(true); + + const quickSwap = useCallback(() => { + setInputToken((prevInput) => { + const prevOut = outputToken; + setOutputToken(prevInput || null); + return prevOut || null; + }); + setAmount(null); + }, [outputToken]); + + const { + data: swapQuote, + isLoading: isQuoteLoading, + error, + } = useSwapQuote({ + origin: inputToken ? inputToken : null, + destination: outputToken ? outputToken : null, + amount: amount, + isInputAmount: isAmountOrigin, + }); + + const approvalData: SwapApprovalData | undefined = useMemo(() => { + if (!swapQuote) return undefined; + return { + approvalTxns: swapQuote.approvalTxns, + swapTx: swapQuote.swapTx as any, + }; + }, [swapQuote]); + + const approvalAction = useSwapApprovalAction( + inputToken?.chainId || 0, + approvalData + ); + + const validation = useMemo(() => { + let errorType: AmountInputError | undefined = undefined; + // invalid or empty amount + if (!amount || amount.lte(0)) { + errorType = AmountInputError.INVALID; + } + // balance check for origin-side inputs + if (!errorType && isAmountOrigin && inputToken?.balance) { + if (amount && amount.gt(inputToken.balance)) { + errorType = AmountInputError.INSUFFICIENT_BALANCE; + } + } + // backend availability + if (!errorType && error && axios.isAxiosError(error)) { + const code = (error.response?.data as any)?.code as string | undefined; + if (code === "AMOUNT_TOO_LOW") { + errorType = AmountInputError.AMOUNT_TOO_LOW; + } else if (code === "SWAP_QUOTE_UNAVAILABLE") { + errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; + } + } + return { + error: errorType, + warn: undefined as AmountInputError | undefined, + }; + }, [amount, isAmountOrigin, inputToken, error]); + + const expectedInputAmount = useMemo(() => { + return swapQuote?.inputAmount?.toString(); + }, [swapQuote]); + + const expectedOutputAmount = useMemo(() => { + return swapQuote?.expectedOutputAmount?.toString(); + }, [swapQuote]); + + const onConfirm = useCallback(async () => { + const txHash = await approvalAction.buttonActionHandler(); + return txHash as string; + }, [approvalAction]); + + return { + inputToken, + outputToken, + setInputToken, + setOutputToken, + quickSwap, + + amount, + setAmount, + isAmountOrigin, + setIsAmountOrigin, + + swapQuote, + isQuoteLoading, + expectedInputAmount, + expectedOutputAmount, + + validationError: validation.error, + validationWarning: validation.warn, + + isConnected: approvalAction.isConnected, + isWrongNetwork: approvalAction.isWrongNetwork, + isSubmitting: approvalAction.isButtonActionLoading, + buttonDisabled: + approvalAction.buttonDisabled || + !!validation.error || + !inputToken || + !outputToken || + !amount || + amount.lte(0), + onConfirm, + }; +} + +export default useSwapAndBridge; diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts new file mode 100644 index 000000000..5bfdb296d --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts @@ -0,0 +1,35 @@ +import { useMutation } from "@tanstack/react-query"; +import { + SwapApprovalActionStrategy, + SwapApprovalData, +} from "./strategies/types"; + +export function createSwapApprovalActionHook( + strategy: SwapApprovalActionStrategy +) { + return function useSwapApprovalAction(approvalData?: SwapApprovalData) { + const isConnected = strategy.isConnected(); + const isWrongNetwork = approvalData + ? strategy.isWrongNetwork(approvalData.swapTx.chainId) + : false; + + const action = useMutation({ + mutationFn: async () => { + if (!approvalData) throw new Error("Missing approval data"); + const txHash = await strategy.swap(approvalData); + return txHash; + }, + }); + + const buttonDisabled = !approvalData || (isConnected && action.isPending); + + return { + isConnected, + isWrongNetwork, + buttonActionHandler: action.mutateAsync, + isButtonActionLoading: action.isPending, + didActionError: action.isError, + buttonDisabled, + }; + }; +} diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/index.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/index.ts new file mode 100644 index 000000000..f861b783e --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/index.ts @@ -0,0 +1,28 @@ +import { createSwapApprovalActionHook } from "./factory"; +import { useConnectionSVM } from "hooks/useConnectionSVM"; +import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { EVMSwapApprovalActionStrategy } from "./strategies/evm"; +import { SVMSwapApprovalActionStrategy } from "./strategies/svm"; +import { getEcosystem } from "utils"; +import { SwapApprovalData } from "./strategies/types"; + +export function useSwapApprovalAction( + originChainId: number, + approvalData?: SwapApprovalData +) { + const connectionEVM = useConnectionEVM(); + const connectionSVM = useConnectionSVM(); + + const evmHook = createSwapApprovalActionHook( + new EVMSwapApprovalActionStrategy(connectionEVM) + ); + const svmHook = createSwapApprovalActionHook( + new SVMSwapApprovalActionStrategy(connectionSVM, connectionEVM) + ); + + return getEcosystem(originChainId) === "evm" + ? evmHook(approvalData) + : svmHook(approvalData); +} + +export type { SwapApprovalData } from "./strategies/types"; diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts new file mode 100644 index 000000000..31b1346e8 --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts @@ -0,0 +1,20 @@ +import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { SwapApprovalActionStrategy } from "./types"; + +export abstract class AbstractSwapApprovalActionStrategy + implements SwapApprovalActionStrategy +{ + constructor(readonly evmConnection: ReturnType) {} + + abstract isConnected(): boolean; + abstract isWrongNetwork(requiredChainId: number): boolean; + abstract switchNetwork(requiredChainId: number): Promise; + abstract swap(approvalData: any): Promise; + + async assertCorrectNetwork(requiredChainId: number) { + const currentChainId = this.evmConnection.chainId; + if (currentChainId !== requiredChainId) { + await this.evmConnection.setChain(requiredChainId); + } + } +} diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts new file mode 100644 index 000000000..cce0602e4 --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts @@ -0,0 +1,59 @@ +import { AbstractSwapApprovalActionStrategy } from "./abstract"; +import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { ApprovalTxn, SwapApprovalData, SwapTx } from "./types"; + +export class EVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStrategy { + constructor(evmConnection: ReturnType) { + super(evmConnection); + } + + private get signer() { + const { signer } = this.evmConnection; + if (!signer) { + throw new Error("No signer available"); + } + return signer; + } + + isConnected(): boolean { + return this.evmConnection.isConnected; + } + + isWrongNetwork(requiredChainId: number): boolean { + const connectedChainId = this.evmConnection.chainId; + return connectedChainId !== requiredChainId; + } + + async switchNetwork(requiredChainId: number): Promise { + await this.evmConnection.setChain(requiredChainId); + } + + async swap(approvalData: SwapApprovalData): Promise { + const signer = this.signer; + // approvals first + const approvals: ApprovalTxn[] = approvalData.approvalTxns || []; + for (const approval of approvals) { + await this.switchNetwork(approval.chainId); + await signer.sendTransaction({ + to: approval.to, + data: approval.data, + chainId: approval.chainId, + }); + } + // then final swap + const swapTx: SwapTx = approvalData.swapTx; + await this.switchNetwork(swapTx.chainId); + await this.assertCorrectNetwork(swapTx.chainId); + const tx = await signer.sendTransaction({ + to: swapTx.to, + data: swapTx.data, + value: swapTx.value, + chainId: swapTx.chainId, + gasPrice: undefined, + maxFeePerGas: swapTx.maxFeePerGas as any, + maxPriorityFeePerGas: swapTx.maxPriorityFeePerGas as any, + gasLimit: swapTx.gas as any, + }); + return tx.hash; + } +} diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts new file mode 100644 index 000000000..d65578aef --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts @@ -0,0 +1,37 @@ +import { AbstractSwapApprovalActionStrategy } from "./abstract"; +import { useConnectionSVM } from "hooks/useConnectionSVM"; +import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { SwapApprovalData, SwapTx } from "./types"; + +export class SVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStrategy { + constructor( + private readonly svmConnection: ReturnType, + evmConnection: ReturnType + ) { + super(evmConnection); + } + + isConnected(): boolean { + return this.svmConnection.isConnected; + } + + isWrongNetwork(_: number): boolean { + return !this.svmConnection.isConnected; + } + + async switchNetwork(_: number): Promise { + await this.svmConnection.connect(); + } + + async swap(approvalData: SwapApprovalData): Promise { + if (!this.svmConnection.wallet?.adapter) { + throw new Error("Wallet needs to be connected"); + } + + const swapTx: SwapTx = approvalData.swapTx; + const sig = await this.svmConnection.provider.sendRawTransaction( + Buffer.from(swapTx.data, "base64") + ); + return sig; + } +} diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts new file mode 100644 index 000000000..a0e95a60e --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts @@ -0,0 +1,32 @@ +export type ApprovalTxn = { + chainId: number; + to: string; + data: string; +}; + +export type SwapTx = { + simulationSuccess: boolean; + chainId: number; + to: string; + data: string; + value?: string; + gas?: string; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; +}; + +export type SwapApprovalData = { + approvalTxns?: ApprovalTxn[]; + swapTx: SwapTx; +}; + +export type ApproveAndExecuteParams = { + approvalData: SwapApprovalData; +}; + +export type SwapApprovalActionStrategy = { + isConnected(): boolean; + isWrongNetwork(requiredChainId: number): boolean; + switchNetwork(requiredChainId: number): Promise; + swap(approvalData: SwapApprovalData): Promise; +}; From 1ccac280ce8a8d6d7b23336a99c7f22284b3e23f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 26 Sep 2025 18:24:03 +0200 Subject: [PATCH 005/122] get bridge and swap routes. refactor Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 78 +++++++++++++-- src/views/Bridge/components/AmountInput.tsx | 2 +- .../components/ChainTokenSelector/Modal.tsx | 1 + .../SwapAndBridge/components/InputForm.tsx | 96 ++++++++++++++++++- src/views/SwapAndBridge/hooks/useSwapQuote.ts | 79 ++++++--------- src/views/SwapAndBridge/index.tsx | 83 +++++----------- 6 files changed, 218 insertions(+), 121 deletions(-) diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 1ced8551d..3f199733d 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -2,6 +2,7 @@ import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; import { useQuery } from "@tanstack/react-query"; import getApiEndpoint from "utils/serverless-api"; import { SwapChain, SwapToken } from "utils/serverless-api/types"; +import { getConfig } from "utils/config"; export type LifiToken = { chainId: number; @@ -12,9 +13,9 @@ export type LifiToken = { priceUSD: string; coinKey: string; logoURI: string; + routeSource: "bridge" | "swap" | "both"; }; -// TODO: Currently stubbed and will need to be added to the swap API export default function useAvailableCrosschainRoutes() { return useQuery({ queryKey: ["availableCrosschainRoutes"], @@ -27,7 +28,8 @@ export default function useAvailableCrosschainRoutes() { const allowedChainIds = new Set(Object.values(MAINNET_CHAIN_IDs)); - const tokenByChain = (tokens as SwapToken[]).reduce( + // 1) Build swap token map by chain + const swapTokensByChain = (tokens as SwapToken[]).reduce( (acc, token) => { if (!allowedChainIds.has(token.chainId)) { return acc; @@ -41,6 +43,7 @@ export default function useAvailableCrosschainRoutes() { logoURI: token.logoUrl || "", priceUSD: token.priceUsd || "0", coinKey: token.symbol, + routeSource: "swap", }; if (!acc[token.chainId]) { acc[token.chainId] = []; @@ -51,14 +54,71 @@ export default function useAvailableCrosschainRoutes() { {} as Record> ); - // Ensure chains with no tokens are present as empty arrays - (chains as SwapChain[]).forEach((c) => { - if (allowedChainIds.has(c.chainId) && !tokenByChain[c.chainId]) { - tokenByChain[c.chainId] = []; - } - }); + // 2) Build bridge token map by origin chain from generated routes + const config = getConfig(); + const enabledRoutes = config.getEnabledRoutes(); + const bridgeOriginChains = Array.from( + new Set(enabledRoutes.map((r) => r.fromChain)) + ); + + const bridgeTokensByChain = bridgeOriginChains.reduce( + (acc, fromChainId) => { + if (!allowedChainIds.has(fromChainId)) { + return acc; + } + const reachable = config.filterReachableTokens(fromChainId); + const lifiTokens: LifiToken[] = reachable.map((t) => ({ + chainId: fromChainId, + address: t.address, + name: t.name, + symbol: t.displaySymbol || t.symbol, + decimals: t.decimals, + logoURI: t.logoURI || "", + // We do not have price data from the routes; default to 0 + priceUSD: "0", + coinKey: t.symbol, + routeSource: "bridge", + })); + acc[fromChainId] = lifiTokens; + return acc; + }, + {} as Record> + ); + + // 3) Merge swap and bridge tokens, de-duplicating by address (case-insensitive) + const chainIdsInSwap = new Set( + (chains as SwapChain[]).map((c) => c.chainId) + ); + const chainIdsInBridge = new Set( + Object.keys(bridgeTokensByChain).map(Number) + ); + const chainIds = Array.from( + new Set([...chainIdsInSwap, ...chainIdsInBridge]) + ).filter((id) => allowedChainIds.has(id)); + + const blendedByChain: Record> = {}; + for (const chainId of chainIds) { + const mapByAddr = new Map(); + // Prefer swap tokens first (they include price) + (swapTokensByChain[chainId] || []).forEach((t) => { + mapByAddr.set(t.address.toLowerCase(), t); + }); + // Add bridge tokens, merging routeSource when duplicate + (bridgeTokensByChain[chainId] || []).forEach((t) => { + const key = t.address.toLowerCase(); + const existing = mapByAddr.get(key); + if (!existing) { + mapByAddr.set(key, t); + } else { + // Merge: if token exists from swap, mark as both + mapByAddr.set(key, { ...existing, routeSource: "both" }); + } + }); + + blendedByChain[chainId] = Array.from(mapByAddr.values()); + } - return tokenByChain; + return blendedByChain; }, }); } diff --git a/src/views/Bridge/components/AmountInput.tsx b/src/views/Bridge/components/AmountInput.tsx index b3eb30c28..a9c5cb4cc 100644 --- a/src/views/Bridge/components/AmountInput.tsx +++ b/src/views/Bridge/components/AmountInput.tsx @@ -6,7 +6,7 @@ import { AmountInputError, SelectedRoute } from "../utils"; import { formatUnitsWithMaxFractions, getToken } from "utils"; import { BridgeLimits } from "hooks"; -const validationErrorTextMap: Record = { +export const validationErrorTextMap: Record = { [AmountInputError.INSUFFICIENT_BALANCE]: "Insufficient balance to process this transfer.", [AmountInputError.PAUSED_DEPOSITS]: diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index e0a900a12..589fc9533 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -74,6 +74,7 @@ export default function ChainTokenSelectorModal({ const displayedChains = useMemo(() => { return Object.fromEntries( Object.entries(crossChainRoutes.data || {}).filter(([chainId]) => { + // why ar we filtering out Boba? if ([288].includes(Number(chainId))) { return false; } diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 18e9b501b..efec1fe2a 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -7,6 +7,7 @@ import styled from "@emotion/styled"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BigNumber, utils } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; +import { AmountInputError } from "../../Bridge/utils"; export const InputForm = ({ inputToken, @@ -100,6 +101,36 @@ const TokenInput = ({ }) => { const [amountString, setAmountString] = useState(""); const [justTyped, setJustTyped] = useState(false); + const [validationError, setValidationError] = useState( + undefined + ); + + const getValidationErrorText = useCallback( + (error?: AmountInputError) => { + if (!error || !token) return undefined; + const validationErrorTextMap: Record = { + [AmountInputError.INSUFFICIENT_BALANCE]: + "Insufficient balance to process this transfer.", + [AmountInputError.PAUSED_DEPOSITS]: + "[INPUT_TOKEN] deposits are temporarily paused.", + [AmountInputError.INSUFFICIENT_LIQUIDITY]: + "Input amount exceeds limits set to maintain optimal service for all users. Decrease amount to [MAX_DEPOSIT] or lower.", + [AmountInputError.INVALID]: + "Only positive numbers are allowed as an input.", + [AmountInputError.AMOUNT_TOO_LOW]: + "The amount you are trying to bridge is too low.", + [AmountInputError.PRICE_IMPACT_TOO_HIGH]: + "Price impact is too high. Check back later when liquidity is restored.", + [AmountInputError.SWAP_QUOTE_UNAVAILABLE]: + "Swap quote temporarily unavailable. Please try again later.", + }; + + return validationErrorTextMap[error] + .replace("[INPUT_TOKEN]", token.symbol) + .replace("[MAX_DEPOSIT]", ""); + }, + [token] + ); // Handle user input changes useEffect(() => { @@ -108,9 +139,41 @@ const TokenInput = ({ } setJustTyped(false); try { - setAmount(utils.parseUnits(amountString, token!.decimals)); + if (!token) { + setAmount(null); + setValidationError(undefined); + return; + } + const parsed = utils.parseUnits(amountString, token.decimals); + if (isOrigin) { + if (parsed.lt(0)) { + setValidationError(getValidationErrorText(AmountInputError.INVALID)); + setAmount(null); + return; + } + if (token.balance && parsed.gt(token.balance)) { + setValidationError( + getValidationErrorText(AmountInputError.INSUFFICIENT_BALANCE) + ); + } else { + setValidationError(undefined); + } + } else { + if (parsed.lt(0)) { + setValidationError(getValidationErrorText(AmountInputError.INVALID)); + setAmount(null); + return; + } + setValidationError(undefined); + } + setAmount(parsed); } catch (e) { setAmount(null); + if (amountString !== "") { + setValidationError(getValidationErrorText(AmountInputError.INVALID)); + } else { + setValidationError(undefined); + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [amountString]); @@ -132,6 +195,7 @@ const TokenInput = ({ setAmountString( formatUnitsWithMaxFractions(expectedAmount, token.decimals) ); + setValidationError(undefined); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -166,14 +230,22 @@ const TokenInput = ({ placeholder="0.00" value={amountString} onChange={(e) => { - setJustTyped(true); - setAmountString(e.target.value); + const value = e.target.value; + if (value === "" || /^\d*\.?\d*$/.test(value)) { + setJustTyped(true); + setAmountString(value); + } }} disabled={shouldUpdate && isUpdateLoading} /> {estimatedUsdAmount && <>Value: ~${estimatedUsdAmount}} + {validationError && ( + + {validationError} + + )} @@ -246,6 +327,15 @@ const TokenAmountInputEstimatedUsd = styled.div` line-height: 130%; `; +const TokenAmountInputValidationError = styled.div` + color: #f96c6c; + font-family: Barlow; + font-size: 12px; + font-weight: 400; + line-height: 130%; + margin-top: 4px; +`; + const TokenInputWrapper = styled.div` display: flex; height: 132px; diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index deb6bc1fc..e84854424 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -3,15 +3,15 @@ import axios from "axios"; import { BigNumber } from "ethers"; import { useConnection } from "hooks"; import { vercelApiBaseUrl } from "utils"; - -type TokenParam = { - address: string; - chainId: number; -}; +import { + SwapApiToken, + SwapApprovalApiQueryParams, + SwapApprovalApiResponse, +} from "utils/serverless-api/prod/swap-approval"; type SwapQuoteParams = { - origin: TokenParam | null; - destination: TokenParam | null; + origin: SwapApiToken | null; + destination: SwapApiToken | null; amount: BigNumber | null; isInputAmount: boolean; recipient?: string; @@ -21,33 +21,6 @@ type SwapQuoteParams = { slippageTolerance?: number; }; -type SwapTransaction = { - simulationSuccess: boolean; - chainId: number; - to: string; - data: string; - value?: string; - gas?: string; - maxFeePerGas: string; - maxPriorityFeePerGas: string; -}; - -type SwapQuoteResponse = { - checks: object; - steps: object; - refundToken: object; - inputAmount: string; - expectedOutputAmount: string; - minOutputAmount: string; - expectedFillTime: number; - swapTx: SwapTransaction; - approvalTxns: { - chainId: number; - data: string; - to: string; - }[]; -}; - const useSwapQuote = ({ origin, destination, @@ -70,31 +43,37 @@ const useSwapQuote = ({ depositor, recipient, ], - queryFn: async (): Promise => { + queryFn: async (): Promise => { + if (!origin || !destination || !amount || !depositor) { + throw new Error("Missing required swap quote parameters"); + } + + const params: SwapApprovalApiQueryParams = { + tradeType: isInputAmount ? "exactInput" : "minOutput", + inputToken: origin.address, + outputToken: destination.address, + originChainId: origin.chainId, + destinationChainId: destination.chainId, + depositor, + recipient: recipient || depositor, + amount: amount.toString(), + refundOnOrigin, + slippageTolerance, + ...(integratorId ? { integratorId } : {}), + ...(refundAddress ? { refundAddress } : {}), + }; + const { data } = await axios.get( `${vercelApiBaseUrl}/api/swap/approval`, { - params: { - tradeType: isInputAmount ? "exactInput" : "minOutput", - inputToken: origin?.address, - outputToken: destination?.address, - originChainId: origin?.chainId, - destinationChainId: destination?.chainId, - depositor, - recipient: recipient || depositor, - ...(integratorId && { integratorId }), - ...(refundAddress && { refundAddress }), - amount: amount?.toString(), - refundOnOrigin, - slippageTolerance, - }, + params, } ); return data; }, enabled: !!origin?.address && !!destination?.address && !!amount && !!depositor, - refetchInterval: 2_000, + refetchInterval: 5_000, }); return { data, isLoading, error }; diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index cb7b05434..8b81a4e94 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -1,70 +1,37 @@ import { LayoutV2 } from "components"; -import { EnrichedTokenSelect } from "./components/ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; -import { useCallback, useState } from "react"; +import { useCallback } from "react"; import { InputForm } from "./components/InputForm"; -import { BigNumber } from "ethers"; import ConfirmationButton from "./components/ConfirmationButton"; -import useSwapQuote from "./hooks/useSwapQuote"; -import { useConnection } from "hooks"; import { useHistory } from "react-router-dom"; +import useSwapAndBridge from "./hooks/useSwapAndBridge"; export default function SwapAndBridge() { - const [inputToken, setInputToken] = useState( - null - ); - const [outputToken, setOutputToken] = useState( - null - ); - const [amount, setAmount] = useState(null); - const [isAmountOrigin, setIsAmountOrigin] = useState(true); - const { signer } = useConnection(); + const { + inputToken, + outputToken, + setInputToken, + setOutputToken, + amount, + setAmount, + isAmountOrigin, + setIsAmountOrigin, + swapQuote, + isQuoteLoading, + expectedInputAmount, + expectedOutputAmount, + onConfirm, + } = useSwapAndBridge(); const history = useHistory(); - const { data: swapData, isLoading: isUpdateLoading } = useSwapQuote({ - origin: inputToken - ? { - address: inputToken.address, - chainId: inputToken.chainId, - } - : null, - destination: outputToken - ? { - address: outputToken.address, - chainId: outputToken.chainId, - } - : null, - amount: amount, - isInputAmount: isAmountOrigin, - }); - // Handle confirmation (placeholder for now) const handleConfirm = useCallback(async () => { - if (!swapData || !signer) { - return; - } - - if (swapData.approvalTxns?.length > 0) { - for (const approvalTxn of swapData.approvalTxns) { - await signer.sendTransaction({ - data: approvalTxn.data, - to: approvalTxn.to, - chainId: approvalTxn.chainId, - }); - } - } - - const tx = await signer.sendTransaction({ - data: swapData.swapTx.data, - to: swapData.swapTx.to, - chainId: swapData.swapTx.chainId, - }); - + const txHash = await onConfirm(); history.push( - `/bridge/${tx.hash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` + `/bridge/${txHash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [swapData, signer]); + }, [onConfirm, inputToken, outputToken]); return ( @@ -77,16 +44,16 @@ export default function SwapAndBridge() { setAmount={setAmount} isAmountOrigin={isAmountOrigin} setIsAmountOrigin={setIsAmountOrigin} - isQuoteLoading={isUpdateLoading} - expectedOutputAmount={swapData?.expectedOutputAmount} - expectedInputAmount={swapData?.inputAmount} + isQuoteLoading={isQuoteLoading} + expectedOutputAmount={expectedOutputAmount} + expectedInputAmount={expectedInputAmount} /> From 32401c39e4a073ee0bb436e2f6447d9546e9d9f4 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sat, 27 Sep 2025 09:58:49 +0200 Subject: [PATCH 006/122] centralize navigation links Signed-off-by: Gerhard Steenkamp --- src/Routes.tsx | 22 +++++---- src/components/Header/Header.tsx | 11 +---- .../Sidebar/components/NavigationContent.tsx | 45 ++++++++----------- 3 files changed, 34 insertions(+), 44 deletions(-) diff --git a/src/Routes.tsx b/src/Routes.tsx index a7943530a..99ef012b7 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -8,14 +8,9 @@ import { } from "react-router-dom"; import { Header, Sidebar } from "components"; import { useConnection, useError } from "hooks"; -import { - enableMigration, - stringValueInArray, - getConfig, - chainEndpointToId, -} from "utils"; +import { stringValueInArray, getConfig, chainEndpointToId } from "utils"; import lazyWithRetry from "utils/lazy-with-retry"; - +import { enableMigration } from "utils"; import Toast from "components/Toast"; import BouncingDotsLoader from "components/BouncingDotsLoader"; import NotFound from "./views/NotFound"; @@ -23,6 +18,15 @@ import ScrollToTop from "components/ScrollToTop"; import { AmpliTrace } from "components/AmpliTrace"; import Banners from "components/Banners"; +export const NAVIGATION_LINKS = !enableMigration + ? [ + { href: "/bridge-and-swap", name: "Bridge & Swap" }, + { href: "/pool", name: "Pool" }, + { href: "/rewards", name: "Rewards" }, + { href: "/transactions", name: "Transactions" }, + ] + : []; + const LiquidityPool = lazyWithRetry( () => import(/* webpackChunkName: "LiquidityPools" */ "./views/LiquidityPool") ); @@ -140,13 +144,13 @@ const Routes: React.FC = () => { } }} /> - + diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 3ab13f5ef..051bfb9f9 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -13,19 +13,12 @@ import { StyledLogo, } from "./Header.styles"; import MenuToggle from "./MenuToggle"; -import { enableMigration } from "utils"; import useScrollPosition from "hooks/useScrollPosition"; import { isChildPath } from "./utils"; import { useSidebarContext } from "hooks/useSidebarContext"; +import { NAVIGATION_LINKS } from "Routes"; -const LINKS = !enableMigration - ? [ - { href: "/bridge", name: "Bridge" }, - { href: "/pool", name: "Pool" }, - { href: "/rewards", name: "Rewards" }, - { href: "/transactions", name: "Transactions" }, - ] - : []; +export const LINKS = NAVIGATION_LINKS; interface Props { transparentHeader?: boolean; diff --git a/src/components/Sidebar/components/NavigationContent.tsx b/src/components/Sidebar/components/NavigationContent.tsx index a39d13000..ece53394f 100644 --- a/src/components/Sidebar/components/NavigationContent.tsx +++ b/src/components/Sidebar/components/NavigationContent.tsx @@ -5,27 +5,20 @@ import { useSidebarContext } from "hooks/useSidebarContext"; import { AccountContent } from "./AccountContent"; import { SidebarItem } from "./SidebarItem"; import { TermsOfServiceDisclaimer } from "./TermsOfServiceDisclaimer"; +import { NAVIGATION_LINKS } from "Routes"; -const sidebarNavigationLinks = [ - { - pathName: "/bridge", - title: "Bridge", - }, - { - pathName: "/pool", - title: "Pool", - }, - { - pathName: "/rewards", - title: "Rewards", - }, - { - pathName: "/transactions", - title: "Transactions", - }, +type NavigationLInk = { + href: string; + name: string; + isExternalLink?: boolean; + rightIcon?: React.ReactNode; +}; + +const sidebarNavigationLinks: NavigationLInk[] = [ + ...NAVIGATION_LINKS, { - pathName: "https://docs.across.to/", - title: "Docs", + href: "https://docs.across.to/", + name: "Docs", isExternalLink: true, rightIcon: , }, @@ -75,20 +68,20 @@ export function NavigationContent() { <> {sidebarNavigationLinks.map((item) => - item.isExternalLink ? ( + item?.isExternalLink ? ( ) : ( ) )} From 37398788ef32cb10d4a967968fd404c56946aaf3 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sat, 27 Sep 2025 12:09:22 +0200 Subject: [PATCH 007/122] style selectorButton Signed-off-by: Gerhard Steenkamp --- .../ChainTokenSelector/SelectorButton.tsx | 84 +++++++++---------- .../SwapAndBridge/components/InputForm.tsx | 22 +++-- 2 files changed, 48 insertions(+), 58 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index 1909ce16f..c806cb3db 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -2,7 +2,6 @@ import styled from "@emotion/styled"; import { BigNumber } from "ethers"; import { useCallback, useEffect, useState } from "react"; import { COLORS, getChainInfo } from "utils"; -import TokenMask from "assets/mask/token-mask.svg"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; import ChainTokenSelectorModal from "./Modal"; @@ -24,13 +23,14 @@ type Props = { onSelect?: (token: EnrichedTokenSelect) => void; isOriginToken: boolean; marginBottom?: string; + className?: string; }; export default function SelectorButton({ onSelect, selectedToken, isOriginToken, - marginBottom, + className, }: Props) { const [displayModal, setDisplayModal] = useState(false); @@ -52,18 +52,15 @@ export default function SelectorButton({ if (!selectedToken) { return ( <> - setDisplayModal(true)} - marginBottom={marginBottom} - > + setDisplayModal(true)}> - Select a token + Select token - + - setDisplayModal(true)} - marginBottom={marginBottom} - > + setDisplayModal(true)}> + {selectedToken.symbol} {chain.name} @@ -105,36 +100,28 @@ export default function SelectorButton({ ); } -const Wrapper = styled.div<{ marginBottom?: string }>` +const Wrapper = styled.div` + --height: 48px; + --padding: 8px; + height: var(--height); + position: relative; display: flex; flex-direction: row; justify-content: space-between; - margin-bottom: ${({ marginBottom }) => marginBottom || "0"}; - - border-radius: 8px; - border: 1px solid #3f4247; - background: #e0f3ff0d; - padding: 8px 12px; - height: 64px; - - gap: 12px; - width: 184px; + border-radius: 12px; + border: 1px solid rgba(224, 243, 255, 0.05); + background: rgba(224, 243, 255, 0.05); cursor: pointer; `; -const SelectWrapper = styled(Wrapper)` - height: 48px; -`; - const VerticalDivider = styled.div` width: 1px; - height: calc(100% + 16px); + height: calc(100% - (var(--padding) * 2)); + margin-top: var(--padding); - margin-top: -8px; - - background: #3f4247; + background: rgba(224, 243, 255, 0.05); `; const ChevronStack = styled.div` @@ -142,13 +129,15 @@ const ChevronStack = styled.div` align-items: center; justify-content: center; height: 100%; + width: var(--height); `; const NamesStack = styled.div` display: flex; flex-direction: column; - gap: 6px; - + gap: 2px; + padding-inline: var(--padding); + white-space: nowrap; height: 100%; flex-grow: 1; @@ -174,33 +163,36 @@ const ChainName = styled.div` line-height: 12px; font-weight: 400; color: #e0f3ff; + opacity: 0.5; `; const TokenStack = styled.div` - width: 32px; - height: 48px; + height: 100%; + width: var(--height); + padding-inline: var(--padding); position: relative; - flex-grow: 0; `; const TokenImg = styled.img` + border-radius: 50%; position: absolute; - top: 0; - left: 0; - width: 32px; - height: 32px; + top: var(--padding); + left: var(--padding); + width: calc(var(--height) * 0.66); + height: calc(var(--height) * 0.66); z-index: 1; - - mask: url(${TokenMask}) no-repeat center center; `; const ChainImg = styled.img` + border-radius: 50%; + border: 1px solid transparent; + background: ${COLORS["grey-600"]}; position: absolute; - bottom: 0; - left: 4.5px; - width: 24px; - height: 24px; + bottom: calc(var(--padding) / 2); + right: calc(var(--padding) / 2); + width: 30%; + height: 30%; z-index: 2; `; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index efec1fe2a..cb5ecbae0 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -224,7 +224,7 @@ const TokenInput = ({ - {isOrigin ? "Sell" : "Buy"} + {isOrigin ? "From" : "To"} COLORS.aqua}; + color: ${COLORS.aqua}; font-size: 16px; - font-weight: 400; + font-weight: 500; line-height: 130%; `; -const TokenAmountInput = styled.input` - color: #e0f3ff; +const TokenAmountInput = styled.input<{ value: string }>` font-family: Barlow; font-size: 48px; font-weight: 300; line-height: 120%; letter-spacing: -1.92px; + width: 100%; + color: ${(value) => (value ? COLORS.aqua : COLORS["light-200"])}; outline: none; border: none; @@ -320,7 +321,7 @@ const TokenAmountInput = styled.input` `; const TokenAmountInputEstimatedUsd = styled.div` - color: #e0f3ff; + color: ${COLORS["light-200"]}; font-family: Barlow; font-size: 14px; font-weight: 400; @@ -344,8 +345,7 @@ const TokenInputWrapper = styled.div` align-items: center; align-self: stretch; border-radius: 12px; - border: 1px solid rgba(224, 243, 255, 0.05); - background: #2d2e32; + background: transparent; position: relative; `; @@ -357,7 +357,6 @@ const BalanceSelectorWrapper = styled.div` const Wrapper = styled.div` position: relative; - display: flex; flex-direction: column; align-items: flex-start; @@ -366,8 +365,7 @@ const Wrapper = styled.div` align-self: stretch; padding: 12px; border-radius: 24px; - border: 1px solid rgba(224, 243, 255, 0.05); - background: #34353b; + background: ${COLORS["black-700"]}; box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); `; @@ -382,7 +380,7 @@ const QuickSwapButton = styled.button` gap: 8px; border-radius: 32px; border: 1px solid #4c4e57; - background: #34353b; + background: ${COLORS["black-700"]}; box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); cursor: pointer; From d66a0daa4767c56ad36087578b65ef838a55b879 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sat, 27 Sep 2025 12:29:33 +0200 Subject: [PATCH 008/122] style input form Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/components/InputForm.tsx | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index cb5ecbae0..592e2d8cc 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -239,7 +239,10 @@ const TokenInput = ({ disabled={shouldUpdate && isUpdateLoading} /> - {estimatedUsdAmount && <>Value: ~${estimatedUsdAmount}} + + {" "} + Value: ${estimatedUsdAmount ?? "0.00"} + {validationError && ( @@ -283,6 +286,18 @@ const TokenInput = ({ ); }; +const ValueRow = styled.div` + font-size: 16px; + span { + margin-left: 4px; + } + span, + svg { + display: inline-block; + vertical-align: middle; + } +`; + const TokenAmountStack = styled.div` display: flex; flex-direction: column; @@ -326,27 +341,30 @@ const TokenAmountInputEstimatedUsd = styled.div` font-size: 14px; font-weight: 400; line-height: 130%; + opacity: 0.5; `; const TokenAmountInputValidationError = styled.div` color: #f96c6c; font-family: Barlow; font-size: 12px; - font-weight: 400; + font-weight: 600; line-height: 130%; margin-top: 4px; `; const TokenInputWrapper = styled.div` display: flex; - height: 132px; - padding: 16px; + min-height: 148px; justify-content: space-between; align-items: center; align-self: stretch; - border-radius: 12px; background: transparent; position: relative; + padding: 24px; + border-radius: 24px; + background: ${COLORS["black-700"]}; + box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); `; const BalanceSelectorWrapper = styled.div` @@ -361,12 +379,8 @@ const Wrapper = styled.div` flex-direction: column; align-items: flex-start; justify-content: center; - gap: 12px; + gap: 8px; align-self: stretch; - padding: 12px; - border-radius: 24px; - background: ${COLORS["black-700"]}; - box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); `; const QuickSwapButton = styled.button` From 62d8fb4e69c127752c4f77b068bf098f85ed1792 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sat, 27 Sep 2025 13:44:11 +0200 Subject: [PATCH 009/122] style balance selector Signed-off-by: Gerhard Steenkamp --- .../components/BalanceSelector.tsx | 22 +++++++++--- .../SwapAndBridge/components/InputForm.tsx | 34 ++++++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index 89ebfc280..11e24228a 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -81,7 +81,9 @@ export default function BalanceSelector({ ))} - Balance: {formattedBalance} + + Balance: {formattedBalance} + ); } @@ -91,19 +93,30 @@ const BalanceWrapper = styled.div` align-items: center; gap: 12px; margin-top: 8px; + position: relative; + justify-content: flex-end; + margin-left: auto; `; -const BalanceText = styled.span` - color: ${() => COLORS.aqua}; +const BalanceText = styled.div` + color: ${COLORS.white}; + opacity: 1; font-size: 14px; font-weight: 400; line-height: 130%; + + span { + opacity: 0.5; + } `; const PillsContainer = styled.div` + --spacing: 4px; display: flex; align-items: center; - gap: 4px; + gap: var(--spacing); + position: absolute; + right: calc(100% + (var(--spacing) * 2)); .pill { display: flex; @@ -114,6 +127,7 @@ const PillsContainer = styled.div` justify-content: center; font-size: 12px; font-weight: 600; + border: 1px solid rgba(224, 243, 255, 0.5); background-color: rgba(224, 243, 255, 0.05); color: rgba(224, 243, 255, 0.5); cursor: pointer; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 592e2d8cc..f6e2b0db6 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -250,14 +250,15 @@ const TokenInput = ({ )} - - {token && ( - + + + + {token && ( - - )} + )} + ); }; +const TokenSelectorColumn = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; +`; + const ValueRow = styled.div` font-size: 16px; span { @@ -367,12 +375,6 @@ const TokenInputWrapper = styled.div` box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.08); `; -const BalanceSelectorWrapper = styled.div` - position: absolute; - bottom: 16px; - right: 16px; -`; - const Wrapper = styled.div` position: relative; display: flex; From 23181f334392b123f45e29e58d9e6676b656527a Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sat, 27 Sep 2025 17:04:45 +0200 Subject: [PATCH 010/122] use swap quote fees Signed-off-by: Gerhard Steenkamp --- .../ChainTokenSelector/SelectorButton.tsx | 5 + .../components/ConfirmationButton.tsx | 134 ++++++++++++------ 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index c806cb3db..0f05ab9d5 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -114,6 +114,10 @@ const Wrapper = styled.div` background: rgba(224, 243, 255, 0.05); cursor: pointer; + + &:hover { + background: rgba(224, 243, 255, 0.1); + } `; const VerticalDivider = styled.div` @@ -156,6 +160,7 @@ const TokenName = styled.div` const SelectTokenName = styled(TokenName)` color: ${COLORS["aqua"]}; + padding-inline: 8px; `; const ChainName = styled.div` diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 2e5a4e69d..4f49a326a 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -5,7 +5,8 @@ import { ReactComponent as LoadingIcon } from "assets/icons/loading.svg"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; -import { COLORS } from "utils"; +import { COLORS, formatUSD, getConfig } from "utils"; +import { useTokenConversion } from "hooks/useTokenConversion"; import { useConnection, useIsWrongNetwork } from "hooks"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; @@ -215,42 +216,102 @@ export const ConfirmationButton: React.FC = ({ const state = getButtonState(); // Calculate display values from swapQuote + // Resolve conversion helpers outside memo to respect hooks rules + const bridgeTokenSymbol = + (swapQuote as any)?.steps?.bridge?.tokenOut?.symbol || + outputToken?.symbol || + "ETH"; + const destinationNativeSymbol = getConfig().getNativeTokenInfo( + outputToken?.chainId || 1 + ).symbol; + const { convertTokenToBaseCurrency: convertInputTokenToUsd } = + useTokenConversion(inputToken?.symbol || "ETH", "usd"); + const { convertTokenToBaseCurrency: convertBridgeTokenToUsd } = + useTokenConversion(bridgeTokenSymbol, "usd"); + const { convertTokenToBaseCurrency: convertDestinationNativeToUsd } = + useTokenConversion(destinationNativeSymbol, "usd"); + const displayValues = React.useMemo(() => { + const toBN = (v: any) => { + try { + return BigNumber.from(v ?? 0); + } catch { + return BigNumber.from(0); + } + }; + + const formatUsdString = (v?: BigNumber) => { + if (!v) return "-"; + try { + return `$${formatUSD(v)}`; + } catch { + return "-"; + } + }; + if (!swapQuote || !inputToken || !outputToken) { return { - fee: "$0.05", - time: "~2 min", - bridgeFee: "$0.01", - destinationGasFee: "$0", - extraFee: "$0.04", + fee: "-", + time: "-", + bridgeFee: "-", + destinationGasFee: "-", + extraFee: "-", route: "Across V4", - estimatedTime: "~2 secs", - netFee: "$0.05", + estimatedTime: "-", + netFee: "-", }; } - // Calculate fees based on swapQuote data - // This is a placeholder - you'd calculate actual fees from the quote - const bridgeFee = "$0.01"; - const destinationGasFee = "$0"; - const extraFee = "$0.04"; - const netFee = "$0.05"; + const fees = (swapQuote as any)?.steps?.bridge?.fees || {}; + const relayerCapitalTotal = toBN(fees?.relayerCapital?.total); + const lpTotal = toBN(fees?.lp?.total); + const relayerGasTotal = toBN(fees?.relayerGas?.total); + + // Convert components to USD + const bridgeFeeTokenAmount = relayerCapitalTotal.add(lpTotal); + const bridgeFeeUsd = convertBridgeTokenToUsd(bridgeFeeTokenAmount); + const gasFeeUsd = convertDestinationNativeToUsd(relayerGasTotal); + + // Approximate swap fee in USD if we have user input and bridge input + const bridgeInputAmount = toBN( + (swapQuote as any)?.steps?.bridge?.inputAmount + ); + const inputAmountUsd = convertInputTokenToUsd(amount ?? BigNumber.from(0)); + const bridgeInputUsd = convertBridgeTokenToUsd(bridgeInputAmount); + const swapFeeUsd = + inputAmountUsd && bridgeInputUsd && inputAmountUsd.gt(bridgeInputUsd) + ? inputAmountUsd.sub(bridgeInputUsd) + : BigNumber.from(0); + + const netFeeUsd = (bridgeFeeUsd || BigNumber.from(0)) + .add(gasFeeUsd || BigNumber.from(0)) + .add(swapFeeUsd || BigNumber.from(0)); // Format time from expectedFillTime (in seconds) - const timeInMinutes = Math.ceil(swapQuote.expectedFillTime / 60); + const timeInMinutes = Math.ceil( + ((swapQuote as any).expectedFillTime || 0) / 60 + ); const time = timeInMinutes < 1 ? "~30 sec" : `~${timeInMinutes} min`; return { - fee: netFee, + fee: formatUsdString(netFeeUsd), time, - bridgeFee, - destinationGasFee, - extraFee, + bridgeFee: formatUsdString(bridgeFeeUsd), + destinationGasFee: formatUsdString(gasFeeUsd), + extraFee: formatUsdString(swapFeeUsd), route: "Across V4", estimatedTime: timeInMinutes < 1 ? "~30 secs" : `~${timeInMinutes} mins`, - netFee, + netFee: formatUsdString(netFeeUsd), }; - }, [swapQuote, inputToken, outputToken]); + }, [ + swapQuote, + inputToken, + outputToken, + amount, + convertInputTokenToUsd, + convertBridgeTokenToUsd, + convertDestinationNativeToUsd, + ]); // Handle confirmation const handleConfirm = async () => { @@ -266,12 +327,6 @@ export const ConfirmationButton: React.FC = ({ } }; - // Compute target height based on state and expansion - let targetHeight = 88; - if (state === "readyToConfirm") { - targetHeight = expanded ? 300 : 128; - } - // Render state-specific content let content: React.ReactNode = null; switch (state) { @@ -281,14 +336,10 @@ export const ConfirmationButton: React.FC = ({ = ({ return ( {content} @@ -663,7 +709,7 @@ const StyledChevronDown = styled(ChevronDownIcon)<{ expanded: boolean }>` const ExpandableContent = styled.div<{ expanded: boolean }>` overflow: hidden; transition: all 0.3s ease; - max-height: ${({ expanded }) => (expanded ? "160px" : "0")}; + max-height: ${({ expanded }) => (expanded ? "500px" : "0")}; margin-top: ${({ expanded }) => (expanded ? "8px" : "0")}; `; @@ -673,7 +719,7 @@ const StyledButton = styled.button<{ fullHeight?: boolean; }>` width: 100%; - height: ${({ fullHeight }) => (fullHeight ? "100%" : "64px")}; + height: 64px; border-radius: 12px; font-weight: 600; font-size: 16px; From 22cacbfce958580ba64f2ed2628798956105fc66 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sat, 27 Sep 2025 18:00:07 +0200 Subject: [PATCH 011/122] better button states Signed-off-by: Gerhard Steenkamp --- src/assets/icons/loading-2.svg | 6 + .../components/ConfirmationButton.tsx | 183 +++++++----------- 2 files changed, 75 insertions(+), 114 deletions(-) create mode 100644 src/assets/icons/loading-2.svg diff --git a/src/assets/icons/loading-2.svg b/src/assets/icons/loading-2.svg new file mode 100644 index 000000000..547982fb3 --- /dev/null +++ b/src/assets/icons/loading-2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 4f49a326a..a9d1b80bb 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -1,7 +1,9 @@ "use client"; import { ButtonHTMLAttributes } from "react"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; -import { ReactComponent as LoadingIcon } from "assets/icons/loading.svg"; +import { ReactComponent as LoadingIcon } from "assets/icons/loading-2.svg"; +import { ReactComponent as Info } from "assets/icons/info.svg"; +import { ReactComponent as Wallet } from "assets/icons/wallet.svg"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; @@ -28,7 +30,8 @@ export type BridgeButtonState = | "awaitingAmountInput" | "readyToConfirm" | "submitting" - | "wrongNetwork"; + | "wrongNetwork" + | "loadingQuote"; interface ConfirmationButtonProps extends ButtonHTMLAttributes { @@ -40,15 +43,6 @@ interface ConfirmationButtonProps onConfirm?: () => void; } -const stateLabels: Record = { - notConnected: "Connect Wallet", - awaitingTokenSelection: "Select Token", - awaitingAmountInput: "Input Amount", - readyToConfirm: "Confirm Swap", - submitting: "Submitting...", - wrongNetwork: "Switch Network", -}; - // Expandable label section component const ExpandableLabelSection: React.FC< React.PropsWithChildren<{ @@ -146,7 +140,7 @@ const ExpandableLabelSection: React.FC< // Core button component, used by all states const ButtonCore: React.FC< ConfirmationButtonProps & { - label: string; + label: React.ReactNode; loading?: boolean; aqua?: boolean; state: BridgeButtonState; @@ -171,16 +165,13 @@ const ButtonCore: React.FC< > - - {loading && } - {!loading && label} - + {label} @@ -210,6 +201,7 @@ export const ConfirmationButton: React.FC = ({ if (!inputToken || !outputToken) return "awaitingTokenSelection"; if (!amount || amount.lte(0)) return "awaitingAmountInput"; if (isWrongNetwork) return "wrongNetwork"; + if (isQuoteLoading) return "loadingQuote"; return "readyToConfirm"; }; @@ -287,11 +279,11 @@ export const ConfirmationButton: React.FC = ({ .add(gasFeeUsd || BigNumber.from(0)) .add(swapFeeUsd || BigNumber.from(0)); - // Format time from expectedFillTime (in seconds) - const timeInMinutes = Math.ceil( - ((swapQuote as any).expectedFillTime || 0) / 60 - ); - const time = timeInMinutes < 1 ? "~30 sec" : `~${timeInMinutes} min`; + const totalSeconds = Math.max(0, Number(swapQuote.expectedFillTime || 0)); + const underOneMinute = totalSeconds < 60; + const time = underOneMinute + ? `~${Math.max(1, Math.round(totalSeconds))} secs` + : `~${Math.ceil(totalSeconds / 60)} min`; return { fee: formatUsdString(netFeeUsd), @@ -300,7 +292,7 @@ export const ConfirmationButton: React.FC = ({ destinationGasFee: formatUsdString(gasFeeUsd), extraFee: formatUsdString(swapFeeUsd), route: "Across V4", - estimatedTime: timeInMinutes < 1 ? "~30 secs" : `~${timeInMinutes} mins`, + estimatedTime: time, netFee: formatUsdString(netFeeUsd), }; }, [ @@ -327,6 +319,26 @@ export const ConfirmationButton: React.FC = ({ } }; + const stateLabels: Record = { + notConnected: ( + <> + + Connect Wallet + + ), + awaitingTokenSelection: "Select Token", + awaitingAmountInput: "Input Amount", + readyToConfirm: "Confirm Swap", + submitting: "Submitting...", + wrongNetwork: "Switch Network", + loadingQuote: ( + <> + + Finalizing Quote + + ), + }; + // Render state-specific content let content: React.ReactNode = null; switch (state) { @@ -377,97 +389,16 @@ export const ConfirmationButton: React.FC = ({ - - - - - - - + Est. Time {displayValues.estimatedTime} - - - - - - - + Net Fee - - - - - - - + {displayValues.netFee} @@ -588,7 +519,25 @@ export const ConfirmationButton: React.FC = ({ + ); + break; + case "loadingQuote": + content = ( + = ({ return ( @@ -708,7 +656,9 @@ const StyledChevronDown = styled(ChevronDownIcon)<{ expanded: boolean }>` const ExpandableContent = styled.div<{ expanded: boolean }>` overflow: hidden; - transition: all 0.3s ease; + transition: + max-height 0.3s ease, + margin-top 0.3s ease; max-height: ${({ expanded }) => (expanded ? "500px" : "0")}; margin-top: ${({ expanded }) => (expanded ? "8px" : "0")}; `; @@ -756,11 +706,11 @@ const ButtonContent = styled.span` gap: 8px; `; -const StyledLoadingIcon = styled(LoadingIcon)<{ aqua?: boolean }>` +const StyledLoadingIcon = styled(LoadingIcon)` width: 16px; height: 16px; animation: spin 1s linear infinite; - color: ${({ aqua }) => (aqua ? COLORS.aqua : "#000000")}; + color: inherit; @keyframes spin { from { @@ -773,7 +723,7 @@ const StyledLoadingIcon = styled(LoadingIcon)<{ aqua?: boolean }>` `; const ButtonContainer = styled.div<{ expanded: boolean }>` - margin-top: ${({ expanded }) => (expanded ? "24px" : "0")}; + flex: 0 0 auto; `; const ExpandedDetails = styled.div` @@ -807,7 +757,7 @@ const RouteIcon = styled.svg` height: 20px; `; -const InfoIcon = styled.svg` +const InfoIconSvg = styled.svg` width: 20px; height: 20px; `; @@ -842,4 +792,9 @@ const FeeBreakdownValue = styled.span` color: #e0f3ff; `; +const SmallInfoIcon = styled(Info)` + width: 16px; + height: 16px; +`; + export default ConfirmationButton; From 8911b7baaf9e04d4e417a07def116cb1db8fc366 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Sun, 28 Sep 2025 13:40:45 +0200 Subject: [PATCH 012/122] refactor Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 410 +++++++++--------- 1 file changed, 199 insertions(+), 211 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index a9d1b80bb..492fb46e1 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -12,6 +12,7 @@ import { useTokenConversion } from "hooks/useTokenConversion"; import { useConnection, useIsWrongNetwork } from "hooks"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; +import { AmountInputError } from "../../Bridge/utils"; type SwapQuoteResponse = { checks: object; @@ -41,6 +42,8 @@ interface ConfirmationButtonProps swapQuote: SwapQuoteResponse | null; isQuoteLoading: boolean; onConfirm?: () => void; + validationError?: AmountInputError; + validationWarning?: AmountInputError; } // Expandable label section component @@ -51,88 +54,168 @@ const ExpandableLabelSection: React.FC< expanded: boolean; onToggle: () => void; visible: boolean; + state: BridgeButtonState; + validationError?: AmountInputError; + validationWarning?: AmountInputError; }> -> = ({ fee, time, expanded, onToggle, visible, children }) => { - return ( - - {visible && ( - - - - = ({ fee, time, expanded, onToggle, state, children }) => { + // Render state-specific content + let content: React.ReactNode = null; + switch (state) { + case "notConnected": + content = ( + <> + + + + + Fast & Secure + + + + - - - Fast & Secure - - - - - - - - - {fee} - - - - - - - - - {time} - - - - - - {expanded && children} - - - )} + + + + + {fee} + + + + + + + + + {time} + + + + ); + break; + + case "readyToConfirm": + content = ( + <> + + + + + Fast & Secure + + + + + + + + + {fee} + + + + + + + + + {time} + + + + + ); + break; + default: + break; + } + return ( + + + + {content} + + + {expanded && children} + + ); }; @@ -142,24 +225,14 @@ const ButtonCore: React.FC< ConfirmationButtonProps & { label: React.ReactNode; loading?: boolean; - aqua?: boolean; state: BridgeButtonState; fullHeight?: boolean; } -> = ({ - label, - loading, - disabled, - aqua, - state, - onConfirm, - onClick, - fullHeight, -}) => ( +> = ({ label, loading, disabled, state, onConfirm, onClick, fullHeight }) => ( @@ -184,6 +257,8 @@ export const ConfirmationButton: React.FC = ({ swapQuote, isQuoteLoading, onConfirm, + validationError, + validationWarning, ...props }) => { const { account, connect } = useConnection(); @@ -339,12 +414,28 @@ export const ConfirmationButton: React.FC = ({ ), }; - // Render state-specific content - let content: React.ReactNode = null; - switch (state) { - case "readyToConfirm": - content = ( - <> + // Map visual and behavior from state + const isExpandable = state === "readyToConfirm"; + const buttonLabel = stateLabels[state]; + const buttonLoading = state === "loadingQuote" || state === "submitting"; + const buttonDisabled = + state === "awaitingTokenSelection" || + state === "awaitingAmountInput" || + state === "loadingQuote" || + state === "submitting"; + + const clickHandler = + state === "notConnected" + ? () => connect() + : state === "wrongNetwork" + ? () => isWrongNetworkHandler() + : undefined; + + // Render unified group driven by state + const content = ( + <> + + {isExpandable && ( = ({ expanded={expanded} onToggle={() => setExpanded((e) => !e)} visible={true} + state={state} + validationError={validationError} + validationWarning={validationWarning} > {expanded ? ( @@ -426,133 +520,27 @@ export const ConfirmationButton: React.FC = ({ ) : null} - - - - - ); - break; - case "notConnected": - content = ( + )} + + connect()} + fullHeight={state !== "readyToConfirm"} + onClick={clickHandler} /> - ); - break; - case "wrongNetwork": - content = ( - isWrongNetworkHandler()} - /> - ); - break; - case "awaitingTokenSelection": - content = ( - - ); - break; - case "awaitingAmountInput": - content = ( - - ); - break; - case "submitting": - content = ( - - ); - break; - case "loadingQuote": - content = ( - - ); - break; - default: - content = null; - } + + + ); return ( Date: Sun, 28 Sep 2025 16:42:49 +0200 Subject: [PATCH 013/122] show validation Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 453 +++++++++--------- .../SwapAndBridge/components/InputForm.tsx | 82 +--- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 4 +- src/views/SwapAndBridge/index.tsx | 4 + 4 files changed, 234 insertions(+), 309 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 492fb46e1..4dd6d35ff 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -4,6 +4,7 @@ import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg import { ReactComponent as LoadingIcon } from "assets/icons/loading-2.svg"; import { ReactComponent as Info } from "assets/icons/info.svg"; import { ReactComponent as Wallet } from "assets/icons/wallet.svg"; +import { ReactComponent as Across } from "assets/token-logos/acx.svg"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; @@ -13,6 +14,7 @@ import { useConnection, useIsWrongNetwork } from "hooks"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; +import { validationErrorTextMap } from "views/Bridge/components/AmountInput"; type SwapQuoteResponse = { checks: object; @@ -32,7 +34,8 @@ export type BridgeButtonState = | "readyToConfirm" | "submitting" | "wrongNetwork" - | "loadingQuote"; + | "loadingQuote" + | "validationError"; interface ConfirmationButtonProps extends ButtonHTMLAttributes { @@ -55,146 +58,122 @@ const ExpandableLabelSection: React.FC< onToggle: () => void; visible: boolean; state: BridgeButtonState; + hasQuote: boolean; validationError?: AmountInputError; validationWarning?: AmountInputError; }> -> = ({ fee, time, expanded, onToggle, state, children }) => { +> = ({ + fee, + time, + expanded, + onToggle, + state, + children, + hasQuote, + validationError, +}) => { // Render state-specific content let content: React.ReactNode = null; - switch (state) { - case "notConnected": - content = ( - <> - - + + + {validationErrorTextMap[validationError]} + + + ); + } else if (hasQuote) { + content = ( + <> + + + + + Fast & Secure + + + + - - - Fast & Secure - - - - - - - - - {fee} - - - - - - - - - {time} - - - - ); - break; - - case "readyToConfirm": - content = ( - <> - - + + + + {fee} + + + + - - - Fast & Secure - - - - - - - - - {fee} - - - - - - - - - {time} - - - - - ); - break; - default: - break; + + + + + {time} + + + + + ); + } else { + content = ( + <> + + + + + Fast & Secure + + + Across V4. More Chains Faster. + + + ); } + return ( = ({ label, loading, disabled, state, onConfirm, onClick, fullHeight }) => ( +> = ({ label, loading, disabled, state, onClick, fullHeight }) => ( @@ -277,6 +256,7 @@ export const ConfirmationButton: React.FC = ({ if (!amount || amount.lte(0)) return "awaitingAmountInput"; if (isWrongNetwork) return "wrongNetwork"; if (isQuoteLoading) return "loadingQuote"; + if (validationError) return "validationError"; return "readyToConfirm"; }; @@ -412,115 +392,117 @@ export const ConfirmationButton: React.FC = ({ Finalizing Quote ), + validationError: "Confirm Swap", }; // Map visual and behavior from state - const isExpandable = state === "readyToConfirm"; const buttonLabel = stateLabels[state]; const buttonLoading = state === "loadingQuote" || state === "submitting"; const buttonDisabled = state === "awaitingTokenSelection" || state === "awaitingAmountInput" || state === "loadingQuote" || - state === "submitting"; + state === "submitting" || + state === "validationError"; const clickHandler = state === "notConnected" ? () => connect() : state === "wrongNetwork" ? () => isWrongNetworkHandler() - : undefined; + : state === "readyToConfirm" + ? () => handleConfirm() + : undefined; // Render unified group driven by state const content = ( <> - - {isExpandable && ( - + + setExpanded((e) => !e)} + visible={true} + state={state} + validationError={validationError} + validationWarning={validationWarning} + hasQuote={!!swapQuote} > - setExpanded((e) => !e)} - visible={true} - state={state} - validationError={validationError} - validationWarning={validationWarning} - > - {expanded ? ( - - - - - - - - - Route - - - - {displayValues.route} - - - - - - Est. Time - - {displayValues.estimatedTime} - - - - - Net Fee - - - {displayValues.netFee} - - - - Bridge Fee - - {displayValues.bridgeFee} - - - - Destination Gas Fee - - {displayValues.destinationGasFee} - - - - Extra Fee - - {displayValues.extraFee} - - - - - ) : null} - - - )} + {expanded && state === "readyToConfirm" ? ( + + + + + + + + + Route + + + + {displayValues.route} + + + + + + Est. Time + + {displayValues.estimatedTime} + + + + + Net Fee + + + {displayValues.netFee} + + + + Bridge Fee + + {displayValues.bridgeFee} + + + + Destination Gas Fee + + {displayValues.destinationGasFee} + + + + Extra Fee + + {displayValues.extraFee} + + + + + ) : null} + + = ({ label={buttonLabel} loading={buttonLoading} disabled={buttonDisabled} - onConfirm={handleConfirm} inputToken={inputToken} outputToken={outputToken} amount={amount} @@ -553,16 +534,26 @@ export const ConfirmationButton: React.FC = ({ ); }; +const ValidationText = styled.div` + color: ${COLORS.white}; + font-size: 14px; + font-weight: 400; + margin-inline: auto; + display: flex; + align-items: center; + gap: 4px; +`; + // Styled components const Container = styled(motion.div)<{ state: BridgeButtonState }>` - background: rgba(108, 249, 216, 0.1); + background: ${({ state }) => + state === "validationError" + ? COLORS["grey-400-5"] + : "rgba(108, 249, 216, 0.1)"}; border-radius: 24px; display: flex; flex-direction: column; - padding: ${({ state }) => - state === "readyToConfirm" || state === "submitting" - ? "4px 12px 12px 12px" - : "0"}; + padding: 8px 12px 12px 12px; width: 100%; overflow: hidden; gap: ${({ state }) => (state === "readyToConfirm" ? "8px" : "0")}; @@ -589,6 +580,7 @@ const ExpandableLabelLeft = styled.span` display: flex; align-items: center; gap: 8px; + justify-content: flex-start; `; const ShieldIcon = styled.svg` @@ -606,6 +598,12 @@ const ExpandableLabelRight = styled.div` gap: 8px; font-size: 12px; color: #e0f3ff; + justify-content: flex-end; +`; + +const ExpandableLabelRightAccent = styled(ExpandableLabelLeft)` + text-align: right; + justify-content: flex-end; `; const FeeTimeItem = styled.span` @@ -665,10 +663,11 @@ const StyledButton = styled.button<{ border: none; cursor: pointer; - background: ${({ aqua }) => (aqua ? "transparent" : COLORS.aqua)}; - color: ${({ aqua }) => (aqua ? COLORS.aqua : "#000000")}; + background: ${({ aqua }) => + aqua ? COLORS.aqua : "rgba(224, 243, 255, 0.05)"}; + color: ${({ aqua }) => (aqua ? "#2D2E33" : "#E0F3FF")}; - &:hover { + &:not(:disabled):hover { ${({ aqua }) => aqua ? `background: rgba(108, 249, 216, 0.1);` @@ -678,12 +677,14 @@ const StyledButton = styled.button<{ `} } - &:focus { + &:not(:disabled):focus { ${({ aqua }) => !aqua && `box-shadow: 0 0 16px 0 ${COLORS.aqua};`} } &:disabled { - ${({ loading }) => loading && "opacity: 0.6; cursor: wait;"} + cursor: ${({ loading }) => (loading ? "wait" : "not-allowed")}; + box-shadow: none; + opacity: ${({ loading }) => (loading ? 0.9 : 0.6)}; } `; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index f6e2b0db6..4ed1f4a49 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -101,36 +101,7 @@ const TokenInput = ({ }) => { const [amountString, setAmountString] = useState(""); const [justTyped, setJustTyped] = useState(false); - const [validationError, setValidationError] = useState( - undefined - ); - - const getValidationErrorText = useCallback( - (error?: AmountInputError) => { - if (!error || !token) return undefined; - const validationErrorTextMap: Record = { - [AmountInputError.INSUFFICIENT_BALANCE]: - "Insufficient balance to process this transfer.", - [AmountInputError.PAUSED_DEPOSITS]: - "[INPUT_TOKEN] deposits are temporarily paused.", - [AmountInputError.INSUFFICIENT_LIQUIDITY]: - "Input amount exceeds limits set to maintain optimal service for all users. Decrease amount to [MAX_DEPOSIT] or lower.", - [AmountInputError.INVALID]: - "Only positive numbers are allowed as an input.", - [AmountInputError.AMOUNT_TOO_LOW]: - "The amount you are trying to bridge is too low.", - [AmountInputError.PRICE_IMPACT_TOO_HIGH]: - "Price impact is too high. Check back later when liquidity is restored.", - [AmountInputError.SWAP_QUOTE_UNAVAILABLE]: - "Swap quote temporarily unavailable. Please try again later.", - }; - - return validationErrorTextMap[error] - .replace("[INPUT_TOKEN]", token.symbol) - .replace("[MAX_DEPOSIT]", ""); - }, - [token] - ); + const [validationError] = useState(undefined); // Handle user input changes useEffect(() => { @@ -141,39 +112,12 @@ const TokenInput = ({ try { if (!token) { setAmount(null); - setValidationError(undefined); return; } const parsed = utils.parseUnits(amountString, token.decimals); - if (isOrigin) { - if (parsed.lt(0)) { - setValidationError(getValidationErrorText(AmountInputError.INVALID)); - setAmount(null); - return; - } - if (token.balance && parsed.gt(token.balance)) { - setValidationError( - getValidationErrorText(AmountInputError.INSUFFICIENT_BALANCE) - ); - } else { - setValidationError(undefined); - } - } else { - if (parsed.lt(0)) { - setValidationError(getValidationErrorText(AmountInputError.INVALID)); - setAmount(null); - return; - } - setValidationError(undefined); - } setAmount(parsed); } catch (e) { setAmount(null); - if (amountString !== "") { - setValidationError(getValidationErrorText(AmountInputError.INVALID)); - } else { - setValidationError(undefined); - } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [amountString]); @@ -195,7 +139,6 @@ const TokenInput = ({ setAmountString( formatUnitsWithMaxFractions(expectedAmount, token.decimals) ); - setValidationError(undefined); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -244,11 +187,6 @@ const TokenInput = ({ Value: ${estimatedUsdAmount ?? "0.00"} - {validationError && ( - - {validationError} - - )} @@ -352,15 +281,6 @@ const TokenAmountInputEstimatedUsd = styled.div` opacity: 0.5; `; -const TokenAmountInputValidationError = styled.div` - color: #f96c6c; - font-family: Barlow; - font-size: 12px; - font-weight: 600; - line-height: 130%; - margin-top: 4px; -`; - const TokenInputWrapper = styled.div` display: flex; min-height: 148px; diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index d64a0edc9..ee74a7023 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -82,8 +82,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const validation = useMemo(() => { let errorType: AmountInputError | undefined = undefined; - // invalid or empty amount - if (!amount || amount.lte(0)) { + // invalid amount (allow empty/no amount without error) + if (amount && amount.lte(0)) { errorType = AmountInputError.INVALID; } // balance check for origin-side inputs diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 8b81a4e94..6a533654f 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -21,6 +21,8 @@ export default function SwapAndBridge() { expectedInputAmount, expectedOutputAmount, onConfirm, + validationError, + validationWarning, } = useSwapAndBridge(); const history = useHistory(); @@ -55,6 +57,8 @@ export default function SwapAndBridge() { swapQuote={swapQuote || null} isQuoteLoading={isQuoteLoading} onConfirm={handleConfirm} + validationError={validationError} + validationWarning={validationWarning} /> From 28e9560635d2ba14ea09afe90ab79289a902dffc Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 13:14:40 +0200 Subject: [PATCH 014/122] refactor Signed-off-by: Gerhard Steenkamp --- src/assets/icons/gas.svg | 5 + src/assets/icons/route.svg | 5 + src/assets/icons/shield.svg | 3 + src/assets/icons/time.svg | 5 + src/assets/icons/wallet.svg | 8 +- .../components/ConfirmationButton.tsx | 201 ++++-------------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 91 +++++--- .../hooks/useSwapApprovalAction/factory.ts | 2 +- .../strategies/abstract.ts | 2 +- .../useSwapApprovalAction/strategies/evm.ts | 24 ++- .../useSwapApprovalAction/strategies/svm.ts | 10 + .../useSwapApprovalAction/strategies/types.ts | 2 +- .../hooks/useValidateSwapAndBridge.ts | 47 ++++ src/views/SwapAndBridge/index.tsx | 10 +- 14 files changed, 206 insertions(+), 209 deletions(-) create mode 100644 src/assets/icons/gas.svg create mode 100644 src/assets/icons/route.svg create mode 100644 src/assets/icons/shield.svg create mode 100644 src/assets/icons/time.svg create mode 100644 src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts diff --git a/src/assets/icons/gas.svg b/src/assets/icons/gas.svg new file mode 100644 index 000000000..bba1d5579 --- /dev/null +++ b/src/assets/icons/gas.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/route.svg b/src/assets/icons/route.svg new file mode 100644 index 000000000..8671c3c4b --- /dev/null +++ b/src/assets/icons/route.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icons/shield.svg b/src/assets/icons/shield.svg new file mode 100644 index 000000000..503c3a2e5 --- /dev/null +++ b/src/assets/icons/shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/time.svg b/src/assets/icons/time.svg new file mode 100644 index 000000000..055c9a2b4 --- /dev/null +++ b/src/assets/icons/time.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/wallet.svg b/src/assets/icons/wallet.svg index 37c3b159d..9bc251d8a 100644 --- a/src/assets/icons/wallet.svg +++ b/src/assets/icons/wallet.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 4dd6d35ff..fce19d789 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -5,12 +5,16 @@ import { ReactComponent as LoadingIcon } from "assets/icons/loading-2.svg"; import { ReactComponent as Info } from "assets/icons/info.svg"; import { ReactComponent as Wallet } from "assets/icons/wallet.svg"; import { ReactComponent as Across } from "assets/token-logos/acx.svg"; +import { ReactComponent as Route } from "assets/icons/route.svg"; +import { ReactComponent as Shield } from "assets/icons/shield.svg"; +import { ReactComponent as Gas } from "assets/icons/gas.svg"; +import { ReactComponent as Time } from "assets/icons/time.svg"; + import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; import { COLORS, formatUSD, getConfig } from "utils"; import { useTokenConversion } from "hooks/useTokenConversion"; -import { useConnection, useIsWrongNetwork } from "hooks"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; @@ -47,6 +51,11 @@ interface ConfirmationButtonProps onConfirm?: () => void; validationError?: AmountInputError; validationWarning?: AmountInputError; + // External state props + buttonState: BridgeButtonState; + buttonDisabled: boolean; + buttonLoading: boolean; + buttonLabel?: string; } // Expandable label section component @@ -87,60 +96,17 @@ const ExpandableLabelSection: React.FC< content = ( <> - - - + Fast & Secure - - - - - + {fee} - - - - - + {time} @@ -151,20 +117,7 @@ const ExpandableLabelSection: React.FC< content = ( <> - - - + Fast & Secure @@ -200,14 +153,14 @@ const ExpandableLabelSection: React.FC< }; // Core button component, used by all states -const ButtonCore: React.FC< - ConfirmationButtonProps & { - label: React.ReactNode; - loading?: boolean; - state: BridgeButtonState; - fullHeight?: boolean; - } -> = ({ label, loading, disabled, state, onClick, fullHeight }) => ( +const ButtonCore: React.FC<{ + label: React.ReactNode; + loading?: boolean; + disabled?: boolean; + state: BridgeButtonState; + fullHeight?: boolean; + onClick?: () => void; +}> = ({ label, loading, disabled, state, onClick, fullHeight }) => ( - {label} + + {loading && } + {state === "notConnected" && ( + + )} + {label} + @@ -238,29 +197,14 @@ export const ConfirmationButton: React.FC = ({ onConfirm, validationError, validationWarning, - ...props + buttonState, + buttonDisabled, + buttonLoading, + buttonLabel, }) => { - const { account, connect } = useConnection(); const [expanded, setExpanded] = React.useState(false); - const [isSubmitting, setIsSubmitting] = React.useState(false); - - const { isWrongNetworkHandler, isWrongNetwork } = useIsWrongNetwork( - inputToken?.chainId - ); - - // Determine the current state - const getButtonState = (): BridgeButtonState => { - if (isSubmitting) return "submitting"; - if (!account) return "notConnected"; - if (!inputToken || !outputToken) return "awaitingTokenSelection"; - if (!amount || amount.lte(0)) return "awaitingAmountInput"; - if (isWrongNetwork) return "wrongNetwork"; - if (isQuoteLoading) return "loadingQuote"; - if (validationError) return "validationError"; - return "readyToConfirm"; - }; - const state = getButtonState(); + const state = buttonState; // Calculate display values from swapQuote // Resolve conversion helpers outside memo to respect hooks rules @@ -360,59 +304,7 @@ export const ConfirmationButton: React.FC = ({ convertDestinationNativeToUsd, ]); - // Handle confirmation - const handleConfirm = async () => { - if (!onConfirm) return; - - setIsSubmitting(true); - try { - onConfirm(); - } catch (error) { - console.error("Confirmation failed:", error); - } finally { - setIsSubmitting(false); - } - }; - - const stateLabels: Record = { - notConnected: ( - <> - - Connect Wallet - - ), - awaitingTokenSelection: "Select Token", - awaitingAmountInput: "Input Amount", - readyToConfirm: "Confirm Swap", - submitting: "Submitting...", - wrongNetwork: "Switch Network", - loadingQuote: ( - <> - - Finalizing Quote - - ), - validationError: "Confirm Swap", - }; - - // Map visual and behavior from state - const buttonLabel = stateLabels[state]; - const buttonLoading = state === "loadingQuote" || state === "submitting"; - const buttonDisabled = - state === "awaitingTokenSelection" || - state === "awaitingAmountInput" || - state === "loadingQuote" || - state === "submitting" || - state === "validationError"; - - const clickHandler = - state === "notConnected" - ? () => connect() - : state === "wrongNetwork" - ? () => isWrongNetworkHandler() - : state === "readyToConfirm" - ? () => handleConfirm() - : undefined; + const clickHandler = onConfirm; // Render unified group driven by state const content = ( @@ -441,22 +333,7 @@ export const ConfirmationButton: React.FC = ({ - - - - - + Route @@ -506,16 +383,10 @@ export const ConfirmationButton: React.FC = ({ diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index ee74a7023..1cf013140 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -1,6 +1,5 @@ import { useCallback, useMemo, useState } from "react"; import { BigNumber } from "ethers"; -import axios from "axios"; import { AmountInputError } from "../../Bridge/utils"; import useSwapQuote from "./useSwapQuote"; @@ -9,6 +8,8 @@ import { useSwapApprovalAction, SwapApprovalData, } from "./useSwapApprovalAction"; +import { useValidateSwapAndBridge } from "./useValidateSwapAndBridge"; +import { BridgeButtonState } from "../components/ConfirmationButton"; export type UseSwapAndBridgeReturn = { inputToken: EnrichedTokenSelect | null; @@ -30,10 +31,16 @@ export type UseSwapAndBridgeReturn = { validationError?: AmountInputError; validationWarning?: AmountInputError; + // Button state information + buttonState: BridgeButtonState; + buttonDisabled: boolean; + buttonLoading: boolean; + buttonLabel: string; + + // Legacy properties isConnected: boolean; isWrongNetwork: boolean; isSubmitting: boolean; - buttonDisabled: boolean; onConfirm: () => Promise; }; @@ -80,32 +87,12 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { approvalData ); - const validation = useMemo(() => { - let errorType: AmountInputError | undefined = undefined; - // invalid amount (allow empty/no amount without error) - if (amount && amount.lte(0)) { - errorType = AmountInputError.INVALID; - } - // balance check for origin-side inputs - if (!errorType && isAmountOrigin && inputToken?.balance) { - if (amount && amount.gt(inputToken.balance)) { - errorType = AmountInputError.INSUFFICIENT_BALANCE; - } - } - // backend availability - if (!errorType && error && axios.isAxiosError(error)) { - const code = (error.response?.data as any)?.code as string | undefined; - if (code === "AMOUNT_TOO_LOW") { - errorType = AmountInputError.AMOUNT_TOO_LOW; - } else if (code === "SWAP_QUOTE_UNAVAILABLE") { - errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; - } - } - return { - error: errorType, - warn: undefined as AmountInputError | undefined, - }; - }, [amount, isAmountOrigin, inputToken, error]); + const validation = useValidateSwapAndBridge( + amount, + isAmountOrigin, + inputToken, + error + ); const expectedInputAmount = useMemo(() => { return swapQuote?.inputAmount?.toString(); @@ -120,6 +107,31 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { return txHash as string; }, [approvalAction]); + // Button state logic + const buttonState: BridgeButtonState = useMemo(() => { + if (isQuoteLoading) return "loadingQuote"; + if (!approvalAction.isConnected) return "notConnected"; + if (approvalAction.isButtonActionLoading) return "submitting"; + if (!inputToken || !outputToken) return "awaitingTokenSelection"; + if (!amount || amount.lte(0)) return "awaitingAmountInput"; + if (validation.error) return "validationError"; + return "readyToConfirm"; + }, [ + approvalAction.isButtonActionLoading, + approvalAction.isConnected, + inputToken, + outputToken, + amount, + isQuoteLoading, + validation.error, + ]); + + const buttonLoading = useMemo(() => { + return buttonState === "loadingQuote" || buttonState === "submitting"; + }, [buttonState]); + + const buttonLabel = buttonLabels[buttonState]; + return { inputToken, outputToken, @@ -140,9 +152,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { validationError: validation.error, validationWarning: validation.warn, - isConnected: approvalAction.isConnected, - isWrongNetwork: approvalAction.isWrongNetwork, - isSubmitting: approvalAction.isButtonActionLoading, + // Button state information + buttonState, buttonDisabled: approvalAction.buttonDisabled || !!validation.error || @@ -150,8 +161,24 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { !outputToken || !amount || amount.lte(0), + buttonLoading, + buttonLabel, + + // Legacy properties + isConnected: approvalAction.isConnected, + isWrongNetwork: approvalAction.isWrongNetwork, + isSubmitting: approvalAction.isButtonActionLoading, onConfirm, }; } -export default useSwapAndBridge; +const buttonLabels: Record = { + notConnected: "Connect Wallet", + awaitingTokenSelection: "Select a token", + awaitingAmountInput: "Enter an amount", + readyToConfirm: "Confirm Swap", + submitting: "Confirming...", + wrongNetwork: "Switch network and confirm transaction", + loadingQuote: "Finalizing quote", + validationError: "Confirm Swap", +}; diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts index 5bfdb296d..314f1328b 100644 --- a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/factory.ts @@ -16,7 +16,7 @@ export function createSwapApprovalActionHook( const action = useMutation({ mutationFn: async () => { if (!approvalData) throw new Error("Missing approval data"); - const txHash = await strategy.swap(approvalData); + const txHash = await strategy.execute(approvalData); return txHash; }, }); diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts index 31b1346e8..ba311c19c 100644 --- a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/abstract.ts @@ -9,7 +9,7 @@ export abstract class AbstractSwapApprovalActionStrategy abstract isConnected(): boolean; abstract isWrongNetwork(requiredChainId: number): boolean; abstract switchNetwork(requiredChainId: number): Promise; - abstract swap(approvalData: any): Promise; + abstract execute(approvalData: any): Promise; async assertCorrectNetwork(requiredChainId: number) { const currentChainId = this.evmConnection.chainId; diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts index cce0602e4..53ecaa6e7 100644 --- a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/evm.ts @@ -7,7 +7,7 @@ export class EVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStr super(evmConnection); } - private get signer() { + private getSigner() { const { signer } = this.evmConnection; if (!signer) { throw new Error("No signer available"); @@ -28,19 +28,25 @@ export class EVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStr await this.evmConnection.setChain(requiredChainId); } - async swap(approvalData: SwapApprovalData): Promise { - const signer = this.signer; + async approve(approvalData: SwapApprovalData): Promise { + const signer = this.getSigner(); // approvals first const approvals: ApprovalTxn[] = approvalData.approvalTxns || []; for (const approval of approvals) { await this.switchNetwork(approval.chainId); + await this.assertCorrectNetwork(approval.chainId); await signer.sendTransaction({ to: approval.to, data: approval.data, chainId: approval.chainId, }); } - // then final swap + return true; + } + + async swap(approvalData: SwapApprovalData): Promise { + const signer = this.getSigner(); + const swapTx: SwapTx = approvalData.swapTx; await this.switchNetwork(swapTx.chainId); await this.assertCorrectNetwork(swapTx.chainId); @@ -56,4 +62,14 @@ export class EVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStr }); return tx.hash; } + + async execute(approvalData: SwapApprovalData): Promise { + try { + await this.approve(approvalData); + return await this.swap(approvalData); + } catch (e) { + console.error(e); + throw e; + } + } } diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts index d65578aef..ac72768b2 100644 --- a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts @@ -23,6 +23,11 @@ export class SVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStr await this.svmConnection.connect(); } + // stubbed for now + approve(approvalData: SwapApprovalData): boolean { + return true; + } + async swap(approvalData: SwapApprovalData): Promise { if (!this.svmConnection.wallet?.adapter) { throw new Error("Wallet needs to be connected"); @@ -34,4 +39,9 @@ export class SVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStr ); return sig; } + + async execute(approvalData: SwapApprovalData): Promise { + this.approve(approvalData); + return this.swap(approvalData); + } } diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts index a0e95a60e..d2b77ec07 100644 --- a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/types.ts @@ -28,5 +28,5 @@ export type SwapApprovalActionStrategy = { isConnected(): boolean; isWrongNetwork(requiredChainId: number): boolean; switchNetwork(requiredChainId: number): Promise; - swap(approvalData: SwapApprovalData): Promise; + execute(approvalData: SwapApprovalData): Promise; }; diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts new file mode 100644 index 000000000..3e26e9f16 --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -0,0 +1,47 @@ +import { useMemo } from "react"; +import { BigNumber } from "ethers"; +import axios from "axios"; + +import { AmountInputError } from "../../Bridge/utils"; +import { EnrichedTokenSelect } from "../components/ChainTokenSelector/SelectorButton"; + +export type ValidationResult = { + error?: AmountInputError; + warn?: AmountInputError; +}; + +export function useValidateSwapAndBridge( + amount: BigNumber | null, + isAmountOrigin: boolean, + inputToken: EnrichedTokenSelect | null, + error: any +): ValidationResult { + const validation = useMemo(() => { + let errorType: AmountInputError | undefined = undefined; + // invalid amount (allow empty/no amount without error) + if (amount && amount.lte(0)) { + errorType = AmountInputError.INVALID; + } + // balance check for origin-side inputs + if (!errorType && isAmountOrigin && inputToken?.balance) { + if (amount && amount.gt(inputToken.balance)) { + errorType = AmountInputError.INSUFFICIENT_BALANCE; + } + } + // backend availability + if (!errorType && error && axios.isAxiosError(error)) { + const code = (error.response?.data as any)?.code as string | undefined; + if (code === "AMOUNT_TOO_LOW") { + errorType = AmountInputError.AMOUNT_TOO_LOW; + } else if (code === "SWAP_QUOTE_UNAVAILABLE") { + errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; + } + } + return { + error: errorType, + warn: undefined as AmountInputError | undefined, + }; + }, [amount, isAmountOrigin, inputToken, error]); + + return validation; +} diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 6a533654f..12d33ae72 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -4,7 +4,7 @@ import { useCallback } from "react"; import { InputForm } from "./components/InputForm"; import ConfirmationButton from "./components/ConfirmationButton"; import { useHistory } from "react-router-dom"; -import useSwapAndBridge from "./hooks/useSwapAndBridge"; +import { useSwapAndBridge } from "./hooks/useSwapAndBridge"; export default function SwapAndBridge() { const { @@ -23,6 +23,10 @@ export default function SwapAndBridge() { onConfirm, validationError, validationWarning, + buttonState, + buttonDisabled, + buttonLoading, + buttonLabel, } = useSwapAndBridge(); const history = useHistory(); @@ -59,6 +63,10 @@ export default function SwapAndBridge() { onConfirm={handleConfirm} validationError={validationError} validationWarning={validationWarning} + buttonState={buttonState} + buttonDisabled={buttonDisabled} + buttonLoading={buttonLoading} + buttonLabel={buttonLabel} /> From 2d3a9f9cd12cb307a139a776710eaffcaee822eb Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 13:17:15 +0200 Subject: [PATCH 015/122] clean up Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 30 ++----------------- .../SwapAndBridge/components/InputForm.tsx | 2 -- .../useSwapApprovalAction/strategies/svm.ts | 2 +- 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index fce19d789..84c18b2ad 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -87,7 +87,7 @@ const ExpandableLabelSection: React.FC< content = ( <> - + {validationErrorTextMap[validationError]} @@ -193,7 +193,6 @@ export const ConfirmationButton: React.FC = ({ outputToken, amount, swapQuote, - isQuoteLoading, onConfirm, validationError, validationWarning, @@ -333,7 +332,7 @@ export const ConfirmationButton: React.FC = ({ - + Route @@ -454,11 +453,6 @@ const ExpandableLabelLeft = styled.span` justify-content: flex-start; `; -const ShieldIcon = styled.svg` - width: 16px; - height: 16px; -`; - const FastSecureText = styled.span` color: ${COLORS.aqua}; `; @@ -483,16 +477,6 @@ const FeeTimeItem = styled.span` gap: 4px; `; -const GasIcon = styled.svg` - width: 16px; - height: 16px; -`; - -const TimeIcon = styled.svg` - width: 16px; - height: 16px; -`; - const Divider = styled.span` margin: 0 8px; height: 16px; @@ -612,16 +596,6 @@ const DetailRight = styled.div` gap: 8px; `; -const RouteIcon = styled.svg` - width: 20px; - height: 20px; -`; - -const InfoIconSvg = styled.svg` - width: 20px; - height: 20px; -`; - const RouteDot = styled.span` display: inline-block; width: 20px; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 4ed1f4a49..6771b4a4f 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -7,7 +7,6 @@ import styled from "@emotion/styled"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BigNumber, utils } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; -import { AmountInputError } from "../../Bridge/utils"; export const InputForm = ({ inputToken, @@ -101,7 +100,6 @@ const TokenInput = ({ }) => { const [amountString, setAmountString] = useState(""); const [justTyped, setJustTyped] = useState(false); - const [validationError] = useState(undefined); // Handle user input changes useEffect(() => { diff --git a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts index ac72768b2..9dca85820 100644 --- a/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts +++ b/src/views/SwapAndBridge/hooks/useSwapApprovalAction/strategies/svm.ts @@ -24,7 +24,7 @@ export class SVMSwapApprovalActionStrategy extends AbstractSwapApprovalActionStr } // stubbed for now - approve(approvalData: SwapApprovalData): boolean { + approve(_approvalData: SwapApprovalData): boolean { return true; } From 7fd9e839c8bf08313004c8cd524403625c76b2a4 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 15:14:35 +0200 Subject: [PATCH 016/122] update validation warning Signed-off-by: Gerhard Steenkamp --- src/assets/icons/warning_triangle.svg | 3 +++ .../components/BalanceSelector.tsx | 12 ++++++++---- .../components/ConfirmationButton.tsx | 3 ++- .../SwapAndBridge/components/InputForm.tsx | 18 ++++++++++++++++-- src/views/SwapAndBridge/index.tsx | 1 + 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/assets/icons/warning_triangle.svg diff --git a/src/assets/icons/warning_triangle.svg b/src/assets/icons/warning_triangle.svg new file mode 100644 index 000000000..762ee642c --- /dev/null +++ b/src/assets/icons/warning_triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index 11e24228a..093625c4b 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -9,6 +9,7 @@ type BalanceSelectorProps = { decimals: number; setAmount: (amount: BigNumber | null) => void; disableHover?: boolean; + error?: boolean; }; export default function BalanceSelector({ @@ -16,6 +17,7 @@ export default function BalanceSelector({ decimals, setAmount, disableHover, + error = false, }: BalanceSelectorProps) { const [isHovered, setIsHovered] = useState(false); if (!balance || balance.lte(0)) return null; @@ -81,7 +83,7 @@ export default function BalanceSelector({ ))} - + Balance: {formattedBalance} @@ -98,15 +100,17 @@ const BalanceWrapper = styled.div` margin-left: auto; `; -const BalanceText = styled.div` - color: ${COLORS.white}; +const BalanceText = styled.div<{ error?: boolean }>` + color: ${({ error }) => (error ? COLORS.error : COLORS.white)}; opacity: 1; font-size: 14px; - font-weight: 400; + font-weight: 600; line-height: 130%; span { opacity: 0.5; + color: ${COLORS.white}; + font-weight: 400; } `; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 84c18b2ad..904df0aba 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -9,6 +9,7 @@ import { ReactComponent as Route } from "assets/icons/route.svg"; import { ReactComponent as Shield } from "assets/icons/shield.svg"; import { ReactComponent as Gas } from "assets/icons/gas.svg"; import { ReactComponent as Time } from "assets/icons/time.svg"; +import { ReactComponent as Warning } from "assets/icons/warning_triangle.svg"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; @@ -87,7 +88,7 @@ const ExpandableLabelSection: React.FC< content = ( <> - + {validationErrorTextMap[validationError]} diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 6771b4a4f..169fc02be 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -7,6 +7,7 @@ import styled from "@emotion/styled"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BigNumber, utils } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; +import { AmountInputError } from "views/Bridge/utils"; export const InputForm = ({ inputToken, @@ -19,6 +20,7 @@ export const InputForm = ({ isQuoteLoading, expectedOutputAmount, expectedInputAmount, + validationError, }: { inputToken: EnrichedTokenSelect | null; setInputToken: (token: EnrichedTokenSelect | null) => void; @@ -34,6 +36,7 @@ export const InputForm = ({ isAmountOrigin: boolean; setIsAmountOrigin: (isAmountOrigin: boolean) => void; + validationError: AmountInputError | undefined; }) => { const quickSwap = useCallback(() => { const origin = inputToken; @@ -59,6 +62,9 @@ export const InputForm = ({ expectedAmount={expectedInputAmount} shouldUpdate={!isAmountOrigin} isUpdateLoading={isQuoteLoading} + insufficientInputBalance={ + validationError === AmountInputError.INSUFFICIENT_BALANCE + } /> @@ -89,6 +95,7 @@ const TokenInput = ({ expectedAmount, shouldUpdate, isUpdateLoading, + insufficientInputBalance = false, }: { setToken: (token: EnrichedTokenSelect) => void; token: EnrichedTokenSelect | null; @@ -97,6 +104,7 @@ const TokenInput = ({ expectedAmount: string | undefined; shouldUpdate: boolean; isUpdateLoading: boolean; + insufficientInputBalance?: boolean; }) => { const [amountString, setAmountString] = useState(""); const [justTyped, setJustTyped] = useState(false); @@ -178,6 +186,7 @@ const TokenInput = ({ } }} disabled={shouldUpdate && isUpdateLoading} + error={insufficientInputBalance} /> @@ -199,6 +208,7 @@ const TokenInput = ({ balance={token.balance} disableHover={!isOrigin} decimals={token.decimals} + error={insufficientInputBalance} setAmount={(amount) => { if (amount) { setAmount(amount); @@ -250,14 +260,18 @@ const TokenAmountInputTitle = styled.div` line-height: 130%; `; -const TokenAmountInput = styled.input<{ value: string }>` +const TokenAmountInput = styled.input<{ + value: string; + error: boolean; +}>` font-family: Barlow; font-size: 48px; font-weight: 300; line-height: 120%; letter-spacing: -1.92px; width: 100%; - color: ${(value) => (value ? COLORS.aqua : COLORS["light-200"])}; + color: ${({ value, error }) => + error ? COLORS.error : value ? COLORS.aqua : COLORS["light-200"]}; outline: none; border: none; diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 12d33ae72..54ba56c2c 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -53,6 +53,7 @@ export default function SwapAndBridge() { isQuoteLoading={isQuoteLoading} expectedOutputAmount={expectedOutputAmount} expectedInputAmount={expectedInputAmount} + validationError={validationError} /> Date: Tue, 30 Sep 2025 15:39:54 +0200 Subject: [PATCH 017/122] update icons and validation logic Signed-off-by: Gerhard Steenkamp --- src/assets/icons/dollar.svg | 5 +++++ src/views/Bridge/components/AmountInput.tsx | 2 +- .../components/ConfirmationButton.tsx | 18 +++++++++++------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 3 ++- .../hooks/useValidateSwapAndBridge.ts | 19 +++++++++++++++++++ src/views/SwapAndBridge/index.tsx | 2 ++ 6 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/assets/icons/dollar.svg diff --git a/src/assets/icons/dollar.svg b/src/assets/icons/dollar.svg new file mode 100644 index 000000000..fa7dcf539 --- /dev/null +++ b/src/assets/icons/dollar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/views/Bridge/components/AmountInput.tsx b/src/views/Bridge/components/AmountInput.tsx index a9c5cb4cc..7a2b55677 100644 --- a/src/views/Bridge/components/AmountInput.tsx +++ b/src/views/Bridge/components/AmountInput.tsx @@ -8,7 +8,7 @@ import { BridgeLimits } from "hooks"; export const validationErrorTextMap: Record = { [AmountInputError.INSUFFICIENT_BALANCE]: - "Insufficient balance to process this transfer.", + "Not enough [INPUT_TOKEN] to process this transfer.", [AmountInputError.PAUSED_DEPOSITS]: "[INPUT_TOKEN] deposits are temporarily paused.", [AmountInputError.INSUFFICIENT_LIQUIDITY]: diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 904df0aba..520cd2db2 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -7,7 +7,7 @@ import { ReactComponent as Wallet } from "assets/icons/wallet.svg"; import { ReactComponent as Across } from "assets/token-logos/acx.svg"; import { ReactComponent as Route } from "assets/icons/route.svg"; import { ReactComponent as Shield } from "assets/icons/shield.svg"; -import { ReactComponent as Gas } from "assets/icons/gas.svg"; +import { ReactComponent as Dollar } from "assets/icons/dollar.svg"; import { ReactComponent as Time } from "assets/icons/time.svg"; import { ReactComponent as Warning } from "assets/icons/warning_triangle.svg"; @@ -19,7 +19,6 @@ import { useTokenConversion } from "hooks/useTokenConversion"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; -import { validationErrorTextMap } from "views/Bridge/components/AmountInput"; type SwapQuoteResponse = { checks: object; @@ -52,6 +51,7 @@ interface ConfirmationButtonProps onConfirm?: () => void; validationError?: AmountInputError; validationWarning?: AmountInputError; + validationErrorFormatted?: string; // External state props buttonState: BridgeButtonState; buttonDisabled: boolean; @@ -71,6 +71,7 @@ const ExpandableLabelSection: React.FC< hasQuote: boolean; validationError?: AmountInputError; validationWarning?: AmountInputError; + validationErrorFormatted?: string; }> > = ({ fee, @@ -81,15 +82,16 @@ const ExpandableLabelSection: React.FC< children, hasQuote, validationError, + validationErrorFormatted, }) => { // Render state-specific content let content: React.ReactNode = null; - if (validationError) { + if (validationError && validationErrorFormatted) { content = ( <> - {validationErrorTextMap[validationError]} + {validationErrorFormatted} ); @@ -102,7 +104,7 @@ const ExpandableLabelSection: React.FC< - + {fee} @@ -197,6 +199,7 @@ export const ConfirmationButton: React.FC = ({ onConfirm, validationError, validationWarning, + validationErrorFormatted, buttonState, buttonDisabled, buttonLoading, @@ -327,6 +330,7 @@ export const ConfirmationButton: React.FC = ({ state={state} validationError={validationError} validationWarning={validationWarning} + validationErrorFormatted={validationErrorFormatted} hasQuote={!!swapQuote} > {expanded && state === "readyToConfirm" ? ( @@ -343,14 +347,14 @@ export const ConfirmationButton: React.FC = ({ - + {displayValues.estimatedTime} - + Net Fee diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 1cf013140..436824a53 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -30,6 +30,7 @@ export type UseSwapAndBridgeReturn = { validationError?: AmountInputError; validationWarning?: AmountInputError; + validationErrorFormatted?: string | undefined; // Button state information buttonState: BridgeButtonState; @@ -148,7 +149,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { isQuoteLoading, expectedInputAmount, expectedOutputAmount, - + validationErrorFormatted: validation.errorFormatted, validationError: validation.error, validationWarning: validation.warn, diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts index 3e26e9f16..71ad0cbec 100644 --- a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -4,10 +4,12 @@ import axios from "axios"; import { AmountInputError } from "../../Bridge/utils"; import { EnrichedTokenSelect } from "../components/ChainTokenSelector/SelectorButton"; +import { validationErrorTextMap } from "views/Bridge/components/AmountInput"; export type ValidationResult = { error?: AmountInputError; warn?: AmountInputError; + errorFormatted?: string; }; export function useValidateSwapAndBridge( @@ -40,8 +42,25 @@ export function useValidateSwapAndBridge( return { error: errorType, warn: undefined as AmountInputError | undefined, + errorFormatted: getValidationErrorText({ + validationError: errorType, + inputToken, + }), }; }, [amount, isAmountOrigin, inputToken, error]); return validation; } + +function getValidationErrorText(props: { + validationError?: AmountInputError; + inputToken: EnrichedTokenSelect | null; +}): string | undefined { + if (!props.validationError) { + return; + } + return validationErrorTextMap[props.validationError]?.replace( + "[INPUT_TOKEN]", + props.inputToken!.symbol + ); +} diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 54ba56c2c..cc1d35d78 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -23,6 +23,7 @@ export default function SwapAndBridge() { onConfirm, validationError, validationWarning, + validationErrorFormatted, buttonState, buttonDisabled, buttonLoading, @@ -64,6 +65,7 @@ export default function SwapAndBridge() { onConfirm={handleConfirm} validationError={validationError} validationWarning={validationWarning} + validationErrorFormatted={validationErrorFormatted} buttonState={buttonState} buttonDisabled={buttonDisabled} buttonLoading={buttonLoading} From 0b0b0c25158be11be2be1bc89c8597fcc06bbb3a Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 15:50:27 +0200 Subject: [PATCH 018/122] add tooltip Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/components/ConfirmationButton.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 520cd2db2..e60c763d7 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -19,6 +19,7 @@ import { useTokenConversion } from "hooks/useTokenConversion"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; +import { Tooltip } from "components/Tooltip"; type SwapQuoteResponse = { checks: object; @@ -356,7 +357,12 @@ export const ConfirmationButton: React.FC = ({ Net Fee - + + + {displayValues.netFee} From 51396253680e3224dbb99e9358d72e6e4d9d77b5 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 21:59:54 +0200 Subject: [PATCH 019/122] rsolve swap token info Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 66 ++++++++----------- src/hooks/useSwapChains.ts | 12 ++++ src/hooks/useSwapTokens.ts | 13 ++++ src/hooks/useTokenConversion.ts | 38 ++++++++++- .../components/ConfirmationButton.tsx | 16 +---- 5 files changed, 90 insertions(+), 55 deletions(-) create mode 100644 src/hooks/useSwapChains.ts create mode 100644 src/hooks/useSwapTokens.ts diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 3f199733d..2209a2708 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -1,8 +1,7 @@ -import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; import { useQuery } from "@tanstack/react-query"; -import getApiEndpoint from "utils/serverless-api"; -import { SwapChain, SwapToken } from "utils/serverless-api/types"; import { getConfig } from "utils/config"; +import { useSwapChains } from "./useSwapChains"; +import { useSwapTokens } from "./useSwapTokens"; export type LifiToken = { chainId: number; @@ -13,27 +12,19 @@ export type LifiToken = { priceUSD: string; coinKey: string; logoURI: string; - routeSource: "bridge" | "swap" | "both"; + routeSource: "bridge" | "swap"; }; export default function useAvailableCrosschainRoutes() { + const swapChainsQuery = useSwapChains(); + const swapTokensQuery = useSwapTokens(); + return useQuery({ queryKey: ["availableCrosschainRoutes"], queryFn: async () => { - const api = getApiEndpoint(); - const [chains, tokens] = await Promise.all([ - api.swapChains(), - api.swapTokens(), - ]); - - const allowedChainIds = new Set(Object.values(MAINNET_CHAIN_IDs)); - // 1) Build swap token map by chain - const swapTokensByChain = (tokens as SwapToken[]).reduce( + const swapTokensByChain = (swapTokensQuery.data || []).reduce( (acc, token) => { - if (!allowedChainIds.has(token.chainId)) { - return acc; - } const mapped: LifiToken = { chainId: token.chainId, address: token.address, @@ -63,9 +54,6 @@ export default function useAvailableCrosschainRoutes() { const bridgeTokensByChain = bridgeOriginChains.reduce( (acc, fromChainId) => { - if (!allowedChainIds.has(fromChainId)) { - return acc; - } const reachable = config.filterReachableTokens(fromChainId); const lifiTokens: LifiToken[] = reachable.map((t) => ({ chainId: fromChainId, @@ -85,40 +73,40 @@ export default function useAvailableCrosschainRoutes() { {} as Record> ); - // 3) Merge swap and bridge tokens, de-duplicating by address (case-insensitive) + // 3) Combine swap and bridge tokens, deduplicating by address const chainIdsInSwap = new Set( - (chains as SwapChain[]).map((c) => c.chainId) + (swapChainsQuery.data || []).map((c) => c.chainId) ); const chainIdsInBridge = new Set( Object.keys(bridgeTokensByChain).map(Number) ); const chainIds = Array.from( new Set([...chainIdsInSwap, ...chainIdsInBridge]) - ).filter((id) => allowedChainIds.has(id)); + ); - const blendedByChain: Record> = {}; + const combinedByChain: Record> = {}; for (const chainId of chainIds) { - const mapByAddr = new Map(); - // Prefer swap tokens first (they include price) - (swapTokensByChain[chainId] || []).forEach((t) => { - mapByAddr.set(t.address.toLowerCase(), t); + const swapTokens = swapTokensByChain[chainId] || []; + const bridgeTokens = bridgeTokensByChain[chainId] || []; + + // Deduplicate by address (case-insensitive), preferring swap tokens for price data + const tokenMap = new Map(); + + // Add bridge tokens first + bridgeTokens.forEach((token) => { + tokenMap.set(token.address.toLowerCase(), token); }); - // Add bridge tokens, merging routeSource when duplicate - (bridgeTokensByChain[chainId] || []).forEach((t) => { - const key = t.address.toLowerCase(); - const existing = mapByAddr.get(key); - if (!existing) { - mapByAddr.set(key, t); - } else { - // Merge: if token exists from swap, mark as both - mapByAddr.set(key, { ...existing, routeSource: "both" }); - } + + // Add swap tokens, overriding bridge tokens if same address (swap has price data) + swapTokens.forEach((token) => { + tokenMap.set(token.address.toLowerCase(), token); }); - blendedByChain[chainId] = Array.from(mapByAddr.values()); + combinedByChain[chainId] = Array.from(tokenMap.values()); } - return blendedByChain; + return combinedByChain; }, + enabled: swapChainsQuery.isSuccess && swapTokensQuery.isSuccess, }); } diff --git a/src/hooks/useSwapChains.ts b/src/hooks/useSwapChains.ts new file mode 100644 index 000000000..96356deb0 --- /dev/null +++ b/src/hooks/useSwapChains.ts @@ -0,0 +1,12 @@ +import { useQuery } from "@tanstack/react-query"; +import getApiEndpoint from "utils/serverless-api"; + +export function useSwapChains() { + return useQuery({ + queryKey: ["swapChains"], + queryFn: async () => { + const api = getApiEndpoint(); + return await api.swapChains(); + }, + }); +} diff --git a/src/hooks/useSwapTokens.ts b/src/hooks/useSwapTokens.ts new file mode 100644 index 000000000..fe9fecb3d --- /dev/null +++ b/src/hooks/useSwapTokens.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import getApiEndpoint from "utils/serverless-api"; +import { SwapTokensQuery } from "utils/serverless-api/prod/swap-tokens"; + +export function useSwapTokens(query?: SwapTokensQuery) { + return useQuery({ + queryKey: ["swapTokens", query], + queryFn: async () => { + const api = getApiEndpoint(); + return await api.swapTokens(query); + }, + }); +} diff --git a/src/hooks/useTokenConversion.ts b/src/hooks/useTokenConversion.ts index dc283c24b..a1f769599 100644 --- a/src/hooks/useTokenConversion.ts +++ b/src/hooks/useTokenConversion.ts @@ -10,6 +10,7 @@ import { hubPoolChainId, } from "utils"; import { ConvertDecimals } from "utils/convertdecimals"; +import useAvailableCrosschainRoutes from "./useAvailableCrosschainRoutes"; const config = getConfig(); @@ -18,7 +19,42 @@ export function useTokenConversion( baseCurrency: string, historicalDateISO?: string ) { - const token = getToken(symbol); + const availableCrosschainRoutes = useAvailableCrosschainRoutes(); + + // Try to get token from constants first, fallback to swap API data + let token; + try { + token = getToken(symbol); + } catch (error) { + // If token not found in constants, try to find it in swap API data + const swapTokens = availableCrosschainRoutes.data; + if (swapTokens) { + // Search across all chains for a token with matching symbol + for (const chainId of Object.keys(swapTokens)) { + const tokensOnChain = swapTokens[Number(chainId)]; + const foundToken = tokensOnChain.find( + (t) => t.symbol.toUpperCase() === symbol.toUpperCase() + ); + if (foundToken) { + // Convert LifiToken to TokenInfo format + token = { + symbol: foundToken.symbol, + name: foundToken.name, + decimals: foundToken.decimals, + addresses: { [foundToken.chainId]: foundToken.address }, + mainnetAddress: foundToken.address, // Use the found address as mainnet address + logoURI: foundToken.logoURI, + }; + break; + } + } + } + + // If still not found, re-throw the original error + if (!token) { + throw error; + } + } // If the token is OP, we need to use the address of the token on Optimism const l1Token = diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index e60c763d7..95e44b76c 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -342,7 +342,7 @@ export const ConfirmationButton: React.FC = ({ Route - + {displayValues.route} @@ -607,15 +607,6 @@ const DetailRight = styled.div` gap: 8px; `; -const RouteDot = styled.span` - display: inline-block; - width: 20px; - height: 20px; - background: ${COLORS.aqua}; - border-radius: 50%; - opacity: 0.8; -`; - const FeeBreakdown = styled.div` padding-left: 24px; border-left: 1px solid rgba(224, 243, 255, 0.1); @@ -637,9 +628,4 @@ const FeeBreakdownValue = styled.span` color: #e0f3ff; `; -const SmallInfoIcon = styled(Info)` - width: 16px; - height: 16px; -`; - export default ConfirmationButton; From 79c617de3234fdda8686ccee8b6e45cc8354b5ae Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 22:27:26 +0200 Subject: [PATCH 020/122] disable unreachable tokens Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 66 +++++++++++++- .../components/ChainTokenSelector/Modal.tsx | 91 ++++++++++++++++--- .../ChainTokenSelector/SelectorButton.tsx | 4 + .../SwapAndBridge/components/InputForm.tsx | 5 + 4 files changed, 149 insertions(+), 17 deletions(-) diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 2209a2708..ff0e45923 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -13,14 +13,28 @@ export type LifiToken = { coinKey: string; logoURI: string; routeSource: "bridge" | "swap"; + isReachable?: boolean; // Added to mark if token is reachable from the other token }; -export default function useAvailableCrosschainRoutes() { +export type TokenInfo = { + chainId: number; + address: string; + symbol: string; +}; + +export type RouteFilterParams = { + inputToken?: TokenInfo | null; + outputToken?: TokenInfo | null; +}; + +export default function useAvailableCrosschainRoutes( + filterParams?: RouteFilterParams +) { const swapChainsQuery = useSwapChains(); const swapTokensQuery = useSwapTokens(); return useQuery({ - queryKey: ["availableCrosschainRoutes"], + queryKey: ["availableCrosschainRoutes", filterParams], queryFn: async () => { // 1) Build swap token map by chain const swapTokensByChain = (swapTokensQuery.data || []).reduce( @@ -105,6 +119,54 @@ export default function useAvailableCrosschainRoutes() { combinedByChain[chainId] = Array.from(tokenMap.values()); } + // 4) Apply route filtering if filterParams are provided + if (filterParams?.inputToken || filterParams?.outputToken) { + const config = getConfig(); + const otherToken = filterParams.inputToken || filterParams.outputToken; + const isFilteringForInput = !!filterParams.inputToken; + + // Mark tokens as reachable/unreachable based on route validation + for (const chainId of Object.keys(combinedByChain)) { + combinedByChain[Number(chainId)] = combinedByChain[ + Number(chainId) + ].map((token) => { + const fromChain = isFilteringForInput + ? Number(chainId) + : otherToken!.chainId; + const toChain = isFilteringForInput + ? otherToken!.chainId + : Number(chainId); + const fromTokenSymbol = isFilteringForInput + ? token.symbol + : otherToken!.symbol; + const toTokenSymbol = isFilteringForInput + ? otherToken!.symbol + : token.symbol; + + let isReachable = true; + + // For same chain (swap), always reachable + if (fromChain === toChain) { + isReachable = true; + } else { + // For bridge, check if there's an explicit bridge route + const bridgeRoutes = config.filterRoutes({ + fromChain, + toChain, + fromTokenSymbol, + toTokenSymbol, + }); + isReachable = bridgeRoutes.length > 0; + } + + return { + ...token, + isReachable, + }; + }); + } + } + return combinedByChain; }, enabled: swapChainsQuery.isSuccess && swapTokensQuery.isSuccess, diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 589fc9533..bd3e0ef4e 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -22,6 +22,7 @@ import { BigNumber } from "ethers"; type Props = { onSelect: (token: EnrichedTokenSelect) => void; isOriginToken: boolean; + otherToken?: EnrichedTokenSelect | null; // The currently selected token on the other side displayModal: boolean; setDisplayModal: (displayModal: boolean) => void; @@ -32,10 +33,21 @@ export default function ChainTokenSelectorModal({ displayModal, setDisplayModal, onSelect, + otherToken, }: Props) { const balances = useEnrichedCrosschainBalances(); - const crossChainRoutes = useAvailableCrosschainRoutes(); + const crossChainRoutes = useAvailableCrosschainRoutes( + otherToken + ? { + [isOriginToken ? "outputToken" : "inputToken"]: { + chainId: otherToken.chainId, + address: otherToken.address, + symbol: otherToken.symbol, + }, + } + : undefined + ); const [selectedChain, setSelectedChain] = useState(null); @@ -48,8 +60,22 @@ export default function ChainTokenSelectorModal({ if (tokens.length === 0 && selectedChain === null) { tokens = Object.values(balances).flatMap((t) => t); } + + // Enrich tokens with reachability information from the hook + const enrichedTokens = tokens.map((token) => { + // Find the corresponding token in crossChainRoutes to check isReachable + const routeToken = crossChainRoutes.data?.[token.chainId]?.find( + (rt) => rt.address.toLowerCase() === token.address.toLowerCase() + ); + + return { + ...token, + isReachable: routeToken?.isReachable, + }; + }); + // Return ordering top 100 tokens ordering highest balanceUsd to lowest (fallback alphabetical) - const sortedTokens = tokens.slice(0, 100).sort((a, b) => { + const sortedTokens = enrichedTokens.slice(0, 100).sort((a, b) => { if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); } @@ -69,11 +95,11 @@ export default function ChainTokenSelectorModal({ keyword.includes(tokenSearch.toLowerCase().replaceAll(" ", "")) ); }); - }, [selectedChain, balances, tokenSearch]); + }, [selectedChain, balances, tokenSearch, otherToken, crossChainRoutes.data]); const displayedChains = useMemo(() => { - return Object.fromEntries( - Object.entries(crossChainRoutes.data || {}).filter(([chainId]) => { + const chainsWithDisabledState = Object.entries(crossChainRoutes.data || {}) + .filter(([chainId]) => { // why ar we filtering out Boba? if ([288].includes(Number(chainId))) { return false; @@ -87,8 +113,23 @@ export default function ChainTokenSelectorModal({ keyword.toLowerCase().includes(chainSearch.toLowerCase()) ); }) - ); - }, [chainSearch, crossChainRoutes.data]); + .map(([chainId, tokens]) => { + let isDisabled = false; + + // If there's an other token selected, check if this chain has any reachable tokens + if (otherToken) { + const tokensOnChain = tokens || []; + const hasReachableTokens = tokensOnChain.some( + (token) => token.isReachable !== false + ); + isDisabled = !hasReachableTokens; + } + + return [chainId, { tokens, isDisabled }]; + }); + + return Object.fromEntries(chainsWithDisabledState); + }, [chainSearch, crossChainRoutes.data, otherToken]); return ( setSelectedChain(null)} /> - {Object.entries(displayedChains).map(([chainId]) => ( + {Object.entries(displayedChains).map(([chainId, chainData]) => ( setSelectedChain(Number(chainId))} /> ))} @@ -168,10 +212,12 @@ const ChainEntry = ({ chainId, isSelected, onClick, + isDisabled = false, }: { chainId: number | null; isSelected: boolean; onClick: () => void; + isDisabled?: boolean; }) => { const chainInfo = chainId ? getChainInfo(chainId) @@ -180,7 +226,11 @@ const ChainEntry = ({ name: "All", }; return ( - + {chainInfo.name} {isSelected && } @@ -198,8 +248,14 @@ const TokenEntry = ({ onClick: () => void; }) => { const hasBalance = token.balance.gt(0) && token.balanceUsd > 0.01; + const isDisabled = token.isReachable === false; + return ( - + {token.name} @@ -351,7 +407,7 @@ const ListWrapper = styled.div` scrollbar-color: rgba(255, 255, 255, 0.1) transparent; `; -const EntryItem = styled.div<{ isSelected: boolean }>` +const EntryItem = styled.div<{ isSelected: boolean; isDisabled?: boolean }>` display: flex; flex-direction: row; justify-content: space-between; @@ -369,13 +425,18 @@ const EntryItem = styled.div<{ isSelected: boolean }>` background: ${({ isSelected }) => isSelected ? COLORS["aqua-5"] : "transparent"}; - cursor: pointer; + cursor: ${({ isDisabled }) => (isDisabled ? "not-allowed" : "pointer")}; + opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)}; - transition: background 0.2s ease-in-out; + transition: + background 0.2s ease-in-out, + opacity 0.2s ease-in-out; &:hover { - background: ${({ isSelected }) => - isSelected ? COLORS["aqua-15"] : COLORS["grey-400-15"]}; + background: ${({ isSelected, isDisabled }) => { + if (isDisabled) return "transparent"; + return isSelected ? COLORS["aqua-15"] : COLORS["grey-400-15"]; + }}; } `; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index 0f05ab9d5..8c0aeeb8f 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -22,6 +22,7 @@ type Props = { selectedToken: EnrichedTokenSelect | null; onSelect?: (token: EnrichedTokenSelect) => void; isOriginToken: boolean; + otherToken?: EnrichedTokenSelect | null; // The currently selected token on the other side marginBottom?: string; className?: string; }; @@ -30,6 +31,7 @@ export default function SelectorButton({ onSelect, selectedToken, isOriginToken, + otherToken, className, }: Props) { const [displayModal, setDisplayModal] = useState(false); @@ -66,6 +68,7 @@ export default function SelectorButton({ displayModal={displayModal} setDisplayModal={setDisplayModal} isOriginToken={isOriginToken} + otherToken={otherToken} /> ); @@ -95,6 +98,7 @@ export default function SelectorButton({ displayModal={displayModal} setDisplayModal={setDisplayModal} isOriginToken={isOriginToken} + otherToken={otherToken} /> ); diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 169fc02be..66c3d8862 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -65,6 +65,7 @@ export const InputForm = ({ insufficientInputBalance={ validationError === AmountInputError.INSUFFICIENT_BALANCE } + otherToken={outputToken} /> @@ -82,6 +83,7 @@ export const InputForm = ({ expectedAmount={expectedOutputAmount} shouldUpdate={isAmountOrigin} isUpdateLoading={isQuoteLoading} + otherToken={inputToken} /> ); @@ -96,6 +98,7 @@ const TokenInput = ({ shouldUpdate, isUpdateLoading, insufficientInputBalance = false, + otherToken, }: { setToken: (token: EnrichedTokenSelect) => void; token: EnrichedTokenSelect | null; @@ -105,6 +108,7 @@ const TokenInput = ({ shouldUpdate: boolean; isUpdateLoading: boolean; insufficientInputBalance?: boolean; + otherToken?: EnrichedTokenSelect | null; }) => { const [amountString, setAmountString] = useState(""); const [justTyped, setJustTyped] = useState(false); @@ -201,6 +205,7 @@ const TokenInput = ({ isOriginToken={isOrigin} marginBottom={token ? "24px" : "0px"} selectedToken={token} + otherToken={otherToken} /> {token && ( From 526ca7692b8370bab19ea7f627656c86b1bd4b24 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 22:37:18 +0200 Subject: [PATCH 021/122] filter and sort Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index bd3e0ef4e..146b41d73 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -75,7 +75,17 @@ export default function ChainTokenSelectorModal({ }); // Return ordering top 100 tokens ordering highest balanceUsd to lowest (fallback alphabetical) + // Push disabled tokens to the bottom const sortedTokens = enrichedTokens.slice(0, 100).sort((a, b) => { + // First, sort by disabled status - disabled tokens go to bottom + const aDisabled = a.isReachable === false; + const bDisabled = b.isReachable === false; + + if (aDisabled !== bDisabled) { + return aDisabled ? 1 : -1; + } + + // Then sort by balance (for enabled tokens) or alphabetically (for disabled tokens) if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); } @@ -105,6 +115,11 @@ export default function ChainTokenSelectorModal({ return false; } + // Filter out the chain of the other token (same chain can't be both input and output) + if (otherToken && Number(chainId) === otherToken.chainId) { + return false; + } + const keywords = [ String(chainId), getChainInfo(Number(chainId)).name.toLowerCase().replace(" ", ""), @@ -126,6 +141,23 @@ export default function ChainTokenSelectorModal({ } return [chainId, { tokens, isDisabled }]; + }) + // Sort chains to push disabled ones to the bottom + .sort(([chainIdA, chainDataA], [chainIdB, chainDataB]) => { + const aDisabled = (chainDataA as { tokens: any; isDisabled: boolean }) + .isDisabled; + const bDisabled = (chainDataB as { tokens: any; isDisabled: boolean }) + .isDisabled; + + // First, sort by disabled status - disabled chains go to bottom + if (aDisabled !== bDisabled) { + return aDisabled ? 1 : -1; + } + + // Then sort alphabetically by chain name + const chainInfoA = getChainInfo(Number(chainIdA)); + const chainInfoB = getChainInfo(Number(chainIdB)); + return chainInfoA.name.localeCompare(chainInfoB.name); }); return Object.fromEntries(chainsWithDisabledState); From dd6cb80282d14eee3e42af9c96564937752cbb50 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 30 Sep 2025 22:43:36 +0200 Subject: [PATCH 022/122] fixup Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 146b41d73..f7654c624 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -68,9 +68,26 @@ export default function ChainTokenSelectorModal({ (rt) => rt.address.toLowerCase() === token.address.toLowerCase() ); + // Determine if token should be disabled based on new requirements: + // Only disable tokens if the token is NOT a swap token AND it is not reachable via a bridge route + let shouldDisable = false; + if (routeToken) { + // If it's a swap token, never disable it + if (routeToken.routeSource === "swap") { + shouldDisable = false; + } else { + // If it's not a swap token, disable it only if it's not reachable via bridge + shouldDisable = routeToken.isReachable === false; + } + } else { + // If no route token found, disable it (not available for any routes) + shouldDisable = true; + } + return { ...token, - isReachable: routeToken?.isReachable, + isReachable: !shouldDisable, + routeSource: routeToken?.routeSource || "bridge", // Default to bridge if not found }; }); @@ -129,32 +146,13 @@ export default function ChainTokenSelectorModal({ ); }) .map(([chainId, tokens]) => { - let isDisabled = false; - - // If there's an other token selected, check if this chain has any reachable tokens - if (otherToken) { - const tokensOnChain = tokens || []; - const hasReachableTokens = tokensOnChain.some( - (token) => token.isReachable !== false - ); - isDisabled = !hasReachableTokens; - } + // Never disable chains - requirement 1 + const isDisabled = false; return [chainId, { tokens, isDisabled }]; }) - // Sort chains to push disabled ones to the bottom - .sort(([chainIdA, chainDataA], [chainIdB, chainDataB]) => { - const aDisabled = (chainDataA as { tokens: any; isDisabled: boolean }) - .isDisabled; - const bDisabled = (chainDataB as { tokens: any; isDisabled: boolean }) - .isDisabled; - - // First, sort by disabled status - disabled chains go to bottom - if (aDisabled !== bDisabled) { - return aDisabled ? 1 : -1; - } - - // Then sort alphabetically by chain name + // Sort chains alphabetically by name (no need to sort by disabled status since none are disabled) + .sort(([chainIdA], [chainIdB]) => { const chainInfoA = getChainInfo(Number(chainIdA)); const chainInfoB = getChainInfo(Number(chainIdB)); return chainInfoA.name.localeCompare(chainInfoB.name); From efd6b76e4c4ea6170573317ae1bffb1878b1845b Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 1 Oct 2025 13:46:45 +0200 Subject: [PATCH 023/122] mobile token selector Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 455 +++++++++++++++--- .../ChainTokenSelector/Searchbar.tsx | 10 +- 2 files changed, 402 insertions(+), 63 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index f7654c624..1d874389b 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -13,10 +13,12 @@ import { getChainInfo, parseUnits, } from "utils"; -import { useMemo, useState } from "react"; +import { useMemo, useState, useEffect } from "react"; import { ReactComponent as CheckmarkCircle } from "assets/icons/checkmark-circle.svg"; +import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; import useEnrichedCrosschainBalances from "hooks/useEnrichedCrosschainBalances"; +import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; type Props = { @@ -36,6 +38,7 @@ export default function ChainTokenSelectorModal({ otherToken, }: Props) { const balances = useEnrichedCrosschainBalances(); + const { isMobile } = useCurrentBreakpoint(); const crossChainRoutes = useAvailableCrosschainRoutes( otherToken @@ -50,10 +53,19 @@ export default function ChainTokenSelectorModal({ ); const [selectedChain, setSelectedChain] = useState(null); + const [mobileStep, setMobileStep] = useState<"chain" | "token">("chain"); const [tokenSearch, setTokenSearch] = useState(""); const [chainSearch, setChainSearch] = useState(""); + // Reset mobile step when modal opens/closes + useEffect(() => { + if (displayModal) { + setMobileStep("chain"); + setSelectedChain(null); + } + }, [displayModal]); + const displayedTokens = useMemo(() => { let tokens = selectedChain ? (balances[selectedChain] ?? []) : []; @@ -161,33 +173,220 @@ export default function ChainTokenSelectorModal({ return Object.fromEntries(chainsWithDisabledState); }, [chainSearch, crossChainRoutes.data, otherToken]); + return isMobile ? ( + { + setSelectedChain(chainId); + setMobileStep("token"); + }} + onTokenSelect={onSelect} + /> + ) : ( + + ); +} + +// Mobile Modal Component +const MobileModal = ({ + isOriginToken, + displayModal, + setDisplayModal, + mobileStep, + setMobileStep, + selectedChain, + chainSearch, + setChainSearch, + tokenSearch, + setTokenSearch, + displayedChains, + displayedTokens, + onChainSelect, + onTokenSelect, +}: { + isOriginToken: boolean; + displayModal: boolean; + setDisplayModal: (display: boolean) => void; + mobileStep: "chain" | "token"; + setMobileStep: (step: "chain" | "token") => void; + selectedChain: number | null; + chainSearch: string; + setChainSearch: (search: string) => void; + tokenSearch: string; + setTokenSearch: (search: string) => void; + displayedChains: any; + displayedTokens: any[]; + onChainSelect: (chainId: number | null) => void; + onTokenSelect: (token: EnrichedTokenSelect) => void; +}) => { return ( Select {isOriginToken ? "Origin" : "Destination"} Token + + {mobileStep === "token" && ( + { + setMobileStep("chain"); + setTokenSearch(""); // Clear token search when going back + }} + > + + + )} + + {mobileStep === "chain" + ? `Select ${isOriginToken ? "Origin" : "Destination"} Chain` + : `Select ${isOriginToken ? "Origin" : "Destination"} Token`} + + } isOpen={displayModal} padding="thin" exitModalHandler={() => setDisplayModal(false)} exitOnOutsideClick - width={720} + width={400} + height={600} + titleBorder + > + setDisplayModal(false)} + /> + + ); +}; + +// Desktop Modal Component +const DesktopModal = ({ + isOriginToken, + displayModal, + setDisplayModal, + selectedChain, + chainSearch, + setChainSearch, + tokenSearch, + setTokenSearch, + displayedChains, + displayedTokens, + onChainSelect, + onTokenSelect, +}: { + isOriginToken: boolean; + displayModal: boolean; + setDisplayModal: (display: boolean) => void; + selectedChain: number | null; + chainSearch: string; + setChainSearch: (search: string) => void; + tokenSearch: string; + setTokenSearch: (search: string) => void; + displayedChains: any; + displayedTokens: any[]; + onChainSelect: (chainId: number | null) => void; + onTokenSelect: (token: EnrichedTokenSelect) => void; +}) => { + return ( + setDisplayModal(false)} + exitOnOutsideClick + width={1100} height={800} titleBorder > - - - - - + setDisplayModal(false)} + /> + + ); +}; + +// Mobile Layout Component - 2-step process +const MobileLayout = ({ + mobileStep, + selectedChain, + chainSearch, + setChainSearch, + tokenSearch, + setTokenSearch, + displayedChains, + displayedTokens, + onChainSelect, + onTokenSelect, + onModalClose, +}: { + mobileStep: "chain" | "token"; + selectedChain: number | null; + chainSearch: string; + setChainSearch: (search: string) => void; + tokenSearch: string; + setTokenSearch: (search: string) => void; + displayedChains: any; + displayedTokens: any[]; + onChainSelect: (chainId: number | null) => void; + onTokenSelect: (token: EnrichedTokenSelect) => void; + onModalClose: () => void; +}) => { + return ( + + {mobileStep === "chain" ? ( + // Step 1: Chain Selection + + setSelectedChain(null)} + onClick={() => onChainSelect(null)} /> {Object.entries(displayedChains).map(([chainId, chainData]) => ( setSelectedChain(Number(chainId))} + onClick={() => onChainSelect(Number(chainId))} /> ))} - - - - - - + + ) : ( + // Step 2: Token Selection + + {displayedTokens.map((token) => ( { - onSelect({ + onTokenSelect({ chainId: token.chainId, symbolUri: token.logoURI, symbol: token.symbol, @@ -227,16 +425,100 @@ export default function ChainTokenSelectorModal({ priceUsd: parseUnits(token.priceUSD, 18), decimals: token.decimals, }); - setDisplayModal(false); + onModalClose(); }} /> ))} - - - + + )} + ); -} +}; + +// Desktop Layout Component - Side-by-side columns +const DesktopLayout = ({ + selectedChain, + chainSearch, + setChainSearch, + tokenSearch, + setTokenSearch, + displayedChains, + displayedTokens, + onChainSelect, + onTokenSelect, + onModalClose, +}: { + selectedChain: number | null; + chainSearch: string; + setChainSearch: (search: string) => void; + tokenSearch: string; + setTokenSearch: (search: string) => void; + displayedChains: any; + displayedTokens: any[]; + onChainSelect: (chainId: number | null) => void; + onTokenSelect: (token: EnrichedTokenSelect) => void; + onModalClose: () => void; +}) => { + return ( + + + + + onChainSelect(null)} + /> + {Object.entries(displayedChains).map(([chainId, chainData]) => ( + onChainSelect(Number(chainId))} + /> + ))} + + + + + + + {displayedTokens.map((token) => ( + { + onTokenSelect({ + chainId: token.chainId, + symbolUri: token.logoURI, + symbol: token.symbol, + address: token.address, + balance: token.balance, + priceUsd: parseUnits(token.priceUSD, 18), + decimals: token.decimals, + }); + onModalClose(); + }} + /> + ))} + + + + ); +}; const ChainEntry = ({ chainId, @@ -320,6 +602,10 @@ const TokenItemImage = ({ token }: { token: LifiToken }) => { ); }; +const SearchBarStyled = styled(Searchbar)` + flex-shrink: 0; +`; + const TokenItemImageWrapper = styled.div` width: 32px; height: 32px; @@ -354,25 +640,94 @@ const TokenItemChainImage = styled.img` right: 0; `; -const InnerWrapper = styled.div` +// Mobile Layout Styled Components +const MobileInnerWrapper = styled.div` width: 100%; - height: 100%; + height: 600px; /* Constrain height to enable scrolling */ + display: flex; + flex-direction: column; +`; + +const MobileChainWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-height: 0; +`; + +const MobileTokenWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-height: 0; +`; +// Desktop Layout Styled Components +const DesktopInnerWrapper = styled.div` + width: 100%; + height: 800px; display: flex; flex-direction: row; gap: 12px; `; +const DesktopChainWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; + min-width: 230px; +`; + +const DesktopTokenWrapper = styled.div` + flex: 2; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; +`; + const VerticalDivider = styled.div` width: 1px; + margin: -16px 0; + background-color: #3f4247; + flex-shrink: 0; +`; - height: 400px; +const TitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 12px; + width: 100%; +`; - margin: -16px 0; +const BackButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + background: transparent; + color: var(--Base-bright-gray, #e0f3ff); + cursor: pointer; + border-radius: 6px; + transition: background 0.2s ease-in-out; - background-color: #3f4247; + &:hover { + background: rgba(255, 255, 255, 0.1); + } - flex-shrink: 0; + svg { + width: 16px; + height: 16px; + transform: rotate(180deg); /* Rotate chevron-right to make it point left */ + } `; const Title = styled.div` @@ -386,35 +741,13 @@ const Title = styled.div` line-height: 130%; /* 26px */ `; -const ChainWrapper = styled.div` - width: calc(33% - 0.5px); - height: 100%; - - display: flex; - flex-direction: column; - gap: 16px; -`; - -const TokenWrapper = styled.div` - width: calc(67% - 0.5px); - height: 100%; - - display: flex; - flex-direction: column; - gap: 8px; -`; - -const SearchWrapper = styled.div` - padding: 0px 8px; -`; - const ListWrapper = styled.div` display: flex; flex-direction: column; gap: 4px; - - overflow-y: scroll; - max-height: 300px; + overflow-y: auto; + flex: 1; /* Take up remaining space in parent */ + min-height: 0; /* Allow flex child to shrink below content size */ &::-webkit-scrollbar { width: 8px; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx index 9b2603699..dc4a4ab51 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -6,11 +6,17 @@ type Props = { searchTopic: string; search: string; setSearch: (search: string) => void; + className?: string; }; -export default function Searchbar({ searchTopic, search, setSearch }: Props) { +export default function Searchbar({ + searchTopic, + search, + setSearch, + className, +}: Props) { return ( - + Date: Wed, 1 Oct 2025 14:12:42 +0200 Subject: [PATCH 024/122] add sections Signed-off-by: Gerhard Steenkamp --- src/utils/constants.ts | 4 + .../components/ChainTokenSelector/Modal.tsx | 242 ++++++++++++++++-- 2 files changed, 218 insertions(+), 28 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index dcff81ea4..af7b62b10 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,10 +1,14 @@ import assert from "assert"; import { BigNumber, ethers, providers } from "ethers"; + import { CHAIN_IDs, PUBLIC_NETWORKS, TOKEN_SYMBOLS_MAP, } from "@across-protocol/constants"; + +export { CHAIN_IDs } from "@across-protocol/constants"; + import * as superstruct from "superstruct"; import { parseEtherLike } from "./format"; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 1d874389b..784eb7336 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -7,6 +7,7 @@ import useAvailableCrosschainRoutes, { LifiToken, } from "hooks/useAvailableCrosschainRoutes"; import { + CHAIN_IDs, COLORS, formatUnitsWithMaxFractions, formatUSD, @@ -21,6 +22,14 @@ import useEnrichedCrosschainBalances from "hooks/useEnrichedCrosschainBalances"; import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; +const popularChains = [ + CHAIN_IDs.MAINNET, + CHAIN_IDs.BASE, + CHAIN_IDs.OPTIMISM, + CHAIN_IDs.ARBITRUM, + CHAIN_IDs.POLYGON, +]; + type Props = { onSelect: (token: EnrichedTokenSelect) => void; isOriginToken: boolean; @@ -103,9 +112,31 @@ export default function ChainTokenSelectorModal({ }; }); - // Return ordering top 100 tokens ordering highest balanceUsd to lowest (fallback alphabetical) - // Push disabled tokens to the bottom - const sortedTokens = enrichedTokens.slice(0, 100).sort((a, b) => { + // Filter by search first + const filteredTokens = enrichedTokens.filter((t) => { + if (tokenSearch === "") { + return true; + } + const keywords = [ + t.symbol.toLowerCase().replaceAll(" ", ""), + t.name.toLowerCase().replaceAll(" ", ""), + t.address.toLowerCase().replaceAll(" ", ""), + ]; + return keywords.some((keyword) => + keyword.includes(tokenSearch.toLowerCase().replaceAll(" ", "")) + ); + }); + + // Separate tokens with balance from tokens without balance + const tokensWithBalance = filteredTokens.filter( + (token) => token.balance.gt(0) && token.balanceUsd > 0.01 + ); + const tokensWithoutBalance = filteredTokens.filter( + (token) => token.balance.eq(0) || token.balanceUsd <= 0.01 + ); + + // Sort tokens with balance by balanceUsd (highest first), then alphabetically + const sortedTokensWithBalance = tokensWithBalance.sort((a, b) => { // First, sort by disabled status - disabled tokens go to bottom const aDisabled = a.isReachable === false; const bDisabled = b.isReachable === false; @@ -114,26 +145,31 @@ export default function ChainTokenSelectorModal({ return aDisabled ? 1 : -1; } - // Then sort by balance (for enabled tokens) or alphabetically (for disabled tokens) + // Then sort by balance if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); } return b.balanceUsd - a.balanceUsd; }); - return sortedTokens.filter((t) => { - if (tokenSearch === "") { - return true; + // Sort tokens without balance alphabetically, with disabled tokens at bottom + const sortedTokensWithoutBalance = tokensWithoutBalance.sort((a, b) => { + // First, sort by disabled status - disabled tokens go to bottom + const aDisabled = a.isReachable === false; + const bDisabled = b.isReachable === false; + + if (aDisabled !== bDisabled) { + return aDisabled ? 1 : -1; } - const keywords = [ - t.symbol.toLowerCase().replaceAll(" ", ""), - t.name.toLowerCase().replaceAll(" ", ""), - t.address.toLowerCase().replaceAll(" ", ""), - ]; - return keywords.some((keyword) => - keyword.includes(tokenSearch.toLowerCase().replaceAll(" ", "")) - ); + + // Then sort alphabetically + return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); }); + + return { + withBalance: sortedTokensWithBalance.slice(0, 50), // Limit to 50 tokens with balance + withoutBalance: sortedTokensWithoutBalance.slice(0, 50), // Limit to 50 tokens without balance + }; }, [selectedChain, balances, tokenSearch, otherToken, crossChainRoutes.data]); const displayedChains = useMemo(() => { @@ -162,15 +198,38 @@ export default function ChainTokenSelectorModal({ const isDisabled = false; return [chainId, { tokens, isDisabled }]; - }) - // Sort chains alphabetically by name (no need to sort by disabled status since none are disabled) - .sort(([chainIdA], [chainIdB]) => { + }); + + // Separate popular chains from all chains + const popularChainsData: typeof chainsWithDisabledState = []; + + chainsWithDisabledState.forEach((entry) => { + const [chainId] = entry; + if (popularChains.includes(Number(chainId))) { + popularChainsData.push(entry); + } + }); + + // Sort popular chains by the order they appear in popularChains array + popularChainsData.sort(([chainIdA], [chainIdB]) => { + const indexA = popularChains.indexOf(Number(chainIdA)); + const indexB = popularChains.indexOf(Number(chainIdB)); + return indexA - indexB; + }); + + // Combine all chains for the "All Chains" section (sorted alphabetically) + const allChainsData = [...chainsWithDisabledState].sort( + ([chainIdA], [chainIdB]) => { const chainInfoA = getChainInfo(Number(chainIdA)); const chainInfoB = getChainInfo(Number(chainIdB)); return chainInfoA.name.localeCompare(chainInfoB.name); - }); + } + ); - return Object.fromEntries(chainsWithDisabledState); + return { + popular: Object.fromEntries(popularChainsData), + all: Object.fromEntries(allChainsData), + }; }, [chainSearch, crossChainRoutes.data, otherToken]); return isMobile ? ( @@ -239,7 +298,10 @@ const MobileModal = ({ tokenSearch: string; setTokenSearch: (search: string) => void; displayedChains: any; - displayedTokens: any[]; + displayedTokens: { + withBalance: any[]; + withoutBalance: any[]; + }; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; }) => { @@ -314,7 +376,10 @@ const DesktopModal = ({ tokenSearch: string; setTokenSearch: (search: string) => void; displayedChains: any; - displayedTokens: any[]; + displayedTokens: { + withBalance: any[]; + withoutBalance: any[]; + }; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; }) => { @@ -367,7 +432,10 @@ const MobileLayout = ({ tokenSearch: string; setTokenSearch: (search: string) => void; displayedChains: any; - displayedTokens: any[]; + displayedTokens: { + withBalance: any[]; + withoutBalance: any[]; + }; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; onModalClose: () => void; @@ -388,7 +456,31 @@ const MobileLayout = ({ isSelected={selectedChain === null} onClick={() => onChainSelect(null)} /> - {Object.entries(displayedChains).map(([chainId, chainData]) => ( + + {/* Popular Chains Section */} + {Object.keys(displayedChains.popular).length > 0 && ( + <> + Popular Chains + {Object.entries(displayedChains.popular).map( + ([chainId, chainData]) => ( + onChainSelect(Number(chainId))} + /> + ) + )} + + )} + + {/* All Chains Section */} + All Chains + {Object.entries(displayedChains.all).map(([chainId, chainData]) => ( - {displayedTokens.map((token) => ( + {/* Your Tokens Section */} + {displayedTokens.withBalance.length > 0 && ( + <> + Your Tokens + {displayedTokens.withBalance.map((token) => ( + { + onTokenSelect({ + chainId: token.chainId, + symbolUri: token.logoURI, + symbol: token.symbol, + address: token.address, + balance: token.balance, + priceUsd: parseUnits(token.priceUSD, 18), + decimals: token.decimals, + }); + onModalClose(); + }} + /> + ))} + + )} + + {/* All Tokens Section */} + All Tokens + {displayedTokens.withoutBalance.map((token) => ( void; displayedChains: any; - displayedTokens: any[]; + displayedTokens: { + withBalance: any[]; + withoutBalance: any[]; + }; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; onModalClose: () => void; @@ -474,7 +597,31 @@ const DesktopLayout = ({ isSelected={selectedChain === null} onClick={() => onChainSelect(null)} /> - {Object.entries(displayedChains).map(([chainId, chainData]) => ( + + {/* Popular Chains Section */} + {Object.keys(displayedChains.popular).length > 0 && ( + <> + Popular Chains + {Object.entries(displayedChains.popular).map( + ([chainId, chainData]) => ( + onChainSelect(Number(chainId))} + /> + ) + )} + + )} + + {/* All Chains Section */} + All Chains + {Object.entries(displayedChains.all).map(([chainId, chainData]) => ( - {displayedTokens.map((token) => ( + {/* Your Tokens Section */} + {displayedTokens.withBalance.length > 0 && ( + <> + Your Tokens + {displayedTokens.withBalance.map((token) => ( + { + onTokenSelect({ + chainId: token.chainId, + symbolUri: token.logoURI, + symbol: token.symbol, + address: token.address, + balance: token.balance, + priceUsd: parseUnits(token.priceUSD, 18), + decimals: token.decimals, + }); + onModalClose(); + }} + /> + ))} + + )} + + {/* All Tokens Section */} + All Tokens + {displayedTokens.withoutBalance.map((token) => ( Date: Wed, 1 Oct 2025 14:22:17 +0200 Subject: [PATCH 025/122] better types Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 85 +++++++++---------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 784eb7336..e701fc5a9 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -30,6 +30,29 @@ const popularChains = [ CHAIN_IDs.POLYGON, ]; +// Type definitions for better typing +type ChainData = { + tokens: LifiToken[]; + isDisabled: boolean; +}; + +type DisplayedChains = { + popular: Record; + all: Record; +}; + +type EnrichedToken = LifiToken & { + balance: BigNumber; + balanceUsd: number; + isReachable?: boolean; + routeSource: "bridge" | "swap"; +}; + +type DisplayedTokens = { + withBalance: EnrichedToken[]; + withoutBalance: EnrichedToken[]; +}; + type Props = { onSelect: (token: EnrichedTokenSelect) => void; isOriginToken: boolean; @@ -180,11 +203,6 @@ export default function ChainTokenSelectorModal({ return false; } - // Filter out the chain of the other token (same chain can't be both input and output) - if (otherToken && Number(chainId) === otherToken.chainId) { - return false; - } - const keywords = [ String(chainId), getChainInfo(Number(chainId)).name.toLowerCase().replace(" ", ""), @@ -194,10 +212,13 @@ export default function ChainTokenSelectorModal({ ); }) .map(([chainId, tokens]) => { - // Never disable chains - requirement 1 - const isDisabled = false; - - return [chainId, { tokens, isDisabled }]; + return [ + chainId, + { + tokens, + isDisabled: otherToken && Number(chainId) === otherToken.chainId, // same chain can't be both input and output + }, + ]; }); // Separate popular chains from all chains @@ -297,11 +318,8 @@ const MobileModal = ({ setChainSearch: (search: string) => void; tokenSearch: string; setTokenSearch: (search: string) => void; - displayedChains: any; - displayedTokens: { - withBalance: any[]; - withoutBalance: any[]; - }; + displayedChains: DisplayedChains; + displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; }) => { @@ -375,11 +393,8 @@ const DesktopModal = ({ setChainSearch: (search: string) => void; tokenSearch: string; setTokenSearch: (search: string) => void; - displayedChains: any; - displayedTokens: { - withBalance: any[]; - withoutBalance: any[]; - }; + displayedChains: DisplayedChains; + displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; }) => { @@ -431,11 +446,8 @@ const MobileLayout = ({ setChainSearch: (search: string) => void; tokenSearch: string; setTokenSearch: (search: string) => void; - displayedChains: any; - displayedTokens: { - withBalance: any[]; - withoutBalance: any[]; - }; + displayedChains: DisplayedChains; + displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; onModalClose: () => void; @@ -467,10 +479,7 @@ const MobileLayout = ({ key={chainId} chainId={Number(chainId)} isSelected={selectedChain === Number(chainId)} - isDisabled={ - (chainData as { tokens: any; isDisabled: boolean }) - .isDisabled - } + isDisabled={chainData.isDisabled} onClick={() => onChainSelect(Number(chainId))} /> ) @@ -485,9 +494,7 @@ const MobileLayout = ({ key={chainId} chainId={Number(chainId)} isSelected={selectedChain === Number(chainId)} - isDisabled={ - (chainData as { tokens: any; isDisabled: boolean }).isDisabled - } + isDisabled={chainData.isDisabled} onClick={() => onChainSelect(Number(chainId))} /> ))} @@ -574,11 +581,8 @@ const DesktopLayout = ({ setChainSearch: (search: string) => void; tokenSearch: string; setTokenSearch: (search: string) => void; - displayedChains: any; - displayedTokens: { - withBalance: any[]; - withoutBalance: any[]; - }; + displayedChains: DisplayedChains; + displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedTokenSelect) => void; onModalClose: () => void; @@ -608,10 +612,7 @@ const DesktopLayout = ({ key={chainId} chainId={Number(chainId)} isSelected={selectedChain === Number(chainId)} - isDisabled={ - (chainData as { tokens: any; isDisabled: boolean }) - .isDisabled - } + isDisabled={chainData.isDisabled} onClick={() => onChainSelect(Number(chainId))} /> ) @@ -626,9 +627,7 @@ const DesktopLayout = ({ key={chainId} chainId={Number(chainId)} isSelected={selectedChain === Number(chainId)} - isDisabled={ - (chainData as { tokens: any; isDisabled: boolean }).isDisabled - } + isDisabled={chainData.isDisabled} onClick={() => onChainSelect(Number(chainId))} /> ))} From 020fedb77011a23a5e10dd912934cff2840badc9 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 1 Oct 2025 14:46:09 +0200 Subject: [PATCH 026/122] sort Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index e701fc5a9..86c764e6c 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -37,8 +37,8 @@ type ChainData = { }; type DisplayedChains = { - popular: Record; - all: Record; + popular: [string, ChainData][]; + all: [string, ChainData][]; }; type EnrichedToken = LifiToken & { @@ -196,7 +196,9 @@ export default function ChainTokenSelectorModal({ }, [selectedChain, balances, tokenSearch, otherToken, crossChainRoutes.data]); const displayedChains = useMemo(() => { - const chainsWithDisabledState = Object.entries(crossChainRoutes.data || {}) + const chainsWithDisabledState: [string, ChainData][] = Object.entries( + crossChainRoutes.data || {} + ) .filter(([chainId]) => { // why ar we filtering out Boba? if ([288].includes(Number(chainId))) { @@ -218,7 +220,7 @@ export default function ChainTokenSelectorModal({ tokens, isDisabled: otherToken && Number(chainId) === otherToken.chainId, // same chain can't be both input and output }, - ]; + ] as [string, ChainData]; }); // Separate popular chains from all chains @@ -248,8 +250,8 @@ export default function ChainTokenSelectorModal({ ); return { - popular: Object.fromEntries(popularChainsData), - all: Object.fromEntries(allChainsData), + popular: popularChainsData, + all: allChainsData, }; }, [chainSearch, crossChainRoutes.data, otherToken]); @@ -470,26 +472,24 @@ const MobileLayout = ({ /> {/* Popular Chains Section */} - {Object.keys(displayedChains.popular).length > 0 && ( + {displayedChains.popular.length > 0 && ( <> Popular Chains - {Object.entries(displayedChains.popular).map( - ([chainId, chainData]) => ( - onChainSelect(Number(chainId))} - /> - ) - )} + {displayedChains.popular.map(([chainId, chainData]) => ( + onChainSelect(Number(chainId))} + /> + ))} )} {/* All Chains Section */} All Chains - {Object.entries(displayedChains.all).map(([chainId, chainData]) => ( + {displayedChains.all.map(([chainId, chainData]) => ( {/* Popular Chains Section */} - {Object.keys(displayedChains.popular).length > 0 && ( + {displayedChains.popular.length > 0 && ( <> Popular Chains - {Object.entries(displayedChains.popular).map( - ([chainId, chainData]) => ( - onChainSelect(Number(chainId))} - /> - ) - )} + {displayedChains.popular.map(([chainId, chainData]) => ( + onChainSelect(Number(chainId))} + /> + ))} )} {/* All Chains Section */} All Chains - {Object.entries(displayedChains.all).map(([chainId, chainData]) => ( + {displayedChains.all.map(([chainId, chainData]) => ( Date: Wed, 1 Oct 2025 14:46:22 +0200 Subject: [PATCH 027/122] refactor Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenConversion.ts | 62 ++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/hooks/useTokenConversion.ts b/src/hooks/useTokenConversion.ts index a1f769599..cff38f6e1 100644 --- a/src/hooks/useTokenConversion.ts +++ b/src/hooks/useTokenConversion.ts @@ -8,9 +8,10 @@ import { isDefined, getConfig, hubPoolChainId, + TokenInfo, } from "utils"; import { ConvertDecimals } from "utils/convertdecimals"; -import useAvailableCrosschainRoutes from "./useAvailableCrosschainRoutes"; +import { useSwapTokens } from "./useSwapTokens"; const config = getConfig(); @@ -19,51 +20,50 @@ export function useTokenConversion( baseCurrency: string, historicalDateISO?: string ) { - const availableCrosschainRoutes = useAvailableCrosschainRoutes(); + const { data: swapTokens } = useSwapTokens(); // Try to get token from constants first, fallback to swap API data - let token; + let token: TokenInfo | undefined; try { token = getToken(symbol); } catch (error) { // If token not found in constants, try to find it in swap API data - const swapTokens = availableCrosschainRoutes.data; if (swapTokens) { // Search across all chains for a token with matching symbol - for (const chainId of Object.keys(swapTokens)) { - const tokensOnChain = swapTokens[Number(chainId)]; - const foundToken = tokensOnChain.find( - (t) => t.symbol.toUpperCase() === symbol.toUpperCase() - ); - if (foundToken) { - // Convert LifiToken to TokenInfo format - token = { - symbol: foundToken.symbol, - name: foundToken.name, - decimals: foundToken.decimals, - addresses: { [foundToken.chainId]: foundToken.address }, - mainnetAddress: foundToken.address, // Use the found address as mainnet address - logoURI: foundToken.logoURI, - }; - break; - } + const foundToken = swapTokens.find( + (t) => t.symbol.toUpperCase() === symbol.toUpperCase() + ); + if (foundToken) { + // Convert SwapToken to TokenInfo format + token = { + symbol: foundToken.symbol, + name: foundToken.name, + decimals: foundToken.decimals, + addresses: { [foundToken.chainId]: foundToken.address }, + mainnetAddress: foundToken.address, // Use the found address as mainnet address + logoURI: foundToken.logoUrl || "", // Use logoUrl from SwapToken + }; } - } - // If still not found, re-throw the original error - if (!token) { + // If still not found, re-throw the original error + if (!token) { + throw error; + } + } else { + // If swapTokens is not available, re-throw the original error + console.error(`Unable to resolve token info for symbol ${symbol}`); throw error; } } // If the token is OP, we need to use the address of the token on Optimism const l1Token = - token.symbol === "OP" + token?.symbol === "OP" ? TOKEN_SYMBOLS_MAP["OP"].addresses[10] - : token.mainnetAddress!; + : token?.mainnetAddress; const query = useCoingeckoPrice( - l1Token, + l1Token || "", baseCurrency, historicalDateISO, isDefined(l1Token) @@ -74,7 +74,9 @@ export function useTokenConversion( const price = query.data?.price; const decimals = token?.decimals ?? - config.getTokenInfoByAddressSafe(hubPoolChainId, l1Token)?.decimals; + (l1Token + ? config.getTokenInfoByAddressSafe(hubPoolChainId, l1Token)?.decimals + : undefined); if (!isDefined(price) || !isDefined(amount) || !isDefined(decimals)) { return undefined; @@ -91,7 +93,9 @@ export function useTokenConversion( const price = query.data?.price; const decimals = token?.decimals ?? - config.getTokenInfoByAddressSafe(hubPoolChainId, l1Token)?.decimals; + (l1Token + ? config.getTokenInfoByAddressSafe(hubPoolChainId, l1Token)?.decimals + : undefined); if (!isDefined(price) || !isDefined(amount) || !isDefined(decimals)) { return undefined; From 945f63a1ec445faac6e7737ee06eb4d5e10620ad Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 2 Oct 2025 12:00:25 +0200 Subject: [PATCH 028/122] improve searchbar styles Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Searchbar.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx index dc4a4ab51..c3ab567b2 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -1,6 +1,7 @@ import styled from "@emotion/styled"; import { ReactComponent as SearchIcon } from "assets/icons/search.svg"; import { ReactComponent as ProductIcon } from "assets/icons/product.svg"; +import { COLORS } from "utils"; type Props = { searchTopic: string; @@ -34,14 +35,19 @@ const Wrapper = styled.div` padding: 0px 12px; align-items: center; gap: 8px; - flex-direction: row; justify-content: space-between; border-radius: 8px; - background: rgba(224, 243, 255, 0.05); + background: transparent; width: 100%; + + &:hover, + &:active, + &:focus-visible { + background: rgba(224, 243, 255, 0.05); + } `; const StyledSearchIcon = styled(SearchIcon)` @@ -69,6 +75,12 @@ const Input = styled.input` color: #e0f3ff4d; } + &:hover, + &:active, + &:focus-visible { + color: ${COLORS.aqua}; + } + background: transparent; border: none; From 06d5f1f9cacde2b2b033507168607bbd586542fa Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 2 Oct 2025 13:18:08 +0200 Subject: [PATCH 029/122] restrict tabbing inside modal Signed-off-by: Gerhard Steenkamp --- src/components/Modal/Modal.styles.ts | 26 ++++++--- src/components/Modal/Modal.tsx | 14 ++++- src/hooks/useTabIndexManager.ts | 53 +++++++++++++++++++ .../components/ChainTokenSelector/Modal.tsx | 12 +++-- .../ChainTokenSelector/Searchbar.tsx | 19 +++++-- 5 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 src/hooks/useTabIndexManager.ts diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 90ffe8db1..823b9a076 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -1,7 +1,6 @@ import { keyframes } from "@emotion/react"; import styled from "@emotion/styled"; -import { ReactComponent as CrossIcon } from "assets/icons/cross.svg"; -import { QUERIESV2 } from "utils"; +import { COLORS, QUERIESV2 } from "utils"; import { ModalDirection } from "./Modal"; const fadeBackground = keyframes` @@ -148,10 +147,6 @@ export const Title = styled.p` } `; -export const StyledExitIcon = styled(CrossIcon)` - cursor: pointer; -`; - export const ElementRowDivider = styled.div` height: 1px; min-height: 1px; @@ -160,3 +155,22 @@ export const ElementRowDivider = styled.div` margin-left: calc(0px - var(--padding-modal-content)); width: calc(100% + (2 * var(--padding-modal-content))); `; + +export const CloseButton = styled.button` + border: none; + background-color: transparent; + display: inline-flex; + outline: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + + &:hover, + &:focus-visible { + background-color: ${COLORS["grey-400-15"]}; + } + + &:focus-visible { + outline: 2px solid ${COLORS.aqua}; + } +`; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 5639631e0..6948893d5 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,14 +1,16 @@ import usePageScrollLock from "hooks/usePageScrollLock"; +import { useTabIndexManager } from "hooks/useTabIndexManager"; import React, { useEffect, useRef, useState, useLayoutEffect } from "react"; import { createPortal } from "react-dom"; import { + CloseButton, ElementRowDivider, ModalContentWrapper, - StyledExitIcon, Title, TitleAndExitWrapper, Wrapper, } from "./Modal.styles"; +import { ReactComponent as ExitIcon } from "assets/icons/cross.svg"; type ModalDirectionOrientation = "middle" | "top" | "bottom"; export type ModalDirection = { @@ -77,6 +79,9 @@ const Modal = ({ const [forwardAnimation, setForwardAnimation] = useState(true); const { lockScroll, unlockScroll } = usePageScrollLock(); + // Manage tab indices when modal is open - only elements inside modal will be focusable + useTabIndexManager(!!isOpen, modalContentRef); + const offModalClickHandler = (event: React.MouseEvent) => { if ( modalContentRef.current && @@ -153,7 +158,12 @@ const Modal = ({
{title}
)} - externalModalExitHandler()} /> + externalModalExitHandler()} + > + + {titleBorder && } {children} diff --git a/src/hooks/useTabIndexManager.ts b/src/hooks/useTabIndexManager.ts new file mode 100644 index 000000000..6877073e8 --- /dev/null +++ b/src/hooks/useTabIndexManager.ts @@ -0,0 +1,53 @@ +import { useEffect, useRef } from "react"; + +/** + * Custom hook that manages tab indices for modal content + * When active, it sets all elements outside the modal to tabindex="-1" + * and restores their original tabindex values when inactive + * @param isActive - Whether the tab index management should be active + * @param containerRef - Reference to the container element to preserve tab indices within + */ +export const useTabIndexManager = ( + isActive: boolean, + containerRef: React.RefObject +) => { + const originalTabIndices = useRef>(new Map()); + + useEffect(() => { + if (!isActive || !containerRef.current) { + return; + } + + const modalElement = containerRef.current; + + // Only target elements that are naturally focusable + const focusableElements = document.querySelectorAll( + "button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex='-1'])" + ) as NodeListOf; + + // Store original tabindex values and set elements outside modal to tabindex="-1" + focusableElements.forEach((element) => { + // Skip elements inside the modal + if (modalElement.contains(element)) { + return; + } + + const currentTabIndex = element.getAttribute("tabindex"); + originalTabIndices.current.set(element, currentTabIndex); + element.setAttribute("tabindex", "-1"); + }); + + // Cleanup function + return () => { + // Restore original tabindex values + originalTabIndices.current.forEach((originalTabIndex, element) => { + if (originalTabIndex === null) { + element.removeAttribute("tabindex"); + } else { + element.setAttribute("tabindex", originalTabIndex); + } + }); + originalTabIndices.current.clear(); + }; + }, [isActive, containerRef]); +}; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 86c764e6c..75abd2feb 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -1,7 +1,7 @@ import Modal from "components/Modal"; import { EnrichedTokenSelect } from "./SelectorButton"; import styled from "@emotion/styled"; -import Searchbar from "./Searchbar"; +import { Searchbar } from "./Searchbar"; import TokenMask from "assets/mask/token-mask-corner.svg"; import useAvailableCrosschainRoutes, { LifiToken, @@ -591,11 +591,14 @@ const DesktopLayout = ({ - + - + {/* Your Tokens Section */} {displayedTokens.withBalance.length > 0 && ( <> diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx index c3ab567b2..333a0af50 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -2,32 +2,38 @@ import styled from "@emotion/styled"; import { ReactComponent as SearchIcon } from "assets/icons/search.svg"; import { ReactComponent as ProductIcon } from "assets/icons/product.svg"; import { COLORS } from "utils"; +import React from "react"; type Props = { searchTopic: string; search: string; setSearch: (search: string) => void; className?: string; + inputProps?: React.ComponentPropsWithoutRef<"input">; }; -export default function Searchbar({ +export const Searchbar = ({ searchTopic, search, setSearch, className, -}: Props) { + inputProps, +}: Props) => { return ( setSearch(e.target.value)} + {...inputProps} /> {search ? setSearch("")} /> :
} ); -} +}; const Wrapper = styled.div` display: flex; @@ -44,10 +50,13 @@ const Wrapper = styled.div` width: 100%; &:hover, - &:active, - &:focus-visible { + &:active { background: rgba(224, 243, 255, 0.05); } + + &:focus-within { + background: rgba(224, 243, 255, 0.1); + } `; const StyledSearchIcon = styled(SearchIcon)` From 2bfe97b45da199f33d147bb1e1afafcf4d295e11 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 2 Oct 2025 13:24:17 +0200 Subject: [PATCH 030/122] clean up Signed-off-by: Gerhard Steenkamp --- src/hooks/useTabIndexManager.ts | 7 ++++--- .../SwapAndBridge/components/ChainTokenSelector/Modal.tsx | 6 +----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/hooks/useTabIndexManager.ts b/src/hooks/useTabIndexManager.ts index 6877073e8..1d898af30 100644 --- a/src/hooks/useTabIndexManager.ts +++ b/src/hooks/useTabIndexManager.ts @@ -19,6 +19,7 @@ export const useTabIndexManager = ( } const modalElement = containerRef.current; + const tabIndicesMap = originalTabIndices.current; // Only target elements that are naturally focusable const focusableElements = document.querySelectorAll( @@ -33,21 +34,21 @@ export const useTabIndexManager = ( } const currentTabIndex = element.getAttribute("tabindex"); - originalTabIndices.current.set(element, currentTabIndex); + tabIndicesMap.set(element, currentTabIndex); element.setAttribute("tabindex", "-1"); }); // Cleanup function return () => { // Restore original tabindex values - originalTabIndices.current.forEach((originalTabIndex, element) => { + tabIndicesMap.forEach((originalTabIndex, element) => { if (originalTabIndex === null) { element.removeAttribute("tabindex"); } else { element.setAttribute("tabindex", originalTabIndex); } }); - originalTabIndices.current.clear(); + tabIndicesMap.clear(); }; }, [isActive, containerRef]); }; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 75abd2feb..6ceaabc9e 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -193,7 +193,7 @@ export default function ChainTokenSelectorModal({ withBalance: sortedTokensWithBalance.slice(0, 50), // Limit to 50 tokens with balance withoutBalance: sortedTokensWithoutBalance.slice(0, 50), // Limit to 50 tokens without balance }; - }, [selectedChain, balances, tokenSearch, otherToken, crossChainRoutes.data]); + }, [selectedChain, balances, tokenSearch, crossChainRoutes.data]); const displayedChains = useMemo(() => { const chainsWithDisabledState: [string, ChainData][] = Object.entries( @@ -970,10 +970,6 @@ const EntryItem = styled.div<{ isSelected: boolean; isDisabled?: boolean }>` cursor: ${({ isDisabled }) => (isDisabled ? "not-allowed" : "pointer")}; opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)}; - transition: - background 0.2s ease-in-out, - opacity 0.2s ease-in-out; - &:hover { background: ${({ isSelected, isDisabled }) => { if (isDisabled) return "transparent"; From 950be654805811924048167edac398a19f9e1220 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 2 Oct 2025 18:51:52 +0200 Subject: [PATCH 031/122] add dialog Signed-off-by: Gerhard Steenkamp --- src/assets/icons/research.svg | 5 + src/assets/icons/siren.svg | 6 + src/components/Dialogs/Dialog.tsx | 236 +++++++++++++++++++ src/components/GlobalStyles/GlobalStyles.tsx | 4 + src/components/Modal/Modal.styles.ts | 6 +- src/components/Modal/Modal.tsx | 3 + src/utils/colors.ts | 13 + src/utils/index.ts | 1 + 8 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 src/assets/icons/research.svg create mode 100644 src/assets/icons/siren.svg create mode 100644 src/components/Dialogs/Dialog.tsx create mode 100644 src/utils/colors.ts diff --git a/src/assets/icons/research.svg b/src/assets/icons/research.svg new file mode 100644 index 000000000..065b3b72c --- /dev/null +++ b/src/assets/icons/research.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/siren.svg b/src/assets/icons/siren.svg new file mode 100644 index 000000000..b616bd545 --- /dev/null +++ b/src/assets/icons/siren.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/Dialogs/Dialog.tsx b/src/components/Dialogs/Dialog.tsx new file mode 100644 index 000000000..d1f894736 --- /dev/null +++ b/src/components/Dialogs/Dialog.tsx @@ -0,0 +1,236 @@ +import Modal from "components/Modal"; +import { ModalProps } from "components/Modal/Modal"; +import styled from "@emotion/styled"; +import { COLORS, QUERIES, withOpacity } from "utils"; +import { ReactComponent as Warning } from "assets/icons/warning_triangle.svg"; +import { ReactComponent as Siren } from "assets/icons/siren.svg"; +import { ReactComponent as Info } from "assets/icons/info.svg"; +import { PropsWithChildren } from "react"; + +type Variant = "warn" | "error" | "info"; + +const defaultIcons: Record = { + warn: , + error: , + info: , +}; + +const defaultColors: Record = { + warn: "rgba(255, 149, 0, 1)", + error: COLORS.error, + info: COLORS.white, +}; + +// DialogWrapper - The main container that wraps everything +export type DialogWrapperProps = ModalProps & { + className?: string; +}; + +export function DialogWrapper({ + children, + className, + ...props +}: DialogWrapperProps) { + return ( + + {children} + + ); +} + +// DialogIcon - For displaying icons with variant-based styling +export type DialogIconProps = { + variant?: Variant; + icon?: React.ReactNode; + color?: string; + className?: string; +}; + +export function DialogIcon({ + variant = "info", + icon, + color, + className, +}: DialogIconProps) { + const Icon = icon ?? defaultIcons[variant]; + const iconColor = color ?? defaultColors[variant]; + + return ( + + {Icon} + + ); +} + +// DialogContent - For the main content area +export type DialogContentProps = { + children: React.ReactNode; + className?: string; +}; + +export function DialogContent({ children, className }: DialogContentProps) { + return {children}; +} + +// DialogButtonRow - Container for buttons +export type DialogButtonRowProps = { + children: React.ReactNode; + className?: string; +}; + +export function DialogButtonRow({ children, className }: DialogButtonRowProps) { + return {children}; +} + +// DialogButtonPrimary - Primary action button +export type DialogButtonPrimaryProps = { + children?: React.ReactNode; + onClick?: () => void; + className?: string; +}; + +export function DialogButtonPrimary({ + children, + onClick, + className, +}: DialogButtonPrimaryProps) { + return ( + + {children} + + ); +} + +// DialogButtonSecondary - Secondary action button +export type DialogButtonSecondaryProps = { + children?: React.ReactNode; + onClick?: () => void; + className?: string; +}; + +export function DialogButtonSecondary({ + children, + onClick, + className, +}: DialogButtonSecondaryProps) { + return ( + + {children} + + ); +} + +// Legacy Dialog component for backward compatibility +export type DoYourOwnResearchDialogProps = ModalProps & { + variant: Variant; + primaryAction?: () => void; + secondaryAction?: () => void; + icon?: React.ReactNode; + color?: string; + className?: string; +}; + +export function Dialog({ + children, + className, + variant, + icon, + primaryAction, + secondaryAction, + ...props +}: DoYourOwnResearchDialogProps) { + const Icon = icon ?? defaultIcons[variant]; + return ( + + {Icon} + {children} + {(secondaryAction || primaryAction) && ( + + {secondaryAction && } + {primaryAction && } + + )} + + ); +} + +// Styled components +const ButtonRow = styled.div` + display: flex; + gap: 16px; + justify-content: center; + width: 100%; + align-items: center; + flex-direction: column; + flex-wrap: wrap; + + @media ${QUERIES.tabletAndUp} { + flex-direction: row; + } +`; + +const PrimaryButton = styled.button` + display: flex; + height: 48px; + width: auto; + padding: 0 var(--Spacing-Medium, 16px); + justify-content: center; + align-items: center; + border-radius: 12px; + background: rgba(224, 243, 255, 0.1); + cursor: pointer; + color: white; + border: 1px solid transparent; + flex: 1 0 auto; + font-size: 16px; + font-weight: 600; + + &:hover { + background: none; + border: 1px solid rgba(224, 243, 255, 0.1); + } +`; + +const SecondaryButton = styled(PrimaryButton)` + background: transparent; + border: 1px solid rgba(224, 243, 255, 0.1); + + &:hover { + background: rgba(224, 243, 255, 0.1); + border-color: transparent; + } +`; + +const Wrapper = styled(Modal)` + width: 100%; + max-width: 450px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 12px; +`; + +const IconWrapper = styled.div<{ color: string }>` + background-color: ${({ color }) => withOpacity(color, 0.2)}; + border-radius: 50%; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 32px; + height: 32px; + color: ${({ color }) => color}; + } +`; + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +`; diff --git a/src/components/GlobalStyles/GlobalStyles.tsx b/src/components/GlobalStyles/GlobalStyles.tsx index bf4d75d9d..bb4dc96bd 100644 --- a/src/components/GlobalStyles/GlobalStyles.tsx +++ b/src/components/GlobalStyles/GlobalStyles.tsx @@ -105,6 +105,10 @@ const globalStyles = css` -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: auto; } + button { + border: none; + background-color: none; + } html, body { min-height: 100vh; diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 823b9a076..94b39272c 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -12,6 +12,7 @@ type WrapperType = { reverseAnimation?: boolean; direction: ModalDirection; }; + export const Wrapper = styled.div` position: fixed; top: 0; @@ -27,7 +28,7 @@ export const Wrapper = styled.div` z-index: 99998; - animation: ${fadeBackground} 0.5s linear; + animation: ${fadeBackground} 0.3s linear; animation-fill-mode: forwards; opacity: ${({ reverseAnimation }) => (reverseAnimation ? 0 : 1)}; @@ -99,7 +100,6 @@ export const ModalContentWrapper = styled.div` ? `min(calc(100svh - ${minimumMargin * 2}px - ${topYOffset ?? 0}px), ${height}px)` : "calc(100svh - 64px)"}; max-width: ${({ width }) => width ?? 800}px; - height: fit-content; width: calc(100% - 32px); @@ -116,7 +116,7 @@ export const ModalContentWrapper = styled.div` background: #202024; border: 1px solid #34353b; box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.32); - border-radius: 16px; + border-radius: 24px; position: relative; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 6948893d5..6b0a31964 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -39,6 +39,7 @@ export type ModalProps = { children?: React.ReactNode; titleBorder?: boolean; + className?: string; }; const Modal = ({ @@ -54,6 +55,7 @@ const Modal = ({ topYOffset, bottomYOffset, padding, + className, "data-cy": dataCy, titleBorder = false, }: ModalProps) => { @@ -150,6 +152,7 @@ const Modal = ({ topYOffset={topYOffset} bottomYOffset={bottomYOffset} padding={padding ?? "normal"} + className={className} > {typeof title === "string" ? ( diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 000000000..83bbf4d88 --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,13 @@ +/** + * Simple utility to change color opacity using color-mix + */ + +/** + * Creates a color with a specific opacity using color-mix + * @param color - The base color (any valid CSS color) + * @param opacity - The opacity value (0-1) + * @returns The color with the specified opacity + */ +export function withOpacity(color: string, opacity: number): string { + return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 03639f72f..ff0a45976 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -23,3 +23,4 @@ export * from "./url"; export * from "./sdk"; export * from "./hyperliquid"; export * from "./bignumber"; +export * from "./colors"; From c6f7540a2417765d4129148f5061ec30bb2f5449 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 3 Oct 2025 12:00:28 +0200 Subject: [PATCH 032/122] fixup Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index ff0e45923..d1ae8b874 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -143,13 +143,13 @@ export default function useAvailableCrosschainRoutes( ? otherToken!.symbol : token.symbol; - let isReachable = true; + let isReachable = false; - // For same chain (swap), always reachable + // For same chain, not reachable (no swaps allowed on same chain) if (fromChain === toChain) { - isReachable = true; + isReachable = false; } else { - // For bridge, check if there's an explicit bridge route + // For different chains, check if there's an explicit bridge route const bridgeRoutes = config.filterRoutes({ fromChain, toChain, From 2062525a1af2d9339f77a59df3a98832f662ac7f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 3 Oct 2025 12:26:34 +0200 Subject: [PATCH 033/122] move balance call to endpoint Signed-off-by: Gerhard Steenkamp --- api/_providers.ts | 13 +++ api/user-token-balances.ts | 111 +++++++++++++++++++++ src/hooks/useEnrichedCrosschainBalances.ts | 8 +- src/hooks/useTokenBalancesOnChain.ts | 95 ++++++++---------- 4 files changed, 170 insertions(+), 57 deletions(-) create mode 100644 api/user-token-balances.ts diff --git a/api/_providers.ts b/api/_providers.ts index f0a19e3d3..c71c37040 100644 --- a/api/_providers.ts +++ b/api/_providers.ts @@ -210,3 +210,16 @@ export function getProviderHeaders( return rpcHeaders?.[String(chainId)]; } + +/** + * Gets the Alchemy RPC URL for a given chain ID from the rpc-providers.json configuration + * @param chainId The chain ID to get the Alchemy RPC URL for + * @returns The Alchemy RPC URL or undefined if not available + */ +export function getAlchemyRpcFromConfigJson( + chainId: number +): string | undefined { + const { providers } = rpcProvidersJson; + const alchemyUrls = providers.urls.alchemy as Record; + return alchemyUrls?.[String(chainId)]; +} diff --git a/api/user-token-balances.ts b/api/user-token-balances.ts new file mode 100644 index 000000000..1661ece59 --- /dev/null +++ b/api/user-token-balances.ts @@ -0,0 +1,111 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, type } from "superstruct"; +import { TypedVercelRequest } from "./_types"; +import { getLogger, handleErrorCondition, validAddress } from "./_utils"; +import { getAlchemyRpcFromConfigJson } from "./_providers"; +import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; +import { BigNumber } from "ethers"; + +const UserTokenBalancesQueryParamsSchema = type({ + account: validAddress(), +}); + +type UserTokenBalancesQueryParams = Infer< + typeof UserTokenBalancesQueryParamsSchema +>; + +const fetchTokenBalancesForChain = async ( + chainId: number, + account: string +): Promise<{ + chainId: number; + balances: Array<{ address: string; balance: string }>; +}> => { + const rpcUrl = getAlchemyRpcFromConfigJson(chainId); + + if (!rpcUrl) { + throw new Error(`No Alchemy RPC URL found for chain ${chainId}`); + } + + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "alchemy_getTokenBalances", + params: [account], + }), + }); + + const data = await response.json(); + + const balances = ( + data.result.tokenBalances as { + contractAddress: string; + tokenBalance: string; + }[] + ) + .filter((t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0)) + .map((t) => ({ + address: t.contractAddress, + balance: BigNumber.from(t.tokenBalance).toString(), + })); + + return { + chainId, + balances, + }; +}; + +const handler = async ( + request: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + + try { + const { query } = request; + assert(query, UserTokenBalancesQueryParamsSchema); + const { account } = query; + + // Get all available chain IDs that have Alchemy RPC URLs + const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs) + .sort((a, b) => a - b) + .filter((chainId) => !!getAlchemyRpcFromConfigJson(chainId)); + + // Fetch balances for all chains in parallel + const balancePromises = chainIdsAvailable.map((chainId) => + fetchTokenBalancesForChain(chainId, account) + ); + + const chainBalances = await Promise.all(balancePromises); + + const responseData = { + account, + balances: chainBalances.map(({ chainId, balances }) => ({ + chainId: chainId.toString(), + balances, + })), + }; + + logger.debug({ + at: "UserTokenBalances", + message: "Response data", + responseJson: responseData, + }); + + // Cache for 3 minutes + response.setHeader( + "Cache-Control", + "s-maxage=180, stale-while-revalidate=60" + ); + response.status(200).json(responseData); + } catch (error: unknown) { + return handleErrorCondition("user-token-balances", response, logger, error); + } +}; + +export default handler; diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts index 4b07e0c29..e50d78ae8 100644 --- a/src/hooks/useEnrichedCrosschainBalances.ts +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -11,21 +11,21 @@ export default function useEnrichedCrosschainBalances() { const availableCrosschainRoutes = useAvailableCrosschainRoutes(); return useMemo(() => { - if (availableCrosschainRoutes.isLoading) { + if (availableCrosschainRoutes.isLoading || tokenBalances.isLoading) { return {}; } const chains = Object.keys(availableCrosschainRoutes.data || {}); return chains.reduce( (acc, chainId) => { - const balancesForChain = tokenBalances.find( - (t) => t.isSuccess && t.data.chainId === Number(chainId) + const balancesForChain = tokenBalances.data?.find( + (t) => t.chainId === Number(chainId) ); const tokens = availableCrosschainRoutes.data![Number(chainId)]; const enrichedTokens = tokens .map((t) => { - const balance = balancesForChain?.data?.balances.find((b) => + const balance = balancesForChain?.balances.find((b) => compareAddressesSimple(b.address, t.address) ); return { diff --git a/src/hooks/useTokenBalancesOnChain.ts b/src/hooks/useTokenBalancesOnChain.ts index 09ad766e8..8d1fa24bc 100644 --- a/src/hooks/useTokenBalancesOnChain.ts +++ b/src/hooks/useTokenBalancesOnChain.ts @@ -1,68 +1,57 @@ import { useConnection } from "./useConnection"; -import { CHAIN_IDs, MAINNET_CHAIN_IDs } from "@across-protocol/constants"; -import { useQueries } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { BigNumber } from "ethers"; -const CHAIN_TO_ALCHEMY = { - [CHAIN_IDs.MAINNET]: "eth-mainnet", - [CHAIN_IDs.OPTIMISM]: "opt-mainnet", - [CHAIN_IDs.POLYGON]: "polygon-mainnet", - [CHAIN_IDs.BASE]: "base-mainnet", - [CHAIN_IDs.LINEA]: "linea-mainnet", - [CHAIN_IDs.ARBITRUM]: "arb-mainnet", +type TokenBalance = { + address: string; + balance: BigNumber; }; -// TODO: delete this, move to serverless /batch-account-balance -const getAlchemyRpcUrl = (chainId: number) => { - const chain = CHAIN_TO_ALCHEMY[chainId]; - return `https://${chain}.g.alchemy.com/v2/${process.env.REACT_APP_ALCHEMY_KEY}`; +type ChainBalances = { + chainId: number; + balances: TokenBalance[]; +}; + +type UserTokenBalancesResponse = { + account: string; + balances: Array<{ + chainId: string; + balances: Array<{ + address: string; + balance: string; + }>; + }>; }; export default function useTokenBalancesOnChain() { const { account } = useConnection(); - const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs) - .sort((a, b) => a - b) - .filter((chainId) => !!CHAIN_TO_ALCHEMY[chainId]); - return useQueries({ - queries: chainIdsAvailable.map((chainId) => ({ - queryKey: ["tokenBalancesOnChain", chainId], - enabled: account !== undefined, - queryFn: async () => { - const rpcUrl = getAlchemyRpcUrl(chainId); + return useQuery({ + queryKey: ["userTokenBalances", account], + queryFn: async (): Promise => { + const response = await fetch( + `/api/user-token-balances?account=${account}` + ); - const balances = await fetch(rpcUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: 1, - method: "alchemy_getTokenBalances", - params: [account], - }), - }); + if (!response.ok) { + throw new Error( + `Failed to fetch token balances: ${response.statusText}` + ); + } - const data = await balances.json(); + const data: UserTokenBalancesResponse = await response.json(); - return { - chainId, - balances: ( - data.result.tokenBalances as { - contractAddress: string; - tokenBalance: string; - }[] - ) - .filter( - (t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0) - ) - .map((t) => ({ - address: t.contractAddress, - balance: BigNumber.from(t.tokenBalance), - })), - }; - }, - })), + // Convert string balances back to BigNumber and transform the response + return data.balances.map(({ chainId, balances }) => ({ + chainId: Number(chainId), + balances: balances.map(({ address, balance }) => ({ + address, + balance: BigNumber.from(balance ?? "0"), + })), + })); + }, + enabled: !!account, + refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes + staleTime: 3 * 60 * 1000, // Consider data stale after 3 minutes }); } From 77d3c74d6c4c5e0ed22264597a822ba319b86897 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 3 Oct 2025 12:40:54 +0200 Subject: [PATCH 034/122] refactor Signed-off-by: Gerhard Steenkamp --- src/hooks/useEnrichedCrosschainBalances.ts | 21 ++++--- src/hooks/useTokenBalancesOnChain.ts | 57 ------------------- src/hooks/useUserTokenBalances.ts | 21 +++++++ src/utils/serverless-api/mocked/index.ts | 2 + .../mocked/user-token-balances.mocked.ts | 48 ++++++++++++++++ src/utils/serverless-api/prod/index.ts | 2 + .../prod/user-token-balances.ts | 25 ++++++++ src/utils/serverless-api/types.ts | 21 +++++++ 8 files changed, 133 insertions(+), 64 deletions(-) delete mode 100644 src/hooks/useTokenBalancesOnChain.ts create mode 100644 src/hooks/useUserTokenBalances.ts create mode 100644 src/utils/serverless-api/mocked/user-token-balances.mocked.ts create mode 100644 src/utils/serverless-api/prod/user-token-balances.ts diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts index e50d78ae8..acc5c957b 100644 --- a/src/hooks/useEnrichedCrosschainBalances.ts +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -2,12 +2,12 @@ import { useMemo } from "react"; import useAvailableCrosschainRoutes, { LifiToken, } from "./useAvailableCrosschainRoutes"; -import useTokenBalancesOnChain from "./useTokenBalancesOnChain"; +import { useUserTokenBalances } from "./useUserTokenBalances"; import { compareAddressesSimple } from "utils"; import { BigNumber, utils } from "ethers"; export default function useEnrichedCrosschainBalances() { - const tokenBalances = useTokenBalancesOnChain(); + const tokenBalances = useUserTokenBalances(); const availableCrosschainRoutes = useAvailableCrosschainRoutes(); return useMemo(() => { @@ -18,8 +18,8 @@ export default function useEnrichedCrosschainBalances() { return chains.reduce( (acc, chainId) => { - const balancesForChain = tokenBalances.data?.find( - (t) => t.chainId === Number(chainId) + const balancesForChain = tokenBalances.data?.balances.find( + (t) => t.chainId === String(chainId) ); const tokens = availableCrosschainRoutes.data![Number(chainId)]; @@ -30,14 +30,21 @@ export default function useEnrichedCrosschainBalances() { ); return { ...t, - balance: balance?.balance ?? BigNumber.from(0), + balance: balance?.balance + ? BigNumber.from(balance.balance) + : BigNumber.from(0), balanceUsd: balance?.balance && t - ? Number(utils.formatUnits(balance.balance, t.decimals)) * - Number(t.priceUSD) + ? Number( + utils.formatUnits( + BigNumber.from(balance.balance), + t.decimals + ) + ) * Number(t.priceUSD) : 0, }; }) + // TODO: consider removing // Filter out tokens that don't have a logoURI .filter((t) => t.logoURI !== undefined); diff --git a/src/hooks/useTokenBalancesOnChain.ts b/src/hooks/useTokenBalancesOnChain.ts deleted file mode 100644 index 8d1fa24bc..000000000 --- a/src/hooks/useTokenBalancesOnChain.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useConnection } from "./useConnection"; -import { useQuery } from "@tanstack/react-query"; -import { BigNumber } from "ethers"; - -type TokenBalance = { - address: string; - balance: BigNumber; -}; - -type ChainBalances = { - chainId: number; - balances: TokenBalance[]; -}; - -type UserTokenBalancesResponse = { - account: string; - balances: Array<{ - chainId: string; - balances: Array<{ - address: string; - balance: string; - }>; - }>; -}; - -export default function useTokenBalancesOnChain() { - const { account } = useConnection(); - - return useQuery({ - queryKey: ["userTokenBalances", account], - queryFn: async (): Promise => { - const response = await fetch( - `/api/user-token-balances?account=${account}` - ); - - if (!response.ok) { - throw new Error( - `Failed to fetch token balances: ${response.statusText}` - ); - } - - const data: UserTokenBalancesResponse = await response.json(); - - // Convert string balances back to BigNumber and transform the response - return data.balances.map(({ chainId, balances }) => ({ - chainId: Number(chainId), - balances: balances.map(({ address, balance }) => ({ - address, - balance: BigNumber.from(balance ?? "0"), - })), - })); - }, - enabled: !!account, - refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes - staleTime: 3 * 60 * 1000, // Consider data stale after 3 minutes - }); -} diff --git a/src/hooks/useUserTokenBalances.ts b/src/hooks/useUserTokenBalances.ts new file mode 100644 index 000000000..e2fa9ae84 --- /dev/null +++ b/src/hooks/useUserTokenBalances.ts @@ -0,0 +1,21 @@ +import { useQuery } from "@tanstack/react-query"; +import { useConnection } from "./useConnection"; +import { UserTokenBalancesResponse } from "utils/serverless-api/types"; +import getApiEndpoint from "utils/serverless-api"; + +export function useUserTokenBalances() { + const { account } = useConnection(); + + return useQuery({ + queryKey: ["userTokenBalances", account], + queryFn: async (): Promise => { + if (!account) { + throw new Error("No account connected"); + } + return await getApiEndpoint().userTokenBalances(account); + }, + enabled: !!account, + refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes + staleTime: 3 * 60 * 1000, // Consider data stale after 3 minutes + }); +} diff --git a/src/utils/serverless-api/mocked/index.ts b/src/utils/serverless-api/mocked/index.ts index 6f7ef7496..5ff8edc78 100644 --- a/src/utils/serverless-api/mocked/index.ts +++ b/src/utils/serverless-api/mocked/index.ts @@ -13,6 +13,7 @@ import { poolsUserApiCall } from "./pools-user.mocked"; import { swapApprovalApiCall } from "../prod/swap-approval"; import { swapChainsApiCall } from "../prod/swap-chains"; import { swapTokensApiCall } from "../prod/swap-tokens"; +import { userTokenBalancesMockedApiCall } from "./user-token-balances.mocked"; export const mockedEndpoints: ServerlessAPIEndpoints = { coingecko: coingeckoMockedApiCall, @@ -33,4 +34,5 @@ export const mockedEndpoints: ServerlessAPIEndpoints = { swapApproval: swapApprovalApiCall, swapChains: swapChainsApiCall, swapTokens: swapTokensApiCall, + userTokenBalances: userTokenBalancesMockedApiCall, }; diff --git a/src/utils/serverless-api/mocked/user-token-balances.mocked.ts b/src/utils/serverless-api/mocked/user-token-balances.mocked.ts new file mode 100644 index 000000000..0d231fdc2 --- /dev/null +++ b/src/utils/serverless-api/mocked/user-token-balances.mocked.ts @@ -0,0 +1,48 @@ +import { UserTokenBalancesResponse } from "../types"; + +/** + * Mocked implementation of the user token balances API call + * @param account The Ethereum address to query token balances for + * @returns Mocked token balances data + */ +export async function userTokenBalancesMockedApiCall( + account: string +): Promise { + // Return mock data for testing/development + return { + account, + balances: [ + { + chainId: "1", + balances: [ + { + address: "0xA0b86a33E6441b8c4C8C0e4A0e4A0e4A0e4A0e4A0", + balance: "1000000000000000000", // 1 ETH + }, + { + address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + balance: "1000000000", // 1000 USDT + }, + ], + }, + { + chainId: "10", + balances: [ + { + address: "0x4200000000000000000000000000000000000006", + balance: "500000000000000000", // 0.5 WETH + }, + ], + }, + { + chainId: "137", + balances: [ + { + address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + balance: "2000000000", // 2000 USDC + }, + ], + }, + ], + }; +} diff --git a/src/utils/serverless-api/prod/index.ts b/src/utils/serverless-api/prod/index.ts index 58926c9aa..3dc618594 100644 --- a/src/utils/serverless-api/prod/index.ts +++ b/src/utils/serverless-api/prod/index.ts @@ -13,6 +13,7 @@ import { poolsUserApiCall } from "./pools-user"; import { swapApprovalApiCall } from "./swap-approval"; import { swapChainsApiCall } from "./swap-chains"; import { swapTokensApiCall } from "./swap-tokens"; +import { userTokenBalancesApiCall } from "./user-token-balances"; export const prodEndpoints: ServerlessAPIEndpoints = { coingecko: coingeckoApiCall, suggestedFees: suggestedFeesApiCall, @@ -32,4 +33,5 @@ export const prodEndpoints: ServerlessAPIEndpoints = { swapApproval: swapApprovalApiCall, swapChains: swapChainsApiCall, swapTokens: swapTokensApiCall, + userTokenBalances: userTokenBalancesApiCall, }; diff --git a/src/utils/serverless-api/prod/user-token-balances.ts b/src/utils/serverless-api/prod/user-token-balances.ts new file mode 100644 index 000000000..b9b2396da --- /dev/null +++ b/src/utils/serverless-api/prod/user-token-balances.ts @@ -0,0 +1,25 @@ +import axios from "axios"; +import { vercelApiBaseUrl } from "utils/constants"; +import { UserTokenBalancesResponse } from "../types"; + +export type UserTokenBalancesCall = typeof userTokenBalancesApiCall; + +/** + * Creates an HTTP call to the `user-token-balances` API endpoint + * @param account The Ethereum address to query token balances for + * @returns The result of the HTTP call to `api/user-token-balances` + */ +export async function userTokenBalancesApiCall( + account: string +): Promise { + const response = await axios.get( + `${vercelApiBaseUrl}/api/user-token-balances`, + { + params: { + account, + }, + } + ); + + return response.data; +} diff --git a/src/utils/serverless-api/types.ts b/src/utils/serverless-api/types.ts index 706376018..a5a66bd51 100644 --- a/src/utils/serverless-api/types.ts +++ b/src/utils/serverless-api/types.ts @@ -8,6 +8,7 @@ import { PoolsUserApiCall } from "./prod/pools-user"; import { SwapApprovalApiCall } from "./prod/swap-approval"; import { SwapChainsApiCall } from "./prod/swap-chains"; import { SwapTokensApiCall } from "./prod/swap-tokens"; +import { UserTokenBalancesCall } from "./prod/user-token-balances"; export type ServerlessAPIEndpoints = { coingecko: CoingeckoApiCall; @@ -28,6 +29,7 @@ export type ServerlessAPIEndpoints = { swapApproval: SwapApprovalApiCall; swapChains: SwapChainsApiCall; swapTokens: SwapTokensApiCall; + userTokenBalances: UserTokenBalancesCall; }; export type RewardsApiFunction = @@ -133,3 +135,22 @@ export type SwapToken = { logoUrl?: string; priceUsd: string | null; }; + +export interface UserTokenBalance { + address: string; + balance: string; +} + +export interface ChainBalances { + chainId: string; + balances: UserTokenBalance[]; +} + +export interface UserTokenBalancesResponse { + account: string; + balances: ChainBalances[]; +} + +export type UserTokenBalancesApiCall = ( + account: string +) => Promise; From fcaaf6d55b4561793c494e88076c8ed373c4cdcf Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 3 Oct 2025 13:03:56 +0200 Subject: [PATCH 035/122] fixup Signed-off-by: Gerhard Steenkamp --- api/user-token-balances.ts | 124 ++++++++++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 28 deletions(-) diff --git a/api/user-token-balances.ts b/api/user-token-balances.ts index 1661ece59..61b6b9881 100644 --- a/api/user-token-balances.ts +++ b/api/user-token-balances.ts @@ -21,43 +21,111 @@ const fetchTokenBalancesForChain = async ( chainId: number; balances: Array<{ address: string; balance: string }>; }> => { + const logger = getLogger(); const rpcUrl = getAlchemyRpcFromConfigJson(chainId); if (!rpcUrl) { - throw new Error(`No Alchemy RPC URL found for chain ${chainId}`); + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "No Alchemy RPC URL found for chain, returning empty balances", + chainId, + }); + return { + chainId, + balances: [], + }; } - const response = await fetch(rpcUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ + try { + const requestBody = { jsonrpc: "2.0", id: 1, method: "alchemy_getTokenBalances", params: [account], - }), - }); - - const data = await response.json(); - - const balances = ( - data.result.tokenBalances as { - contractAddress: string; - tokenBalance: string; - }[] - ) - .filter((t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0)) - .map((t) => ({ - address: t.contractAddress, - balance: BigNumber.from(t.tokenBalance).toString(), - })); - - return { - chainId, - balances, - }; + }; + + logger.debug({ + at: "fetchTokenBalancesForChain", + message: "Making request to Alchemy API", + chainId, + account, + rpcUrl, + }); + + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "HTTP error from Alchemy API, returning empty balances", + chainId, + status: response.status, + statusText: response.statusText, + }); + return { + chainId, + balances: [], + }; + } + + const data = await response.json(); + + logger.debug({ + at: "fetchTokenBalancesForChain", + message: "Received response from Alchemy API", + chainId, + responseData: data, + }); + + // Validate the response structure + if (!data || !data.result || !data.result.tokenBalances) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "Invalid response from Alchemy API, returning empty balances", + chainId, + responseData: data, + }); + return { + chainId, + balances: [], + }; + } + + const balances = ( + data.result.tokenBalances as { + contractAddress: string; + tokenBalance: string; + }[] + ) + .filter((t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0)) + .map((t) => ({ + address: t.contractAddress, + balance: BigNumber.from(t.tokenBalance).toString(), + })); + + return { + chainId, + balances, + }; + } catch (error) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: + "Error fetching token balances from Alchemy API, returning empty balances", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return { + chainId, + balances: [], + }; + } }; const handler = async ( From 8ef24530134880dcf1a72054774e63b23f0ba93d Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 3 Oct 2025 15:26:29 +0200 Subject: [PATCH 036/122] style switch button Signed-off-by: Gerhard Steenkamp --- src/assets/icons/arrows-cross.svg | 10 +++--- .../SwapAndBridge/components/InputForm.tsx | 34 ++++++++----------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/assets/icons/arrows-cross.svg b/src/assets/icons/arrows-cross.svg index 9e67a3f24..5df5e97f1 100644 --- a/src/assets/icons/arrows-cross.svg +++ b/src/assets/icons/arrows-cross.svg @@ -1,12 +1,12 @@ - + - - - - diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 66c3d8862..3d78bee1f 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -67,11 +67,9 @@ export const InputForm = ({ } otherToken={outputToken} /> - - - - - + + + Date: Mon, 6 Oct 2025 16:48:03 +0200 Subject: [PATCH 037/122] reset search on close Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/components/ChainTokenSelector/Modal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 6ceaabc9e..2e4142521 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -92,10 +92,10 @@ export default function ChainTokenSelectorModal({ // Reset mobile step when modal opens/closes useEffect(() => { - if (displayModal) { - setMobileStep("chain"); - setSelectedChain(null); - } + setMobileStep("chain"); + setChainSearch(""); + setTokenSearch(""); + setSelectedChain(null); }, [displayModal]); const displayedTokens = useMemo(() => { From 2b6fdfe2712ecae50b5a1d08069cceb55f859dfd Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 6 Oct 2025 18:27:35 +0200 Subject: [PATCH 038/122] show no results warning Signed-off-by: Gerhard Steenkamp --- src/assets/icons/search_results.svg | 15 +++++++ .../components/ChainTokenSelector/Modal.tsx | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/assets/icons/search_results.svg diff --git a/src/assets/icons/search_results.svg b/src/assets/icons/search_results.svg new file mode 100644 index 000000000..fec5b3cfe --- /dev/null +++ b/src/assets/icons/search_results.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 2e4142521..1eb09fe61 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -17,10 +17,12 @@ import { import { useMemo, useState, useEffect } from "react"; import { ReactComponent as CheckmarkCircle } from "assets/icons/checkmark-circle.svg"; import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; +import { ReactComponent as SearchResults } from "assets/icons/search_results.svg"; import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; import useEnrichedCrosschainBalances from "hooks/useEnrichedCrosschainBalances"; import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; +import { Text } from "components"; const popularChains = [ CHAIN_IDs.MAINNET, @@ -489,6 +491,9 @@ const MobileLayout = ({ {/* All Chains Section */} All Chains + {!displayedChains.all.length && chainSearch && ( + + )} {displayedChains.all.map(([chainId, chainData]) => ( {/* Your Tokens Section */} + {!displayedTokens.withBalance.length && tokenSearch && ( + + )} {displayedTokens.withBalance.length > 0 && ( <> Your Tokens @@ -623,6 +631,9 @@ const DesktopLayout = ({ {/* All Chains Section */} All Chains + {!displayedChains.all.length && chainSearch && ( + + )} {displayedChains.all.map(([chainId, chainData]) => ( {/* Your Tokens Section */} + {!displayedTokens.withBalance.length && tokenSearch && ( + + )} {displayedTokens.withBalance.length > 0 && ( <> Your Tokens @@ -673,6 +687,9 @@ const DesktopLayout = ({ {/* All Tokens Section */} All Tokens + {!displayedTokens.withoutBalance.length && tokenSearch && ( + + )} {displayedTokens.withoutBalance.map((token) => ( { ); }; +const EmptySearchResults = ({ + className, + query, +}: { + query: string; + className?: string; +}) => { + return ( + + + No results for {query} + + ); +}; + +const SearchResultsWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + color: white; + padding: 24px; +`; + const SearchBarStyled = styled(Searchbar)` flex-shrink: 0; `; From d155f1cd336192a1bc022a0dff9e295a249415a4 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 6 Oct 2025 19:09:24 +0200 Subject: [PATCH 039/122] popular tokens, refine search behaviour Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 207 ++++++++++-------- 1 file changed, 114 insertions(+), 93 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 1eb09fe61..fd09033e3 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -13,6 +13,7 @@ import { formatUSD, getChainInfo, parseUnits, + TOKEN_SYMBOLS_MAP, } from "utils"; import { useMemo, useState, useEffect } from "react"; import { ReactComponent as CheckmarkCircle } from "assets/icons/checkmark-circle.svg"; @@ -27,9 +28,17 @@ import { Text } from "components"; const popularChains = [ CHAIN_IDs.MAINNET, CHAIN_IDs.BASE, - CHAIN_IDs.OPTIMISM, + CHAIN_IDs.UNICHAIN, CHAIN_IDs.ARBITRUM, - CHAIN_IDs.POLYGON, + CHAIN_IDs.SOLANA, +]; + +const popularTokens = [ + TOKEN_SYMBOLS_MAP.USDC.symbol, + TOKEN_SYMBOLS_MAP.USDT.symbol, + TOKEN_SYMBOLS_MAP.ETH.symbol, + TOKEN_SYMBOLS_MAP.WETH.symbol, + TOKEN_SYMBOLS_MAP.WBTC.symbol, ]; // Type definitions for better typing @@ -51,8 +60,8 @@ type EnrichedToken = LifiToken & { }; type DisplayedTokens = { - withBalance: EnrichedToken[]; - withoutBalance: EnrichedToken[]; + popular: EnrichedToken[]; + all: EnrichedToken[]; }; type Props = { @@ -86,7 +95,9 @@ export default function ChainTokenSelectorModal({ : undefined ); - const [selectedChain, setSelectedChain] = useState(null); + const [selectedChain, setSelectedChain] = useState( + popularChains[0] + ); const [mobileStep, setMobileStep] = useState<"chain" | "token">("chain"); const [tokenSearch, setTokenSearch] = useState(""); @@ -97,7 +108,7 @@ export default function ChainTokenSelectorModal({ setMobileStep("chain"); setChainSearch(""); setTokenSearch(""); - setSelectedChain(null); + setSelectedChain(popularChains[0]); }, [displayModal]); const displayedTokens = useMemo(() => { @@ -152,48 +163,53 @@ export default function ChainTokenSelectorModal({ ); }); - // Separate tokens with balance from tokens without balance - const tokensWithBalance = filteredTokens.filter( - (token) => token.balance.gt(0) && token.balanceUsd > 0.01 + // Separate popular tokens from all tokens + const popularTokensList = filteredTokens.filter((token) => + popularTokens.includes(token.symbol) ); - const tokensWithoutBalance = filteredTokens.filter( - (token) => token.balance.eq(0) || token.balanceUsd <= 0.01 + const allTokensList = filteredTokens.filter( + (token) => !popularTokens.includes(token.symbol) ); - // Sort tokens with balance by balanceUsd (highest first), then alphabetically - const sortedTokensWithBalance = tokensWithBalance.sort((a, b) => { - // First, sort by disabled status - disabled tokens go to bottom - const aDisabled = a.isReachable === false; - const bDisabled = b.isReachable === false; + // Sort function that prioritizes tokens with balance, then by balance amount, then alphabetically + const sortTokens = (tokens: EnrichedToken[]) => { + return tokens.sort((a, b) => { + // First, sort by disabled status - disabled tokens go to bottom + const aDisabled = a.isReachable === false; + const bDisabled = b.isReachable === false; - if (aDisabled !== bDisabled) { - return aDisabled ? 1 : -1; - } + if (aDisabled !== bDisabled) { + return aDisabled ? 1 : -1; + } - // Then sort by balance - if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { - return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); - } - return b.balanceUsd - a.balanceUsd; - }); + // Then sort by balance - tokens with balance go to top + const aHasBalance = a.balance.gt(0) && a.balanceUsd > 0.01; + const bHasBalance = b.balance.gt(0) && b.balanceUsd > 0.01; + + if (aHasBalance !== bHasBalance) { + return aHasBalance ? -1 : 1; + } - // Sort tokens without balance alphabetically, with disabled tokens at bottom - const sortedTokensWithoutBalance = tokensWithoutBalance.sort((a, b) => { - // First, sort by disabled status - disabled tokens go to bottom - const aDisabled = a.isReachable === false; - const bDisabled = b.isReachable === false; + // If both have balance or both don't have balance, sort by balance amount + if (aHasBalance && bHasBalance) { + if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { + return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); + } + return b.balanceUsd - a.balanceUsd; + } - if (aDisabled !== bDisabled) { - return aDisabled ? 1 : -1; - } + // If neither has balance, sort alphabetically + return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); + }); + }; - // Then sort alphabetically - return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); - }); + // Sort both sections + const sortedPopularTokens = sortTokens(popularTokensList); + const sortedAllTokens = sortTokens(allTokensList); return { - withBalance: sortedTokensWithBalance.slice(0, 50), // Limit to 50 tokens with balance - withoutBalance: sortedTokensWithoutBalance.slice(0, 50), // Limit to 50 tokens without balance + popular: sortedPopularTokens.slice(0, 50), // Limit to 50 popular tokens + all: sortedAllTokens.slice(0, 50), // Limit to 50 all tokens }; }, [selectedChain, balances, tokenSearch, crossChainRoutes.data]); @@ -514,14 +530,11 @@ const MobileLayout = ({ setSearch={setTokenSearch} /> - {/* Your Tokens Section */} - {!displayedTokens.withBalance.length && tokenSearch && ( - - )} - {displayedTokens.withBalance.length > 0 && ( + {/* Popular Tokens Section */} + {displayedTokens.popular.length > 0 && ( <> - Your Tokens - {displayedTokens.withBalance.map((token) => ( + Popular Tokens + {displayedTokens.popular.map((token) => ( All Tokens - {displayedTokens.withoutBalance.map((token) => ( - { - onTokenSelect({ - chainId: token.chainId, - symbolUri: token.logoURI, - symbol: token.symbol, - address: token.address, - balance: token.balance, - priceUsd: parseUnits(token.priceUSD, 18), - decimals: token.decimals, - }); - onModalClose(); - }} - /> - ))} + {!displayedTokens.all.length && tokenSearch && ( + + )} + {displayedTokens.all.length > 0 && ( + <> + All Tokens + {displayedTokens.all.map((token) => ( + { + onTokenSelect({ + chainId: token.chainId, + symbolUri: token.logoURI, + symbol: token.symbol, + address: token.address, + balance: token.balance, + priceUsd: parseUnits(token.priceUSD, 18), + decimals: token.decimals, + }); + onModalClose(); + }} + /> + ))} + + )} )} @@ -656,14 +676,11 @@ const DesktopLayout = ({ setSearch={setTokenSearch} /> - {/* Your Tokens Section */} - {!displayedTokens.withBalance.length && tokenSearch && ( - - )} - {displayedTokens.withBalance.length > 0 && ( + {/* Popular Tokens Section */} + {displayedTokens.popular.length > 0 && ( <> - Your Tokens - {displayedTokens.withBalance.map((token) => ( + Popular Tokens + {displayedTokens.popular.map((token) => ( All Tokens - {!displayedTokens.withoutBalance.length && tokenSearch && ( + {!displayedTokens.all.length && tokenSearch && ( )} - {displayedTokens.withoutBalance.map((token) => ( - { - onTokenSelect({ - chainId: token.chainId, - symbolUri: token.logoURI, - symbol: token.symbol, - address: token.address, - balance: token.balance, - priceUsd: parseUnits(token.priceUSD, 18), - decimals: token.decimals, - }); - onModalClose(); - }} - /> - ))} + {displayedTokens.all.length > 0 && ( + <> + All Tokens + {displayedTokens.all.map((token) => ( + { + onTokenSelect({ + chainId: token.chainId, + symbolUri: token.logoURI, + symbol: token.symbol, + address: token.address, + balance: token.balance, + priceUsd: parseUnits(token.priceUSD, 18), + decimals: token.decimals, + }); + onModalClose(); + }} + /> + ))} + + )} From 9b4673bd6b55e1730bc1af347779aea115d58e04 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 7 Oct 2025 12:08:36 +0200 Subject: [PATCH 040/122] update packages Signed-off-by: Gerhard Steenkamp --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 670b1abde..e4922d998 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.80", "@across-protocol/contracts": "^4.1.9", "@across-protocol/contracts-v4.1.1": "npm:@across-protocol/contracts@4.1.1", - "@across-protocol/sdk": "^4.3.67", + "@across-protocol/sdk": "^4.3.70", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@coral-xyz/borsh": "^0.30.1", diff --git a/yarn.lock b/yarn.lock index 2b99a434e..5b1bfc946 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,7 +20,7 @@ version "3.1.80" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.80.tgz#a1515f9c8ca19a5a7c2e709da08c1d1f3ac01b28" integrity sha512-/MtvKygLNoxTFAIOU6FmR4TeEeueL1hyoWit9BtL0RVyXZ1h3zvNr58TYxcoXIFF8Vb+yizyACX32b+bdk7fGg== - + "@across-protocol/contracts-v4.1.1@npm:@across-protocol/contracts@4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-4.1.1.tgz#91c8e0fc867911a17f21b2d79d586f95417cb912" @@ -93,10 +93,10 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^4.3.67": - version "4.3.67" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.3.67.tgz#a3b39ecd3334d16ea27bb5340baf391883a46bd6" - integrity sha512-XTDbi7Qm7mbxVAhTns8fZujsEJ2gzT0HQwXbKofbsJKwGXSW9z8MqsjghjagvvB4HqanY7EHrW0cWfSvu73hjA== +"@across-protocol/sdk@^4.3.70": + version "4.3.70" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.3.70.tgz#af47935074260410b5c3731ce005c445b3d50693" + integrity sha512-4vURM69nrUDIyh2E9dVOhJ9Fn8OS1hh5nT09Uj4BB0d+V7raBnIf9LPbCLLAyu3R43TPJkfrwX/VEERbMqicSg== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.78" From 3dd29c4b2a87f3d074eeb45f79df56e60361234d Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 7 Oct 2025 12:32:58 +0200 Subject: [PATCH 041/122] add hype logo Signed-off-by: Gerhard Steenkamp --- src/assets/token-logos/hype.svg | 4 ++++ src/constants/tokens.ts | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 src/assets/token-logos/hype.svg diff --git a/src/assets/token-logos/hype.svg b/src/assets/token-logos/hype.svg new file mode 100644 index 000000000..35f596fe8 --- /dev/null +++ b/src/assets/token-logos/hype.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 860e0d939..5c0df9a47 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -23,6 +23,7 @@ import unknownLogo from "assets/icons/question-circle.svg"; import cakeLogo from "assets/token-logos/cake.svg"; import bnbLogo from "assets/token-logos/bnb.svg"; import vlrLogo from "assets/token-logos/vlr.svg"; +import hypeLogo from "assets/token-logos/hype.svg"; import { BRIDGED_USDC_SYMBOLS } from "../utils/sdk"; @@ -114,4 +115,5 @@ export const orderedTokenLogos = { BNB: bnbLogo, WBNB: bnbLogo, VLR: vlrLogo, + HYPE: hypeLogo, } as const satisfies Partial>; From a4ae00f730eb13c157743bee8f7112d9c077f93f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 7 Oct 2025 16:52:54 +0200 Subject: [PATCH 042/122] disable and debounce inputs Signed-off-by: Gerhard Steenkamp --- package.json | 1 + src/hooks/useEnrichedCrosschainBalances.ts | 6 +++--- .../components/ChainTokenSelector/Modal.tsx | 2 +- src/views/SwapAndBridge/components/InputForm.tsx | 11 ++++++++++- src/views/SwapAndBridge/hooks/useSwapAndBridge.ts | 5 ++++- yarn.lock | 5 +++++ 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e4922d998..768d1ac67 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@tanstack/react-query": "v5", "@tanstack/react-query-devtools": "v5", "@types/bn.js": "^5.1.6", + "@uidotdev/usehooks": "^2.4.1", "@uniswap/sdk-core": "^7.7.2", "@uniswap/smart-order-router": "^4.22.16", "@uniswap/v3-sdk": "^3.25.2", diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts index acc5c957b..a94451d50 100644 --- a/src/hooks/useEnrichedCrosschainBalances.ts +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -6,7 +6,7 @@ import { useUserTokenBalances } from "./useUserTokenBalances"; import { compareAddressesSimple } from "utils"; import { BigNumber, utils } from "ethers"; -export default function useEnrichedCrosschainBalances() { +export function useEnrichedCrosschainBalances() { const tokenBalances = useUserTokenBalances(); const availableCrosschainRoutes = useAvailableCrosschainRoutes(); @@ -49,13 +49,13 @@ export default function useEnrichedCrosschainBalances() { .filter((t) => t.logoURI !== undefined); // Sort high to low balanceUsd - const orderedEnrichedTokens = enrichedTokens.sort( + const sortedByBalance = enrichedTokens.sort( (a, b) => b.balanceUsd - a.balanceUsd ); return { ...acc, - [Number(chainId)]: orderedEnrichedTokens, + [Number(chainId)]: sortedByBalance, }; }, {} as Record< diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index fd09033e3..c6c065ae6 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -20,7 +20,7 @@ import { ReactComponent as CheckmarkCircle } from "assets/icons/checkmark-circle import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; import { ReactComponent as SearchResults } from "assets/icons/search_results.svg"; import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; -import useEnrichedCrosschainBalances from "hooks/useEnrichedCrosschainBalances"; +import { useEnrichedCrosschainBalances } from "hooks/useEnrichedCrosschainBalances"; import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; import { Text } from "components"; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 3d78bee1f..4134260ff 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -66,6 +66,7 @@ export const InputForm = ({ validationError === AmountInputError.INSUFFICIENT_BALANCE } otherToken={outputToken} + disabled={!outputToken || !outputToken} /> @@ -82,6 +83,7 @@ export const InputForm = ({ shouldUpdate={isAmountOrigin} isUpdateLoading={isQuoteLoading} otherToken={inputToken} + disabled={!outputToken || !outputToken} /> ); @@ -97,6 +99,7 @@ const TokenInput = ({ isUpdateLoading, insufficientInputBalance = false, otherToken, + disabled, }: { setToken: (token: EnrichedTokenSelect) => void; token: EnrichedTokenSelect | null; @@ -106,6 +109,7 @@ const TokenInput = ({ shouldUpdate: boolean; isUpdateLoading: boolean; insufficientInputBalance?: boolean; + disabled?: boolean; otherToken?: EnrichedTokenSelect | null; }) => { const [amountString, setAmountString] = useState(""); @@ -171,6 +175,11 @@ const TokenInput = ({ } }, [amountString, token]); + const inputDisabled = (() => { + if (disabled) return true; + return Boolean(shouldUpdate && isUpdateLoading); + })(); + return ( @@ -187,7 +196,7 @@ const TokenInput = ({ setAmountString(value); } }} - disabled={shouldUpdate && isUpdateLoading} + disabled={inputDisabled} error={insufficientInputBalance} /> diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 436824a53..e9a6c4984 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -10,6 +10,7 @@ import { } from "./useSwapApprovalAction"; import { useValidateSwapAndBridge } from "./useValidateSwapAndBridge"; import { BridgeButtonState } from "../components/ConfirmationButton"; +import { useDebounce } from "@uidotdev/usehooks"; export type UseSwapAndBridgeReturn = { inputToken: EnrichedTokenSelect | null; @@ -55,6 +56,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const [amount, setAmount] = useState(null); const [isAmountOrigin, setIsAmountOrigin] = useState(true); + const debouncedAmount = useDebounce(amount, 300); + const quickSwap = useCallback(() => { setInputToken((prevInput) => { const prevOut = outputToken; @@ -71,7 +74,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { } = useSwapQuote({ origin: inputToken ? inputToken : null, destination: outputToken ? outputToken : null, - amount: amount, + amount: debouncedAmount, isInputAmount: isAmountOrigin, }); diff --git a/yarn.lock b/yarn.lock index 5b1bfc946..c132cd54e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10369,6 +10369,11 @@ "@typescript-eslint/types" "7.13.1" eslint-visitor-keys "^3.4.3" +"@uidotdev/usehooks@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@uidotdev/usehooks/-/usehooks-2.4.1.tgz#4b733eaeae09a7be143c6c9ca158b56cc1ea75bf" + integrity sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg== + "@uma/common@^2.17.0", "@uma/common@^2.37.3": version "2.37.3" resolved "https://registry.yarnpkg.com/@uma/common/-/common-2.37.3.tgz#0d7fda1227e3a05563544bb36f418a790c81129d" From e62fd3a6498c4d882ba54f08b749c1029d00ba1b Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 7 Oct 2025 16:53:08 +0200 Subject: [PATCH 043/122] add sol logo Signed-off-by: Gerhard Steenkamp --- src/assets/token-logos/sol.svg | 19 +++++++++++++++++++ src/constants/tokens.ts | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 src/assets/token-logos/sol.svg diff --git a/src/assets/token-logos/sol.svg b/src/assets/token-logos/sol.svg new file mode 100644 index 000000000..a1a56cbdb --- /dev/null +++ b/src/assets/token-logos/sol.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 5c0df9a47..255fa0b91 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -24,6 +24,7 @@ import cakeLogo from "assets/token-logos/cake.svg"; import bnbLogo from "assets/token-logos/bnb.svg"; import vlrLogo from "assets/token-logos/vlr.svg"; import hypeLogo from "assets/token-logos/hype.svg"; +import solLogo from "assets/token-logos/sol.svg"; import { BRIDGED_USDC_SYMBOLS } from "../utils/sdk"; @@ -116,4 +117,5 @@ export const orderedTokenLogos = { WBNB: bnbLogo, VLR: vlrLogo, HYPE: hypeLogo, + SOL: solLogo, } as const satisfies Partial>; From f1cb30a195914a1e29214417213cfbf19648c6de Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 8 Oct 2025 15:52:55 +0200 Subject: [PATCH 044/122] separate chains Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 91 ++++++++----------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index c6c065ae6..c63f37d57 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -8,6 +8,7 @@ import useAvailableCrosschainRoutes, { } from "hooks/useAvailableCrosschainRoutes"; import { CHAIN_IDs, + ChainInfo, COLORS, formatUnitsWithMaxFractions, formatUSD, @@ -41,15 +42,13 @@ const popularTokens = [ TOKEN_SYMBOLS_MAP.WBTC.symbol, ]; -// Type definitions for better typing -type ChainData = { - tokens: LifiToken[]; +type ChainData = ChainInfo & { isDisabled: boolean; }; type DisplayedChains = { - popular: [string, ChainData][]; - all: [string, ChainData][]; + popular: ChainData[]; + all: ChainData[]; }; type EnrichedToken = LifiToken & { @@ -214,63 +213,51 @@ export default function ChainTokenSelectorModal({ }, [selectedChain, balances, tokenSearch, crossChainRoutes.data]); const displayedChains = useMemo(() => { - const chainsWithDisabledState: [string, ChainData][] = Object.entries( - crossChainRoutes.data || {} - ) - .filter(([chainId]) => { - // why ar we filtering out Boba? - if ([288].includes(Number(chainId))) { + const chainsWithDisabledState = Object.keys(crossChainRoutes.data || {}) + .map((chainId) => getChainInfo(Number(chainId))) + .filter((chainInfo) => { + // TODO: check why we are filtering out Boba? + if (chainInfo.chainId === 288) { return false; } const keywords = [ - String(chainId), - getChainInfo(Number(chainId)).name.toLowerCase().replace(" ", ""), + String(chainInfo.chainId), + chainInfo.name.toLowerCase().replace(" ", ""), ]; return keywords.some((keyword) => keyword.toLowerCase().includes(chainSearch.toLowerCase()) ); }) - .map(([chainId, tokens]) => { - return [ - chainId, - { - tokens, - isDisabled: otherToken && Number(chainId) === otherToken.chainId, // same chain can't be both input and output - }, - ] as [string, ChainData]; + .map((chainInfo) => { + return { + ...chainInfo, + isDisabled: + otherToken && Number(chainInfo.chainId) === otherToken.chainId, // same chain can't be both input and output + }; }); // Separate popular chains from all chains - const popularChainsData: typeof chainsWithDisabledState = []; - - chainsWithDisabledState.forEach((entry) => { - const [chainId] = entry; - if (popularChains.includes(Number(chainId))) { - popularChainsData.push(entry); - } - }); - - // Sort popular chains by the order they appear in popularChains array - popularChainsData.sort(([chainIdA], [chainIdB]) => { - const indexA = popularChains.indexOf(Number(chainIdA)); - const indexB = popularChains.indexOf(Number(chainIdB)); - return indexA - indexB; - }); + const popularChainsData = chainsWithDisabledState + .filter((chain) => popularChains.includes(chain.chainId)) + .sort((chainA, chainB) => { + const indexA = popularChains.indexOf(Number(chainA.chainId)); + const indexB = popularChains.indexOf(Number(chainB.chainId)); + return indexA - indexB; + }); - // Combine all chains for the "All Chains" section (sorted alphabetically) - const allChainsData = [...chainsWithDisabledState].sort( - ([chainIdA], [chainIdB]) => { - const chainInfoA = getChainInfo(Number(chainIdA)); - const chainInfoB = getChainInfo(Number(chainIdB)); + const allChainsData = chainsWithDisabledState + .filter((chain) => !popularChains.includes(chain.chainId)) + .sort((chainA, chainB) => { + const chainInfoA = getChainInfo(Number(chainA.chainId)); + const chainInfoB = getChainInfo(Number(chainB.chainId)); return chainInfoA.name.localeCompare(chainInfoB.name); - } - ); + }); return { popular: popularChainsData, all: allChainsData, - }; + } as DisplayedChains; }, [chainSearch, crossChainRoutes.data, otherToken]); return isMobile ? ( @@ -493,12 +480,12 @@ const MobileLayout = ({ {displayedChains.popular.length > 0 && ( <> Popular Chains - {displayedChains.popular.map(([chainId, chainData]) => ( + {displayedChains.popular.map(({ chainId, isDisabled }) => ( onChainSelect(Number(chainId))} /> ))} @@ -510,12 +497,12 @@ const MobileLayout = ({ {!displayedChains.all.length && chainSearch && ( )} - {displayedChains.all.map(([chainId, chainData]) => ( + {displayedChains.all.map(({ chainId, isDisabled }) => ( onChainSelect(Number(chainId))} /> ))} @@ -637,12 +624,12 @@ const DesktopLayout = ({ {displayedChains.popular.length > 0 && ( <> Popular Chains - {displayedChains.popular.map(([chainId, chainData]) => ( + {displayedChains.popular.map(({ chainId, isDisabled }) => ( onChainSelect(Number(chainId))} /> ))} @@ -654,12 +641,12 @@ const DesktopLayout = ({ {!displayedChains.all.length && chainSearch && ( )} - {displayedChains.all.map(([chainId, chainData]) => ( + {displayedChains.all.map(({ chainId, isDisabled }) => ( onChainSelect(Number(chainId))} /> ))} From f08e50277d17417b799ec08fdac73f16be7393db Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 8 Oct 2025 20:18:01 +0200 Subject: [PATCH 045/122] add footer Signed-off-by: Gerhard Steenkamp --- package.json | 1 + src/components/Modal/Modal.tsx | 3 + .../components/ChainTokenSelector/Modal.tsx | 68 ++++++++++++++++--- yarn.lock | 5 ++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 768d1ac67..02b36a3e9 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react": "v18", "react-dom": "v18", "react-feather": "^2.0.9", + "react-hotkeys-hook": "^5.1.0", "react-pro-sidebar": "^1.1.0", "react-router-dom": "v5", "react-tooltip": "^5.18.0", diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 6b0a31964..066a49481 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -38,6 +38,7 @@ export type ModalProps = { bottomYOffset?: number; children?: React.ReactNode; + footer?: React.ReactNode; titleBorder?: boolean; className?: string; }; @@ -51,6 +52,7 @@ const Modal = ({ exitModalHandler: externalModalExitHandler, disableExitOverride, children, + footer, verticalLocation: _verticalLocation, topYOffset, bottomYOffset, @@ -170,6 +172,7 @@ const Modal = ({ {titleBorder && } {children} + {footer} , container.current diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index c63f37d57..c2d0fe7f9 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -67,7 +67,6 @@ type Props = { onSelect: (token: EnrichedTokenSelect) => void; isOriginToken: boolean; otherToken?: EnrichedTokenSelect | null; // The currently selected token on the other side - displayModal: boolean; setDisplayModal: (displayModal: boolean) => void; }; @@ -719,6 +718,20 @@ const DesktopLayout = ({ )} + + + Use shortcuts for fast navigation + + + Next itemtab + + + Selectreturn + + + Closeesc + + ); }; @@ -960,7 +973,6 @@ const BackButton = styled.button` const Title = styled.div` overflow: hidden; color: var(--Base-bright-gray, #e0f3ff); - font-family: Barlow; font-size: 20px; font-style: normal; @@ -975,7 +987,7 @@ const ListWrapper = styled.div` overflow-y: auto; flex: 1; /* Take up remaining space in parent */ min-height: 0; /* Allow flex child to shrink below content size */ - padding-bottom: 32px; /* Add padding to prevent clipping of last item */ + padding-bottom: calc(32px + 56px * 2); &::-webkit-scrollbar { width: 8px; @@ -998,10 +1010,10 @@ const ListWrapper = styled.div` scrollbar-color: rgba(255, 255, 255, 0.1) transparent; `; -const EntryItem = styled.div<{ isSelected: boolean; isDisabled?: boolean }>` +const EntryItem = styled.button<{ isSelected: boolean; isDisabled?: boolean }>` display: flex; flex-direction: row; - justify-content: space-between; + justify-content: flex-start; width: 100%; flex-shrink: 0; @@ -1030,7 +1042,6 @@ const EntryItem = styled.div<{ isSelected: boolean; isDisabled?: boolean }>` const ChainItemImage = styled.img` width: 32px; height: 32px; - flex-shrink: 0; `; @@ -1044,13 +1055,12 @@ const ChainItemName = styled.div` font-style: normal; font-weight: 400; line-height: 130%; /* 20.8px */ - - width: 100%; `; const ChainItemCheckmark = styled(CheckmarkCircle)` width: 20px; height: 20px; + margin-left: auto; `; const TokenNameSymbolWrapper = styled.div` @@ -1103,6 +1113,7 @@ const TokenBalanceStack = styled.div` align-items: flex-end; gap: 4px; + margin-left: auto; `; const TokenBalance = styled.div` @@ -1135,3 +1146,44 @@ const SectionHeader = styled.div` padding: 8px 0px 4px 0px; letter-spacing: 0.5px; `; + +const KeyboardShortcutsTitle = styled.span` + margin-right: auto; +`; + +const KeyboardShortcutsSection = styled.div` + position: absolute; + bottom: 0; + left: 0; + height: 56px; + width: 100%; + border-top: 1px solid #34353b; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 16px; + padding-inline: 24px; + margin-top: auto; + color: #e0f3ff7f; + background: #202024; +`; + +const KeyboardLegendItem = styled.div` + height: 100%; + display: flex; + align-items: center; + gap: 6px; +`; + +const Key = styled.div` + height: 24px; + border: 1px solid #34353b; + border-radius: 4px; + padding-inline: 8px; + font-size: 14px; + font-weight: 400; + display: flex; + align-items: center; + flex-shrink: 0; + color: rgba(224, 243, 255, 0.4); +`; diff --git a/yarn.lock b/yarn.lock index c132cd54e..768f2098e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23066,6 +23066,11 @@ react-focus-lock@^2.5.2: use-callback-ref "^1.2.5" use-sidecar "^1.0.5" +react-hotkeys-hook@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-5.1.0.tgz#794eaa4428363b2312e26b19d49301fb05052e24" + integrity sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g== + react-inspector@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d" From 19a04b46b86aecc95b245d349b32aa0ddb859900 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 8 Oct 2025 21:04:28 +0200 Subject: [PATCH 046/122] add hotkey Signed-off-by: Gerhard Steenkamp --- src/components/Modal/Modal.styles.ts | 37 ++++++++++++++--- src/components/Modal/Modal.tsx | 41 +++++++++++-------- .../components/ChainTokenSelector/Modal.tsx | 14 +++++-- .../ChainTokenSelector/Searchbar.tsx | 4 ++ 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 94b39272c..8992cbbe0 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -95,21 +95,19 @@ const minimumMargin = 32; export const ModalContentWrapper = styled.div` --padding-modal-content: ${({ padding }) => padding === "normal" ? "24px" : "16px"}; - max-height: ${({ height, topYOffset }) => + height: ${({ height, topYOffset }) => height ? `min(calc(100svh - ${minimumMargin * 2}px - ${topYOffset ?? 0}px), ${height}px)` : "calc(100svh - 64px)"}; max-width: ${({ width }) => width ?? 800}px; - height: fit-content; width: calc(100% - 32px); display: flex; flex-direction: column; align-items: flex-start; - gap: var(--padding-modal-content); margin: 0 auto; - padding: var(--padding-modal-content); + padding: 0; margin-top: ${({ topYOffset }) => topYOffset ?? 0}px; margin-bottom: ${({ bottomYOffset }) => bottomYOffset ?? 0}px; @@ -131,7 +129,7 @@ export const TitleAndExitWrapper = styled.div` align-items: center; gap: 12px; - padding: 0px; + padding-bottom: var(--padding-modal-content); width: 100%; `; @@ -174,3 +172,32 @@ export const CloseButton = styled.button` outline: 2px solid ${COLORS.aqua}; } `; + +export const ModalHeader = styled.div` + position: sticky; + top: 0; + z-index: 10; + background: #202024; + padding: var(--padding-modal-content); + padding-bottom: 0; + flex-shrink: 0; + width: 100%; +`; + +export const ModalContent = styled.div` + flex: 1; + overflow: hidden; + padding: var(--padding-modal-content); + min-height: 0; + width: 100%; +`; + +export const ModalFooter = styled.div` + position: sticky; + bottom: 0; + z-index: 10; + background: #202024; + padding: var(--padding-modal-content); + padding-top: 0; + flex-shrink: 0; +`; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 066a49481..5f49f71c1 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -6,6 +6,9 @@ import { CloseButton, ElementRowDivider, ModalContentWrapper, + ModalHeader, + ModalContent, + ModalFooter, Title, TitleAndExitWrapper, Wrapper, @@ -156,23 +159,27 @@ const Modal = ({ padding={padding ?? "normal"} className={className} > - - {typeof title === "string" ? ( - {title} - ) : ( -
{title}
- )} - - externalModalExitHandler()} - > - - -
- {titleBorder && } - {children} - {footer} + + + {typeof title === "string" ? ( + {title} + ) : ( +
{title}
+ )} + + externalModalExitHandler()} + > + + +
+ {titleBorder && } +
+ + {children} + + {footer && {footer}} , container.current diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index c2d0fe7f9..37bb17664 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -25,6 +25,7 @@ import { useEnrichedCrosschainBalances } from "hooks/useEnrichedCrosschainBalanc import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; import { Text } from "components"; +import { useHotkeys } from "react-hotkeys-hook"; const popularChains = [ CHAIN_IDs.MAINNET, @@ -601,6 +602,7 @@ const DesktopLayout = ({ onTokenSelect: (token: EnrichedTokenSelect) => void; onModalClose: () => void; }) => { + useHotkeys("esc", () => onModalClose()); return ( @@ -985,9 +987,9 @@ const ListWrapper = styled.div` flex-direction: column; gap: 4px; overflow-y: auto; - flex: 1; /* Take up remaining space in parent */ - min-height: 0; /* Allow flex child to shrink below content size */ - padding-bottom: calc(32px + 56px * 2); + flex: 1; + min-height: 0; + padding-bottom: calc(56px * 2 + 23px); // account for footer &::-webkit-scrollbar { width: 8px; @@ -1025,6 +1027,7 @@ const EntryItem = styled.button<{ isSelected: boolean; isDisabled?: boolean }>` gap: 8px; border-radius: 8px; + border: 2px solid transparent; background: ${({ isSelected }) => isSelected ? COLORS["aqua-5"] : "transparent"}; @@ -1037,6 +1040,11 @@ const EntryItem = styled.button<{ isSelected: boolean; isDisabled?: boolean }>` return isSelected ? COLORS["aqua-15"] : COLORS["grey-400-15"]; }}; } + + :focus-visible { + outline: none; + border-color: ${COLORS.aqua}; + } `; const ChainItemImage = styled.img` diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx index 333a0af50..b387971d5 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -43,6 +43,7 @@ const Wrapper = styled.div` gap: 8px; flex-direction: row; justify-content: space-between; + border: 2px solid transparent; border-radius: 8px; background: transparent; @@ -53,6 +54,9 @@ const Wrapper = styled.div` &:active { background: rgba(224, 243, 255, 0.05); } + &:has(:focus-visible) { + border-color: ${COLORS.aqua}; + } &:focus-within { background: rgba(224, 243, 255, 0.1); From 4e88d9952cee57d164bee74e74c42d940252d990 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 9 Oct 2025 12:58:18 +0200 Subject: [PATCH 047/122] use swap fees Signed-off-by: Gerhard Steenkamp --- src/utils/format.ts | 4 + .../serverless-api/prod/swap-approval.ts | 95 ++++++++++++++ .../components/ConfirmationButton.tsx | 119 +++++------------- 3 files changed, 127 insertions(+), 91 deletions(-) diff --git a/src/utils/format.ts b/src/utils/format.ts index 626a1d06a..e4d29c475 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -300,6 +300,10 @@ export function formatUSD(value: BigNumberish): string { return numeral(Number(formattedString).toFixed(2)).format("0,0.00"); } +export function formatUSDString(value: string, decimals = 2): string { + return `$${numeral(Number(value).toFixed(decimals)).format("0,0.00")}`; +} + /** * A fault-tolerant version of `parseUnits` that will attempt to parse * a string while being mindful of truncation. diff --git a/src/utils/serverless-api/prod/swap-approval.ts b/src/utils/serverless-api/prod/swap-approval.ts index 17714424f..49cd441b1 100644 --- a/src/utils/serverless-api/prod/swap-approval.ts +++ b/src/utils/serverless-api/prod/swap-approval.ts @@ -99,6 +99,101 @@ export type SwapApprovalApiResponse = { maxFeePerGas?: string; maxPriorityFeePerGas?: string; }; + fees: { + total: { + amount: string; + amountUsd: string; + pct: string; + token: { + decimals: number; + symbol: string; + address: string; + name: string; + chainId: number; + }; + }; + originGas: { + amount: string; + amountUsd: string; + token: { + chainId: number; + address: string; + decimals: number; + symbol: string; + }; + }; + destinationGas: { + amount: string; + amountUsd: string; + pct: string; + token: { + chainId: number; + address: string; + decimals: number; + symbol: string; + }; + }; + relayerCapital: { + amount: string; + amountUsd: string; + pct: string; + token: { + decimals: number; + symbol: string; + address: string; + name: string; + chainId: number; + }; + }; + lpFee: { + amount: string; + amountUsd: string; + pct: string; + token: { + decimals: number; + symbol: string; + address: string; + name: string; + chainId: number; + }; + }; + relayerTotal: { + amount: string; + amountUsd: string; + pct: string; + token: { + decimals: number; + symbol: string; + address: string; + name: string; + chainId: number; + }; + }; + app: { + amount: string; + amountUsd: string; + pct: string; + token: { + decimals: number; + symbol: string; + address: string; + name: string; + chainId: number; + }; + }; + swap?: { + amount: string; + amountUsd: string; + pct: string; + token: { + decimals: number; + symbol: string; + address: string; + name: string; + chainId: number; + }; + }; + }; }; export type SwapApprovalApiQueryParams = { diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 95e44b76c..a9a6e6d77 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -14,23 +14,13 @@ import { ReactComponent as Warning } from "assets/icons/warning_triangle.svg"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; -import { COLORS, formatUSD, getConfig } from "utils"; +import { COLORS, formatUSDString, getConfig, isDefined } from "utils"; import { useTokenConversion } from "hooks/useTokenConversion"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; import { Tooltip } from "components/Tooltip"; - -type SwapQuoteResponse = { - checks: object; - steps: object; - refundToken: object; - inputAmount: string; - expectedOutputAmount: string; - minOutputAmount: string; - expectedFillTime: number; - swapTx: object; -}; +import { SwapApprovalApiResponse } from "utils/serverless-api/prod/swap-approval"; export type BridgeButtonState = | "notConnected" @@ -47,7 +37,7 @@ interface ConfirmationButtonProps inputToken: EnrichedTokenSelect | null; outputToken: EnrichedTokenSelect | null; amount: BigNumber | null; - swapQuote: SwapQuoteResponse | null; + swapQuote: SwapApprovalApiResponse | null; isQuoteLoading: boolean; onConfirm?: () => void; validationError?: AmountInputError; @@ -212,75 +202,29 @@ export const ConfirmationButton: React.FC = ({ // Calculate display values from swapQuote // Resolve conversion helpers outside memo to respect hooks rules - const bridgeTokenSymbol = - (swapQuote as any)?.steps?.bridge?.tokenOut?.symbol || - outputToken?.symbol || - "ETH"; - const destinationNativeSymbol = getConfig().getNativeTokenInfo( - outputToken?.chainId || 1 - ).symbol; - const { convertTokenToBaseCurrency: convertInputTokenToUsd } = - useTokenConversion(inputToken?.symbol || "ETH", "usd"); - const { convertTokenToBaseCurrency: convertBridgeTokenToUsd } = - useTokenConversion(bridgeTokenSymbol, "usd"); - const { convertTokenToBaseCurrency: convertDestinationNativeToUsd } = - useTokenConversion(destinationNativeSymbol, "usd"); const displayValues = React.useMemo(() => { - const toBN = (v: any) => { - try { - return BigNumber.from(v ?? 0); - } catch { - return BigNumber.from(0); - } - }; - - const formatUsdString = (v?: BigNumber) => { - if (!v) return "-"; - try { - return `$${formatUSD(v)}`; - } catch { - return "-"; - } - }; - if (!swapQuote || !inputToken || !outputToken) { return { fee: "-", time: "-", bridgeFee: "-", - destinationGasFee: "-", - extraFee: "-", + gasFee: "-", + swapFee: "-", route: "Across V4", estimatedTime: "-", netFee: "-", }; } - const fees = (swapQuote as any)?.steps?.bridge?.fees || {}; - const relayerCapitalTotal = toBN(fees?.relayerCapital?.total); - const lpTotal = toBN(fees?.lp?.total); - const relayerGasTotal = toBN(fees?.relayerGas?.total); - - // Convert components to USD - const bridgeFeeTokenAmount = relayerCapitalTotal.add(lpTotal); - const bridgeFeeUsd = convertBridgeTokenToUsd(bridgeFeeTokenAmount); - const gasFeeUsd = convertDestinationNativeToUsd(relayerGasTotal); - - // Approximate swap fee in USD if we have user input and bridge input - const bridgeInputAmount = toBN( - (swapQuote as any)?.steps?.bridge?.inputAmount - ); - const inputAmountUsd = convertInputTokenToUsd(amount ?? BigNumber.from(0)); - const bridgeInputUsd = convertBridgeTokenToUsd(bridgeInputAmount); - const swapFeeUsd = - inputAmountUsd && bridgeInputUsd && inputAmountUsd.gt(bridgeInputUsd) - ? inputAmountUsd.sub(bridgeInputUsd) - : BigNumber.from(0); - - const netFeeUsd = (bridgeFeeUsd || BigNumber.from(0)) - .add(gasFeeUsd || BigNumber.from(0)) - .add(swapFeeUsd || BigNumber.from(0)); + // Get fees from the top-level fees object (new structure) + const bridgeFeesUsd = swapQuote.fees.relayerTotal.amountUsd; + const gasFeeUsd = ( + Number(swapQuote.fees.originGas.amountUsd) + + Number(swapQuote.fees.destinationGas.amountUsd) + ).toString(); + const swapFeeUsd = swapQuote.fees.swap?.amountUsd; + const totalFeeUsd = swapQuote.fees.total.amountUsd; const totalSeconds = Math.max(0, Number(swapQuote.expectedFillTime || 0)); const underOneMinute = totalSeconds < 60; @@ -289,24 +233,15 @@ export const ConfirmationButton: React.FC = ({ : `~${Math.ceil(totalSeconds / 60)} min`; return { - fee: formatUsdString(netFeeUsd), + fee: formatUSDString(totalFeeUsd), time, - bridgeFee: formatUsdString(bridgeFeeUsd), - destinationGasFee: formatUsdString(gasFeeUsd), - extraFee: formatUsdString(swapFeeUsd), + bridgeFee: formatUSDString(bridgeFeesUsd), + gasFee: formatUSDString(gasFeeUsd), + swapFee: swapFeeUsd ? formatUSDString(swapFeeUsd) : undefined, route: "Across V4", estimatedTime: time, - netFee: formatUsdString(netFeeUsd), }; - }, [ - swapQuote, - inputToken, - outputToken, - amount, - convertInputTokenToUsd, - convertBridgeTokenToUsd, - convertDestinationNativeToUsd, - ]); + }, [swapQuote, inputToken, outputToken, amount]); const clickHandler = onConfirm; @@ -374,17 +309,19 @@ export const ConfirmationButton: React.FC = ({ - Destination Gas Fee - - {displayValues.destinationGasFee} - - - - Extra Fee + Gas Fee - {displayValues.extraFee} + {displayValues.gasFee} + {isDefined(displayValues.swapFee) && ( + + Swap Fee + + {displayValues.swapFee} + + + )} ) : null} From 9575121d3f66cbb58740480b0c7ec1ffcaa35840 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 9 Oct 2025 14:26:59 +0200 Subject: [PATCH 048/122] cache swap tokens at build time, refetch every minute Signed-off-by: Gerhard Steenkamp --- api/swap/tokens/_service.ts | 172 + api/swap/tokens/index.ts | 171 +- package.json | 3 +- scripts/cache-swap-tokens.ts | 44 + src/data/swap-tokens.json | 10869 +++++++++++++++++++++++++++++++++ src/hooks/useSwapTokens.ts | 37 + 6 files changed, 11129 insertions(+), 167 deletions(-) create mode 100644 api/swap/tokens/_service.ts create mode 100644 scripts/cache-swap-tokens.ts create mode 100644 src/data/swap-tokens.json diff --git a/api/swap/tokens/_service.ts b/api/swap/tokens/_service.ts new file mode 100644 index 000000000..36ac70ec8 --- /dev/null +++ b/api/swap/tokens/_service.ts @@ -0,0 +1,172 @@ +import axios from "axios"; +import { constants } from "ethers"; +import mainnetChains from "../../../src/data/chains_1.json"; +import indirectChains from "../../../src/data/indirect_chains_1.json"; +import { CHAIN_IDs } from "../../_constants"; + +export type SwapToken = { + chainId: number; + address: string; + name: string; + symbol: string; + decimals: number; + logoUrl: string; + priceUsd: string | null; +}; + +const chains = mainnetChains; +const chainIds = chains.map((chain) => chain.chainId); + +// List of tokens that are statically defined locally +const staticTokens = indirectChains.flatMap((chain) => + chain.outputTokens.map((token) => ({ + chainId: chain.chainId, + address: token.address, + name: token.name, + symbol: token.symbol, + decimals: token.decimals, + logoUrl: token.logoUrl, + priceUsd: token.symbol === "USDT-SPOT" ? "1" : null, + })) +); + +function getUniswapTokens( + uniswapResponse: any, + chainIds: number[], + pricesForLifiTokens: Record> +): SwapToken[] { + return uniswapResponse.tokens.reduce((acc: SwapToken[], token: any) => { + if (chainIds.includes(token.chainId)) { + acc.push({ + chainId: token.chainId, + address: token.address, + name: token.name, + symbol: token.symbol, + decimals: token.decimals, + logoUrl: token.logoURI, + priceUsd: pricesForLifiTokens[token.chainId]?.[token.address] || null, + }); + } + return acc; + }, []); +} + +function getNativeTokensFromLifiTokens( + lifiTokensResponse: any, + chainIds: number[], + pricesForLifiTokens: Record> +): SwapToken[] { + return chainIds.reduce((acc: SwapToken[], chainId) => { + const nativeToken = lifiTokensResponse?.tokens?.[chainId]?.find( + (token: any) => token.address === constants.AddressZero + ); + if (nativeToken) { + acc.push({ + chainId, + address: nativeToken.address, + name: nativeToken.name, + symbol: nativeToken.symbol, + decimals: nativeToken.decimals, + logoUrl: nativeToken.logoURI, + priceUsd: pricesForLifiTokens[chainId]?.[nativeToken.address] || null, + }); + } + return acc; + }, []); +} + +function getPricesForLifiTokens(lifiTokensResponse: any, chainIds: number[]) { + return chainIds.reduce( + (acc, chainId) => { + const tokens = lifiTokensResponse.tokens[chainId]; + if (!tokens) { + return acc; + } + tokens.forEach((token: any) => { + if (!acc[chainId]) { + acc[chainId] = {}; + } + acc[chainId][token.address] = token.priceUSD; + }); + return acc; + }, + {} as Record> + ); +} + +function getJupiterTokens( + jupiterTokensResponse: any[], + chainIds: number[] +): SwapToken[] { + if (!chainIds.includes(CHAIN_IDs.SOLANA)) { + return []; + } + + return jupiterTokensResponse.reduce((acc: SwapToken[], token: any) => { + if (token.organicScoreLabel === "high") { + acc.push({ + chainId: CHAIN_IDs.SOLANA, + address: token.id, + name: token.name, + symbol: token.symbol, + decimals: token.decimals, + logoUrl: token.icon, + priceUsd: token.usdPrice?.toString() || null, + }); + } + return acc; + }, []); +} + +function getStaticTokens(chainIds: number[]): SwapToken[] { + return staticTokens.filter((token) => chainIds.includes(token.chainId)); +} + +export async function fetchSwapTokensData( + filteredChainIds?: number[] +): Promise { + const targetChainIds = filteredChainIds || chainIds; + + const [uniswapTokensResponse, lifiTokensResponse, jupiterTokensResponse] = + await Promise.all([ + axios.get("https://tokens.uniswap.org"), + axios.get("https://li.quest/v1/tokens"), + axios.get("https://lite-api.jup.ag/tokens/v2/toporganicscore/24h"), + ]); + + const pricesForLifiTokens = getPricesForLifiTokens( + lifiTokensResponse.data, + targetChainIds + ); + + const responseJson: SwapToken[] = []; + + // Add Uniswap tokens + const uniswapTokens = getUniswapTokens( + uniswapTokensResponse.data, + targetChainIds, + pricesForLifiTokens + ); + responseJson.push(...uniswapTokens); + + // Add native tokens from LiFi + const nativeTokens = getNativeTokensFromLifiTokens( + lifiTokensResponse.data, + targetChainIds, + pricesForLifiTokens + ); + responseJson.push(...nativeTokens); + + // Add Jupiter tokens + const jupiterTokens = getJupiterTokens( + jupiterTokensResponse.data, + targetChainIds + ); + responseJson.push(...jupiterTokens); + + // Add static tokens + const staticTokens = getStaticTokens(targetChainIds); + responseJson.push(...staticTokens); + + return responseJson; +} diff --git a/api/swap/tokens/index.ts b/api/swap/tokens/index.ts index e0946d07c..953704ea1 100644 --- a/api/swap/tokens/index.ts +++ b/api/swap/tokens/index.ts @@ -1,6 +1,4 @@ import { VercelResponse } from "@vercel/node"; -import axios from "axios"; -import { constants } from "ethers"; import { type, assert, Infer, optional, array, union } from "superstruct"; import { @@ -11,40 +9,10 @@ import { } from "../../_utils"; import { TypedVercelRequest } from "../../_types"; -import mainnetChains from "../../../src/data/chains_1.json"; -import indirectChains from "../../../src/data/indirect_chains_1.json"; import { getRequestId, setRequestSpanAttributes } from "../../_request_utils"; import { sendResponse } from "../../_response_utils"; import { tracer, processor } from "../../../instrumentation"; -import { CHAIN_IDs } from "../../_constants"; - -type Token = { - chainId: number; - address: string; - name: string; - symbol: string; - decimals: number; - logoUrl: string; - priceUsd: string | null; -}; - -const chains = mainnetChains; -const chainIds = chains.map((chain) => chain.chainId); - -// List of tokens that are statically defined locally. Currently, this list is used for -// indirect chain tokens, e.g. USDT-SPOT on HyperCore. -const staticTokens = indirectChains.flatMap((chain) => - chain.outputTokens.map((token) => ({ - chainId: chain.chainId, - address: token.address, - name: token.name, - symbol: token.symbol, - decimals: token.decimals, - logoUrl: token.logoUrl, - // TODO: Add more generic price resolution logic for static tokens - priceUsd: token.symbol === "USDT-SPOT" ? "1" : null, - })) -); +import { fetchSwapTokensData, SwapToken } from "./_service"; const SwapTokensQueryParamsSchema = type({ chainId: optional(union([positiveIntStr(), array(positiveIntStr())])), @@ -72,48 +40,11 @@ export default async function handler( const { chainId } = query; const filteredChainIds = chainId - ? paramToArray(chainId)?.map(Number) || chainIds - : chainIds; - - const [uniswapTokensResponse, lifiTokensResponse, jupiterTokensResponse] = - await Promise.all([ - axios.get("https://tokens.uniswap.org"), - axios.get("https://li.quest/v1/tokens"), - axios.get("https://lite-api.jup.ag/tokens/v2/toporganicscore/24h"), - ]); - const pricesForLifiTokens = getPricesForLifiTokens( - lifiTokensResponse.data, - filteredChainIds - ); - - const responseJson: Token[] = []; + ? paramToArray(chainId)?.map(Number) + : undefined; - // Add Uniswap tokens - const uniswapTokens = getUniswapTokens( - uniswapTokensResponse.data, - filteredChainIds, - pricesForLifiTokens - ); - responseJson.push(...uniswapTokens); - - // Add native tokens from LiFi - const nativeTokens = getNativeTokensFromLifiTokens( - lifiTokensResponse.data, - filteredChainIds, - pricesForLifiTokens - ); - responseJson.push(...nativeTokens); - - // Add Jupiter tokens - const jupiterTokens = getJupiterTokens( - jupiterTokensResponse.data, - filteredChainIds - ); - responseJson.push(...jupiterTokens); - - // Add static tokens - const staticTokens = getStaticTokens(filteredChainIds); - responseJson.push(...staticTokens); + const responseJson: SwapToken[] = + await fetchSwapTokensData(filteredChainIds); logger.debug({ at: "swap/tokens", @@ -143,95 +74,3 @@ export default async function handler( } }); } - -function getUniswapTokens( - uniswapResponse: any, - chainIds: number[], - pricesForLifiTokens: Record> -): Token[] { - return uniswapResponse.tokens.reduce((acc: Token[], token: any) => { - if (chainIds.includes(token.chainId)) { - acc.push({ - chainId: token.chainId, - address: token.address, - name: token.name, - symbol: token.symbol, - decimals: token.decimals, - logoUrl: token.logoURI, - priceUsd: pricesForLifiTokens[token.chainId]?.[token.address] || null, - }); - } - return acc; - }, []); -} - -function getNativeTokensFromLifiTokens( - lifiTokensResponse: any, - chainIds: number[], - pricesForLifiTokens: Record> -): Token[] { - return chainIds.reduce((acc: Token[], chainId) => { - const nativeToken = lifiTokensResponse?.tokens?.[chainId]?.find( - (token: any) => token.address === constants.AddressZero - ); - if (nativeToken) { - acc.push({ - chainId, - address: nativeToken.address, - name: nativeToken.name, - symbol: nativeToken.symbol, - decimals: nativeToken.decimals, - logoUrl: nativeToken.logoURI, - priceUsd: pricesForLifiTokens[chainId]?.[nativeToken.address] || null, - }); - } - return acc; - }, []); -} - -function getPricesForLifiTokens(lifiTokensResponse: any, chainIds: number[]) { - return chainIds.reduce( - (acc, chainId) => { - const tokens = lifiTokensResponse.tokens[chainId]; - if (!tokens) { - return acc; - } - tokens.forEach((token: any) => { - if (!acc[chainId]) { - acc[chainId] = {}; - } - acc[chainId][token.address] = token.priceUSD; - }); - return acc; - }, - {} as Record> - ); // chainId -> tokenAddress -> price -} - -function getJupiterTokens( - jupiterTokensResponse: any[], - chainIds: number[] -): Token[] { - if (!chainIds.includes(CHAIN_IDs.SOLANA)) { - return []; - } - - return jupiterTokensResponse.reduce((acc: Token[], token: any) => { - if (token.organicScoreLabel === "high") { - acc.push({ - chainId: CHAIN_IDs.SOLANA, - address: token.id, - name: token.name, - symbol: token.symbol, - decimals: token.decimals, - logoUrl: token.icon, - priceUsd: token.usdPrice?.toString() || null, - }); - } - return acc; - }, []); -} - -function getStaticTokens(chainIds: number[]): Token[] { - return staticTokens.filter((token) => chainIds.includes(token.chainId)); -} diff --git a/package.json b/package.json index 02b36a3e9..5620ce3a7 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,10 @@ "dev": "export REACT_APP_GIT_COMMIT_HASH=$(git rev-parse HEAD) && vite --port $PORT --host", "dev:api": "vercel dev --listen 127.0.0.1:3000", "build": "tsc && vite build", - "prebuild": "export REACT_APP_GIT_COMMIT_HASH=$(git rev-parse HEAD) && yarn remote-config && yarn remote-env", + "prebuild": "export REACT_APP_GIT_COMMIT_HASH=$(git rev-parse HEAD) && yarn remote-config && yarn remote-env && yarn cache-swap-tokens", "remote-config": "tsx scripts/fetch-remote-config.ts", "remote-env": "tsx ./scripts/fetch-remote-env.ts", + "cache-swap-tokens": "tsx scripts/cache-swap-tokens.ts", "analyze": "yarn build && rollup-plugin-visualizer --open ./bundle-size-analysis.json", "test": "export REACT_APP_GIT_COMMIT_HASH=$(git rev-parse HEAD) && jest --config jest.frontend.config.cjs", "serve": "vite preview --port 3000", diff --git a/scripts/cache-swap-tokens.ts b/scripts/cache-swap-tokens.ts new file mode 100644 index 000000000..f722472f7 --- /dev/null +++ b/scripts/cache-swap-tokens.ts @@ -0,0 +1,44 @@ +import { writeFileSync, mkdirSync } from "fs"; +import { join } from "path"; +import { fetchSwapTokensData, SwapToken } from "../api/swap/tokens/_service"; + +export interface SwapTokensCache { + tokens: SwapToken[]; + timestamp: number; + version: string; +} + +async function main() { + try { + console.log("Fetching swap tokens data..."); + const tokens = await fetchSwapTokensData(); + + // Create cache directory if it doesn't exist + const cacheDir = join(process.cwd(), "src", "data"); + mkdirSync(cacheDir, { recursive: true }); + + // Write cached tokens to file + const cacheFile = join(cacheDir, "swap-tokens.json"); + const cacheData: SwapTokensCache = { + tokens, + timestamp: Date.now(), + version: "1.0.0", + }; + + writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2)); + + console.log( + `✅ Successfully cached ${tokens.length} swap tokens to ${cacheFile}` + ); + console.log( + `📊 Cache size: ${Math.round(JSON.stringify(cacheData).length / 1024)}KB` + ); + } catch (error) { + console.error("❌ Failed to cache swap tokens:", error); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} diff --git a/src/data/swap-tokens.json b/src/data/swap-tokens.json new file mode 100644 index 000000000..e7d0aea55 --- /dev/null +++ b/src/data/swap-tokens.json @@ -0,0 +1,10869 @@ +{ + "tokens": [ + { + "chainId": 1, + "address": "0x111111111117dC0aa78b770fA6A738034120C302", + "name": "1inch", + "symbol": "1INCH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceUsd": "0.24835" + }, + { + "chainId": 1, + "address": "0x3E5A19c91266aD8cE2477B91585d1856B84062dF", + "name": "Ancient8", + "symbol": "A8", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39170/standard/A8_Token-04_200x200.png?1720798300", + "priceUsd": "0.08491677302193065" + }, + { + "chainId": 1, + "address": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": "276.06" + }, + { + "chainId": 1, + "address": "0xB98d4C97425d9908E66E53A6fDf673ACcA0BE986", + "name": "Arcblock", + "symbol": "ABT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2341/thumb/arcblock.png?1547036543", + "priceUsd": "0.6021527578836832" + }, + { + "chainId": 1, + "address": "0xEd04915c23f00A313a544955524EB7DBD823143d", + "name": "Alchemy Pay", + "symbol": "ACH", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/12390/thumb/ACH_%281%29.png?1599691266", + "priceUsd": "0.018528970984471187" + }, + { + "chainId": 1, + "address": "0x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F", + "name": "Across Protocol Token", + "symbol": "ACX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", + "priceUsd": "0.111262" + }, + { + "chainId": 1, + "address": "0xADE00C28244d5CE17D72E40330B1c318cD12B7c3", + "name": "Ambire AdEx", + "symbol": "ADX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x91Af0fBB28ABA7E31403Cb457106Ce79397FD4E6", + "name": "Aergo", + "symbol": "AERGO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4490/thumb/aergo.png?1647696770", + "priceUsd": "0.12097457985117842" + }, + { + "chainId": 1, + "address": "0xB528edBef013aff855ac3c50b381f253aF13b997", + "name": "Aevo", + "symbol": "AEVO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35893/standard/aevo.png", + "priceUsd": "0.096988" + }, + { + "chainId": 1, + "address": "0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8", + "name": "agEur", + "symbol": "agEUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", + "priceUsd": "1.1635785053489096" + }, + { + "chainId": 1, + "address": "0x32353A6C91143bfd6C7d363B546e62a9A2489A20", + "name": "Adventure Gold", + "symbol": "AGLD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", + "priceUsd": "0.6450181756960583" + }, + { + "chainId": 1, + "address": "0x626E8036dEB333b408Be468F951bdB42433cBF18", + "name": "AIOZ Network", + "symbol": "AIOZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", + "priceUsd": "0.269038" + }, + { + "chainId": 1, + "address": "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF", + "name": "Alchemix", + "symbol": "ALCX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14113/thumb/Alchemix.png?1614409874", + "priceUsd": "8.75" + }, + { + "chainId": 1, + "address": "0x27702a26126e0B3702af63Ee09aC4d1A084EF628", + "name": "Aleph im", + "symbol": "ALEPH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", + "priceUsd": "0.06990075260438454" + }, + { + "chainId": 1, + "address": "0x6B0b3a982b4634aC68dD83a4DBF02311cE324181", + "name": "Alethea Artificial Liquid Intelligence", + "symbol": "ALI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", + "priceUsd": "0.005723599473239193" + }, + { + "chainId": 1, + "address": "0xAC51066d7bEC65Dc4589368da368b212745d63E8", + "name": "My Neighbor Alice", + "symbol": "ALICE", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", + "priceUsd": "0.3825432255973373" + }, + { + "chainId": 1, + "address": "0xa1faa113cbE53436Df28FF0aEe54275c13B40975", + "name": "Alpha Venture DAO", + "symbol": "ALPHA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", + "priceUsd": "0.01461347" + }, + { + "chainId": 1, + "address": "0x8457CA5040ad67fdebbCC8EdCE889A335Bc0fbFB", + "name": "AltLayer", + "symbol": "ALT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/34608/standard/Logomark_200x200.png", + "priceUsd": "0.02778276" + }, + { + "chainId": 1, + "address": "0xfF20817765cB7f73d4bde2e66e067E58D11095C2", + "name": "Amp", + "symbol": "AMP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12409/thumb/amp-200x200.png?1599625397", + "priceUsd": "0.00299801" + }, + { + "chainId": 1, + "address": "0x8290333ceF9e6D528dD5618Fb97a76f268f3EDD4", + "name": "Ankr", + "symbol": "ANKR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", + "priceUsd": "0.013846381285394373" + }, + { + "chainId": 1, + "address": "0xa117000000f279D81A1D3cc75430fAA017FA5A2e", + "name": "Aragon", + "symbol": "ANT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/681/thumb/JelZ58cv_400x400.png?1601449653", + "priceUsd": "0.49893083327521964" + }, + { + "chainId": 1, + "address": "0x4d224452801ACEd8B2F0aebE155379bb5D594381", + "name": "ApeCoin", + "symbol": "APE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", + "priceUsd": "0.5624223258266647" + }, + { + "chainId": 1, + "address": "0x0b38210ea11411557c13457D4dA7dC6ea731B88a", + "name": "API3", + "symbol": "API3", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13256/thumb/api3.jpg?1606751424", + "priceUsd": "0.823883" + }, + { + "chainId": 1, + "address": "0x594DaaD7D77592a2b97b725A7AD59D7E188b5bFa", + "name": "Apu Apustaja", + "symbol": "APU", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/35986/large/200x200.png?1710308147", + "priceUsd": "0.00013068" + }, + { + "chainId": 1, + "address": "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", + "name": "Arbitrum", + "symbol": "ARB", + "decimals": 18, + "logoUrl": "https://arbitrum.foundation/logo.png", + "priceUsd": "0.418755" + }, + { + "chainId": 1, + "address": "0x6E2a43be0B1d33b726f0CA3b8de60b3482b8b050", + "name": "Arkham", + "symbol": "ARKM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/30929/standard/Arkham_Logo_CG.png?1696529771", + "priceUsd": "0.38184467298899505" + }, + { + "chainId": 1, + "address": "0xBA50933C268F567BDC86E1aC131BE072C6B0b71a", + "name": "ARPA Chain", + "symbol": "ARPA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", + "priceUsd": "0.009285973685280075" + }, + { + "chainId": 1, + "address": "0x64D91f12Ece7362F91A6f8E7940Cd55F05060b92", + "name": "ASH", + "symbol": "ASH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15714/thumb/omnPqaTY.png?1622820503", + "priceUsd": "0.9247673787550553" + }, + { + "chainId": 1, + "address": "0x2565ae0385659badCada1031DB704442E1b69982", + "name": "Assemble Protocol", + "symbol": "ASM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11605/thumb/gpvrlkSq_400x400_%281%29.jpg?1591775789", + "priceUsd": "0.06466856607775083" + }, + { + "chainId": 1, + "address": "0x27054b13b1B798B345b591a4d22e6562d47eA75a", + "name": "AirSwap", + "symbol": "AST", + "decimals": 4, + "logoUrl": "https://assets.coingecko.com/coins/images/1019/thumb/Airswap.png?1630903484", + "priceUsd": "0.029853760156180194" + }, + { + "chainId": 1, + "address": "0xA2120b9e674d3fC3875f415A7DF52e382F141225", + "name": "Automata", + "symbol": "ATA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", + "priceUsd": "0.040714414720865906" + }, + { + "chainId": 1, + "address": "0xbe0Ed4138121EcFC5c0E56B40517da27E6c5226B", + "name": "Aethir Token", + "symbol": "ATH", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/36179/large/logogram_circle_dark_green_vb_green_(1).png?1718232706", + "priceUsd": "0.052467" + }, + { + "chainId": 1, + "address": "0xA9B1Eb5908CfC3cdf91F9B8B3a74108598009096", + "name": "Bounce", + "symbol": "AUCTION", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13860/thumb/1_KtgpRIJzuwfHe0Rl0avP_g.jpeg?1612412025", + "priceUsd": "8.154566945593762" + }, + { + "chainId": 1, + "address": "0x18aAA7115705e8be94bfFEBDE57Af9BFc265B998", + "name": "Audius", + "symbol": "AUDIO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12913/thumb/AudiusCoinLogo_2x.png?1603425727", + "priceUsd": "0.054061721237164004" + }, + { + "chainId": 1, + "address": "0x845576c64f9754CF09d87e45B720E82F3EeF522C", + "name": "Artverse Token", + "symbol": "AVT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19727/thumb/ewnektoB_400x400.png?1635767094", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x467719aD09025FcC6cF6F8311755809d45a5E5f3", + "name": "Axelar", + "symbol": "AXL", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", + "priceUsd": "0.2929734557331972" + }, + { + "chainId": 1, + "address": "0xBB0E17EF65F82Ab018d8EDd776e8DD940327B28b", + "name": "Axie Infinity", + "symbol": "AXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", + "priceUsd": "2.11" + }, + { + "chainId": 1, + "address": "0x3472A5A71965499acd81997a54BBA8D852C6E53d", + "name": "Badger DAO", + "symbol": "BADGER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", + "priceUsd": "0.97573" + }, + { + "chainId": 1, + "address": "0xba100000625a3754423978a60c9317c58a424e3D", + "name": "Balancer", + "symbol": "BAL", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", + "priceUsd": "1.14" + }, + { + "chainId": 1, + "address": "0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55", + "name": "Band Protocol", + "symbol": "BAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9545/thumb/band-protocol.png?1568730326", + "priceUsd": "0.6758955604053999" + }, + { + "chainId": 1, + "address": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "name": "Basic Attention Token", + "symbol": "BAT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", + "priceUsd": "0.1559633056201925" + }, + { + "chainId": 1, + "address": "0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE", + "name": "Beam", + "symbol": "BEAM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32417/standard/chain-logo.png?1698114384", + "priceUsd": "0.00874505" + }, + { + "chainId": 1, + "address": "0xF17e65822b568B3903685a7c9F496CF7656Cc6C2", + "name": "Biconomy", + "symbol": "BICO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", + "priceUsd": "0.09065698071630358" + }, + { + "chainId": 1, + "address": "0x64Bc2cA1Be492bE7185FAA2c8835d9b824c8a194", + "name": "Big Time", + "symbol": "BIGTIME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32251/standard/-6136155493475923781_121.jpg?1696998691", + "priceUsd": "0.047348411285928906" + }, + { + "chainId": 1, + "address": "0xcb1592591996765Ec0eFc1f92599A19767ee5ffA", + "name": "BIO", + "symbol": "BIO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53022/large/bio.jpg?1735011002", + "priceUsd": "0.119872" + }, + { + "chainId": 1, + "address": "0x1A4b46696b2bB4794Eb3D4c26f1c55F9170fa4C5", + "name": "BitDAO", + "symbol": "BIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17627/thumb/rI_YptK8.png?1653983088", + "priceUsd": "2.550847669016368" + }, + { + "chainId": 1, + "address": "0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9", + "name": "HarryPotterObamaSonic10Inu", + "symbol": "BITCOIN", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", + "priceUsd": "0.096997" + }, + { + "chainId": 1, + "address": "0x5283D291DBCF85356A21bA090E6db59121208b44", + "name": "Blur", + "symbol": "BLUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28453/large/blur.png?1670745921", + "priceUsd": "0.0870963325585314" + }, + { + "chainId": 1, + "address": "0x5732046A883704404F284Ce41FfADd5b007FD668", + "name": "Bluzelle", + "symbol": "BLZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2848/thumb/ColorIcon_3x.png?1622516510", + "priceUsd": "0.030750981687807866" + }, + { + "chainId": 1, + "address": "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C", + "name": "Bancor Network Token", + "symbol": "BNT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", + "priceUsd": "0.684579" + }, + { + "chainId": 1, + "address": "0x42bBFa2e77757C645eeaAd1655E0911a7553Efbc", + "name": "Boba Network", + "symbol": "BOBA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20285/thumb/BOBA.png?1636811576", + "priceUsd": "0.08686545841487457" + }, + { + "chainId": 1, + "address": "0x0391D2021f89DC339F60Fff84546EA23E337750f", + "name": "BarnBridge", + "symbol": "BOND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", + "priceUsd": "0.15236644952732825" + }, + { + "chainId": 1, + "address": "0x799ebfABE77a6E34311eeEe9825190B9ECe32824", + "name": "Braintrust", + "symbol": "BTRST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18100/thumb/braintrust.PNG?1630475394", + "priceUsd": "0.3867799487884984" + }, + { + "chainId": 1, + "address": "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + "name": "Binance USD", + "symbol": "BUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", + "priceUsd": "0.9899099170252645" + }, + { + "chainId": 1, + "address": "0xAE12C5930881c53715B369ceC7606B70d8EB229f", + "name": "Coin98", + "symbol": "C98", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17117/thumb/logo.png?1626412904", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", + "name": "PancakeSwap", + "symbol": "CAKE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/12632/large/pancakeswap-cake-logo_%281%29.png?1696512440", + "priceUsd": "3.81" + }, + { + "chainId": 1, + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "name": "Coinbase Wrapped BTC", + "symbol": "cbBTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/40143/standard/cbbtc.webp", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", + "name": "Coinbase Wrapped Staked ETH", + "symbol": "cbETH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/27008/large/cbeth.png", + "priceUsd": "4816.72" + }, + { + "chainId": 1, + "address": "0x3294395e62F4eB6aF3f1Fcf89f5602D90Fb3Ef69", + "name": "Celo native asset (Wormhole)", + "symbol": "CELO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", + "priceUsd": "0.42952459484694416" + }, + { + "chainId": 1, + "address": "0x4F9254C83EB525f9FCf346490bbb3ed28a81C667", + "name": "Celer Network", + "symbol": "CELR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4379/thumb/Celr.png?1554705437", + "priceUsd": "0.0077831751546029015" + }, + { + "chainId": 1, + "address": "0x8A2279d4A90B6fe1C4B30fa660cC9f926797bAA2", + "name": "Chromia", + "symbol": "CHR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/5000/thumb/Chromia.png?1559038018", + "priceUsd": "0.08327870294975556" + }, + { + "chainId": 1, + "address": "0x3506424F91fD33084466F402d5D97f05F8e3b4AF", + "name": "Chiliz", + "symbol": "CHZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8834/thumb/Chiliz.png?1561970540", + "priceUsd": "0.04046281519762788" + }, + { + "chainId": 1, + "address": "0x80C62FE4487E1351b47Ba49809EBD60ED085bf52", + "name": "Clover Finance", + "symbol": "CLV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15278/thumb/clover.png?1645084454", + "priceUsd": "0.021274833674050836" + }, + { + "chainId": 1, + "address": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "name": "Compound", + "symbol": "COMP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", + "priceUsd": "41.59113915910497" + }, + { + "chainId": 1, + "address": "0x44f49ff0da2498bCb1D3Dc7C0f999578F67FD8C6", + "name": "Corn", + "symbol": "CORN", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54471/large/corn.jpg?1739933588", + "priceUsd": "0.096562" + }, + { + "chainId": 1, + "address": "0xDDB3422497E61e13543BeA06989C0789117555c5", + "name": "COTI", + "symbol": "COTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2962/thumb/Coti.png?1559653863", + "priceUsd": "0.05172350426311411" + }, + { + "chainId": 1, + "address": "0x3D658390460295FB963f54dC0899cfb1c30776Df", + "name": "Circuits of Value", + "symbol": "COVAL", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/588/thumb/coval-logo.png?1599493950", + "priceUsd": "0.0006174723528966431" + }, + { + "chainId": 1, + "address": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", + "name": "CoW Protocol", + "symbol": "COW", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24384/large/CoW-token_logo.png?1719524382", + "priceUsd": "0.275109" + }, + { + "chainId": 1, + "address": "0x66761Fa41377003622aEE3c7675Fc7b5c1C2FaC5", + "name": "Clearpool", + "symbol": "CPOOL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19252/large/photo_2022-08-31_12.45.02.jpeg?1696518697", + "priceUsd": "0.124843" + }, + { + "chainId": 1, + "address": "0xD417144312DbF50465b1C641d016962017Ef6240", + "name": "Covalent", + "symbol": "CQT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14168/thumb/covalent-cqt.png?1624545218", + "priceUsd": "0.0012093983234396837" + }, + { + "chainId": 1, + "address": "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", + "name": "Cronos", + "symbol": "CRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", + "priceUsd": "0.192116" + }, + { + "chainId": 1, + "address": "0x08389495D7456E1951ddF7c3a1314A4bfb646d8B", + "name": "Crypterium", + "symbol": "CRPT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1901/thumb/crypt.png?1547036205", + "priceUsd": "0.0006950436675444071" + }, + { + "chainId": 1, + "address": "0xD533a949740bb3306d119CC777fa900bA034cd52", + "name": "Curve DAO Token", + "symbol": "CRV", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", + "priceUsd": "0.719583" + }, + { + "chainId": 1, + "address": "0x491604c0FDF08347Dd1fa4Ee062a822A5DD06B5D", + "name": "Cartesi", + "symbol": "CTSI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", + "priceUsd": "0.08272383644510108" + }, + { + "chainId": 1, + "address": "0x321C2fE4446C7c963dc41Dd58879AF648838f98D", + "name": "Cryptex Finance", + "symbol": "CTX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14932/thumb/glossy_icon_-_C200px.png?1619073171", + "priceUsd": "1.3129731831764002" + }, + { + "chainId": 1, + "address": "0xDf801468a808a32656D2eD2D2d80B72A129739f4", + "name": "Somnium Space CUBEs", + "symbol": "CUBE", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/10687/thumb/CUBE_icon.png?1617026861", + "priceUsd": "0.30914320431849696" + }, + { + "chainId": 1, + "address": "0x41e5560054824eA6B0732E656E3Ad64E20e94E45", + "name": "Civic", + "symbol": "CVC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", + "priceUsd": "0.07239176819765296" + }, + { + "chainId": 1, + "address": "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", + "name": "Convex Finance", + "symbol": "CVX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", + "priceUsd": "3.29" + }, + { + "chainId": 1, + "address": "0x7ABc8A5768E6bE61A6c693a6e4EAcb5B60602C4D", + "name": "Covalent X Token", + "symbol": "CXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39177/large/CXT_Ticker.png?1720829918", + "priceUsd": "0.02199355566421097" + }, + { + "chainId": 1, + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "name": "Dai Stablecoin", + "symbol": "DAI", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", + "priceUsd": "0.999769" + }, + { + "chainId": 1, + "address": "0x081131434f93063751813C619Ecca9C4dC7862a3", + "name": "Mines of Dalarnia", + "symbol": "DAR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/19837/thumb/dar.png?1636014223", + "priceUsd": "0.04350571850144363" + }, + { + "chainId": 1, + "address": "0x3A880652F47bFaa771908C07Dd8673A787dAEd3A", + "name": "DerivaDAO", + "symbol": "DDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13453/thumb/ddx_logo.png?1608741641", + "priceUsd": "0.060304550074008534" + }, + { + "chainId": 1, + "address": "0x3597bfD533a99c9aa083587B074434E61Eb0A258", + "name": "Dent", + "symbol": "DENT", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/1152/thumb/gLCEA2G.png?1604543239", + "priceUsd": "0.0006067585333378868" + }, + { + "chainId": 1, + "address": "0xfB7B4564402E5500dB5bB6d63Ae671302777C75a", + "name": "DexTools", + "symbol": "DEXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", + "priceUsd": "0.468019" + }, + { + "chainId": 1, + "address": "0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419", + "name": "DIA", + "symbol": "DIA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", + "priceUsd": "0.5350519303665007" + }, + { + "chainId": 1, + "address": "0x0AbdAce70D3790235af448C88547603b945604ea", + "name": "district0x", + "symbol": "DNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/849/thumb/district0x.png?1547223762", + "priceUsd": "0.023283129709207685" + }, + { + "chainId": 1, + "address": "0x1494CA1F11D487c2bBe4543E90080AeBa4BA3C2b", + "name": "DeFi Pulse Index", + "symbol": "DPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", + "priceUsd": "101.97356153284423" + }, + { + "chainId": 1, + "address": "0x3Ab6Ed69Ef663bd986Ee59205CCaD8A20F98b4c2", + "name": "Drep", + "symbol": "DREP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14578/thumb/KotgsCgS_400x400.jpg?1617094445", + "priceUsd": "0.00129017" + }, + { + "chainId": 1, + "address": "0xB1D1eae60EEA9525032a6DCb4c1CE336a1dE71BE", + "name": "Derive", + "symbol": "DRV", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", + "priceUsd": "0.039509073309452726" + }, + { + "chainId": 1, + "address": "0x92D6C1e31e14520e676a687F0a93788B716BEff5", + "name": "dYdX", + "symbol": "DYDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", + "priceUsd": "0.5405683524036875" + }, + { + "chainId": 1, + "address": "0x961C8c0B1aaD0c0b10a51FeF6a867E3091BCef17", + "name": "DeFi Yield Protocol", + "symbol": "DYP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13480/thumb/DYP_Logo_Symbol-8.png?1655809066", + "priceUsd": "0.004826650033929355" + }, + { + "chainId": 1, + "address": "0xec53bF9167f50cDEB3Ae105f56099aaaB9061F83", + "name": "EigenLayer", + "symbol": "EIGEN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37441/large/eigen.jpg?1728023974", + "priceUsd": "1.77" + }, + { + "chainId": 1, + "address": "0xe6fd75ff38Adca4B97FBCD938c86b98772431867", + "name": "Elastos", + "symbol": "ELA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2780/thumb/Elastos.png?1597048112", + "priceUsd": "1.76" + }, + { + "chainId": 1, + "address": "0x761D38e5ddf6ccf6Cf7c55759d5210750B5D60F3", + "name": "Dogelon Mars", + "symbol": "ELON", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14962/thumb/6GxcPRo3_400x400.jpg?1619157413", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x57e114B691Db790C35207b2e685D4A43181e6061", + "name": "Ethena", + "symbol": "ENA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/36530/standard/ethena.png", + "priceUsd": "0.557757" + }, + { + "chainId": 1, + "address": "0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c", + "name": "Enjin Coin", + "symbol": "ENJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1102/thumb/enjin-coin-logo.png?1547035078", + "priceUsd": "0.07409959001948155" + }, + { + "chainId": 1, + "address": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + "name": "Ethereum Name Service", + "symbol": "ENS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", + "priceUsd": "20.7" + }, + { + "chainId": 1, + "address": "0xE2AD0BF751834f2fbdC62A41014f84d67cA1de2A", + "name": "Caldera", + "symbol": "ERA", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54475/large/Token_Logo.png?1749676251", + "priceUsd": "0.504864607580487" + }, + { + "chainId": 1, + "address": "0xBBc2AE13b23d715c30720F079fcd9B4a74093505", + "name": "Ethernity Chain", + "symbol": "ERN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", + "priceUsd": "0.084402" + }, + { + "chainId": 1, + "address": "0xFe0c30065B384F05761f15d0CC899D4F9F9Cc0eB", + "name": "Ether.fi", + "symbol": "ETHFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35958/standard/etherfi.jpeg", + "priceUsd": "1.63" + }, + { + "chainId": 1, + "address": "0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b", + "name": "Euler", + "symbol": "EUL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26149/thumb/YCvKDfl8_400x400.jpeg?1656041509", + "priceUsd": "10.1" + }, + { + "chainId": 1, + "address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", + "name": "Euro Coin", + "symbol": "EURC", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", + "priceUsd": "1.1616257172492535" + }, + { + "chainId": 1, + "address": "0x888883b5F5D21fb10Dfeb70e8f9722B9FB0E5E51", + "name": "Schuman EUR P", + "symbol": "EUROP", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52132/large/europ-symbol-rgb.jpg?1732634862", + "priceUsd": "1.16" + }, + { + "chainId": 1, + "address": "0x8dF723295214Ea6f21026eeEb4382d475f146F9f", + "name": "Quantoz EURQ", + "symbol": "EURQ", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51853/large/EURQ_1000px_Color.png?1732071269", + "priceUsd": "1.16" + }, + { + "chainId": 1, + "address": "0x50753CfAf86c094925Bf976f218D043f8791e408", + "name": "StablR Euro", + "symbol": "EURR", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53720/large/stablreuro-logo.png?1737125898", + "priceUsd": "1.16" + }, + { + "chainId": 1, + "address": "0xa0246c9032bC3A600820415aE600c6388619A14D", + "name": "Harvest Finance", + "symbol": "FARM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", + "priceUsd": "26.858663627223446" + }, + { + "chainId": 1, + "address": "0xaea46A60368A7bD060eec7DF8CBa43b7EF41Ad85", + "name": "Fetch ai", + "symbol": "FET", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + "priceUsd": "0.528389" + }, + { + "chainId": 1, + "address": "0xef3A930e1FfFFAcd2fc13434aC81bD278B0ecC8d", + "name": "Stafi", + "symbol": "FIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", + "priceUsd": "0.08054002912988648" + }, + { + "chainId": 1, + "address": "0xcf0C122c6b73ff809C693DB761e7BaeBe62b6a2E", + "name": "FLOKI", + "symbol": "FLOKI", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x720CD16b011b987Da3518fbf38c3071d4F0D1495", + "name": "Flux", + "symbol": "FLUX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png", + "priceUsd": "0.1924176625030025" + }, + { + "chainId": 1, + "address": "0x41545f8b9472D758bB669ed8EaEEEcD7a9C4Ec29", + "name": "Forta", + "symbol": "FORT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", + "priceUsd": "0.044691842757873426" + }, + { + "chainId": 1, + "address": "0x77FbA179C79De5B7653F68b5039Af940AdA60ce0", + "name": "Ampleforth Governance Token", + "symbol": "FORTH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14917/thumb/photo_2021-04-22_00.00.03.jpeg?1619020835", + "priceUsd": "2.619398450138765" + }, + { + "chainId": 1, + "address": "0xc770EEfAd204B5180dF6a14Ee197D99d808ee52d", + "name": "ShapeShift FOX Token", + "symbol": "FOX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x853d955aCEf822Db058eb8505911ED77F175b99e", + "name": "Frax", + "symbol": "FRAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", + "priceUsd": "0.997483" + }, + { + "chainId": 1, + "address": "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", + "name": "Fantom", + "symbol": "FTM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", + "priceUsd": "0.2722528135568368" + }, + { + "chainId": 1, + "address": "0x8c15Ef5b4B21951d50E53E4fbdA8298FFAD25057", + "name": "Function X", + "symbol": "FX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8186/thumb/47271330_590071468072434_707260356350705664_n.jpg?1556096683", + "priceUsd": "0.083957" + }, + { + "chainId": 1, + "address": "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0", + "name": "Frax Share", + "symbol": "FXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", + "priceUsd": "2.13" + }, + { + "chainId": 1, + "address": "0x9C7BEBa8F6eF6643aBd725e45a4E8387eF260649", + "name": "Gravity", + "symbol": "G", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39200/large/gravity.jpg?1721020647", + "priceUsd": "0.00969701" + }, + { + "chainId": 1, + "address": "0x5fAa989Af96Af85384b8a938c2EdE4A7378D9875", + "name": "Galxe", + "symbol": "GAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", + "priceUsd": "0.5782186618036622" + }, + { + "chainId": 1, + "address": "0xd1d2Eb1B1e90B638588728b4130137D262C87cae", + "name": "GALA", + "symbol": "GALA", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/12493/standard/GALA-COINGECKO.png?1696512310", + "priceUsd": "0.01525303" + }, + { + "chainId": 1, + "address": "0xdab396cCF3d84Cf2D07C4454e10C8A6F5b008D2b", + "name": "Goldfinch", + "symbol": "GFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19081/thumb/GOLDFINCH.png?1634369662", + "priceUsd": "0.4738967034334735" + }, + { + "chainId": 1, + "address": "0x3F382DbD960E3a9bbCeaE22651E88158d2791550", + "name": "Aavegotchi", + "symbol": "GHST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", + "priceUsd": "0.37858344359672713" + }, + { + "chainId": 1, + "address": "0x7DD9c5Cba05E151C895FDe1CF355C9A1D5DA6429", + "name": "Golem", + "symbol": "GLM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/542/thumb/Golem_Submark_Positive_RGB.png?1606392013", + "priceUsd": "0.21798478278697406" + }, + { + "chainId": 1, + "address": "0x6810e776880C02933D47DB1b9fc05908e5386b96", + "name": "Gnosis Token", + "symbol": "GNO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", + "priceUsd": "147.07" + }, + { + "chainId": 1, + "address": "0xccC8cb5229B0ac8069C51fd58367Fd1e622aFD97", + "name": "Gods Unchained", + "symbol": "GODS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17139/thumb/10631.png?1635718182", + "priceUsd": "0.11146553704706548" + }, + { + "chainId": 1, + "address": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + "name": "The Graph", + "symbol": "GRT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", + "priceUsd": "0.0807830069851891" + }, + { + "chainId": 1, + "address": "0xDe30da39c46104798bB5aA3fe8B9e0e1F348163F", + "name": "Gitcoin", + "symbol": "GTC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", + "priceUsd": "0.2704470962395942" + }, + { + "chainId": 1, + "address": "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", + "name": "Gemini Dollar", + "symbol": "GUSD", + "decimals": 2, + "logoUrl": "https://assets.coingecko.com/coins/images/5992/thumb/gemini-dollar-gusd.png?1536745278", + "priceUsd": "0.9817744881585663" + }, + { + "chainId": 1, + "address": "0xC08512927D12348F6620a698105e1BAac6EcD911", + "name": "GYEN", + "symbol": "GYEN", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", + "priceUsd": "0.006394246618249068" + }, + { + "chainId": 1, + "address": "0xb3999F658C0391d94A37f7FF328F3feC942BcADC", + "name": "Hashflow", + "symbol": "HFT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26136/large/hashflow-icon-cmc.png", + "priceUsd": "0.07607860664961957" + }, + { + "chainId": 1, + "address": "0x71Ab77b7dbB4fa7e017BC15090b2163221420282", + "name": "Highstreet", + "symbol": "HIGH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", + "priceUsd": "0.45631568495537816" + }, + { + "chainId": 1, + "address": "0xF5581dFeFD8Fb0e4aeC526bE659CFaB1f8c781dA", + "name": "HOPR", + "symbol": "HOPR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14061/thumb/Shared_HOPR_logo_512px.png?1614073468", + "priceUsd": "0.0790552237984121" + }, + { + "chainId": 1, + "address": "0xB705268213D593B8FD88d3FDEFF93AFF5CbDcfAE", + "name": "IDEX", + "symbol": "IDEX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2565/thumb/logomark-purple-286x286.png?1638362736", + "priceUsd": "0.0243306535722624" + }, + { + "chainId": 1, + "address": "0x767FE9EDC9E0dF98E07454847909b5E959D7ca0E", + "name": "Illuvium", + "symbol": "ILV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", + "priceUsd": "14.02" + }, + { + "chainId": 1, + "address": "0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF", + "name": "Immutable X", + "symbol": "IMX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17233/thumb/imx.png?1636691817", + "priceUsd": "0.688854" + }, + { + "chainId": 1, + "address": "0x0954906da0Bf32d5479e25f46056d22f08464cab", + "name": "Index Cooperative", + "symbol": "INDEX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12729/thumb/index.png?1634894321", + "priceUsd": "1.0125325512507388" + }, + { + "chainId": 1, + "address": "0xe28b3B32B6c345A34Ff64674606124Dd5Aceca30", + "name": "Injective", + "symbol": "INJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", + "priceUsd": "12.22" + }, + { + "chainId": 1, + "address": "0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68", + "name": "Inverse Finance", + "symbol": "INV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14205/thumb/inverse_finance.jpg?1614921871", + "priceUsd": "36.83" + }, + { + "chainId": 1, + "address": "0x6fB3e0A217407EFFf7Ca062D46c26E5d60a14d69", + "name": "IoTeX", + "symbol": "IOTX", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", + "priceUsd": "0.023460682934752897" + }, + { + "chainId": 1, + "address": "0x23894DC9da6c94ECb439911cAF7d337746575A72", + "name": "Geojam", + "symbol": "JAM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24648/thumb/ey40AzBN_400x400.jpg?1648507272", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x7420B4b9a0110cdC71fB720908340C03F9Bc03EC", + "name": "JasmyCoin", + "symbol": "JASMY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", + "priceUsd": "0.01241309" + }, + { + "chainId": 1, + "address": "0x4B1E80cAC91e2216EEb63e29B957eB91Ae9C2Be8", + "name": "Jupiter", + "symbol": "JUP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/10351/thumb/logo512.png?1632480932", + "priceUsd": "0.0009570124316578048" + }, + { + "chainId": 1, + "address": "0x85Eee30c52B0b379b046Fb0F85F4f3Dc3009aFEC", + "name": "Keep Network", + "symbol": "KEEP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3373/thumb/IuNzUb5b_400x400.jpg?1589526336", + "priceUsd": "0.07196560259015167" + }, + { + "chainId": 1, + "address": "0x3f80B1c54Ae920Be41a77f8B902259D48cf24cCf", + "name": "KernelDAO", + "symbol": "KERNEL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54326/large/Kernel_token_logo_2x.png?1739827205", + "priceUsd": "0.2171433123141692" + }, + { + "chainId": 1, + "address": "0x4CC19356f2D37338b9802aa8E8fc58B0373296E7", + "name": "SelfKey", + "symbol": "KEY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2034/thumb/selfkey.png?1548608934", + "priceUsd": "0.0002413168419798915" + }, + { + "chainId": 1, + "address": "0xdd974D5C2e2928deA5F71b9825b8b646686BD200", + "name": "Kyber Network Crystal", + "symbol": "KNC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdd974D5C2e2928deA5F71b9825b8b646686BD200/logo.png", + "priceUsd": "0.32736813856347" + }, + { + "chainId": 1, + "address": "0x1cEB5cB57C4D4E2b2433641b95Dd330A33185A44", + "name": "Keep3rV1", + "symbol": "KP3R", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12966/thumb/kp3r_logo.jpg?1607057458", + "priceUsd": "4.43857845013025" + }, + { + "chainId": 1, + "address": "0x464eBE77c293E473B48cFe96dDCf88fcF7bFDAC0", + "name": "KRYLL", + "symbol": "KRL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2807/thumb/krl.png?1547036979", + "priceUsd": "0.29397641553826764" + }, + { + "chainId": 1, + "address": "0x96543ef8d2C75C26387c1a319ae69c0BEE6f3fe7", + "name": "Kujira", + "symbol": "KUJI", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", + "priceUsd": "0.178702" + }, + { + "chainId": 1, + "address": "0x88909D489678dD17aA6D9609F89B0419Bf78FD9a", + "name": "Layer3", + "symbol": "L3", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37768/large/Square.png", + "priceUsd": "0.02944509640772015" + }, + { + "chainId": 1, + "address": "0x0fc2a55d5BD13033f1ee0cdd11f60F7eFe66f467", + "name": "Lagrange", + "symbol": "LA", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/36510.png", + "priceUsd": "0.365682" + }, + { + "chainId": 1, + "address": "0x037A54AaB062628C9Bbae1FDB1583c195585fe41", + "name": "LCX", + "symbol": "LCX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9985/thumb/zRPSu_0o_400x400.jpg?1574327008", + "priceUsd": "0.131983" + }, + { + "chainId": 1, + "address": "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + "name": "Lido DAO", + "symbol": "LDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", + "priceUsd": "1.15" + }, + { + "chainId": 1, + "address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", + "name": "ChainLink Token", + "symbol": "LINK", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", + "priceUsd": "21.77" + }, + { + "chainId": 1, + "address": "0xb59490aB09A0f526Cc7305822aC65f2Ab12f9723", + "name": "Litentry", + "symbol": "LIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", + "priceUsd": "0.407593130417842" + }, + { + "chainId": 1, + "address": "0x61E90A50137E1F645c9eF4a0d3A4f01477738406", + "name": "League of Kingdoms", + "symbol": "LOKA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22572/thumb/loka_64pix.png?1642643271", + "priceUsd": "0.15290724774348907" + }, + { + "chainId": 1, + "address": "0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0", + "name": "Loom Network", + "symbol": "LOOM", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0/logo.png", + "priceUsd": "0.4629528254851496" + }, + { + "chainId": 1, + "address": "0x58b6A8A3302369DAEc383334672404Ee733aB239", + "name": "Livepeer", + "symbol": "LPT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", + "priceUsd": "6.202553939778055" + }, + { + "chainId": 1, + "address": "0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D", + "name": "Liquity", + "symbol": "LQTY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", + "priceUsd": "0.721197" + }, + { + "chainId": 1, + "address": "0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD", + "name": "LoopringCoin V2", + "symbol": "LRC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png", + "priceUsd": "0.0848570920963512" + }, + { + "chainId": 1, + "address": "0xd0a6053f087E87a25dC60701ba6E663b1a548E85", + "name": "BLOCKLORDS", + "symbol": "LRDS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/34775/standard/LRDS_PNG.png", + "priceUsd": "0.107052" + }, + { + "chainId": 1, + "address": "0x8c1BEd5b9a0928467c9B1341Da1D7BD5e10b6549", + "name": "Liquid Staked ETH", + "symbol": "LSETH", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/28848/large/LsETH-receipt-token-circle.png?1696527824", + "priceUsd": "4714.320645775342" + }, + { + "chainId": 1, + "address": "0x6033F7f88332B8db6ad452B7C6D5bB643990aE3f", + "name": "Lisk", + "symbol": "LSK", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/385/large/Lisk_logo.png?1722338450", + "priceUsd": "0.28087277175401854" + }, + { + "chainId": 1, + "address": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", + "name": "Liquity USD", + "symbol": "LUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", + "priceUsd": "1.0047396192340776" + }, + { + "chainId": 1, + "address": "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + "name": "Decentraland", + "symbol": "MANA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", + "priceUsd": "0.313836" + }, + { + "chainId": 1, + "address": "0x69af81e73A73B40adF4f3d4223Cd9b1ECE623074", + "name": "Mask Network", + "symbol": "MASK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", + "priceUsd": "1.2474156598581578" + }, + { + "chainId": 1, + "address": "0x08d967bb0134F2d07f7cfb6E246680c53927DD30", + "name": "MATH", + "symbol": "MATH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", + "priceUsd": "0.08366075498752316" + }, + { + "chainId": 1, + "address": "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + "name": "Polygon", + "symbol": "MATIC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", + "priceUsd": "0.23810539436048594" + }, + { + "chainId": 1, + "address": "0x949D48EcA67b17269629c7194F4b727d4Ef9E5d6", + "name": "Merit Circle", + "symbol": "MC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19304/thumb/Db4XqML.png?1634972154", + "priceUsd": "0.11317889058382306" + }, + { + "chainId": 1, + "address": "0xfC98e825A2264D890F9a1e68ed50E1526abCcacD", + "name": "Moss Carbon Credit", + "symbol": "MCO2", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14414/thumb/ENtxnThA_400x400.jpg?1615948522", + "priceUsd": "0.16624398173852997" + }, + { + "chainId": 1, + "address": "0x814e0908b12A99FeCf5BC101bB5d0b8B5cDf7d26", + "name": "Measurable Data Token", + "symbol": "MDT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2441/thumb/mdt_logo.png?1569813574", + "priceUsd": "0.020457521838596948" + }, + { + "chainId": 1, + "address": "0xb131f4A55907B10d1F0A50d8ab8FA09EC342cd74", + "name": "Memecoin", + "symbol": "MEME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32528/large/memecoin_(2).png", + "priceUsd": "0.0023939440579224596" + }, + { + "chainId": 1, + "address": "0x9E32b13ce7f2E80A01932B42553652E053D6ed8e", + "name": "Metis", + "symbol": "METIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", + "priceUsd": "12.77" + }, + { + "chainId": 1, + "address": "0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3", + "name": "Magic Internet Money", + "symbol": "MIM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", + "priceUsd": "1.0039587746240246" + }, + { + "chainId": 1, + "address": "0x09a3EcAFa817268f77BE1283176B946C4ff2E608", + "name": "Mirror Protocol", + "symbol": "MIR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13295/thumb/mirror_logo_transparent.png?1611554658", + "priceUsd": "0.011270136488385001" + }, + { + "chainId": 1, + "address": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + "name": "Maker", + "symbol": "MKR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png", + "priceUsd": "1573.44" + }, + { + "chainId": 1, + "address": "0xec67005c4E498Ec7f55E092bd1d35cbC47C91892", + "name": "Melon", + "symbol": "MLN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", + "priceUsd": "7.7460223309395335" + }, + { + "chainId": 1, + "address": "0x3c3a81e81dc49A522A592e7622A7E711c06bf354", + "name": "Mantle", + "symbol": "MNT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/30980/large/Mantle-Logo-mark.png?1739213200", + "priceUsd": "2.51" + }, + { + "chainId": 1, + "address": "0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a", + "name": "Mog Coin", + "symbol": "MOG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/31059/large/MOG_LOGO_200x200.png", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x275f5Ad03be0Fa221B4C6649B8AeE09a42D9412A", + "name": "Monavale", + "symbol": "MONA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13298/thumb/monavale_logo.jpg?1607232721", + "priceUsd": "68.6147426671038" + }, + { + "chainId": 1, + "address": "0x58D97B57BB95320F9a05dC918Aef65434969c2B2", + "name": "Morpho Token", + "symbol": "MORPHO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/29837/large/Morpho-token-icon.png?1726771230", + "priceUsd": "1.72" + }, + { + "chainId": 1, + "address": "0x3073f7aAA4DB83f95e9FFf17424F71D4751a3073", + "name": "Movement", + "symbol": "MOVE", + "decimals": 8, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/32452.png", + "priceUsd": "0.10849967087568456" + }, + { + "chainId": 1, + "address": "0x33349B282065b0284d756F0577FB39c158F935e6", + "name": "Maple", + "symbol": "MPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14097/thumb/photo_2021-05-03_14.20.41.jpeg?1620022863", + "priceUsd": "1.0011836814271482" + }, + { + "chainId": 1, + "address": "0xF433089366899D83a9f26A773D59ec7eCF30355e", + "name": "Metal", + "symbol": "MTL", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/763/thumb/Metal.png?1592195010", + "priceUsd": "0.43632525192703936" + }, + { + "chainId": 1, + "address": "0x65Ef703f5594D2573eb71Aaf55BC0CB548492df4", + "name": "Multichain", + "symbol": "MULTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", + "priceUsd": "0.49623364215086785" + }, + { + "chainId": 1, + "address": "0xe2f2a5C287993345a840Db3B0845fbC70f5935a5", + "name": "mStable USD", + "symbol": "MUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11576/thumb/mStable_USD.png?1595591803", + "priceUsd": "0.9812716083616795" + }, + { + "chainId": 1, + "address": "0xB6Ca7399B4F9CA56FC27cBfF44F4d2e4Eef1fc81", + "name": "Muse DAO", + "symbol": "MUSE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13230/thumb/muse_logo.png?1606460453", + "priceUsd": "8.758471004350765" + }, + { + "chainId": 1, + "address": "0xAE788F80F2756A86aa2F410C651F2aF83639B95b", + "name": "GensoKishi Metaverse", + "symbol": "MV", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", + "priceUsd": "0.00752695" + }, + { + "chainId": 1, + "address": "0x5Ca381bBfb58f0092df149bD3D243b08B9a8386e", + "name": "MXC", + "symbol": "MXC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4604/thumb/mxc.png?1655534336", + "priceUsd": "0.00133558610551269" + }, + { + "chainId": 1, + "address": "0x9E46A38F5DaaBe8683E10793b06749EEF7D733d1", + "name": "PolySwarm", + "symbol": "NCT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", + "priceUsd": "0.02426522278959423" + }, + { + "chainId": 1, + "address": "0x812Ba41e071C7b7fA4EBcFB62dF5F45f6fA853Ee", + "name": "Neiro", + "symbol": "Neiro", + "decimals": 9, + "logoUrl": "https://coin-images.coingecko.com/coins/images/39488/large/neiro.jpg?1731449567", + "priceUsd": "0.0002701" + }, + { + "chainId": 1, + "address": "0xD0eC028a3D21533Fdd200838F39c85B03679285D", + "name": "Newton", + "symbol": "NEWT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/66819/large/newton.jpg?1750642513", + "priceUsd": "0.195415" + }, + { + "chainId": 1, + "address": "0x5Cf04716BA20127F1E2297AdDCf4B5035000c9eb", + "name": "NKN", + "symbol": "NKN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3375/thumb/nkn.png?1548329212", + "priceUsd": "0.02482653136836031" + }, + { + "chainId": 1, + "address": "0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671", + "name": "Numeraire", + "symbol": "NMR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png", + "priceUsd": "15.65" + }, + { + "chainId": 1, + "address": "0x4fE83213D56308330EC302a8BD641f1d0113A4Cc", + "name": "NuCypher", + "symbol": "NU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3318/thumb/photo1198982838879365035.jpg?1547037916", + "priceUsd": "0.0582474623958541" + }, + { + "chainId": 1, + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "name": "Ocean Protocol", + "symbol": "OCEAN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", + "priceUsd": "0.252995" + }, + { + "chainId": 1, + "address": "0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26", + "name": "Origin Protocol", + "symbol": "OGN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", + "priceUsd": "0.057906" + }, + { + "chainId": 1, + "address": "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07", + "name": "OMG Network", + "symbol": "OMG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/776/thumb/OMG_Network.jpg?1591167168", + "priceUsd": "0.1473258357496226" + }, + { + "chainId": 1, + "address": "0x36E66fbBce51e4cD5bd3C62B637Eb411b18949D4", + "name": "Omni Network", + "symbol": "OMNI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/36465/standard/Symbol-Color.png?1711511095", + "priceUsd": "4.117278777562853" + }, + { + "chainId": 1, + "address": "0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3", + "name": "Ondo Finance", + "symbol": "ONDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656", + "priceUsd": "0.893449" + }, + { + "chainId": 1, + "address": "0x6F59e0461Ae5E2799F1fB3847f05a63B16d0DbF8", + "name": "ORCA Alliance", + "symbol": "ORCA", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/5183.png", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x0258F474786DdFd37ABCE6df6BBb1Dd5dfC4434a", + "name": "Orion Protocol", + "symbol": "ORN", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", + "priceUsd": "0.26345618305624396" + }, + { + "chainId": 1, + "address": "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", + "name": "Orchid", + "symbol": "OXT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4575f41308EC1483f3d399aa9a2826d74Da13Deb/logo.png", + "priceUsd": "0.05053889030415258" + }, + { + "chainId": 1, + "address": "0xc1D204d77861dEf49b6E769347a883B15EC397Ff", + "name": "PayperEx", + "symbol": "PAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1601/thumb/pax.png?1547035800", + "priceUsd": "0.0017497906655058052" + }, + { + "chainId": 1, + "address": "0x45804880De22913dAFE09f4980848ECE6EcbAf78", + "name": "PAX Gold", + "symbol": "PAXG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", + "priceUsd": "4057.02" + }, + { + "chainId": 1, + "address": "0x0D3CbED3f69EE050668ADF3D9Ea57241cBa33A2B", + "name": "PlayDapp", + "symbol": "PDA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14316/standard/PDA-symbol.png?1710234068", + "priceUsd": "0.0044866604939796054" + }, + { + "chainId": 1, + "address": "0x808507121B80c02388fAd14726482e061B8da827", + "name": "Pendle", + "symbol": "PENDLE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", + "priceUsd": "4.48" + }, + { + "chainId": 1, + "address": "0x6982508145454Ce325dDbE47a25d4ec3d2311933", + "name": "Pepe", + "symbol": "PEPE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1682922725", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0xbC396689893D065F41bc2C6EcbeE5e0085233447", + "name": "Perpetual Protocol", + "symbol": "PERP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", + "priceUsd": "0.28029" + }, + { + "chainId": 1, + "address": "0x7613C48E0cd50E42dD9Bf0f6c235063145f6f8DC", + "name": "Pirate Nation", + "symbol": "PIRATE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/38524/standard/_Pirate_Transparent_200x200.png", + "priceUsd": "0.01974249" + }, + { + "chainId": 1, + "address": "0xD8912C10681D8B21Fd3742244f44658dBA12264E", + "name": "Pluton", + "symbol": "PLU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1241/thumb/pluton.png?1548331624", + "priceUsd": "0.4207111768843342" + }, + { + "chainId": 1, + "address": "0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6", + "name": "Polygon Ecosystem Token", + "symbol": "POL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32440/large/polygon.png?1698233684", + "priceUsd": "0.238289" + }, + { + "chainId": 1, + "address": "0x83e6f1E41cdd28eAcEB20Cb649155049Fac3D5Aa", + "name": "Polkastarter", + "symbol": "POLS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", + "priceUsd": "0.1658678025888471" + }, + { + "chainId": 1, + "address": "0x9992eC3cF6A55b00978cdDF2b27BC6882d88D1eC", + "name": "Polymath", + "symbol": "POLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", + "priceUsd": "0.07416653780725273" + }, + { + "chainId": 1, + "address": "0x57B946008913B82E4dF85f501cbAeD910e58D26C", + "name": "Marlin", + "symbol": "POND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8903/thumb/POND_200x200.png?1622515451", + "priceUsd": "0.00768808934441551" + }, + { + "chainId": 1, + "address": "0x1Bbe973BeF3a977Fc51CbED703E8ffDEfE001Fed", + "name": "Portal", + "symbol": "PORTAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35436/standard/portal.jpeg", + "priceUsd": "0.037255619818756504" + }, + { + "chainId": 1, + "address": "0x595832F8FC6BF59c85C527fEC3740A1b7a361269", + "name": "Power Ledger", + "symbol": "POWR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", + "priceUsd": "0.1407286763918426" + }, + { + "chainId": 1, + "address": "0xb23d80f5FefcDDaa212212F028021B41DEd428CF", + "name": "Prime", + "symbol": "PRIME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29053/large/PRIMELOGOOO.png?1676976222", + "priceUsd": "1.32" + }, + { + "chainId": 1, + "address": "0x226bb599a12C826476e3A771454697EA52E9E220", + "name": "Propy", + "symbol": "PRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/869/thumb/propy.png?1548332100", + "priceUsd": "0.702793" + }, + { + "chainId": 1, + "address": "0x6BEF15D938d4E72056AC92Ea4bDD0D76B1C4ad29", + "name": "Succinct", + "symbol": "PROVE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/67905/large/succinct-logo.png?1754228574", + "priceUsd": "0.764369823768134" + }, + { + "chainId": 1, + "address": "0x362bc847A3a9637d3af6624EeC853618a43ed7D2", + "name": "PARSIQ", + "symbol": "PRQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", + "priceUsd": "0.04791819009420432" + }, + { + "chainId": 1, + "address": "0xfB5c6815cA3AC72Ce9F5006869AE67f18bF77006", + "name": "pSTAKE Finance", + "symbol": "PSTAKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/23931/thumb/PSTAKE_Dark.png?1645709930", + "priceUsd": "0.032128646831839934" + }, + { + "chainId": 1, + "address": "0x4d1C297d39C5c1277964D0E3f8Aa901493664530", + "name": "Puffer Finance", + "symbol": "PUFFER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/50630/large/puffer.jpg?1728545297", + "priceUsd": "0.15212479917503655" + }, + { + "chainId": 1, + "address": "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8", + "name": "PayPal USD", + "symbol": "PYUSD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/31212/large/PYUSD_Logo_%282%29.png?1691458314", + "priceUsd": "0.99982" + }, + { + "chainId": 1, + "address": "0x4a220E6096B25EADb88358cb44068A3248254675", + "name": "Quant", + "symbol": "QNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3370/thumb/5ZOu7brX_400x400.jpg?1612437252", + "priceUsd": "101.79" + }, + { + "chainId": 1, + "address": "0x4123a133ae3c521FD134D7b13A2dEC35b56c2463", + "name": "Qredo", + "symbol": "QRDO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/17541/thumb/qrdo.png?1630637735", + "priceUsd": "0.00034751978050065" + }, + { + "chainId": 1, + "address": "0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d", + "name": "Quantstamp", + "symbol": "QSP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1219/thumb/0_E0kZjb4dG4hUnoDD_.png?1604815917", + "priceUsd": "0.0010119653917936282" + }, + { + "chainId": 1, + "address": "0x6c28AeF8977c9B773996d0e8376d2EE379446F2f", + "name": "Quickswap", + "symbol": "QUICK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13970/thumb/1_pOU6pBMEmiL-ZJVb0CYRjQ.png?1613386659", + "priceUsd": "22.86629961711038" + }, + { + "chainId": 1, + "address": "0x31c8EAcBFFdD875c74b94b077895Bd78CF1E64A3", + "name": "Radicle", + "symbol": "RAD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14013/thumb/radicle.png?1614402918", + "priceUsd": "0.619813" + }, + { + "chainId": 1, + "address": "0x03ab458634910AaD20eF5f1C8ee96F1D6ac54919", + "name": "Rai Reflex Index", + "symbol": "RAI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", + "priceUsd": "4.9331629264933525" + }, + { + "chainId": 1, + "address": "0xba5BDe662c17e2aDFF1075610382B9B691296350", + "name": "SuperRare", + "symbol": "RARE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17753/thumb/RARE.jpg?1629220534", + "priceUsd": "0.049153759507244106" + }, + { + "chainId": 1, + "address": "0xFca59Cd816aB1eaD66534D82bc21E7515cE441CF", + "name": "Rarible", + "symbol": "RARI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", + "priceUsd": "0.8234048885917141" + }, + { + "chainId": 1, + "address": "0xA4EED63db85311E22dF4473f87CcfC3DaDCFA3E3", + "name": "Rubic", + "symbol": "RBC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12629/thumb/200x200.png?1607952509", + "priceUsd": "0.003771143534073512" + }, + { + "chainId": 1, + "address": "0x6123B0049F904d730dB3C36a31167D9d4121fA6B", + "name": "Ribbon Finance", + "symbol": "RBN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15823/thumb/RBN_64x64.png?1633529723", + "priceUsd": "0.12036" + }, + { + "chainId": 1, + "address": "0xc43C6bfeDA065fE2c4c11765Bf838789bd0BB5dE", + "name": "Redstone", + "symbol": "RED", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53640/large/RedStone_Logo_New_White.png?1740640919", + "priceUsd": "0.471204" + }, + { + "chainId": 1, + "address": "0x408e41876cCCDC0F92210600ef50372656052a38", + "name": "Republic Token", + "symbol": "REN", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", + "priceUsd": "0.007042263184421685" + }, + { + "chainId": 1, + "address": "0x1985365e9f78359a9B6AD760e32412f4a445E862", + "name": "Reputation Augur v1", + "symbol": "REP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1985365e9f78359a9B6AD760e32412f4a445E862/logo.png", + "priceUsd": "1.145405351695479" + }, + { + "chainId": 1, + "address": "0x221657776846890989a759BA2973e427DfF5C9bB", + "name": "Reputation Augur v2", + "symbol": "REPv2", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x221657776846890989a759BA2973e427DfF5C9bB/logo.png", + "priceUsd": "1.161385273043465" + }, + { + "chainId": 1, + "address": "0x8f8221aFbB33998d8584A2B05749bA73c37a938a", + "name": "Request", + "symbol": "REQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1031/thumb/Request_icon_green.png?1643250951", + "priceUsd": "0.12918514167848896" + }, + { + "chainId": 1, + "address": "0x557B933a7C2c45672B610F8954A3deB39a51A8Ca", + "name": "REVV", + "symbol": "REVV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", + "priceUsd": "0.02096099219292354" + }, + { + "chainId": 1, + "address": "0x3B50805453023a91a8bf641e279401a0b23FA6F9", + "name": "Renzo", + "symbol": "REZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37327/standard/renzo_200x200.png?1714025012", + "priceUsd": "0.0140748" + }, + { + "chainId": 1, + "address": "0xD291E7a03283640FDc51b121aC401383A46cC623", + "name": "Rari Governance Token", + "symbol": "RGT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12900/thumb/Rari_Logo_Transparent.png?1613978014", + "priceUsd": "4.38412770723148" + }, + { + "chainId": 1, + "address": "0x607F4C5BB672230e8672085532f7e901544a7375", + "name": "iExec RLC", + "symbol": "RLC", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/646/thumb/pL1VuXm.png?1604543202", + "priceUsd": "1.0467243114425002" + }, + { + "chainId": 1, + "address": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD", + "name": "RLUSD", + "symbol": "RLUSD", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/39651/large/RLUSD_200x200_(1).png?1727376633", + "priceUsd": "0.999476" + }, + { + "chainId": 1, + "address": "0xf1f955016EcbCd7321c7266BccFB96c68ea5E49b", + "name": "Rally", + "symbol": "RLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12843/thumb/image.png?1611212077", + "priceUsd": "0.0009478678742264927" + }, + { + "chainId": 1, + "address": "0x6De037ef9aD2725EB40118Bb1702EBb27e4Aeb24", + "name": "Render Token", + "symbol": "RNDR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", + "priceUsd": "3.25" + }, + { + "chainId": 1, + "address": "0xfA5047c9c78B8877af97BDcb85Db743fD7313d4a", + "name": "Rook", + "symbol": "ROOK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13005/thumb/keeper_dao_logo.jpg?1604316506", + "priceUsd": "16.62577043964562" + }, + { + "chainId": 1, + "address": "0xD33526068D116cE69F19A9ee46F0bd304F21A51f", + "name": "Rocket Pool Protocol", + "symbol": "RPL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/2090/large/rocket_pool_%28RPL%29.png?1696503058", + "priceUsd": "4.834628228805114" + }, + { + "chainId": 1, + "address": "0x320623b8E4fF03373931769A31Fc52A4E78B5d70", + "name": "Reserve Rights", + "symbol": "RSR", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", + "priceUsd": "0.00582109" + }, + { + "chainId": 1, + "address": "0x5aFE3855358E112B5647B952709E6165e1c1eEEe", + "name": "Safe", + "symbol": "SAFE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/27032/standard/Artboard_1_copy_8circle-1.png?1696526084", + "priceUsd": "0.357523" + }, + { + "chainId": 1, + "address": "0x3845badAde8e6dFF049820680d1F14bD3903a5d0", + "name": "The Sandbox", + "symbol": "SAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", + "priceUsd": "0.262065" + }, + { + "chainId": 1, + "address": "0x30D20208d987713f46DFD34EF128Bb16C404D10f", + "name": "Stader", + "symbol": "SD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", + "priceUsd": "0.4928079141534901" + }, + { + "chainId": 1, + "address": "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", + "name": "Shiba Inu", + "symbol": "SHIB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11939/thumb/shiba.png?1622619446", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x7C84e62859D0715eb77d1b1C4154Ecd6aBB21BEC", + "name": "Shping", + "symbol": "SHPING", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2588/thumb/r_yabKKi_400x400.jpg?1639470164", + "priceUsd": "0.004139983917264716" + }, + { + "chainId": 1, + "address": "0x00c83aeCC790e8a4453e5dD3B0B4b3680501a7A7", + "name": "SKALE", + "symbol": "SKL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13245/thumb/SKALE_token_300x300.png?1606789574", + "priceUsd": "0.02373514752710204" + }, + { + "chainId": 1, + "address": "0x56072C95FAA701256059aa122697B133aDEd9279", + "name": "SKY Governance Token", + "symbol": "SKY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39925/large/sky.jpg?1724827980", + "priceUsd": "0.066406" + }, + { + "chainId": 1, + "address": "0xCC8Fa225D80b9c7D42F96e9570156c65D6cAAa25", + "name": "Smooth Love Potion", + "symbol": "SLP", + "decimals": 0, + "logoUrl": "https://assets.coingecko.com/coins/images/10366/thumb/SLP.png?1578640057", + "priceUsd": "0.0015687414522509533" + }, + { + "chainId": 1, + "address": "0x744d70FDBE2Ba4CF95131626614a1763DF805B9E", + "name": "Status", + "symbol": "SNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", + "priceUsd": "0.021898746567394167" + }, + { + "chainId": 1, + "address": "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F", + "name": "Synthetix Network Token", + "symbol": "SNX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", + "priceUsd": "1.087" + }, + { + "chainId": 1, + "address": "0x23B608675a2B2fB1890d3ABBd85c5775c51691d5", + "name": "Unisocks", + "symbol": "SOCKS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/10717/thumb/qFrcoiM.png?1582525244", + "priceUsd": "12071.871166689514" + }, + { + "chainId": 1, + "address": "0xD31a59c85aE9D8edEFeC411D448f90841571b89c", + "name": "SOL Wormhole ", + "symbol": "SOL", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", + "priceUsd": "223.82158850406506" + }, + { + "chainId": 1, + "address": "0x090185f2135308BaD17527004364eBcC2D37e5F6", + "name": "Spell Token", + "symbol": "SPELL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", + "priceUsd": "0.00043460246896488155" + }, + { + "chainId": 1, + "address": "0xc20059e0317DE91738d13af027DfC4a50781b066", + "name": "Spark", + "symbol": "SPK", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/38637/large/Spark-Logomark-RGB.png?1744878896", + "priceUsd": "0.04685871" + }, + { + "chainId": 1, + "address": "0xE0f63A424a4439cBE457D80E4f4b51aD25b2c56C", + "name": "SPX6900", + "symbol": "SPX", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", + "priceUsd": "1.45" + }, + { + "chainId": 1, + "address": "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", + "name": "Stargate Finance", + "symbol": "STG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", + "priceUsd": "0.203163" + }, + { + "chainId": 1, + "address": "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC", + "name": "Storj Token", + "symbol": "STORJ", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", + "priceUsd": "0.2253795522833661" + }, + { + "chainId": 1, + "address": "0xCa14007Eff0dB1f8135f4C25B34De49AB0d42766", + "name": "Starknet", + "symbol": "STRK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26433/standard/starknet.png?1696525507", + "priceUsd": "0.156915" + }, + { + "chainId": 1, + "address": "0x006BeA43Baa3f7A6f765F14f10A1a1b08334EF45", + "name": "Stox", + "symbol": "STX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1230/thumb/stox-token.png?1547035256", + "priceUsd": "0.00065308" + }, + { + "chainId": 1, + "address": "0x0763fdCCF1aE541A5961815C0872A8c5Bc6DE4d7", + "name": "SUKU", + "symbol": "SUKU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11969/thumb/UmfW5S6f_400x400.jpg?1596602238", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0xe53EC727dbDEB9E2d5456c3be40cFF031AB40A55", + "name": "SuperFarm", + "symbol": "SUPER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", + "priceUsd": "0.564243" + }, + { + "chainId": 1, + "address": "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51", + "name": "Synth sUSD", + "symbol": "sUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", + "priceUsd": "0.996658" + }, + { + "chainId": 1, + "address": "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2", + "name": "Sushi", + "symbol": "SUSHI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "priceUsd": "0.686039" + }, + { + "chainId": 1, + "address": "0x0a6E7Ba5042B38349e437ec6Db6214AEC7B35676", + "name": "Swell", + "symbol": "SWELL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/28777/large/swell1.png?1727899715", + "priceUsd": "0.00801714707941713" + }, + { + "chainId": 1, + "address": "0x0bb217E40F8a5Cb79Adf04E1aAb60E5abd0dfC1e", + "name": "SWFTCOIN", + "symbol": "SWFTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/2346/thumb/SWFTCoin.jpg?1618392022", + "priceUsd": "0.00829267" + }, + { + "chainId": 1, + "address": "0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9", + "name": "Swipe", + "symbol": "SXP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9368/thumb/swipe.png?1566792311", + "priceUsd": "0.07637650836772185" + }, + { + "chainId": 1, + "address": "0xE6Bfd33F52d82Ccb5b37E16D3dD81f9FFDAbB195", + "name": "Space and Time", + "symbol": "SXT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/55424/large/sxt-token_circle.jpg?1745935919", + "priceUsd": "0.06838082143034907" + }, + { + "chainId": 1, + "address": "0xf293d23BF2CDc05411Ca0edDD588eb1977e8dcd4", + "name": "Sylo", + "symbol": "SYLO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/6430/thumb/SYLO.svg?1589527756", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x0f2D719407FdBeFF09D87557AbB7232601FD9F29", + "name": "Synapse", + "symbol": "SYN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", + "priceUsd": "0.10894392420379893" + }, + { + "chainId": 1, + "address": "0x643C4E15d7d62Ad0aBeC4a9BD4b001aA3Ef52d66", + "name": "Syrup Token", + "symbol": "SYRUP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/51232/standard/IMG_7420.png?1730831572", + "priceUsd": "0.394949" + }, + { + "chainId": 1, + "address": "0xCdF7028ceAB81fA0C6971208e83fa7872994beE5", + "name": "Threshold Network", + "symbol": "T", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22228/thumb/nFPNiSbL_400x400.jpg?1641220340", + "priceUsd": "0.01511999" + }, + { + "chainId": 1, + "address": "0x18084fbA666a33d37592fA2633fD49a74DD93a88", + "name": "tBTC", + "symbol": "tBTC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/0x18084fbA666a33d37592fA2633fD49a74DD93a88/logo.png", + "priceUsd": "122133" + }, + { + "chainId": 1, + "address": "0xC3d21f79C3120A4fFda7A535f8005a7c297799bF", + "name": "Term Finance", + "symbol": "TERM", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/38142/large/terms.png?1716630303", + "priceUsd": "0.4385490642962406" + }, + { + "chainId": 1, + "address": "0x485d17A6f1B8780392d53D64751824253011A260", + "name": "ChronoTech", + "symbol": "TIME", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/604/thumb/time-32x32.png?1627130666", + "priceUsd": "10.22" + }, + { + "chainId": 1, + "address": "0x888888848B652B3E3a0f34c96E00EEC0F3a23F72", + "name": "Alien Worlds", + "symbol": "TLM", + "decimals": 4, + "logoUrl": "https://assets.coingecko.com/coins/images/14676/thumb/kY-C4o7RThfWrDQsLCAG4q4clZhBDDfJQVhWUEKxXAzyQYMj4Jmq1zmFwpRqxhAJFPOa0AsW_PTSshoPuMnXNwq3rU7Imp15QimXTjlXMx0nC088mt1rIwRs75GnLLugWjSllxgzvQ9YrP4tBgclK4_rb17hjnusGj_c0u2fx0AvVokjSNB-v2poTj0xT9BZRCbzRE3-lF1.jpg?1617700061", + "priceUsd": "0.004928335519167" + }, + { + "chainId": 1, + "address": "0x2e9d63788249371f1DFC918a52f8d799F4a38C94", + "name": "Tokemak", + "symbol": "TOKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17495/thumb/tokemak-avatar-200px-black.png?1628131614", + "priceUsd": "0.235122" + }, + { + "chainId": 1, + "address": "0x4507cEf57C46789eF8d1a19EA45f4216bae2B528", + "name": "TokenFi", + "symbol": "TOKEN", + "decimals": 9, + "logoUrl": "https://coin-images.coingecko.com/coins/images/32507/large/MAIN_TokenFi_logo_icon.png?1698918427", + "priceUsd": "0.01213972" + }, + { + "chainId": 1, + "address": "0x2Ab6Bb8408ca3199B8Fa6C92d5b455F820Af03c4", + "name": "TE FOOD", + "symbol": "TONE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2325/thumb/tec.png?1547036538", + "priceUsd": "0.0007407287127434519" + }, + { + "chainId": 1, + "address": "0xaA7a9CA87d3694B5755f213B5D04094b8d0F0A6F", + "name": "OriginTrail", + "symbol": "TRAC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1877/thumb/TRAC.jpg?1635134367", + "priceUsd": "0.488233" + }, + { + "chainId": 1, + "address": "0x88dF592F8eb5D7Bd38bFeF7dEb0fBc02cf3778a0", + "name": "Tellor", + "symbol": "TRB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", + "priceUsd": "32.37765888806222" + }, + { + "chainId": 1, + "address": "0x77146784315Ba81904d654466968e3a7c196d1f3", + "name": "Treehouse Token", + "symbol": "TREE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/67664/large/TREE_logo.png?1753601041", + "priceUsd": "0.2434068837048001" + }, + { + "chainId": 1, + "address": "0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", + "name": "Tribe", + "symbol": "TRIBE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14575/thumb/tribe.PNG?1617487954", + "priceUsd": "0.6305304192551023" + }, + { + "chainId": 1, + "address": "0x4C19596f5aAfF459fA38B0f7eD92F11AE6543784", + "name": "TrueFi", + "symbol": "TRU", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/13180/thumb/truefi_glyph_color.png?1617610941", + "priceUsd": "0.0267452" + }, + { + "chainId": 1, + "address": "0xA35923162C49cF95e6BF26623385eb431ad920D3", + "name": "Turbo", + "symbol": "TURBO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/30117/large/TurboMark-QL_200.png?1708079597", + "priceUsd": "0.00355453" + }, + { + "chainId": 1, + "address": "0xd084B83C305daFD76AE3E1b4E1F1fe2eCcCb3988", + "name": "The Virtua Kolect", + "symbol": "TVK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13330/thumb/virtua_original.png?1656043619", + "priceUsd": "0.025295534498726888" + }, + { + "chainId": 1, + "address": "0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828", + "name": "UMA Voting Token v1", + "symbol": "UMA", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828/logo.png", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x441761326490cACF7aF299725B6292597EE822c2", + "name": "Unifi Protocol DAO", + "symbol": "UNFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13152/thumb/logo-2.png?1605748967", + "priceUsd": "0.144828696111073" + }, + { + "chainId": 1, + "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "name": "Uniswap", + "symbol": "UNI", + "decimals": 18, + "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", + "priceUsd": "7.84" + }, + { + "chainId": 1, + "address": "0x70D2b7C19352bB76e4409858FF5746e500f2B67c", + "name": "Pawtocol", + "symbol": "UPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12186/thumb/pawtocol.jpg?1597962008", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d", + "name": "World Liberty Financial USD", + "symbol": "USD1", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54977/large/USD1_1000x1000_transparent.png?1749297002", + "priceUsd": "0.999627" + }, + { + "chainId": 1, + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.999705" + }, + { + "chainId": 1, + "address": "0xe343167631d89B6Ffc58B88d6b7fB0228795491D", + "name": "Global Dollar", + "symbol": "USDG", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/51281/large/GDN_USDG_Token_200x200.png", + "priceUsd": "0.9816562571382551" + }, + { + "chainId": 1, + "address": "0x8E870D67F660D95d5be530380D0eC0bd388289E1", + "name": "Pax Dollar", + "symbol": "USDP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", + "priceUsd": "1.5569929672028677" + }, + { + "chainId": 1, + "address": "0xc83e27f270cce0A3A3A29521173a83F402c1768b", + "name": "Quantoz USDQ", + "symbol": "USDQ", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51852/large/USDQ_1000px_Color.png?1732071232", + "priceUsd": "0.999799" + }, + { + "chainId": 1, + "address": "0x7B43E3875440B44613DC3bC08E7763e6Da63C8f8", + "name": "StablR USD", + "symbol": "USDR", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53721/large/stablrusd-logo.png?1737126629", + "priceUsd": "0.999648" + }, + { + "chainId": 1, + "address": "0xdC035D45d973E3EC169d2276DDab16f1e407384F", + "name": "USDS Stablecoin", + "symbol": "USDS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39926/large/usds.webp?1726666683", + "priceUsd": "0.999736" + }, + { + "chainId": 1, + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "1" + }, + { + "chainId": 1, + "address": "0xC4441c2BE5d8fA8126822B9929CA0b81Ea0DE38E", + "name": "USUAL", + "symbol": "USUAL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51091/large/USUAL.jpg?1730035787", + "priceUsd": "0.04850135" + }, + { + "chainId": 1, + "address": "0x8DE5B80a0C1B02Fe4976851D030B36122dbb8624", + "name": "VANRY", + "symbol": "VANRY", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/33466/large/apple-touch-icon.png?1701942541", + "priceUsd": "0.0257143" + }, + { + "chainId": 1, + "address": "0x3C4B6E6e1eA3D4863700D7F76b36B7f3D3f13E3d", + "name": "Voyager Token", + "symbol": "VGX", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/794/thumb/Voyager-vgx.png?1575693595", + "priceUsd": "0.7756022866139018" + }, + { + "chainId": 1, + "address": "0xEDB171C18cE90B633DB442f2A6F72874093b49Ef", + "name": "Wrapped Ampleforth", + "symbol": "WAMPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", + "priceUsd": "2.070203016662237" + }, + { + "chainId": 1, + "address": "0xf983da3ca66964C02628189Ea8Ca99fa9E24f66c", + "name": "Wrapped Analog One Token", + "symbol": "WANLOG", + "decimals": 12, + "logoUrl": "https://assets.kraken.com/marketing/web/icons-uni-webp/s_anlog.webp?i=kds", + "priceUsd": "0.0010479955980391838" + }, + { + "chainId": 1, + "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "name": "Wrapped BTC", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", + "priceUsd": "122699" + }, + { + "chainId": 1, + "address": "0xc221b7E65FfC80DE234bbB6667aBDd46593D34F0", + "name": "Wrapped Centrifuge", + "symbol": "WCFG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17106/thumb/WCFG.jpg?1626266462", + "priceUsd": "0.34796245117239233" + }, + { + "chainId": 1, + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 1, + "address": "0xdA5e1988097297dCdc1f90D4dFE7909e847CBeF6", + "name": "World Liberty Financial", + "symbol": "WLFI", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/50767/large/wlfi.png?1756438915", + "priceUsd": "0.178407" + }, + { + "chainId": 1, + "address": "0x4691937a7508860F876c9c0a2a617E7d9E945D4B", + "name": "WOO Network", + "symbol": "WOO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", + "priceUsd": "0.066112" + }, + { + "chainId": 1, + "address": "0xA2cd3D43c775978A96BdBf12d733D5A1ED94fb18", + "name": "Chain", + "symbol": "XCN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", + "priceUsd": "0.01066584" + }, + { + "chainId": 1, + "address": "0x70e8dE73cE538DA2bEEd35d14187F6959a8ecA96", + "name": "XSGD", + "symbol": "XSGD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", + "priceUsd": "0.9610249026701526" + }, + { + "chainId": 1, + "address": "0x55296f69f40Ea6d20E478533C15A6B08B654E758", + "name": "XYO Network", + "symbol": "XYO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", + "priceUsd": "0.00881705" + }, + { + "chainId": 1, + "address": "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e", + "name": "yearn finance", + "symbol": "YFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", + "priceUsd": "5320.34" + }, + { + "chainId": 1, + "address": "0xa1d0E215a23d7030842FC67cE582a6aFa3CCaB83", + "name": "DFI money", + "symbol": "YFII", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11902/thumb/YFII-logo.78631676.png?1598677348", + "priceUsd": "75.88501013667818" + }, + { + "chainId": 1, + "address": "0x25f8087EAD173b73D6e8B84329989A8eEA16CF73", + "name": "Yield Guild Games", + "symbol": "YGG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17358/thumb/le1nzlO6_400x400.jpg?1632465691", + "priceUsd": "0.161404" + }, + { + "chainId": 1, + "address": "0xf091867EC603A6628eD83D274E835539D82e9cc8", + "name": "Zetachain", + "symbol": "Zeta", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26718/standard/Twitter_icon.png?1696525788", + "priceUsd": "0.17037924273459923" + }, + { + "chainId": 1, + "address": "0x6985884C4392D348587B19cb9eAAf157F13271cd", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": "2.34" + }, + { + "chainId": 1, + "address": "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + "name": "0x Protocol Token", + "symbol": "ZRX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png", + "priceUsd": "0.24666301528626908" + }, + { + "chainId": 10, + "address": "0x76FB31fb4af56892A25e32cFC43De717950c9278", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": "276.06" + }, + { + "chainId": 10, + "address": "0x9C9e5fD8bbc25984B178FdCE6117Defa39d2db39", + "name": "Binance USD", + "symbol": "BUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", + "priceUsd": "1.3663341169185133" + }, + { + "chainId": 10, + "address": "0x9b88D293b7a791E40d36A39765FFd5A1B9b5c349", + "name": "Celo native asset (Wormhole)", + "symbol": "CELO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", + "priceUsd": "0.3250550212080028" + }, + { + "chainId": 10, + "address": "0x14778860E937f509e651192a90589dE711Fb88a9", + "name": "CYBER", + "symbol": "CYBER", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31274/large/token.png?1715826754", + "priceUsd": "1.52" + }, + { + "chainId": 10, + "address": "0x33800De7E817A70A694F31476313A7c572BBa100", + "name": "Derive", + "symbol": "DRV", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", + "priceUsd": "0.03596978" + }, + { + "chainId": 10, + "address": "0x3A18dcC9745eDcD1Ef33ecB93b0b6eBA5671e7Ca", + "name": "Kujira", + "symbol": "KUJI", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", + "priceUsd": "0.16075783074532105" + }, + { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000042", + "name": "Optimism", + "symbol": "OP", + "decimals": 18, + "logoUrl": "https://ethereum-optimism.github.io/data/OP/logo.png", + "priceUsd": "0.700864" + }, + { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", + "priceUsd": "0.999705" + }, + { + "chainId": 10, + "address": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "name": "USDCoin (Bridged from Ethereum)", + "symbol": "USDC.e", + "decimals": 6, + "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", + "priceUsd": "0.999447" + }, + { + "chainId": 10, + "address": "0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db", + "name": "Velodrome Finance", + "symbol": "VELO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12538/standard/Logo_200x_200.png?1696512350", + "priceUsd": "0.04545114" + }, + { + "chainId": 10, + "address": "0xeF4461891DfB3AC8572cCf7C794664A8DD927945", + "name": "WalletConnect", + "symbol": "WCT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/50390/large/wc-token1.png?1727569464", + "priceUsd": "0.251522" + }, + { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 10, + "address": "0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1", + "name": "Worldcoin", + "symbol": "WLD", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31069/large/worldcoin.jpeg?1696529903", + "priceUsd": "1.21" + }, + { + "chainId": 10, + "address": "0x6985884C4392D348587B19cb9eAAf157F13271cd", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": "2.34" + }, + { + "chainId": 56, + "address": "0x111111111117dC0aa78b770fA6A738034120C302", + "name": "1inch", + "symbol": "1INCH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceUsd": "0.248156" + }, + { + "chainId": 56, + "address": "0xfb6115445Bff7b52FeB98650C87f44907E58f802", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": "276.06" + }, + { + "chainId": 56, + "address": "0xBc7d6B50616989655AfD682fb42743507003056D", + "name": "Alchemy Pay", + "symbol": "ACH", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/12390/thumb/ACH_%281%29.png?1599691266", + "priceUsd": "0.01847294" + }, + { + "chainId": 56, + "address": "0x6bfF4Fb161347ad7de4A625AE5aa3A1CA7077819", + "name": "Ambire AdEx", + "symbol": "ADX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", + "priceUsd": "0.10140869664084146" + }, + { + "chainId": 56, + "address": "0x12f31B73D812C6Bb0d735a218c086d44D5fe5f89", + "name": "agEur", + "symbol": "agEUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", + "priceUsd": "1.1351829697860463" + }, + { + "chainId": 56, + "address": "0x33d08D8C7a168333a85285a68C0042b39fC3741D", + "name": "AIOZ Network", + "symbol": "AIOZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", + "priceUsd": "0.269038" + }, + { + "chainId": 56, + "address": "0x82D2f8E02Afb160Dd5A480a617692e62de9038C4", + "name": "Aleph im", + "symbol": "ALEPH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", + "priceUsd": "0.064669" + }, + { + "chainId": 56, + "address": "0xAC51066d7bEC65Dc4589368da368b212745d63E8", + "name": "My Neighbor Alice", + "symbol": "ALICE", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", + "priceUsd": "0.3723838300657885" + }, + { + "chainId": 56, + "address": "0xa1faa113cbE53436Df28FF0aEe54275c13B40975", + "name": "Alpha Venture DAO", + "symbol": "ALPHA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", + "priceUsd": "0.014495105590404275" + }, + { + "chainId": 56, + "address": "0xf307910A4c7bbc79691fD374889b36d8531B08e3", + "name": "Ankr", + "symbol": "ANKR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", + "priceUsd": "0.01366072" + }, + { + "chainId": 56, + "address": "0x6F769E65c14Ebd1f68817F5f1DcDb61Cfa2D6f7e", + "name": "ARPA Chain", + "symbol": "ARPA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", + "priceUsd": "0.02054787" + }, + { + "chainId": 56, + "address": "0xA2120b9e674d3fC3875f415A7DF52e382F141225", + "name": "Automata", + "symbol": "ATA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", + "priceUsd": "0.039703888909643666" + }, + { + "chainId": 56, + "address": "0x8b1f4432F943c465A973FeDC6d7aa50Fc96f1f65", + "name": "Axelar", + "symbol": "AXL", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", + "priceUsd": "0.2871530110598361" + }, + { + "chainId": 56, + "address": "0x715D400F88C167884bbCc41C5FeA407ed4D2f8A0", + "name": "Axie Infinity", + "symbol": "AXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", + "priceUsd": "2.11" + }, + { + "chainId": 56, + "address": "0x935a544Bf5816E3A7C13DB2EFe3009Ffda0aCdA2", + "name": "Bluzelle", + "symbol": "BLZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2848/thumb/ColorIcon_3x.png?1622516510", + "priceUsd": "0.03070011" + }, + { + "chainId": 56, + "address": "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", + "name": "Binance USD", + "symbol": "BUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", + "priceUsd": "1.025" + }, + { + "chainId": 56, + "address": "0xaEC945e04baF28b135Fa7c640f624f8D90F1C3a6", + "name": "Coin98", + "symbol": "C98", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17117/thumb/logo.png?1626412904", + "priceUsd": null + }, + { + "chainId": 56, + "address": "0xf9CeC8d50f6c8ad3Fb6dcCEC577e05aA32B224FE", + "name": "Chromia", + "symbol": "CHR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/5000/thumb/Chromia.png?1559038018", + "priceUsd": "0.08382" + }, + { + "chainId": 56, + "address": "0x09E889BB4D5b474f561db0491C38702F367A4e4d", + "name": "Clover Finance", + "symbol": "CLV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15278/thumb/clover.png?1645084454", + "priceUsd": "0.015005887353143784" + }, + { + "chainId": 56, + "address": "0x52CE071Bd9b1C4B00A0b92D298c512478CaD67e8", + "name": "Compound", + "symbol": "COMP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", + "priceUsd": "41.24120729375521" + }, + { + "chainId": 56, + "address": "0xd15CeE1DEaFBad6C0B3Fd7489677Cc102B141464", + "name": "Circuits of Value", + "symbol": "COVAL", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/588/thumb/coval-logo.png?1599493950", + "priceUsd": "0.00064329" + }, + { + "chainId": 56, + "address": "0x8dA443F84fEA710266C8eB6bC34B71702d033EF2", + "name": "Cartesi", + "symbol": "CTSI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", + "priceUsd": "0.07261" + }, + { + "chainId": 56, + "address": "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3", + "name": "Dai Stablecoin", + "symbol": "DAI", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", + "priceUsd": "0.999392" + }, + { + "chainId": 56, + "address": "0x23CE9e926048273eF83be0A3A8Ba9Cb6D45cd978", + "name": "Mines of Dalarnia", + "symbol": "DAR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/19837/thumb/dar.png?1636014223", + "priceUsd": "0.08443555353020463" + }, + { + "chainId": 56, + "address": "0xe91a8D2c584Ca93C7405F15c22CdFE53C29896E3", + "name": "DexTools", + "symbol": "DEXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", + "priceUsd": "0.467603" + }, + { + "chainId": 56, + "address": "0x99956D38059cf7bEDA96Ec91Aa7BB2477E0901DD", + "name": "DIA", + "symbol": "DIA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", + "priceUsd": "0.531795" + }, + { + "chainId": 56, + "address": "0xEC583f25A049CC145dA9A256CDbE9B6201a705Ff", + "name": "Drep", + "symbol": "DREP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14578/thumb/KotgsCgS_400x400.jpg?1617094445", + "priceUsd": "0.00129017" + }, + { + "chainId": 56, + "address": "0x961C8c0B1aaD0c0b10a51FeF6a867E3091BCef17", + "name": "DeFi Yield Protocol", + "symbol": "DYP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13480/thumb/DYP_Logo_Symbol-8.png?1655809066", + "priceUsd": "0.00482137" + }, + { + "chainId": 56, + "address": "0x7bd6FaBD64813c48545C9c0e312A0099d9be2540", + "name": "Dogelon Mars", + "symbol": "ELON", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14962/thumb/6GxcPRo3_400x400.jpg?1619157413", + "priceUsd": null + }, + { + "chainId": 56, + "address": "0x4B5C23cac08a567ecf0c1fFcA8372A45a5D33743", + "name": "Harvest Finance", + "symbol": "FARM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", + "priceUsd": "26.5" + }, + { + "chainId": 56, + "address": "0x031b41e504677879370e9DBcF937283A8691Fa7f", + "name": "Fetch ai", + "symbol": "FET", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + "priceUsd": "0.528389" + }, + { + "chainId": 56, + "address": "0xfb5B838b6cfEEdC2873aB27866079AC55363D37E", + "name": "FLOKI", + "symbol": "FLOKI", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318", + "priceUsd": null + }, + { + "chainId": 56, + "address": "0x90C97F71E18723b0Cf0dfa30ee176Ab653E89F40", + "name": "Frax", + "symbol": "FRAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", + "priceUsd": "1.0049182173515283" + }, + { + "chainId": 56, + "address": "0xAD29AbB318791D579433D831ed122aFeAf29dcfe", + "name": "Fantom", + "symbol": "FTM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", + "priceUsd": "0.28903552563851953" + }, + { + "chainId": 56, + "address": "0xe48A3d7d0Bc88d552f730B62c006bC925eadB9eE", + "name": "Frax Share", + "symbol": "FXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", + "priceUsd": "2.12" + }, + { + "chainId": 56, + "address": "0xe4Cc45Bb5DBDA06dB6183E8bf016569f40497Aa5", + "name": "Galxe", + "symbol": "GAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", + "priceUsd": "0.6165" + }, + { + "chainId": 56, + "address": "0x44Ec807ce2F4a6F2737A92e985f318d035883e47", + "name": "Hashflow", + "symbol": "HFT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26136/large/hashflow-icon-cmc.png", + "priceUsd": "0.07057965602448696" + }, + { + "chainId": 56, + "address": "0x5f4Bde007Dc06b867f86EBFE4802e34A1fFEEd63", + "name": "Highstreet", + "symbol": "HIGH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", + "priceUsd": "0.45628092069831305" + }, + { + "chainId": 56, + "address": "0xa2B726B1145A4773F68593CF171187d8EBe4d495", + "name": "Injective", + "symbol": "INJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", + "priceUsd": "12.22" + }, + { + "chainId": 56, + "address": "0x0231f91e02DebD20345Ae8AB7D71A41f8E140cE7", + "name": "Jupiter", + "symbol": "JUP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/10351/thumb/logo512.png?1632480932", + "priceUsd": "0.00089806" + }, + { + "chainId": 56, + "address": "0x073690e6CE25bE816E68F32dCA3e11067c9FB5Cc", + "name": "Kujira", + "symbol": "KUJI", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", + "priceUsd": "0.178702" + }, + { + "chainId": 56, + "address": "0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD", + "name": "ChainLink Token", + "symbol": "LINK", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", + "priceUsd": "21.77" + }, + { + "chainId": 56, + "address": "0x2eD9a5C8C13b93955103B9a7C167B67Ef4d568a3", + "name": "Mask Network", + "symbol": "MASK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", + "priceUsd": "1.25" + }, + { + "chainId": 56, + "address": "0xF218184Af829Cf2b0019F8E6F0b2423498a36983", + "name": "MATH", + "symbol": "MATH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", + "priceUsd": "0.082979" + }, + { + "chainId": 56, + "address": "0xCC42724C6683B7E57334c4E856f4c9965ED682bD", + "name": "Polygon", + "symbol": "MATIC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", + "priceUsd": "0.23384472184469995" + }, + { + "chainId": 56, + "address": "0x949D48EcA67b17269629c7194F4b727d4Ef9E5d6", + "name": "Merit Circle", + "symbol": "MC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19304/thumb/Db4XqML.png?1634972154", + "priceUsd": "0.106562" + }, + { + "chainId": 56, + "address": "0xe552Fb52a4F19e44ef5A967632DBc320B0820639", + "name": "Metis", + "symbol": "METIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", + "priceUsd": "0.010838862909664453" + }, + { + "chainId": 56, + "address": "0xfE19F0B51438fd612f6FD59C1dbB3eA319f433Ba", + "name": "Magic Internet Money", + "symbol": "MIM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", + "priceUsd": "1.0107794374125223" + }, + { + "chainId": 56, + "address": "0x5B6DcF557E2aBE2323c48445E8CC948910d8c2c9", + "name": "Mirror Protocol", + "symbol": "MIR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13295/thumb/mirror_logo_transparent.png?1611554658", + "priceUsd": "0.01173196" + }, + { + "chainId": 56, + "address": "0x9Fb9a33956351cf4fa040f65A13b835A3C8764E3", + "name": "Multichain", + "symbol": "MULTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", + "priceUsd": "0.05004492060438173" + }, + { + "chainId": 56, + "address": "0x4e7f408be2d4E9D60F49A64B89Bb619c84C7c6F5", + "name": "Perpetual Protocol", + "symbol": "PERP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", + "priceUsd": "0.13016145807432467" + }, + { + "chainId": 56, + "address": "0x7e624FA0E1c4AbFD309cC15719b7E2580887f570", + "name": "Polkastarter", + "symbol": "POLS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", + "priceUsd": "0.164847" + }, + { + "chainId": 56, + "address": "0xd21d29B38374528675C34936bf7d5Dd693D2a577", + "name": "PARSIQ", + "symbol": "PRQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", + "priceUsd": "0.00709433" + }, + { + "chainId": 56, + "address": "0x4C882ec256823eE773B25b414d36F92ef58a7c0C", + "name": "pSTAKE Finance", + "symbol": "PSTAKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/23931/thumb/PSTAKE_Dark.png?1645709930", + "priceUsd": "0.015506478778893242" + }, + { + "chainId": 56, + "address": "0x833F307aC507D47309fD8CDD1F835BeF8D702a93", + "name": "REVV", + "symbol": "REVV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", + "priceUsd": "0.00104648" + }, + { + "chainId": 56, + "address": "0x3BC5AC0dFdC871B365d159f728dd1B9A0B5481E8", + "name": "Stader", + "symbol": "SD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", + "priceUsd": "0.497549" + }, + { + "chainId": 56, + "address": "0xfA54fF1a158B5189Ebba6ae130CEd6bbd3aEA76e", + "name": "SOL Wormhole ", + "symbol": "SOL", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", + "priceUsd": "219.93824557760686" + }, + { + "chainId": 56, + "address": "0xB0D502E938ed5f4df2E681fE6E419ff29631d62b", + "name": "Stargate Finance", + "symbol": "STG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", + "priceUsd": "0.20219556602945252" + }, + { + "chainId": 56, + "address": "0x51BA0b044d96C3aBfcA52B64D733603CCC4F0d4D", + "name": "SuperFarm", + "symbol": "SUPER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", + "priceUsd": "0.35208615538562527" + }, + { + "chainId": 56, + "address": "0x947950BcC74888a40Ffa2593C5798F11Fc9124C4", + "name": "Sushi", + "symbol": "SUSHI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "priceUsd": "0.6794621380930607" + }, + { + "chainId": 56, + "address": "0xE64E30276C2F826FEbd3784958d6Da7B55DfbaD3", + "name": "SWFTCOIN", + "symbol": "SWFTC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2346/thumb/SWFTCoin.jpg?1618392022", + "priceUsd": "0.008268848283498451" + }, + { + "chainId": 56, + "address": "0x47BEAd2563dCBf3bF2c9407fEa4dC236fAbA485A", + "name": "Swipe", + "symbol": "SXP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9368/thumb/swipe.png?1566792311", + "priceUsd": "0.020228734327283678" + }, + { + "chainId": 56, + "address": "0xa4080f1778e69467E905B8d6F72f6e441f9e9484", + "name": "Synapse", + "symbol": "SYN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", + "priceUsd": "0.108305" + }, + { + "chainId": 56, + "address": "0x3b198e26E473b8faB2085b37978e36c9DE5D7f68", + "name": "ChronoTech", + "symbol": "TIME", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/604/thumb/time-32x32.png?1627130666", + "priceUsd": "10.22" + }, + { + "chainId": 56, + "address": "0x2222227E22102Fe3322098e4CBfE18cFebD57c95", + "name": "Alien Worlds", + "symbol": "TLM", + "decimals": 4, + "logoUrl": "https://assets.coingecko.com/coins/images/14676/thumb/kY-C4o7RThfWrDQsLCAG4q4clZhBDDfJQVhWUEKxXAzyQYMj4Jmq1zmFwpRqxhAJFPOa0AsW_PTSshoPuMnXNwq3rU7Imp15QimXTjlXMx0nC088mt1rIwRs75GnLLugWjSllxgzvQ9YrP4tBgclK4_rb17hjnusGj_c0u2fx0AvVokjSNB-v2poTj0xT9BZRCbzRE3-lF1.jpg?1617700061", + "priceUsd": "0.004193545947459356" + }, + { + "chainId": 56, + "address": "0x728C5baC3C3e370E372Fc4671f9ef6916b814d8B", + "name": "Unifi Protocol DAO", + "symbol": "UNFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13152/thumb/logo-2.png?1605748967", + "priceUsd": "0.171649" + }, + { + "chainId": 56, + "address": "0xBf5140A22578168FD562DCcF235E5D43A02ce9B1", + "name": "Uniswap", + "symbol": "UNI", + "decimals": 18, + "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", + "priceUsd": "7.84" + }, + { + "chainId": 56, + "address": "0x0D35A2B85c5A63188d566D104bEbf7C694334Ee4", + "name": "Pawtocol", + "symbol": "UPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12186/thumb/pawtocol.jpg?1597962008", + "priceUsd": null + }, + { + "chainId": 56, + "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.999709" + }, + { + "chainId": 56, + "address": "0x55d398326f99059fF775485246999027B3197955", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "0.999968" + }, + { + "chainId": 56, + "address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + "name": "Wrapped BNB", + "symbol": "WBNB", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/smartchain/assets/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c/logo.png", + "priceUsd": "1281.22" + }, + { + "chainId": 56, + "address": "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4373.98" + }, + { + "chainId": 56, + "address": "0x4691937a7508860F876c9c0a2a617E7d9E945D4B", + "name": "WOO Network", + "symbol": "WOO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", + "priceUsd": "0.06561889846517358" + }, + { + "chainId": 56, + "address": "0x7324c7C0d95CEBC73eEa7E85CbAac0dBdf88a05b", + "name": "Chain", + "symbol": "XCN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", + "priceUsd": "0.01066584" + }, + { + "chainId": 56, + "address": "0x6985884C4392D348587B19cb9eAAf157F13271cd", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": "2.3437558551854827" + }, + { + "chainId": 130, + "address": "0xbe41cde1C5e75a7b6c2c70466629878aa9ACd06E", + "name": "1inch", + "symbol": "1INCH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x44D618C366D7bC85945Bfc922ACad5B1feF7759A", + "name": "Ancient8", + "symbol": "A8", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39170/standard/A8_Token-04_200x200.png?1720798300", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x02a24C380dA560E4032Dc6671d8164cfbEEAAE1e", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xDDCe42b89215548beCaA160048460747Fe5675bC", + "name": "Arcblock", + "symbol": "ABT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2341/thumb/arcblock.png?1547036543", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb8A8e137A2dAa25EF1B3577b6598fE8Be66Ecf77", + "name": "Alchemy Pay", + "symbol": "ACH", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/12390/thumb/ACH_%281%29.png?1599691266", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x34424B3352af905e41078a4029b61EDe62BbB32C", + "name": "Across Protocol Token", + "symbol": "ACX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x3e1C572d8b069fc2f14ac4f8bdCE6e8eA299A500", + "name": "Ambire AdEx", + "symbol": "ADX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xfd38ac2316f6d3631a86065aDb3292f6f15873B5", + "name": "Aergo", + "symbol": "AERGO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4490/thumb/aergo.png?1647696770", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x54FA9210cCB765639b7Fd532f25bCb1060D60F8B", + "name": "Aevo", + "symbol": "AEVO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35893/standard/aevo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xA4eeF95995F40aD0b3D63a474293Fc7CC681A118", + "name": "agEur", + "symbol": "agEUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x14421614587A2A3e9C3Aa3131Fc396aF412721CF", + "name": "Adventure Gold", + "symbol": "AGLD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5F891E74947b0FC400128E5E85333d7a6cF99b1A", + "name": "AIOZ Network", + "symbol": "AIOZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xbf194C82A5Bb9180f9280c1832f886a65Aebdcd6", + "name": "Alchemix", + "symbol": "ALCX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14113/thumb/Alchemix.png?1614409874", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa3E646211a456e08829C33fcE21cC3DC4c15Bb5c", + "name": "Aleph im", + "symbol": "ALEPH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2a87dd1e1F849ed88C18565AFDa98e2EEEc73780", + "name": "Alethea Artificial Liquid Intelligence", + "symbol": "ALI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBb72B8031F590748d8910Aad7e25F8B18860960a", + "name": "My Neighbor Alice", + "symbol": "ALICE", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x44c3E7c49C4Bb6f4f5eCD87E035176dFceBD78d3", + "name": "Alpha Venture DAO", + "symbol": "ALPHA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6D5De04F1a3E0e554B9A15059d03e20cb3589153", + "name": "AltLayer", + "symbol": "ALT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/34608/standard/Logomark_200x200.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4D6B8ecb576dF9BB4bF6E6764A469a762bBc967F", + "name": "Amp", + "symbol": "AMP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12409/thumb/amp-200x200.png?1599625397", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf081Fc8E0878D7eBe6ec381E5d7279d6EFf97622", + "name": "Ankr", + "symbol": "ANKR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x865d184885200B8e86eb2a3Da8b3B4a7d4A31308", + "name": "Aragon", + "symbol": "ANT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/681/thumb/JelZ58cv_400x400.png?1601449653", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD1b8423FdE5F37464FadE603f80903cB314046cf", + "name": "ApeCoin", + "symbol": "APE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xA63122b27308EED0C1D83DD355ADdaA7f678961b", + "name": "API3", + "symbol": "API3", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13256/thumb/api3.jpg?1606751424", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xcDfcE5eb357E8976A80Be84E94a03BA963b9e379", + "name": "Apu Apustaja", + "symbol": "APU", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/35986/large/200x200.png?1710308147", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5cC70a9DF8E293aFFb14DFCa1e7F851418a4b40d", + "name": "Arbitrum", + "symbol": "ARB", + "decimals": 18, + "logoUrl": "https://arbitrum.foundation/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x59F16BaA7A22f49c32680661e0041A53442Ef089", + "name": "Arkham", + "symbol": "ARKM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/30929/standard/Arkham_Logo_CG.png?1696529771", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xE911A809F87490406AB34fad701aabCA88e30b45", + "name": "ARPA Chain", + "symbol": "ARPA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4b355De6Ea44711f0353Ed89545705395a30d7Fb", + "name": "ASH", + "symbol": "ASH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15714/thumb/omnPqaTY.png?1622820503", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1e196D83e2c562de0b1f270Eb72220335bA0ADa7", + "name": "Assemble Protocol", + "symbol": "ASM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11605/thumb/gpvrlkSq_400x400_%281%29.jpg?1591775789", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7F3F14A49FE5D5009E4e0a09e76cB8468C09Ae56", + "name": "AirSwap", + "symbol": "AST", + "decimals": 4, + "logoUrl": "https://assets.coingecko.com/coins/images/1019/thumb/Airswap.png?1630903484", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBAAa314d2f5Af29B00867a612F24F816d890C4B2", + "name": "Automata", + "symbol": "ATA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa249732271cbA6E06Be4ac8B20f0D465FeE183Ab", + "name": "Aethir Token", + "symbol": "ATH", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/36179/large/logogram_circle_dark_green_vb_green_(1).png?1718232706", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x82F90996a4F67Eb388116B3C6F35B6Ea91BeF68E", + "name": "Bounce", + "symbol": "AUCTION", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13860/thumb/1_KtgpRIJzuwfHe0Rl0avP_g.jpeg?1612412025", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x48b8441dE79cEE3604b805093B41028d3c81684B", + "name": "Audius", + "symbol": "AUDIO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12913/thumb/AudiusCoinLogo_2x.png?1603425727", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x38DBf47e2a012a4b83823f15E3F3352A00939999", + "name": "Artverse Token", + "symbol": "AVT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19727/thumb/ewnektoB_400x400.png?1635767094", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xbF678793522638F7439aFE3B94d2D2A3a4cBF2C9", + "name": "Axelar", + "symbol": "AXL", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xDA63AdA216d2079B54F2047B2FdC2576D188f927", + "name": "Axie Infinity", + "symbol": "AXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc2a564b44b441D03f09f5B6B2b358B4a17388406", + "name": "Badger DAO", + "symbol": "BADGER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x01625E26274Ed828Ac1d47694c97221b34a8ADdF", + "name": "Balancer", + "symbol": "BAL", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa264F2b88C630f260AbDcAb577eAB7266A8857d5", + "name": "Band Protocol", + "symbol": "BAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9545/thumb/band-protocol.png?1568730326", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4e373C99199773f9D92d32B8c8Bc0C81508ea589", + "name": "Basic Attention Token", + "symbol": "BAT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe5ECB192f1aE5839eD49886F36dFA670f9500824", + "name": "Beam", + "symbol": "BEAM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32417/standard/chain-logo.png?1698114384", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x604Ff88ADC02325EFb7f93DB3E442dc81D0588E7", + "name": "Biconomy", + "symbol": "BICO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x17f3AfE72cAa6b9090801b60607918b6D2Fa7cdc", + "name": "Big Time", + "symbol": "BIGTIME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32251/standard/-6136155493475923781_121.jpg?1696998691", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xA4Cb2aaf7503641B441e80fC353e6748fb523A5C", + "name": "BitDAO", + "symbol": "BIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17627/thumb/rI_YptK8.png?1653983088", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x41f6e69166e81A9583DBc96604B01D2E9B3D706f", + "name": "HarryPotterObamaSonic10Inu", + "symbol": "BITCOIN", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x942fC6b61686e06fB411cB1bCf5d16DC2b9255eA", + "name": "Blur", + "symbol": "BLUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28453/large/blur.png?1670745921", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe7b3Ca9d9Db06E1867781fd1C5F02E6c8eF471ee", + "name": "Bluzelle", + "symbol": "BLZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2848/thumb/ColorIcon_3x.png?1622516510", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf2Cc2D274dA528AB64DA86bE3f8416E5472c5a62", + "name": "Bancor Network Token", + "symbol": "BNT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBE8E46422fB7F9Ca9D639B3109492D64BbB41b05", + "name": "Boba Network", + "symbol": "BOBA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20285/thumb/BOBA.png?1636811576", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4d5b7e9CCE3Ab81298dA7E1F52b48c9a61Df8972", + "name": "BarnBridge", + "symbol": "BOND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBbE97f3522101e5B6976cBf77376047097BA837F", + "name": "BONK", + "symbol": "BONK", + "decimals": 5, + "logoUrl": "https://assets.coingecko.com/coins/images/28600/standard/bonk.jpg?1696527587", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6A4a359C7453F5892392FCb8eAB7A9A100986B71", + "name": "Braintrust", + "symbol": "BTRST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18100/thumb/braintrust.PNG?1630475394", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa4da5c92F44422dFA3E2E309b53d93bbbDa9f9c6", + "name": "Binance USD", + "symbol": "BUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x29129fa2e0F35594ca7b362fFA8c80f5f8e4f8E1", + "name": "Coin98", + "symbol": "C98", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17117/thumb/logo.png?1626412904", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb6A3E8e5715fd4c99EcEDaaAe121bDe4Ab6a1Ef1", + "name": "Coinbase Wrapped BTC", + "symbol": "cbBTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/40143/standard/cbbtc.webp", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xEb64b50FeF2A363940369285F86Ae9a68211db59", + "name": "Coinbase Wrapped Staked ETH", + "symbol": "cbETH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/27008/large/cbeth.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6008F5BaD83742fDbFf5AAc55e3c51b65A8A8D9C", + "name": "Celo native asset (Wormhole)", + "symbol": "CELO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5AD5d6B1AE6761Aab12066b51D21729248035703", + "name": "Celer Network", + "symbol": "CELR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4379/thumb/Celr.png?1554705437", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xAC930Be88cFAc775A937E9291c4234Bf210a4e5b", + "name": "Chromia", + "symbol": "CHR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/5000/thumb/Chromia.png?1559038018", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb0C69e24450e29afa8008962052007E08b2396b0", + "name": "Chiliz", + "symbol": "CHZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8834/thumb/Chiliz.png?1561970540", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD7212097f6d6B195a9Bc350b8dCE28a7fA41404C", + "name": "Clover Finance", + "symbol": "CLV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15278/thumb/clover.png?1645084454", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xdf78e4F0A8279942ca68046476919A90f2288656", + "name": "Compound", + "symbol": "COMP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", + "priceUsd": "36.57273628569128" + }, + { + "chainId": 130, + "address": "0xc63612B3e697AEeC61C3Ce9baEc0f9Db32F499C3", + "name": "COTI", + "symbol": "COTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2962/thumb/Coti.png?1559653863", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2562DC34c21371613CEF236b321EE63fCC295beC", + "name": "Circuits of Value", + "symbol": "COVAL", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/588/thumb/coval-logo.png?1599493950", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xC3a97c76AA194711E05Ff1d181534090B26D3996", + "name": "CoW Protocol", + "symbol": "COW", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24384/large/CoW-token_logo.png?1719524382", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xF8E7B485CE10D3C7Ac30B8444B98a0cC423dFb57", + "name": "Clearpool", + "symbol": "CPOOL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19252/large/photo_2022-08-31_12.45.02.jpeg?1696518697", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6C28eeB9E018011d3841f42c5b458713621F90C1", + "name": "Covalent", + "symbol": "CQT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14168/thumb/covalent-cqt.png?1624545218", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x73c63A80Ec77BFe31eEc6663828C4beaA30dE818", + "name": "Cronos", + "symbol": "CRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7e7784f13029c7C4BF4746112B1A503818B0D066", + "name": "Crypterium", + "symbol": "CRPT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1901/thumb/crypt.png?1547036205", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xAC73671a1762FE835208Fb93b7aE7490d1c2cCb3", + "name": "Curve DAO Token", + "symbol": "CRV", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa7073F530856cD32c2037150dd9763B9BAaED2C5", + "name": "Cartesi", + "symbol": "CTSI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x36fA435F6def83cbB7a0706d035C9eA062fCb619", + "name": "Cryptex Finance", + "symbol": "CTX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14932/thumb/glossy_icon_-_C200px.png?1619073171", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xE60e9b2E68297d5DF6B383fEe787B7fB92c2F8aF", + "name": "Somnium Space CUBEs", + "symbol": "CUBE", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/10687/thumb/CUBE_icon.png?1617026861", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x35C458aD1e3e68d2717C8349b985384Be85a01Ed", + "name": "Civic", + "symbol": "CVC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1C6789F30e7E335c2Eca2c75EC193aDBF0087Ea5", + "name": "Convex Finance", + "symbol": "CVX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8E29E12B46FeE20E034fE1e812bc12EFf14E5A09", + "name": "Covalent X Token", + "symbol": "CXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39177/large/CXT_Ticker.png?1720829918", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x20CAb320A855b39F724131C69424240519573f81", + "name": "Dai Stablecoin", + "symbol": "DAI", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", + "priceUsd": "1.0005967057868332" + }, + { + "chainId": 130, + "address": "0x2ef0775A19d1bc2258653fc5529F8f8490288086", + "name": "Mines of Dalarnia", + "symbol": "DAR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/19837/thumb/dar.png?1636014223", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x91ED4bb192e3461E45575730508525083A270265", + "name": "DerivaDAO", + "symbol": "DDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13453/thumb/ddx_logo.png?1608741641", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x45a4f750d806498A4c7f7B5267815aaC328e874C", + "name": "Dent", + "symbol": "DENT", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/1152/thumb/gLCEA2G.png?1604543239", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x17C38207334011a131b0Acf200E35Cd81723cddd", + "name": "DexTools", + "symbol": "DEXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4bdc8553cf14EEBCD489cD1d75b7FF463f9543c2", + "name": "DIA", + "symbol": "DIA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x0eb07cE7a28FF84DF132fb5ee5F56Aabc1b9E545", + "name": "district0x", + "symbol": "DNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/849/thumb/district0x.png?1547223762", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xE274f564c37aE15fd2570D544102eD4ACd2f84f1", + "name": "DeFi Pulse Index", + "symbol": "DPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x56aF109D597eb0a0F79ebCD0786Dd88C38EA9Ee7", + "name": "Drep", + "symbol": "DREP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14578/thumb/KotgsCgS_400x400.jpg?1617094445", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x601b11907EAa8d3785C0b10b41C3a7315faeB82c", + "name": "dYdX", + "symbol": "DYDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBdaD8E37a9600F0A35976fE61608a4C89D598610", + "name": "DeFi Yield Protocol", + "symbol": "DYP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13480/thumb/DYP_Logo_Symbol-8.png?1655809066", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc89ab9B82610BB9b748F6757b8F3ac59d016C47D", + "name": "EigenLayer", + "symbol": "EIGEN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37441/large/eigen.jpg?1728023974", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x24aBc32215354Ba3eD224bfa6312E31dD8E8c1ab", + "name": "Elastos", + "symbol": "ELA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2780/thumb/Elastos.png?1597048112", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x91441fE1415B00bEA8930A4354Fe00c426C1DE05", + "name": "Dogelon Mars", + "symbol": "ELON", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14962/thumb/6GxcPRo3_400x400.jpg?1619157413", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x9116E70d613860D349495d9Ef8e2AE1cA6cBD2dd", + "name": "Ethena", + "symbol": "ENA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/36530/standard/ethena.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x9A0D1b7594CAAF0A9e4687cAc9fF4E0B84a6d0A6", + "name": "Enjin Coin", + "symbol": "ENJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1102/thumb/enjin-coin-logo.png?1547035078", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x80756FAf1e7Fec5678bf505670eF176AB5F0383a", + "name": "Ethereum Name Service", + "symbol": "ENS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", + "priceUsd": "39.92084867155041" + }, + { + "chainId": 130, + "address": "0x5E5903C236E6873EB8400C3d1979271Fa93cdB03", + "name": "Ethernity Chain", + "symbol": "ERN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xF8740269F121327D03ff77BeD03a9A3258880821", + "name": "Ether.fi", + "symbol": "ETHFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35958/standard/etherfi.jpeg", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6319F47719b6713b1624C1b3A8e2DBf15b5D03FE", + "name": "Euler", + "symbol": "EUL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26149/thumb/YCvKDfl8_400x400.jpeg?1656041509", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x72f34BC403a005A9Be390762EAa46ED42813B0a8", + "name": "Euro Coin", + "symbol": "EURC", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xEc42461D9BbDF4eFB6481099253bBB7324D7d72d", + "name": "Quantoz EURQ", + "symbol": "EURQ", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51853/large/EURQ_1000px_Color.png?1732071269", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7A1ef7fD6E0d708295D8FD0C30Fd437d9C36FB5f", + "name": "StablR Euro", + "symbol": "EURR", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53720/large/stablreuro-logo.png?1737125898", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x472E8be16Cc9823b9f6a73A34EA55c0c31ee825F", + "name": "Harvest Finance", + "symbol": "FARM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x45343279DefDAd803d81C06fBCf87936DDD7DFE7", + "name": "Fetch ai", + "symbol": "FET", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xec9Be303f204864145CCC193aEb21B5fa10764A6", + "name": "Stafi", + "symbol": "FIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1b3EC249dc44a64bF5Cb8Afdd70e30c26c51fA81", + "name": "FLOKI", + "symbol": "FLOKI", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xB20fD6fD28e1430f98a8C1e9A83C88E5D87D94e5", + "name": "Forta", + "symbol": "FORT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xFa004fa2ad8Ef993C2B0412baB776b182220F12e", + "name": "Ampleforth Governance Token", + "symbol": "FORTH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14917/thumb/photo_2021-04-22_00.00.03.jpeg?1619020835", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe0BB1924C17b39B71758F49a00D7c0363B7a318E", + "name": "ShapeShift FOX Token", + "symbol": "FOX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8c7879bf25D678D9949F305857bD4437d74132B9", + "name": "Frax", + "symbol": "FRAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe99235A02958637a5e01575297fBBa3790dC7F0e", + "name": "Fantom", + "symbol": "FTM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6F32725F82Bbb06FFdC04974db437fec1d7af1Af", + "name": "Function X", + "symbol": "FX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8186/thumb/47271330_590071468072434_707260356350705664_n.jpg?1556096683", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x79301DF2117C7F56859fD01b28bBAA61062021D6", + "name": "Frax Share", + "symbol": "FXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x481cB2C560fc3351833b582b92b965626fd8803C", + "name": "Gravity", + "symbol": "G", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39200/large/gravity.jpg?1721020647", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x70b2b785061d4c91C76CF87692f85B5c443d8675", + "name": "Galxe", + "symbol": "GAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x31A71801291774d267615f74b3a44FCEB560FAc9", + "name": "GALA", + "symbol": "GALA", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/12493/standard/GALA-COINGECKO.png?1696512310", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x0328A0255866706547B79072DEE54976b157d3D0", + "name": "Goldfinch", + "symbol": "GFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19081/thumb/GOLDFINCH.png?1634369662", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4aE5712A153fDfDE81C305fF7f2E4e59840aD24B", + "name": "Aavegotchi", + "symbol": "GHST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x04b747f478AE09AC797d026C8402f409E2C9f2b9", + "name": "Golem", + "symbol": "GLM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/542/thumb/Golem_Submark_Positive_RGB.png?1606392013", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xC4c6c3A3043Ad5ECe5c91290630A7735e125a938", + "name": "Gnosis Token", + "symbol": "GNO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6E74EA6546e1f21Abf581b59114f2Bf5d3683f48", + "name": "Gods Unchained", + "symbol": "GODS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17139/thumb/10631.png?1635718182", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBb2272Ffc0Ef8F439373aDffD45c3591B3204D71", + "name": "The Graph", + "symbol": "GRT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x592620d454a10c47274dBfe3BD922b9a8fE5cf48", + "name": "Gitcoin", + "symbol": "GTC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xEbA12eC786Cdc21b4bd5ba601B595b6A5C0920a9", + "name": "Gemini Dollar", + "symbol": "GUSD", + "decimals": 2, + "logoUrl": "https://assets.coingecko.com/coins/images/5992/thumb/gemini-dollar-gusd.png?1536745278", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xad173F5B5FE39DD1183a0d3C49C57629A574c36F", + "name": "GYEN", + "symbol": "GYEN", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x656104f2028BbFD7144C8f71Fa15daaA8c34A28b", + "name": "Hashflow", + "symbol": "HFT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26136/large/hashflow-icon-cmc.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x99F64C3Db98a4870eFf637315d5C86dcb1374879", + "name": "Highstreet", + "symbol": "HIGH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc32C0c5a52F36D244C552E45C485cBceaf385B36", + "name": "HOPR", + "symbol": "HOPR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14061/thumb/Shared_HOPR_logo_512px.png?1614073468", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4eA052BcAeE7d7ef2E3D61D601e878A560eaBe8e", + "name": "IDEX", + "symbol": "IDEX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2565/thumb/logomark-purple-286x286.png?1638362736", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa76195FA77304Bba4cD8946198f5a90E42F3E51F", + "name": "Illuvium", + "symbol": "ILV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc4Fc8cF76883094404DDb875d2AF15D1F5AA8053", + "name": "Immutable X", + "symbol": "IMX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17233/thumb/imx.png?1636691817", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa5Afe7646f07d2C41AA82Bb6AE09e99E121e39B7", + "name": "Index Cooperative", + "symbol": "INDEX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12729/thumb/index.png?1634894321", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x9361cA28625E12C7f088523B274A25059A89f9F8", + "name": "Injective", + "symbol": "INJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD326ACaB8799fb44C3A5B7f7eFbAaB5f9F7b54fb", + "name": "Inverse Finance", + "symbol": "INV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14205/thumb/inverse_finance.jpg?1614921871", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD749094Bc62615f0c8645467e241b71Ae2B6843F", + "name": "IoTeX", + "symbol": "IOTX", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x428c2B7Fa7a7821891fb529BAE4d80a71d5c61A8", + "name": "Geojam", + "symbol": "JAM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24648/thumb/ey40AzBN_400x400.jpg?1648507272", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8EF0686F380dD07f3e2121831839371922720708", + "name": "JasmyCoin", + "symbol": "JASMY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x781CC305fCBFe7cde376C9Ef5469d5a7E5CaB8b2", + "name": "Jupiter", + "symbol": "JUP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/10351/thumb/logo512.png?1632480932", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xbe51A5e8FA434F09663e8fB4CCe79d0B2381Afad", + "name": "Jupiter", + "symbol": "JUP", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/34188/standard/jup.png?1704266489", + "priceUsd": "0.431489" + }, + { + "chainId": 130, + "address": "0x05DBd720fc26F732c8d42Ea89BD7F442EA6AFE80", + "name": "Keep Network", + "symbol": "KEEP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3373/thumb/IuNzUb5b_400x400.jpg?1589526336", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x68Cea24F675e4F25584607F6c9feFb353f1bBfDc", + "name": "SelfKey", + "symbol": "KEY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2034/thumb/selfkey.png?1548608934", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xB0E4Ad2dFe3754e4a2443A7a828Eda5bB7Cd2284", + "name": "Kyber Network Crystal", + "symbol": "KNC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdd974D5C2e2928deA5F71b9825b8b646686BD200/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x9C41547e404942C173E28bB2B6abE4cf5fad6A74", + "name": "Keep3rV1", + "symbol": "KP3R", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12966/thumb/kp3r_logo.jpg?1607057458", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x14CFFAD448AeB0876c56B7aa28999C9a4f002943", + "name": "KRYLL", + "symbol": "KRL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2807/thumb/krl.png?1547036979", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2206cdcC9B94fF7dB7A9eAbeC77b5cE430258681", + "name": "Kujira", + "symbol": "KUJI", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1201209f55634bdDb67034efE4e8aA4D1B7B482C", + "name": "Layer3", + "symbol": "L3", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37768/large/Square.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb34b3DE63D22ffC90419c1a439de6C7d46687782", + "name": "LCX", + "symbol": "LCX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9985/thumb/zRPSu_0o_400x400.jpg?1574327008", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x68A6dbc7214a0F2b0d875963663F1613814E8829", + "name": "Lido DAO", + "symbol": "LDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5a53B6D19D8EDCb7923F0D840EeBB3f09BBeEfB7", + "name": "ChainLink Token", + "symbol": "LINK", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", + "priceUsd": "21.769495205100565" + }, + { + "chainId": 130, + "address": "0x68648F52B85407806bC1d349B745D13C91be0fDf", + "name": "Litentry", + "symbol": "LIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1D1BFCFC6ae6FE045f151C7e589fB241AAC89733", + "name": "League of Kingdoms", + "symbol": "LOKA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22572/thumb/loka_64pix.png?1642643271", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc68992e0514968BfbA3Dad201fef91f6009f523c", + "name": "Loom Network", + "symbol": "LOOM", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x11c6B34caDC550B65A9666497d7FCb39f35B73E3", + "name": "Livepeer", + "symbol": "LPT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x0176B38b7767451b1B682236eCe2fae853C71a60", + "name": "Liquity", + "symbol": "LQTY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xA2af802b95D7e20167e5aeaC7Fe8fDf4a8aB158A", + "name": "LoopringCoin V2", + "symbol": "LRC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD7eb7348Ba44c5A2f9f1D1d3534623230c7bee3F", + "name": "BLOCKLORDS", + "symbol": "LRDS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/34775/standard/LRDS_PNG.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc13C1Aa97ef67a1eBd56830323B04C3A75df1903", + "name": "Lisk", + "symbol": "LSK", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/385/large/Lisk_logo.png?1722338450", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf81B7485B4cB59645F74528D702c7f8CD72577FB", + "name": "Liquity USD", + "symbol": "LUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x276361c863903751771e9DabA6dDfaAf00FE358b", + "name": "Decentraland", + "symbol": "MANA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xC42B642F5010a2A3bD3CA2396Fe6f2e21B9512C4", + "name": "Mask Network", + "symbol": "MASK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xB999b66186d7a48BF0Eb5d22f4E7053A99eD2C97", + "name": "MATH", + "symbol": "MATH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xF6AC97B05B3bC92f829c7584b25839906507176b", + "name": "Polygon", + "symbol": "MATIC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x460ec1C67e1614Bf1feAb84b98795BAE2d657399", + "name": "Merit Circle", + "symbol": "MC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19304/thumb/Db4XqML.png?1634972154", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x68619Bc0C709FB63555Fe988ed14e78f7E6ACc40", + "name": "Moss Carbon Credit", + "symbol": "MCO2", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14414/thumb/ENtxnThA_400x400.jpg?1615948522", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xB29FddC20D5e4bacE9F54c1d9237953331BFeFF4", + "name": "Measurable Data Token", + "symbol": "MDT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2441/thumb/mdt_logo.png?1569813574", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x397E34AFF8bFc8Ec14aa78F378074F6d8E3E7d06", + "name": "Memecoin", + "symbol": "MEME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32528/large/memecoin_(2).png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xBfBa2A8745e5C85544DB7C8824C6962aB3A8f102", + "name": "Metis", + "symbol": "METIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x397C1f55FefF63C8947624b0d457a2CA3e3602ab", + "name": "Magic Internet Money", + "symbol": "MIM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5FE989EaB3021d7e742099d05a7937bA4A72D717", + "name": "Mirror Protocol", + "symbol": "MIR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13295/thumb/mirror_logo_transparent.png?1611554658", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf7A581f6e26EEa790225d76Af8821EA34Dc3c117", + "name": "Melon", + "symbol": "MLN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x58d68e179864605fEA06EAADF1185c6e78921Ebd", + "name": "Mog Coin", + "symbol": "MOG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/31059/large/MOG_LOGO_200x200.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xAe6065FB0244A68036C82deC9a8dE5501c7A1087", + "name": "Monavale", + "symbol": "MONA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13298/thumb/monavale_logo.jpg?1607232721", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xaa2109f14Bb155766cBA9E7fa8B8D4bF0ff19949", + "name": "Movement", + "symbol": "MOVE", + "decimals": 8, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/32452.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x587e0E022b074015F4e81eCa489c0C41d752A219", + "name": "Maple", + "symbol": "MPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14097/thumb/photo_2021-05-03_14.20.41.jpeg?1620022863", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x71d69d07914d087f1C3536F7A5006a256CfAd9Ea", + "name": "Metal", + "symbol": "MTL", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/763/thumb/Metal.png?1592195010", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1C3a8fB65Ab82D73e26B6403bf505B99d82b4701", + "name": "Multichain", + "symbol": "MULTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x10F109379E231d5c294ee6A5f9Abb2F8b40A8Dd1", + "name": "mStable USD", + "symbol": "MUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11576/thumb/mStable_USD.png?1595591803", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe3d92FB06a4EEbaC5879D3C1073e0eAB81D5f345", + "name": "Muse DAO", + "symbol": "MUSE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13230/thumb/muse_logo.png?1606460453", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD6ec6A24d5365A1811B05099f8D353c0Ff182974", + "name": "GensoKishi Metaverse", + "symbol": "MV", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xCF7c45Ccc1327ac1E9Cb9E098898c59402727794", + "name": "MXC", + "symbol": "MXC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4604/thumb/mxc.png?1655534336", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x328Ed7736871F863C8216Ca6CbB6f29B795032Df", + "name": "PolySwarm", + "symbol": "NCT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc1C06527E810C4A198D8C5d35e1dDBc987696276", + "name": "Neiro", + "symbol": "Neiro", + "decimals": 9, + "logoUrl": "https://coin-images.coingecko.com/coins/images/39488/large/neiro.jpg?1731449567", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x75b93cED9627Cd172912304Fb79Cd3e7336BaF62", + "name": "NKN", + "symbol": "NKN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3375/thumb/nkn.png?1548329212", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x931e587542b8603EA3C6420dD8d3b22eDbdA20FC", + "name": "Numeraire", + "symbol": "NMR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2AEB5256de25ECed47797b82d2F5C404AACEA6b9", + "name": "NuCypher", + "symbol": "NU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3318/thumb/photo1198982838879365035.jpg?1547037916", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x652293F4e9b0ef61C52a78D6615D9f5f3cD79208", + "name": "Ocean Protocol", + "symbol": "OCEAN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa60CE8f7ec6A091535b4708569B39DF5eE18c880", + "name": "Origin Protocol", + "symbol": "OGN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5949b9200dF1e77878dB3D061e43cF878Ee37383", + "name": "OMG Network", + "symbol": "OMG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/776/thumb/OMG_Network.jpg?1591167168", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf5614D20c13D5BF2F9e640f00B7B2B76959Eb0E3", + "name": "Omni Network", + "symbol": "OMNI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/36465/standard/Symbol-Color.png?1711511095", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xaD0bae21db0b471dFfC6f8F9EEacFe9A85321557", + "name": "Ondo Finance", + "symbol": "ONDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xCF2050ebC80B74370C1C2B71bDB635d11be3E8c0", + "name": "ORCA Alliance", + "symbol": "ORCA", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/5183.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x3C5319013FD75976F0f13b0bc0852537B6eaF396", + "name": "Orion Protocol", + "symbol": "ORN", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x9775C2b4f245248dE5596252Ac69311152B98042", + "name": "Orchid", + "symbol": "OXT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4575f41308EC1483f3d399aa9a2826d74Da13Deb/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x3614c8d98Bf905AbE075BfA289231bbc0D292327", + "name": "PayperEx", + "symbol": "PAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1601/thumb/pax.png?1547035800", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x89f7C0870794103744C8042630CC1C846a858E57", + "name": "PAX Gold", + "symbol": "PAXG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xeC37cdfC9a692b3cCd5c85696D14aaA31E75d6aC", + "name": "PlayDapp", + "symbol": "PDA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14316/standard/PDA-symbol.png?1710234068", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD9b5DA95B3D97c3E9872102fDb47d4c09074952B", + "name": "Pepe", + "symbol": "PEPE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1682922725", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5944D2728d5fea7D1F4AA4958E3aEbb3CCFEc7D5", + "name": "Perpetual Protocol", + "symbol": "PERP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xd0F77df9a8f0e855F910361f5f59958118d064c6", + "name": "Pirate Nation", + "symbol": "PIRATE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/38524/standard/_Pirate_Transparent_200x200.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5441619a9754Aee0665c939743cf7611abB6F6C7", + "name": "Pluton", + "symbol": "PLU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1241/thumb/pluton.png?1548331624", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xF6A49aEdbD7861DeD0DA2BE1f21C6954E5682E95", + "name": "Polygon Ecosystem Token", + "symbol": "POL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32440/large/polygon.png?1698233684", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x82a98121eaf30b0E135b08d4208c837Cdc306503", + "name": "Polkastarter", + "symbol": "POLS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2f5cfdC89fb96f2cf6c0FB1Ca6e3501Dd538D863", + "name": "Polymath", + "symbol": "POLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xA2a36541c5a54bd2815985418105091B4D4782d5", + "name": "Marlin", + "symbol": "POND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8903/thumb/POND_200x200.png?1622515451", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x562E588471cA0e710b2b1217867FFb2E0F2a5642", + "name": "Portal", + "symbol": "PORTAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35436/standard/portal.jpeg", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf265af514762286A63d015FeE382B90edfFa6bff", + "name": "Power Ledger", + "symbol": "POWR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD17D5f0DA4200bBfd3D6626AC6aEA2eccbf9fEE0", + "name": "Prime", + "symbol": "PRIME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29053/large/PRIMELOGOOO.png?1676976222", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xC6Fbf362a12804FEca22000f37DB5EFC1F41A7c9", + "name": "Propy", + "symbol": "PRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/869/thumb/propy.png?1548332100", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc7B7dcF3c6CAcAAc13F92c9173f9A0060ABf3def", + "name": "PARSIQ", + "symbol": "PRQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x13FE2c4504f3AA18708561250e2F20E4E7D7CAa2", + "name": "pSTAKE Finance", + "symbol": "PSTAKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/23931/thumb/PSTAKE_Dark.png?1645709930", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xAdf70dc4AaeFbC6D1E7A6cF0B02b0F2138b560d2", + "name": "Puffer Finance", + "symbol": "PUFFER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/50630/large/puffer.jpg?1728545297", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x0D2f98904D88909072eA6e61105CBBf78e6207c5", + "name": "PayPal USD", + "symbol": "PYUSD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/31212/large/PYUSD_Logo_%282%29.png?1691458314", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x3a8723f2929F370c61EaC583d6652e5C98C360d4", + "name": "Quant", + "symbol": "QNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3370/thumb/5ZOu7brX_400x400.jpg?1612437252", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x006254C4664C678e64c3265da28304cc8c1068b8", + "name": "Qredo", + "symbol": "QRDO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/17541/thumb/qrdo.png?1630637735", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb019a038eaDCB2F96321D236F6633C8d6Bb5eAbB", + "name": "Quantstamp", + "symbol": "QSP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1219/thumb/0_E0kZjb4dG4hUnoDD_.png?1604815917", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD815958F92E6aBe63437BCe166E97027f8E6caC2", + "name": "Quickswap", + "symbol": "QUICK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13970/thumb/1_pOU6pBMEmiL-ZJVb0CYRjQ.png?1613386659", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x3F9A30c86DC7F0c657eA17d52Efe09Eff08a1a45", + "name": "Radicle", + "symbol": "RAD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14013/thumb/radicle.png?1614402918", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6164A78F7B2aC49cf9b76c49e5B6909e89f34a66", + "name": "Rai Reflex Index", + "symbol": "RAI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe8a0078aA52ac7e93aE43818DdD64591E025BB6F", + "name": "SuperRare", + "symbol": "RARE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17753/thumb/RARE.jpg?1629220534", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x16F01392Ed7fC6F3C345CF544cf1172103C8561C", + "name": "Rarible", + "symbol": "RARI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x29EA5682024c8C62Cd8BDf691C4f0c5D66B403E3", + "name": "Rubic", + "symbol": "RBC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12629/thumb/200x200.png?1607952509", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x75B2dBb2a7C70073133E42F64366a986c841cd3e", + "name": "Ribbon Finance", + "symbol": "RBN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15823/thumb/RBN_64x64.png?1633529723", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x560603E0bFC941063D1375Ec4E3f9FE38261617E", + "name": "Republic Token", + "symbol": "REN", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x097ca3FC389697080C84148C455Ca839b2816Fc4", + "name": "Reputation Augur v1", + "symbol": "REP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1985365e9f78359a9B6AD760e32412f4a445E862/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xE86B1E5613a5761D005a2D00D8a1B4ad1e72A8c4", + "name": "Reputation Augur v2", + "symbol": "REPv2", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x221657776846890989a759BA2973e427DfF5C9bB/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x9FcC3133779F2039c29908c915b6EFaE9d8663Cd", + "name": "Request", + "symbol": "REQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1031/thumb/Request_icon_green.png?1643250951", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc14a68015fA6396eF97B57839da544910f9Ca657", + "name": "REVV", + "symbol": "REVV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2178f07c1d585C39272CAf69A72beF08aAD6c9AB", + "name": "Renzo", + "symbol": "REZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37327/standard/renzo_200x200.png?1714025012", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8c9606001CF1787CEb80E03DEF3F9BaF946CF284", + "name": "Rari Governance Token", + "symbol": "RGT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12900/thumb/Rari_Logo_Transparent.png?1613978014", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x538fB2719135740b8877607217Dc391FB3347ACb", + "name": "iExec RLC", + "symbol": "RLC", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/646/thumb/pL1VuXm.png?1604543202", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7Ad899b7C793743fDE692d982F190f443F88c889", + "name": "Rally", + "symbol": "RLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12843/thumb/image.png?1611212077", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x965C6DeBFa700F53a38d42DbaeD922c58d649868", + "name": "Render Token", + "symbol": "RNDR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x682B2f07e61022A80Ac2753448f7D95E9de41D99", + "name": "Rook", + "symbol": "ROOK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13005/thumb/keeper_dao_logo.jpg?1604316506", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x993A565A1E6219951323cA3c34Cee0A3b1889066", + "name": "Reserve Rights", + "symbol": "RSR", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x47B72717E48Da346C3F1ED1311c8DCDe10EfD888", + "name": "Safe", + "symbol": "SAFE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/27032/standard/Artboard_1_copy_8circle-1.png?1696526084", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x6A654A2ec95fB988Ea37746dBCca10772CAf25CA", + "name": "The Sandbox", + "symbol": "SAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7ccc67C7b232aa6417d9422e90D91ec4b32d72E5", + "name": "Stader", + "symbol": "SD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xaa571d01057cdF477D73433D36D86fCb5664158e", + "name": "Shiba Inu", + "symbol": "SHIB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11939/thumb/shiba.png?1622619446", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x45Bda7bA10DaC525a86DBEaB3135701A66024F2F", + "name": "Shping", + "symbol": "SHPING", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2588/thumb/r_yabKKi_400x400.jpg?1639470164", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x486Bbb6f250343AdB4782F50Dd09766f8aD20c01", + "name": "SKALE", + "symbol": "SKL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13245/thumb/SKALE_token_300x300.png?1606789574", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5A6058002d0d336e5E8860652e7054a6d07074E4", + "name": "SKY Governance Token", + "symbol": "SKY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39925/large/sky.jpg?1724827980", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xbD2DD310FECBFb1111fC3262F3a97bA696cb03B3", + "name": "Smooth Love Potion", + "symbol": "SLP", + "decimals": 0, + "logoUrl": "https://assets.coingecko.com/coins/images/10366/thumb/SLP.png?1578640057", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x914f7CE2B080B2186159C2213B1e193E265aBF5F", + "name": "Status", + "symbol": "SNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x022D952aBCc6C8271F26e59e37A65dC359E6bc88", + "name": "Synthetix Network Token", + "symbol": "SNX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5e03C123D829505F4DEa87cf679F77c9dC4627ab", + "name": "Unisocks", + "symbol": "SOCKS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/10717/thumb/qFrcoiM.png?1582525244", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4Ff3E944D5Cb54f6f4A1dd035782BE59c3d054FE", + "name": "SOL Wormhole ", + "symbol": "SOL", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xbdE8A5331E8Ac4831cf8ea9e42e229219EafaB97", + "name": "Solana", + "symbol": "SOL", + "decimals": 9, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54680/large/base.png?1749023388", + "priceUsd": "223.57749727520465" + }, + { + "chainId": 130, + "address": "0x739316C7bc4A39Eb39dcFa1b181b64abc17fEF7F", + "name": "Spell Token", + "symbol": "SPELL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x51A7b9a11f10D04C16306D90dc4EC22b036DD629", + "name": "SPX6900", + "symbol": "SPX", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x77c8A8E1dd3b5270d3Ab589543e9A83319373135", + "name": "Stargate Finance", + "symbol": "STG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf13B5B21555092882e69b22282DAf891c9951835", + "name": "Storj Token", + "symbol": "STORJ", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x09f705405677970E509d606348D4635D2332c72e", + "name": "Starknet", + "symbol": "STRK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26433/standard/starknet.png?1696525507", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xEf86E70E534E02AADEAE95b843973d4AcacCeA22", + "name": "Stox", + "symbol": "STX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1230/thumb/stox-token.png?1547035256", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xc05B416738DDEBd14D5A9B790a6e1ce782176525", + "name": "SUKU", + "symbol": "SUKU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11969/thumb/UmfW5S6f_400x400.jpg?1596602238", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x0c288302629Fc22504D59Ddf8fbf8AA92bD86D3D", + "name": "SuperFarm", + "symbol": "SUPER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7251d204c2e867b31096D5c7091298239B3A6a0F", + "name": "Synth sUSD", + "symbol": "sUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x2982Be2D0c6ae4A7D5BC1c8fe7B630E3BDfb3ce5", + "name": "Sushi", + "symbol": "SUSHI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xa8015cbc9f7c58788BA00854c330F027028A5870", + "name": "Swell", + "symbol": "SWELL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/28777/large/swell1.png?1727899715", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x0610cDF9856b8825213672981056CD4945Af1616", + "name": "SWFTCOIN", + "symbol": "SWFTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/2346/thumb/SWFTCoin.jpg?1618392022", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xDcA295E850666753c6332D6B0E0445B09785c2E1", + "name": "Swipe", + "symbol": "SXP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9368/thumb/swipe.png?1566792311", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1BAAc1979527A38F367c6f89bE081aBfcFFCF85E", + "name": "Sylo", + "symbol": "SYLO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/6430/thumb/SYLO.svg?1589527756", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xCeb1F5671C47cee096C3B40353863b6781888A48", + "name": "Synapse", + "symbol": "SYN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8f7F997ba304f426E3138999919c23f68cD6FA96", + "name": "Syrup Token", + "symbol": "SYRUP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/51232/standard/IMG_7420.png?1730831572", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8F43Ab8648F1a3BAEea3782Ba5f562a148f2Ad54", + "name": "Threshold Network", + "symbol": "T", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22228/thumb/nFPNiSbL_400x400.jpg?1641220340", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xAd497996Dc33DC8E8e552824CcEe199420BC7814", + "name": "tBTC", + "symbol": "tBTC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/0x18084fbA666a33d37592fA2633fD49a74DD93a88/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xD9Cbd701bbEA8e9Aaee7d82aa60748451eDa749c", + "name": "ChronoTech", + "symbol": "TIME", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/604/thumb/time-32x32.png?1627130666", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xd649b9AD2104418B5b032a5899fBcd54a9a46c68", + "name": "Alien Worlds", + "symbol": "TLM", + "decimals": 4, + "logoUrl": "https://assets.coingecko.com/coins/images/14676/thumb/kY-C4o7RThfWrDQsLCAG4q4clZhBDDfJQVhWUEKxXAzyQYMj4Jmq1zmFwpRqxhAJFPOa0AsW_PTSshoPuMnXNwq3rU7Imp15QimXTjlXMx0nC088mt1rIwRs75GnLLugWjSllxgzvQ9YrP4tBgclK4_rb17hjnusGj_c0u2fx0AvVokjSNB-v2poTj0xT9BZRCbzRE3-lF1.jpg?1617700061", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x5eD5DA180bB125f229AB7b825E34D2b936213e0B", + "name": "Tokemak", + "symbol": "TOKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17495/thumb/tokemak-avatar-200px-black.png?1628131614", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x502865ECDd2a2929Aa9418297bE7d3C4a7BD5Ac6", + "name": "TE FOOD", + "symbol": "TONE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2325/thumb/tec.png?1547036538", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1ac70C9e29bC19640E64D938DD8D6A46dbAe6f2e", + "name": "OriginTrail", + "symbol": "TRAC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1877/thumb/TRAC.jpg?1635134367", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8e902FDeA73e5CF9621D2Bee82cD79196d8ec63b", + "name": "Tellor", + "symbol": "TRB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x437dD6360Bd17FB353c67376371133Cd33dacdBD", + "name": "Tribe", + "symbol": "TRIBE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14575/thumb/tribe.PNG?1617487954", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x55C65102C26b173696e935B1325e5AaeF30cFE0e", + "name": "TrueFi", + "symbol": "TRU", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/13180/thumb/truefi_glyph_color.png?1617610941", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x1E4339318EcE1d6D9d2Fb129b31C06b9F2d202A1", + "name": "Turbo", + "symbol": "TURBO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/30117/large/TurboMark-QL_200.png?1708079597", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x756fb781389DCaF9D3BC5468927F06A913bD9D5D", + "name": "The Virtua Kolect", + "symbol": "TVK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13330/thumb/virtua_original.png?1656043619", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x478923278640a10A60951E379aFFb60772435f8C", + "name": "UMA Voting Token v1", + "symbol": "UMA", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828/logo.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xe9225a870b54f8FBA42c8188D211271f0408a30B", + "name": "Unifi Protocol DAO", + "symbol": "UNFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13152/thumb/logo-2.png?1605748967", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x8f187aA05619a017077f5308904739877ce9eA21", + "name": "Uniswap", + "symbol": "UNI", + "decimals": 18, + "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", + "priceUsd": "7.84" + }, + { + "chainId": 130, + "address": "0x5EAFF8Fa6f3831Bb86FeEB701E6f98293E264D36", + "name": "Pawtocol", + "symbol": "UPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12186/thumb/pawtocol.jpg?1597962008", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.999705" + }, + { + "chainId": 130, + "address": "0x2A22868610610199D43fE93A16661473A9f86f1E", + "name": "Global Dollar", + "symbol": "USDG", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/51281/large/GDN_USDG_Token_200x200.png", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xF7E6430137eF8087E0D472343f358e986De0FEFF", + "name": "Pax Dollar", + "symbol": "USDP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xf37748D2Cc6E6d5D05945Ce130C03c147b2F3a5F", + "name": "Quantoz USDQ", + "symbol": "USDQ", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51852/large/USDQ_1000px_Color.png?1732071232", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xaC025d055a6B633992dE1F796b97B97F004c06a7", + "name": "StablR USD", + "symbol": "USDR", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53721/large/stablrusd-logo.png?1737126629", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x116EE4d63847fb295dD919aE57B768EA3B2f7Bb4", + "name": "USDS Stablecoin", + "symbol": "USDS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39926/large/usds.webp?1726666683", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x588CE4F028D8e7B53B687865d6A67b3A54C75518", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "1.0004915320963803" + }, + { + "chainId": 130, + "address": "0xc7bA59c95ba747a7c374DC7208a0513798BC5950", + "name": "USUAL", + "symbol": "USUAL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51091/large/USUAL.jpg?1730035787", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x286b5Ecea3749c7c7047104aa3C5749901564A0b", + "name": "VANRY", + "symbol": "VANRY", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/33466/large/apple-touch-icon.png?1701942541", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4afd08AC2416450d9c8b84D287dbfFb68FFe537f", + "name": "Voyager Token", + "symbol": "VGX", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/794/thumb/Voyager-vgx.png?1575693595", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb86a08ec917EeF9f835aC2B26c3a506c06364A49", + "name": "Wrapped Ampleforth", + "symbol": "WAMPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x927B51f251480a681271180DA4de28D44EC4AfB8", + "name": "Wrapped BTC", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", + "priceUsd": "122667.62208327817" + }, + { + "chainId": 130, + "address": "0xaE87B8eb5E313AC72B306CbA7c1E3f23D72e82C4", + "name": "Wrapped Centrifuge", + "symbol": "WCFG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17106/thumb/WCFG.jpg?1626266462", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.813939" + }, + { + "chainId": 130, + "address": "0x97Fadb3D000b953360FD011e173F12cDDB5d70Fa", + "name": "dogwifhat", + "symbol": "WIF", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/33566/standard/dogwifhat.jpg?1702499428", + "priceUsd": "0.706488" + }, + { + "chainId": 130, + "address": "0xef22b9df2dDf4246A827575C4Aa46BDaeFd89E62", + "name": "WOO Network", + "symbol": "WOO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x15261eEb999eD3C3ae3c5319E0035940dc06a12f", + "name": "Chain", + "symbol": "XCN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xb1A9385B500Fe81B58c4d0e3AaCC39d8021265c3", + "name": "XSGD", + "symbol": "XSGD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x43D5EA0f30Bce3907aAD6783e61D56592AEbE4eA", + "name": "XYO Network", + "symbol": "XYO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x52Bf54Eb4210F588320f3e4c151Bca81f84a3201", + "name": "yearn finance", + "symbol": "YFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x62ffD4229bb9a327412D1BE518A1dbAe6c18A07E", + "name": "DFI money", + "symbol": "YFII", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11902/thumb/YFII-logo.78631676.png?1598677348", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0xeA20C2Cf22acBbF3d8311D15bC73FD7076E36f4B", + "name": "Yield Guild Games", + "symbol": "YGG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17358/thumb/le1nzlO6_400x400.jpg?1632465691", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x757dCF360f2FE999FAEEBcc6E80f5Eceb3cb3CA4", + "name": "Zetachain", + "symbol": "Zeta", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26718/standard/Twitter_icon.png?1696525788", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x00ad3704d1e101DF76f87738bEfE67737eD29cFb", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": null + }, + { + "chainId": 130, + "address": "0x7e7e8e5f0eDd7ca2ed3D9609cea1FF37a6E7Edf5", + "name": "0x Protocol Token", + "symbol": "ZRX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x9c2C5fd7b07E95EE044DDeba0E97a665F142394f", + "name": "1inch", + "symbol": "1INCH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceUsd": "0.25067227441032397" + }, + { + "chainId": 137, + "address": "0xD6DF932A45C0f255f85145f286eA0b292B21C90B", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": "276.06" + }, + { + "chainId": 137, + "address": "0xF328b73B6c685831F238c30a23Fc19140CB4D8FC", + "name": "Across Protocol Token", + "symbol": "ACX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", + "priceUsd": "0.11094" + }, + { + "chainId": 137, + "address": "0xdDa7b23D2D72746663E7939743f929a3d85FC975", + "name": "Ambire AdEx", + "symbol": "ADX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", + "priceUsd": "0.1011" + }, + { + "chainId": 137, + "address": "0xE0B52e49357Fd4DAf2c15e02058DCE6BC0057db4", + "name": "agEur", + "symbol": "agEUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", + "priceUsd": "1.1786811136663522" + }, + { + "chainId": 137, + "address": "0x6a6bD53d677F8632631662C48bD47b1D4D6524ee", + "name": "Adventure Gold", + "symbol": "AGLD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", + "priceUsd": "0.08371763155072778" + }, + { + "chainId": 137, + "address": "0xe2341718c6C0CbFa8e6686102DD8FbF4047a9e9B", + "name": "AIOZ Network", + "symbol": "AIOZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", + "priceUsd": "0.04440327700379732" + }, + { + "chainId": 137, + "address": "0x95c300e7740D2A88a44124B424bFC1cB2F9c3b89", + "name": "Alchemix", + "symbol": "ALCX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14113/thumb/Alchemix.png?1614409874", + "priceUsd": "8.792443727" + }, + { + "chainId": 137, + "address": "0x82dCf1Df86AdA26b2dCd9ba6334CeDb8c2448e9e", + "name": "Aleph im", + "symbol": "ALEPH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", + "priceUsd": "0.05846729649268761" + }, + { + "chainId": 137, + "address": "0xbFc70507384047Aa74c29Cdc8c5Cb88D0f7213AC", + "name": "Alethea Artificial Liquid Intelligence", + "symbol": "ALI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", + "priceUsd": "0.005661474156285133" + }, + { + "chainId": 137, + "address": "0x50858d870FAF55da2fD90FB6DF7c34b5648305C6", + "name": "My Neighbor Alice", + "symbol": "ALICE", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", + "priceUsd": "0.2123637158893901" + }, + { + "chainId": 137, + "address": "0x3AE490db48d74B1bC626400135d4616377D0109f", + "name": "Alpha Venture DAO", + "symbol": "ALPHA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", + "priceUsd": "0.01747722995" + }, + { + "chainId": 137, + "address": "0x0621d647cecbFb64b79E44302c1933cB4f27054d", + "name": "Amp", + "symbol": "AMP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12409/thumb/amp-200x200.png?1599625397", + "priceUsd": "0.0032294516702799127" + }, + { + "chainId": 137, + "address": "0x101A023270368c0D50BFfb62780F4aFd4ea79C35", + "name": "Ankr", + "symbol": "ANKR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", + "priceUsd": "0.013521341208785894" + }, + { + "chainId": 137, + "address": "0x2b8504ab5eFc246d0eC5Ec7E74565683227497de", + "name": "Aragon", + "symbol": "ANT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/681/thumb/JelZ58cv_400x400.png?1601449653", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xB7b31a6BC18e48888545CE79e83E06003bE70930", + "name": "ApeCoin", + "symbol": "APE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", + "priceUsd": "0.5576761180501775" + }, + { + "chainId": 137, + "address": "0x45C27821E80F8789b60Fd8B600C73815d34DDa6C", + "name": "API3", + "symbol": "API3", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13256/thumb/api3.jpg?1606751424", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xEE800B277A96B0f490a1A732e1D6395FAD960A26", + "name": "ARPA Chain", + "symbol": "ARPA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", + "priceUsd": "0.02054787" + }, + { + "chainId": 137, + "address": "0x04bEa9FCE76943E90520489cCAb84E84C0198E29", + "name": "AirSwap", + "symbol": "AST", + "decimals": 4, + "logoUrl": "https://assets.coingecko.com/coins/images/1019/thumb/Airswap.png?1630903484", + "priceUsd": "0.46794954741623995" + }, + { + "chainId": 137, + "address": "0x0df0f72EE0e5c9B7ca761ECec42754992B2Da5BF", + "name": "Automata", + "symbol": "ATA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", + "priceUsd": "0.04045548251217933" + }, + { + "chainId": 137, + "address": "0x5eB8D998371971D01954205c7AFE90A7AF6a95AC", + "name": "Audius", + "symbol": "AUDIO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12913/thumb/AudiusCoinLogo_2x.png?1603425727", + "priceUsd": "0.06563845546516388" + }, + { + "chainId": 137, + "address": "0x61BDD9C7d4dF4Bf47A4508c0c8245505F2Af5b7b", + "name": "Axie Infinity", + "symbol": "AXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", + "priceUsd": "2.3073277786729967" + }, + { + "chainId": 137, + "address": "0x1FcbE5937B0cc2adf69772D228fA4205aCF4D9b2", + "name": "Badger DAO", + "symbol": "BADGER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", + "priceUsd": "1.5686363921976594" + }, + { + "chainId": 137, + "address": "0x9a71012B13CA4d3D0Cdc72A177DF3ef03b0E76A3", + "name": "Balancer", + "symbol": "BAL", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", + "priceUsd": "1.137762798245673" + }, + { + "chainId": 137, + "address": "0xA8b1E0764f85f53dfe21760e8AfE5446D82606ac", + "name": "Band Protocol", + "symbol": "BAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9545/thumb/band-protocol.png?1568730326", + "priceUsd": "1.328108957568383" + }, + { + "chainId": 137, + "address": "0x3Cef98bb43d732E2F285eE605a8158cDE967D219", + "name": "Basic Attention Token", + "symbol": "BAT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", + "priceUsd": "0.14918109526611817" + }, + { + "chainId": 137, + "address": "0x91c89A94567980f0e9723b487b0beD586eE96aa7", + "name": "Biconomy", + "symbol": "BICO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", + "priceUsd": "0.07759396775347024" + }, + { + "chainId": 137, + "address": "0x63400d9586873eB03c84F76755D26Ef2a9a2abeF", + "name": "Big Time", + "symbol": "BIGTIME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32251/standard/-6136155493475923781_121.jpg?1696998691", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x438B28C5AA5F00a817b7Def7cE2Fb3d5d1970974", + "name": "Bluzelle", + "symbol": "BLZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2848/thumb/ColorIcon_3x.png?1622516510", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xc26D47d5c33aC71AC5CF9F776D63Ba292a4F7842", + "name": "Bancor Network Token", + "symbol": "BNT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", + "priceUsd": "0.5821143765115562" + }, + { + "chainId": 137, + "address": "0xa4B2B20b2C73c7046ED19AC6bfF5E5285c58F20a", + "name": "Boba Network", + "symbol": "BOBA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20285/thumb/BOBA.png?1636811576", + "priceUsd": "0.02297442390389694" + }, + { + "chainId": 137, + "address": "0xA041544fe2BE56CCe31Ebb69102B965E06aacE80", + "name": "BarnBridge", + "symbol": "BOND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xdAb529f40E671A1D4bF91361c21bf9f0C9712ab7", + "name": "Binance USD", + "symbol": "BUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", + "priceUsd": "0.9939931103245507" + }, + { + "chainId": 137, + "address": "0x91a4635F620766145C099E15889Bd2766906A559", + "name": "Celer Network", + "symbol": "CELR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4379/thumb/Celr.png?1554705437", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x594C984E3318e91313f881B021A0C4203fF5E59F", + "name": "Chromia", + "symbol": "CHR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/5000/thumb/Chromia.png?1559038018", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xf1938Ce12400f9a761084E7A80d37e732a4dA056", + "name": "Chiliz", + "symbol": "CHZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8834/thumb/Chiliz.png?1561970540", + "priceUsd": "0.037329726663262994" + }, + { + "chainId": 137, + "address": "0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c", + "name": "Compound", + "symbol": "COMP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", + "priceUsd": "39.946063251753124" + }, + { + "chainId": 137, + "address": "0x5dCc7FEEEfeF110419549A4417313876D33D354c", + "name": "Clearpool", + "symbol": "CPOOL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19252/large/photo_2022-08-31_12.45.02.jpeg?1696518697", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x93B0fF1C8828F6eB039D345Ff681eD735086d925", + "name": "Covalent", + "symbol": "CQT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14168/thumb/covalent-cqt.png?1624545218", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xAdA58DF0F643D959C2A47c9D4d4c1a4deFe3F11C", + "name": "Cronos", + "symbol": "CRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", + "priceUsd": "0.1661370674868661" + }, + { + "chainId": 137, + "address": "0x172370d5Cd63279eFa6d502DAB29171933a610AF", + "name": "Curve DAO Token", + "symbol": "CRV", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", + "priceUsd": "0.719583" + }, + { + "chainId": 137, + "address": "0x2727Ab1c2D22170ABc9b595177B2D5C6E1Ab7B7B", + "name": "Cartesi", + "symbol": "CTSI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", + "priceUsd": "0.07261" + }, + { + "chainId": 137, + "address": "0x8c208BC2A808a088a78398fed8f2640cab0b6EDb", + "name": "Cryptex Finance", + "symbol": "CTX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14932/thumb/glossy_icon_-_C200px.png?1619073171", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x276C9cbaa4BDf57d7109a41e67BD09699536FA3d", + "name": "Somnium Space CUBEs", + "symbol": "CUBE", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/10687/thumb/CUBE_icon.png?1617026861", + "priceUsd": "0.311835" + }, + { + "chainId": 137, + "address": "0x66Dc5A08091d1968e08C16aA5b27BAC8398b02Be", + "name": "Civic", + "symbol": "CVC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", + "priceUsd": "0.080971" + }, + { + "chainId": 137, + "address": "0x4257EA7637c355F81616050CbB6a9b709fd72683", + "name": "Convex Finance", + "symbol": "CVX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", + "priceUsd": "1.8323999579291146" + }, + { + "chainId": 137, + "address": "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + "name": "Dai Stablecoin", + "symbol": "DAI", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", + "priceUsd": "0.999733" + }, + { + "chainId": 137, + "address": "0x26f5FB1e6C8a65b3A873fF0a213FA16EFF5a7828", + "name": "DerivaDAO", + "symbol": "DDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13453/thumb/ddx_logo.png?1608741641", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xff835562C761205659939B64583dd381a6AA4D92", + "name": "DexTools", + "symbol": "DEXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x993f2CafE9dbE525243f4A78BeBC69DAc8D36000", + "name": "DIA", + "symbol": "DIA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x85955046DF4668e1DD369D2DE9f3AEB98DD2A369", + "name": "DeFi Pulse Index", + "symbol": "DPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", + "priceUsd": "100.65052882009066" + }, + { + "chainId": 137, + "address": "0x4C3bF0a3DE9524aF68327d1D2558a3B70d17D42a", + "name": "dYdX", + "symbol": "DYDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", + "priceUsd": "0.05677536969824203" + }, + { + "chainId": 137, + "address": "0xE0339c80fFDE91F3e20494Df88d4206D86024cdF", + "name": "Dogelon Mars", + "symbol": "ELON", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14962/thumb/6GxcPRo3_400x400.jpg?1619157413", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x7eC26842F195c852Fa843bB9f6D8B583a274a157", + "name": "Enjin Coin", + "symbol": "ENJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1102/thumb/enjin-coin-logo.png?1547035078", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xbD7A5Cf51d22930B8B3Df6d834F9BCEf90EE7c4f", + "name": "Ethereum Name Service", + "symbol": "ENS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", + "priceUsd": "20.6595799873757" + }, + { + "chainId": 137, + "address": "0x0E50BEA95Fe001A370A4F1C220C49AEdCB982DeC", + "name": "Ethernity Chain", + "symbol": "ERN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", + "priceUsd": "0.09578829672260285" + }, + { + "chainId": 137, + "address": "0x8a037dbcA8134FFc72C362e394e35E0Cad618F85", + "name": "Euro Coin", + "symbol": "EURC", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", + "priceUsd": "2856.655882274905" + }, + { + "chainId": 137, + "address": "0x176f5AB638cf4Ff3B6239Ba609C3fadAA46ef5B0", + "name": "Harvest Finance", + "symbol": "FARM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", + "priceUsd": "235803391453.96518" + }, + { + "chainId": 137, + "address": "0x7583FEDDbceFA813dc18259940F76a02710A8905", + "name": "Fetch ai", + "symbol": "FET", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + "priceUsd": "0.533765725858233" + }, + { + "chainId": 137, + "address": "0x7A7B94F18EF6AD056CDa648588181CDA84800f94", + "name": "Stafi", + "symbol": "FIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", + "priceUsd": "0.14041786603509518" + }, + { + "chainId": 137, + "address": "0x853B41823905aB4d63558542b0F06748A5e345fe", + "name": "FLOKI", + "symbol": "FLOKI", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x9ff62d1FC52A907B6DCbA8077c2DDCA6E6a9d3e1", + "name": "Forta", + "symbol": "FORT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", + "priceUsd": "0.0654886814323961" + }, + { + "chainId": 137, + "address": "0x5eCbA59DAcc1ADc5bDEA35f38A732823fc3dE977", + "name": "Ampleforth Governance Token", + "symbol": "FORTH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14917/thumb/photo_2021-04-22_00.00.03.jpeg?1619020835", + "priceUsd": "23.93197328208239" + }, + { + "chainId": 137, + "address": "0x65A05DB8322701724c197AF82C9CaE41195B0aA8", + "name": "ShapeShift FOX Token", + "symbol": "FOX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", + "priceUsd": "0.024032618501119315" + }, + { + "chainId": 137, + "address": "0x104592a158490a9228070E0A8e5343B499e125D0", + "name": "Frax", + "symbol": "FRAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", + "priceUsd": "0.9735439365263314" + }, + { + "chainId": 137, + "address": "0xC9c1c1c20B3658F8787CC2FD702267791f224Ce1", + "name": "Fantom", + "symbol": "FTM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", + "priceUsd": "0.2813189250932908" + }, + { + "chainId": 137, + "address": "0x3e121107F6F22DA4911079845a470757aF4e1A1b", + "name": "Frax Share", + "symbol": "FXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", + "priceUsd": "2.2337826456011975" + }, + { + "chainId": 137, + "address": "0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7", + "name": "Aavegotchi", + "symbol": "GHST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", + "priceUsd": "0.37751" + }, + { + "chainId": 137, + "address": "0x0B220b82F3eA3B7F6d9A1D8ab58930C064A2b5Bf", + "name": "Golem", + "symbol": "GLM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/542/thumb/Golem_Submark_Positive_RGB.png?1606392013", + "priceUsd": "0.22328354370396006" + }, + { + "chainId": 137, + "address": "0x5FFD62D3C3eE2E81C00A7b9079FB248e7dF024A8", + "name": "Gnosis Token", + "symbol": "GNO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", + "priceUsd": "182.05435161122313" + }, + { + "chainId": 137, + "address": "0xF88fc6b493eda7650E4bcf7A290E8d108F677CfE", + "name": "Gods Unchained", + "symbol": "GODS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17139/thumb/10631.png?1635718182", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x5fe2B58c013d7601147DcdD68C143A77499f5531", + "name": "The Graph", + "symbol": "GRT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", + "priceUsd": "0.07989707848246687" + }, + { + "chainId": 137, + "address": "0xdb95f9188479575F3F718a245EcA1B3BF74567EC", + "name": "Gitcoin", + "symbol": "GTC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", + "priceUsd": "0.3381839367" + }, + { + "chainId": 137, + "address": "0xC8A94a3d3D2dabC3C1CaffFFDcA6A7543c3e3e65", + "name": "Gemini Dollar", + "symbol": "GUSD", + "decimals": 2, + "logoUrl": "https://assets.coingecko.com/coins/images/5992/thumb/gemini-dollar-gusd.png?1536745278", + "priceUsd": "0.9487705752187483" + }, + { + "chainId": 137, + "address": "0x482bc619eE7662759CDc0685B4E78f464Da39C73", + "name": "GYEN", + "symbol": "GYEN", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", + "priceUsd": "0.002696942443761723" + }, + { + "chainId": 137, + "address": "0x6cCBF3627b2C83AFEF05bf2F035E7f7B210Fe30D", + "name": "HOPR", + "symbol": "HOPR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14061/thumb/Shared_HOPR_logo_512px.png?1614073468", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x9Cb74C8032b007466865f060ad2c46145d45553D", + "name": "IDEX", + "symbol": "IDEX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2565/thumb/logomark-purple-286x286.png?1638362736", + "priceUsd": "0.021582127459347844" + }, + { + "chainId": 137, + "address": "0xFA46dAf9909e116DBc40Fe1cC95fC0Bb1f452aBE", + "name": "Illuvium", + "symbol": "ILV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", + "priceUsd": "20.204750806525652" + }, + { + "chainId": 137, + "address": "0x183070C90B34A63292cC908Ce1b263Cb56D49A7F", + "name": "Immutable X", + "symbol": "IMX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17233/thumb/imx.png?1636691817", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xfBd8A3b908e764dBcD51e27992464B4432A1132b", + "name": "Index Cooperative", + "symbol": "INDEX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12729/thumb/index.png?1634894321", + "priceUsd": "1.012" + }, + { + "chainId": 137, + "address": "0x4E8dc2149EaC3f3dEf36b1c281EA466338249371", + "name": "Injective", + "symbol": "INJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xF18Ac368001b0DdC80aA6a8374deb49e868EFDb8", + "name": "Inverse Finance", + "symbol": "INV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14205/thumb/inverse_finance.jpg?1614921871", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xf6372cDb9c1d3674E83842e3800F2A62aC9F3C66", + "name": "IoTeX", + "symbol": "IOTX", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", + "priceUsd": "0.02934062641124858" + }, + { + "chainId": 137, + "address": "0xb87f5c1E81077FfcfE821dA240fd20C99c533aF1", + "name": "JasmyCoin", + "symbol": "JASMY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", + "priceUsd": "0.012673890407730974" + }, + { + "chainId": 137, + "address": "0x42f37A1296b2981F7C3cAcEd84c5096b2Eb0C72C", + "name": "Keep Network", + "symbol": "KEEP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3373/thumb/IuNzUb5b_400x400.jpg?1589526336", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x324b28d6565f784d596422B0F2E5aB6e9CFA1Dc7", + "name": "Kyber Network Crystal", + "symbol": "KNC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdd974D5C2e2928deA5F71b9825b8b646686BD200/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x53AEc293212E3B792563Bc16f1be26956adb12e9", + "name": "Keep3rV1", + "symbol": "KP3R", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12966/thumb/kp3r_logo.jpg?1607057458", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xE8A51D0dD1b4525189ddA2187F90ddF0932b5482", + "name": "LCX", + "symbol": "LCX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9985/thumb/zRPSu_0o_400x400.jpg?1574327008", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xC3C7d422809852031b44ab29EEC9F1EfF2A58756", + "name": "Lido DAO", + "symbol": "LDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", + "priceUsd": "1.15" + }, + { + "chainId": 137, + "address": "0x53E0bca35eC356BD5ddDFebbD1Fc0fD03FaBad39", + "name": "ChainLink Token", + "symbol": "LINK", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", + "priceUsd": "21.77" + }, + { + "chainId": 137, + "address": "0xe6E320b7bB22018D6CA1F4D8cea1365eF5d25ced", + "name": "Litentry", + "symbol": "LIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", + "priceUsd": "5.582301013228283" + }, + { + "chainId": 137, + "address": "0x465b67CB20A7E8bC4c51b4C7DA591C1945b41427", + "name": "League of Kingdoms", + "symbol": "LOKA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22572/thumb/loka_64pix.png?1642643271", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x66EfB7cC647e0efab02eBA4316a2d2941193F6b3", + "name": "Loom Network", + "symbol": "LOOM", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x3962F4A0A0051DccE0be73A7e09cEf5756736712", + "name": "Livepeer", + "symbol": "LPT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", + "priceUsd": "0.002134056805326236" + }, + { + "chainId": 137, + "address": "0x8Ab2Fec94d17ae69FB90E7c773f2C85Ed1802c01", + "name": "Liquity", + "symbol": "LQTY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", + "priceUsd": "0.01801536009269893" + }, + { + "chainId": 137, + "address": "0x84e1670F61347CDaeD56dcc736FB990fBB47ddC1", + "name": "LoopringCoin V2", + "symbol": "LRC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x23001f892c0C82b79303EDC9B9033cD190BB21c7", + "name": "Liquity USD", + "symbol": "LUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", + "priceUsd": "1" + }, + { + "chainId": 137, + "address": "0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4", + "name": "Decentraland", + "symbol": "MANA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", + "priceUsd": "0.30852407916468383" + }, + { + "chainId": 137, + "address": "0x2B9E7ccDF0F4e5B24757c1E1a80e311E34Cb10c7", + "name": "Mask Network", + "symbol": "MASK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", + "priceUsd": "1.242368505130622" + }, + { + "chainId": 137, + "address": "0x347ACCAFdA7F8c5BdeC57fa34a5b663CBd1aeca7", + "name": "MATH", + "symbol": "MATH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x0000000000000000000000000000000000001010", + "name": "Polygon", + "symbol": "MATIC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xAa7DbD1598251f856C12f63557A4C4397c253Cea", + "name": "Moss Carbon Credit", + "symbol": "MCO2", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14414/thumb/ENtxnThA_400x400.jpg?1615948522", + "priceUsd": "0.16794996452261213" + }, + { + "chainId": 137, + "address": "0x1B9D40715E757Bdb9bdEC3215B898E46d8a3b71a", + "name": "Metis", + "symbol": "METIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", + "priceUsd": "327272.3171370884" + }, + { + "chainId": 137, + "address": "0x01288e04435bFcd4718FF203D6eD18146C17Cd4b", + "name": "Magic Internet Money", + "symbol": "MIM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x1C5cccA2CB59145A4B25F452660cbA6436DDce9b", + "name": "Mirror Protocol", + "symbol": "MIR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13295/thumb/mirror_logo_transparent.png?1611554658", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x6f7C932e7684666C9fd1d44527765433e01fF61d", + "name": "Maker", + "symbol": "MKR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png", + "priceUsd": "1588.5852189147936" + }, + { + "chainId": 137, + "address": "0xa9f37D84c856fDa3812ad0519Dad44FA0a3Fe207", + "name": "Melon", + "symbol": "MLN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", + "priceUsd": "9.719215562502487" + }, + { + "chainId": 137, + "address": "0x6968105460f67c3BF751bE7C15f92F5286Fd0CE5", + "name": "Monavale", + "symbol": "MONA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13298/thumb/monavale_logo.jpg?1607232721", + "priceUsd": "69.37502697042436" + }, + { + "chainId": 137, + "address": "0xA3c322Ad15218fBFAEd26bA7f616249f7705D945", + "name": "GensoKishi Metaverse", + "symbol": "MV", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", + "priceUsd": "0.007571744848345652" + }, + { + "chainId": 137, + "address": "0x4985E0B13554fB521840e893574D3848C10Fcc6f", + "name": "PolySwarm", + "symbol": "NCT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", + "priceUsd": "0.02050158" + }, + { + "chainId": 137, + "address": "0x0Bf519071b02F22C17E7Ed5F4002ee1911f46729", + "name": "Numeraire", + "symbol": "NMR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x282d8efCe846A88B159800bd4130ad77443Fa1A1", + "name": "Ocean Protocol", + "symbol": "OCEAN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", + "priceUsd": "0.2437154250124526" + }, + { + "chainId": 137, + "address": "0xa63Beffd33AB3a2EfD92a39A7D2361CEE14cEbA8", + "name": "Origin Protocol", + "symbol": "OGN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x62414D03084EeB269E18C970a21f45D2967F0170", + "name": "OMG Network", + "symbol": "OMG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/776/thumb/OMG_Network.jpg?1591167168", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x0EE392bA5ef1354c9bd75a98044667d307C0e773", + "name": "Orion Protocol", + "symbol": "ORN", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", + "priceUsd": "0.1519682246131855" + }, + { + "chainId": 137, + "address": "0x9880e3dDA13c8e7D4804691A45160102d31F6060", + "name": "Orchid", + "symbol": "OXT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4575f41308EC1483f3d399aa9a2826d74Da13Deb/logo.png", + "priceUsd": "4955950.58026542" + }, + { + "chainId": 137, + "address": "0x553d3D295e0f695B9228246232eDF400ed3560B5", + "name": "PAX Gold", + "symbol": "PAXG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", + "priceUsd": "4055.03754680343" + }, + { + "chainId": 137, + "address": "0x1D47e931F82bb9F8D967F0Cc3288268449835806", + "name": "Pendle", + "symbol": "PENDLE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x263534a4Fe3cb249dF46810718B7B612a30ebbff", + "name": "Perpetual Protocol", + "symbol": "PERP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x7dc0cb65EC6019330a6841e9c274f2EE57A6CA6C", + "name": "Pluton", + "symbol": "PLU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1241/thumb/pluton.png?1548331624", + "priceUsd": "67.76933568240494" + }, + { + "chainId": 137, + "address": "0x8dc302e2141DA59c934d900886DbF1518Fd92cd4", + "name": "Polkastarter", + "symbol": "POLS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", + "priceUsd": "133.71606618040283" + }, + { + "chainId": 137, + "address": "0xcB059C5573646047D6d88dDdb87B745C18161d3b", + "name": "Polymath", + "symbol": "POLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", + "priceUsd": "0.2169076673179811" + }, + { + "chainId": 137, + "address": "0x73580A2416A57f1C4b6391DBA688A9e4f7DBECE0", + "name": "Marlin", + "symbol": "POND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8903/thumb/POND_200x200.png?1622515451", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x0AaB8DC887D34f00D50E19aee48371a941390d14", + "name": "Power Ledger", + "symbol": "POWR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", + "priceUsd": "105.6802273292315" + }, + { + "chainId": 137, + "address": "0x82FFdFD1d8699E8886a4e77CeFA9dd9710a7FefD", + "name": "Propy", + "symbol": "PRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/869/thumb/propy.png?1548332100", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x9377Eeb7419486FD4D485671d50baa4BF77c2222", + "name": "PARSIQ", + "symbol": "PRQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", + "priceUsd": "78353034158176720" + }, + { + "chainId": 137, + "address": "0x36B77a184bE8ee56f5E81C56727B20647A42e28E", + "name": "Quant", + "symbol": "QNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3370/thumb/5ZOu7brX_400x400.jpg?1612437252", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x831753DD7087CaC61aB5644b308642cc1c33Dc13", + "name": "Quickswap", + "symbol": "QUICK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13970/thumb/1_pOU6pBMEmiL-ZJVb0CYRjQ.png?1613386659", + "priceUsd": "23.531103099950304" + }, + { + "chainId": 137, + "address": "0x2f81e176471CC57fDC76f7d332FB4511bF2bebDD", + "name": "Radicle", + "symbol": "RAD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14013/thumb/radicle.png?1614402918", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x00e5646f60AC6Fb446f621d146B6E1886f002905", + "name": "Rai Reflex Index", + "symbol": "RAI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", + "priceUsd": "3.625711250113875" + }, + { + "chainId": 137, + "address": "0x780053837cE2CeEaD2A90D9151aA21FC89eD49c2", + "name": "Rarible", + "symbol": "RARI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", + "priceUsd": "0.8924905136483643" + }, + { + "chainId": 137, + "address": "0xc3cFFDAf8F3fdF07da6D5e3A89B8723D5E385ff8", + "name": "Rubic", + "symbol": "RBC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12629/thumb/200x200.png?1607952509", + "priceUsd": "0.010866470034968772" + }, + { + "chainId": 137, + "address": "0x19782D3Dc4701cEeeDcD90f0993f0A9126ed89d0", + "name": "Republic Token", + "symbol": "REN", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", + "priceUsd": "3175614299.350026" + }, + { + "chainId": 137, + "address": "0x6563c1244820CfBd6Ca8820FBdf0f2847363F733", + "name": "Reputation Augur v2", + "symbol": "REPv2", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x221657776846890989a759BA2973e427DfF5C9bB/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xAdf2F2Ed91755eA3f4bcC9107a494879f633ae7C", + "name": "Request", + "symbol": "REQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1031/thumb/Request_icon_green.png?1643250951", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x70c006878a5A50Ed185ac4C87d837633923De296", + "name": "REVV", + "symbol": "REVV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", + "priceUsd": "0.0010976782277687927" + }, + { + "chainId": 137, + "address": "0x3b9dB434F08003A89554CDB43b3e0b1f8734BdE7", + "name": "Rari Governance Token", + "symbol": "RGT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12900/thumb/Rari_Logo_Transparent.png?1613978014", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xbe662058e00849C3Eef2AC9664f37fEfdF2cdbFE", + "name": "iExec RLC", + "symbol": "RLC", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/646/thumb/pL1VuXm.png?1604543202", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x76b8D57e5ac6afAc5D415a054453d1DD2c3C0094", + "name": "Rally", + "symbol": "RLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12843/thumb/image.png?1611212077", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x61299774020dA444Af134c82fa83E3810b309991", + "name": "Render Token", + "symbol": "RNDR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", + "priceUsd": "0.1588243042447544" + }, + { + "chainId": 137, + "address": "0xF92501c8213da1D6C74A76372CCc720Dc8818407", + "name": "Rook", + "symbol": "ROOK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13005/thumb/keeper_dao_logo.jpg?1604316506", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xBbba073C31bF03b8ACf7c28EF0738DeCF3695683", + "name": "The Sandbox", + "symbol": "SAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", + "priceUsd": "0.262065" + }, + { + "chainId": 137, + "address": "0x1d734A02eF1e1f5886e66b0673b71Af5B53ffA94", + "name": "Stader", + "symbol": "SD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", + "priceUsd": "0.5060047497001328" + }, + { + "chainId": 137, + "address": "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec", + "name": "Shiba Inu", + "symbol": "SHIB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11939/thumb/shiba.png?1622619446", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x0C7304fBAf2A320a1c50c46FE03752722F729946", + "name": "Smooth Love Potion", + "symbol": "SLP", + "decimals": 0, + "logoUrl": "https://assets.coingecko.com/coins/images/10366/thumb/SLP.png?1578640057", + "priceUsd": "0.0014142877607304268" + }, + { + "chainId": 137, + "address": "0x50B728D8D964fd00C2d0AAD81718b71311feF68a", + "name": "Synthetix Network Token", + "symbol": "SNX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", + "priceUsd": "1.07340797536133" + }, + { + "chainId": 137, + "address": "0xcdB3C70CD25FD15307D84C4F9D37d5C043B33Fb2", + "name": "Spell Token", + "symbol": "SPELL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xd72357dAcA2cF11A5F155b9FF7880E595A3F5792", + "name": "Storj Token", + "symbol": "STORJ", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", + "priceUsd": "0.2652282806190598" + }, + { + "chainId": 137, + "address": "0xB36e3391B22a970d31A9b620Ae1A414C6c256d2a", + "name": "Stox", + "symbol": "STX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1230/thumb/stox-token.png?1547035256", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x60Ea918FC64360269Da4efBDA11d8fC6514617C6", + "name": "SUKU", + "symbol": "SUKU", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11969/thumb/UmfW5S6f_400x400.jpg?1596602238", + "priceUsd": "0.026236927932920286" + }, + { + "chainId": 137, + "address": "0xa1428174F516F527fafdD146b883bB4428682737", + "name": "SuperFarm", + "symbol": "SUPER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", + "priceUsd": "0.5593452120432967" + }, + { + "chainId": 137, + "address": "0xF81b4Bec6Ca8f9fe7bE01CA734F55B2b6e03A7a0", + "name": "Synth sUSD", + "symbol": "sUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", + "priceUsd": "0.21806966598410188" + }, + { + "chainId": 137, + "address": "0x0b3F868E0BE5597D5DB7fEB59E1CADBb0fdDa50a", + "name": "Sushi", + "symbol": "SUSHI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "priceUsd": "0.684912787715374" + }, + { + "chainId": 137, + "address": "0x6aBB753C1893194DE4a83c6e8B4EadFc105Fd5f5", + "name": "Swipe", + "symbol": "SXP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9368/thumb/swipe.png?1566792311", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xe1708AbDE4847B4929b70547E5197F1Ba1db2250", + "name": "Tokemak", + "symbol": "TOKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17495/thumb/tokemak-avatar-200px-black.png?1628131614", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xA7b98d63a137bF402b4570799ac4caD0BB1c4B1c", + "name": "OriginTrail", + "symbol": "TRAC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1877/thumb/TRAC.jpg?1635134367", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xE3322702BEdaaEd36CdDAb233360B939775ae5f1", + "name": "Tellor", + "symbol": "TRB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", + "priceUsd": "32.42" + }, + { + "chainId": 137, + "address": "0x8676815789211E799a6DC86d02748ADF9cF86836", + "name": "Tribe", + "symbol": "TRIBE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14575/thumb/tribe.PNG?1617487954", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x5b77bCA482bd3E7958b1103d123888EfCCDaF803", + "name": "TrueFi", + "symbol": "TRU", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/13180/thumb/truefi_glyph_color.png?1617610941", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x5667dcC0ab74D1b1355C3b2061893399331B57e2", + "name": "The Virtua Kolect", + "symbol": "TVK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13330/thumb/virtua_original.png?1656043619", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x3066818837c5e6eD6601bd5a91B0762877A6B731", + "name": "UMA Voting Token v1", + "symbol": "UMA", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828/logo.png", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0xb33EaAd8d922B1083446DC23f610c2567fB5180f", + "name": "Uniswap", + "symbol": "UNI", + "decimals": 18, + "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", + "priceUsd": "7.84" + }, + { + "chainId": 137, + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.999705" + }, + { + "chainId": 137, + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "name": "USDCoin (PoS)", + "symbol": "USDC.e", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.99969" + }, + { + "chainId": 137, + "address": "0x6F3B3286fd86d8b47EC737CEB3D0D354cc657B3e", + "name": "Pax Dollar", + "symbol": "USDP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", + "priceUsd": "11967590282.942545" + }, + { + "chainId": 137, + "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "1" + }, + { + "chainId": 137, + "address": "0x8DE5B80a0C1B02Fe4976851D030B36122dbb8624", + "name": "Vanar Chain", + "symbol": "VANRY", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/33466/large/apple-touch-icon.png?1701942541", + "priceUsd": "0.02012472763151889" + }, + { + "chainId": 137, + "address": "0xd0258a3fD00f38aa8090dfee343f10A9D4d30D3F", + "name": "Voxies", + "symbol": "VOXEL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/21260/large/voxies.png", + "priceUsd": "0.04894837" + }, + { + "chainId": 137, + "address": "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + "name": "Wrapped BTC", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", + "priceUsd": "122678" + }, + { + "chainId": 137, + "address": "0x90bb6fEB70A9a43CfAaA615F856BA309FD759A90", + "name": "Wrapped Centrifuge", + "symbol": "WCFG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17106/thumb/WCFG.jpg?1626266462", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4373.11" + }, + { + "chainId": 137, + "address": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "name": "Wrapped Matic", + "symbol": "WMATIC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", + "priceUsd": "0.238182" + }, + { + "chainId": 137, + "address": "0x1B815d120B3eF02039Ee11dC2d33DE7aA4a8C603", + "name": "WOO Network", + "symbol": "WOO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", + "priceUsd": "0.07740721550939068" + }, + { + "chainId": 137, + "address": "0xDC3326e71D45186F113a2F448984CA0e8D201995", + "name": "XSGD", + "symbol": "XSGD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", + "priceUsd": "0.7712373977627099" + }, + { + "chainId": 137, + "address": "0xd2507e7b5794179380673870d88B22F94da6abe0", + "name": "XYO Network", + "symbol": "XYO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", + "priceUsd": "0.008801852407976166" + }, + { + "chainId": 137, + "address": "0xDA537104D6A5edd53c6fBba9A898708E465260b6", + "name": "yearn finance", + "symbol": "YFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", + "priceUsd": "5302.52" + }, + { + "chainId": 137, + "address": "0xb8cb8a7F4C2885C03e57E973C74827909Fdc2032", + "name": "DFI money", + "symbol": "YFII", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11902/thumb/YFII-logo.78631676.png?1598677348", + "priceUsd": null + }, + { + "chainId": 137, + "address": "0x82617aA52dddf5Ed9Bb7B370ED777b3182A30fd1", + "name": "Yield Guild Games", + "symbol": "YGG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17358/thumb/le1nzlO6_400x400.jpg?1632465691", + "priceUsd": "0.13199009150801538" + }, + { + "chainId": 137, + "address": "0x6985884C4392D348587B19cb9eAAf157F13271cd", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": "2.341099101056258" + }, + { + "chainId": 137, + "address": "0x5559Edb74751A0edE9DeA4DC23aeE72cCA6bE3D5", + "name": "0x Protocol Token", + "symbol": "ZRX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png", + "priceUsd": null + }, + { + "chainId": 324, + "address": "0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E", + "name": "ZKsync", + "symbol": "ZK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/38043/large/ZKTokenBlack.png?17186145029", + "priceUsd": "0.0542" + }, + { + "chainId": 480, + "address": "0x79A02482A880bCE3F13e09Da970dC34db4CD24d1", + "name": "Bridged USDC", + "symbol": "USDC.e", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/35218/large/USDC_Icon.png?1707908537", + "priceUsd": "0.999705" + }, + { + "chainId": 480, + "address": "0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3", + "name": "Wrapped BTC", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857", + "priceUsd": "122478.91939409873" + }, + { + "chainId": 480, + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.813939" + }, + { + "chainId": 1868, + "address": "0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "1" + }, + { + "chainId": 1868, + "address": "0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "0.998894" + }, + { + "chainId": 1868, + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4362.48" + }, + { + "chainId": 8453, + "address": "0xc5fecC3a29Fb57B5024eEc8a2239d4621e111CBE", + "name": "1inch", + "symbol": "1INCH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceUsd": "0.24584676791931187" + }, + { + "chainId": 8453, + "address": "0x63706e401c06ac8513145b7687A14804d17f814b", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": "276.06" + }, + { + "chainId": 8453, + "address": "0xe2A8cCB00E328a0EC2204CB0c736309D7c1fa556", + "name": "Arcblock", + "symbol": "ABT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2341/thumb/arcblock.png?1547036543", + "priceUsd": "0.6174385112333802" + }, + { + "chainId": 8453, + "address": "0x3c87e7AF3cDBAe5bB56b4936325Ea95CA3E0EfD9", + "name": "Ambire AdEx", + "symbol": "ADX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0x940181a94A35A4569E4529A3CDfB74e38FD98631", + "name": "Aerodrome Finance", + "symbol": "AERO", + "decimals": 18, + "logoUrl": "https://basescan.org/token/images/aerodrome_32.png", + "priceUsd": "1.072" + }, + { + "chainId": 8453, + "address": "0x4F9Fd6Be4a90f2620860d680c0d4d5Fb53d1A825", + "name": "aixbt by Virtuals", + "symbol": "AIXBT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51784/large/3.png?1731981138", + "priceUsd": "0.091058" + }, + { + "chainId": 8453, + "address": "0x97c806e7665d3AFd84A8Fe1837921403D59F3Dcc", + "name": "Alethea Artificial Liquid Intelligence", + "symbol": "ALI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", + "priceUsd": "0.00568759" + }, + { + "chainId": 8453, + "address": "0x1B4617734C43F6159F3a70b7E06d883647512778", + "name": "AWE Network", + "symbol": "AWE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/8713/large/awe-network.jpg?1747816016", + "priceUsd": "0.096117" + }, + { + "chainId": 8453, + "address": "0xB3B32F9f8827D4634fE7d973Fa1034Ec9fdDB3B3", + "name": "B3", + "symbol": "B3", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54287/large/B3.png?1739001374", + "priceUsd": "0.00258218" + }, + { + "chainId": 8453, + "address": "0x2a06A17CBC6d0032Cac2c6696DA90f29D39a1a29", + "name": "HarryPotterObamaSonic10Inu", + "symbol": "BITCOIN", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", + "priceUsd": "0.096997" + }, + { + "chainId": 8453, + "address": "0x22aF33FE49fD1Fa80c7149773dDe5890D3c76F3b", + "name": "BankrCoin", + "symbol": "BNKR", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52626/large/bankr-static.png?1736405365", + "priceUsd": "0.00055007" + }, + { + "chainId": 8453, + "address": "0xA7d68d155d17cB30e311367c2Ef1E82aB6022b67", + "name": "Braintrust", + "symbol": "BTRST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18100/thumb/braintrust.PNG?1630475394", + "priceUsd": "0.16859347976140426" + }, + { + "chainId": 8453, + "address": "0xcbADA732173e39521CDBE8bf59a6Dc85A9fc7b8c", + "name": "Coinbase Wrapped ADA", + "symbol": "cbADA", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/66647/large/Coinbase_Wrapped_Ada_%28cbADA%29.png?1750129533", + "priceUsd": "0.8149971266903104" + }, + { + "chainId": 8453, + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "name": "Coinbase Wrapped BTC", + "symbol": "cbBTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/40143/standard/cbbtc.webp", + "priceUsd": "122687" + }, + { + "chainId": 8453, + "address": "0xcbD06E5A2B0C65597161de254AA074E489dEb510", + "name": "Coinbase Wrapped DOGE", + "symbol": "CBDOGE", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/66268/large/Coinbase_Wrapped_Doge_%28cbDOGE%29.png?1749023465", + "priceUsd": "0.246198" + }, + { + "chainId": 8453, + "address": "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", + "name": "Coinbase Wrapped Staked ETH", + "symbol": "cbETH", + "decimals": 18, + "logoUrl": "https://ethereum-optimism.github.io/data/cbETH/logo.svg", + "priceUsd": "4816.72" + }, + { + "chainId": 8453, + "address": "0xcb17C9Db87B595717C857a08468793f5bAb6445F", + "name": "Coinbase Wrapped LTC", + "symbol": "cbLTC", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/66646/large/Coinbase_Wrapped_Litecoin_%28cbLTC%29.png?1750129508", + "priceUsd": "116.81383558776494" + }, + { + "chainId": 8453, + "address": "0xcb585250f852C6c6bf90434AB21A00f02833a4af", + "name": "Coinbase Wrapped XRP", + "symbol": "cbXRP", + "decimals": 6, + "logoUrl": "https://coin-images.coingecko.com/coins/images/66267/large/Coinbase_Wrapped_XPR_%28cbXRP%29.png?1749023398", + "priceUsd": "2.83" + }, + { + "chainId": 8453, + "address": "0x1bc0c42215582d5A085795f4baDbaC3ff36d1Bcb", + "name": "tokenbot", + "symbol": "CLANKER", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51440/large/CLANKER.png?1731232869", + "priceUsd": "29.77" + }, + { + "chainId": 8453, + "address": "0x9e1028F5F1D5eDE59748FFceE5532509976840E0", + "name": "Compound", + "symbol": "COMP", + "decimals": 18, + "logoUrl": "https://ethereum-optimism.github.io/data/COMP/logo.svg", + "priceUsd": "41.87896029568512" + }, + { + "chainId": 8453, + "address": "0xC0041EF357B183448B235a8Ea73Ce4E4eC8c265F", + "name": "Cookie", + "symbol": "COOKIE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/38450/large/cookie_token_logo_200x200.png?1733194528", + "priceUsd": "0.118352" + }, + { + "chainId": 8453, + "address": "0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415", + "name": "Curve DAO Token", + "symbol": "CRV", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", + "priceUsd": "0.719583" + }, + { + "chainId": 8453, + "address": "0x259Fac10c5CbFEFE3E710e1D9467f70a76138d45", + "name": "Cartesi", + "symbol": "CTSI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", + "priceUsd": "0.07261" + }, + { + "chainId": 8453, + "address": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + "name": "Dai Stablecoin", + "symbol": "DAI", + "decimals": 18, + "logoUrl": "https://ethereum-optimism.github.io/data/DAI/logo.svg", + "priceUsd": "0.998708" + }, + { + "chainId": 8453, + "address": "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed", + "name": "Degen", + "symbol": "DEGEN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/34515/large/android-chrome-512x512.png?1706198225", + "priceUsd": "0.00274528" + }, + { + "chainId": 8453, + "address": "0x6921B130D297cc43754afba22e5EAc0FBf8Db75b", + "name": "doginme", + "symbol": "doginme", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/35123/large/doginme-logo1-transparent200.png?1710856784", + "priceUsd": "0.00037197" + }, + { + "chainId": 8453, + "address": "0x9d0E8f5b25384C7310CB8C6aE32C8fbeb645d083", + "name": "Derive", + "symbol": "DRV", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", + "priceUsd": "0.03600272" + }, + { + "chainId": 8453, + "address": "0xED6E000dEF95780fb89734c07EE2ce9F6dcAf110", + "name": "Definitive", + "symbol": "EDGE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/55072/large/EDGE-120x120.png?1743598652", + "priceUsd": "0.270258" + }, + { + "chainId": 8453, + "address": "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42", + "name": "EURC", + "symbol": "EURC", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/26045/standard/euro.png", + "priceUsd": "1.16" + }, + { + "chainId": 8453, + "address": "0xb33Ff54b9F7242EF1593d2C9Bcd8f9df46c77935", + "name": "FAI", + "symbol": "FAI", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52315/large/FAI.png?1733076295", + "priceUsd": "0.00974502" + }, + { + "chainId": 8453, + "address": "0x5aB3D4c385B400F3aBB49e80DE2fAF6a88A7B691", + "name": "Flock", + "symbol": "FLOCK", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/53178/large/FLock_Token_Logo.png?1735561398", + "priceUsd": "0.270907" + }, + { + "chainId": 8453, + "address": "0xB78e7D4C5d47Af92942321eD40419dab0E573810", + "name": "Goldfinch", + "symbol": "GFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19081/thumb/GOLDFINCH.png?1634369662", + "priceUsd": "0.4801060307424254" + }, + { + "chainId": 8453, + "address": "0xcD2F22236DD9Dfe2356D7C543161D4d260FD9BcB", + "name": "Aavegotchi", + "symbol": "GHST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", + "priceUsd": "0.37751" + }, + { + "chainId": 8453, + "address": "0x0F4d237B09Cb37d207BA60353Dc254d4530D4dF1", + "name": "The Graph", + "symbol": "GRT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", + "priceUsd": "0.08089360514368812" + }, + { + "chainId": 8453, + "address": "0x4BfAa776991E85e5f8b1255461cbbd216cFc714f", + "name": "HOME", + "symbol": "HOME", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54873/large/defi-app.png?1742235743", + "priceUsd": "0.02909586" + }, + { + "chainId": 8453, + "address": "0xBCBAf311ceC8a4EAC0430193A528d9FF27ae38C1", + "name": "IoTeX", + "symbol": "IOTX", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", + "priceUsd": "0.02332452" + }, + { + "chainId": 8453, + "address": "0xFf9957816c813C5Ad0b9881A8990Df1E3AA2a057", + "name": "Geojam", + "symbol": "JAM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24648/thumb/ey40AzBN_400x400.jpg?1648507272", + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0x98d0baa52b2D063E780DE12F615f963Fe8537553", + "name": "KAITO", + "symbol": "KAITO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54411/large/Qm4DW488_400x400.jpg?1739552780", + "priceUsd": "1.31" + }, + { + "chainId": 8453, + "address": "0x9a26F5433671751C3276a065f57e5a02D2817973", + "name": "Keyboard Cat", + "symbol": "KEYCAT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/36608/large/keyboard_cat.jpeg?1711965348", + "priceUsd": "0.00273519" + }, + { + "chainId": 8453, + "address": "0x7300B37DfdfAb110d83290A29DfB31B1740219fE", + "name": "Mamo", + "symbol": "MAMO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/55958/large/Mamo_Circle_200x200_TransBG.png?1748974093", + "priceUsd": "0.0695" + }, + { + "chainId": 8453, + "address": "0xBAa5CC21fd487B8Fcc2F632f3F4E8D37262a0842", + "name": "Morpho Token", + "symbol": "MORPHO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/29837/large/Morpho-token-icon.png?1726771230", + "priceUsd": "1.72" + }, + { + "chainId": 8453, + "address": "0xE6bAA3Fedb5Dc88b2c59ba4812388Bb0906D19dB", + "name": "PolySwarm", + "symbol": "NCT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", + "priceUsd": "0.020033226668842236" + }, + { + "chainId": 8453, + "address": "0xca73ed1815e5915489570014e024b7EbE65dE679", + "name": "Odos Token", + "symbol": "ODOS", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52914/large/odos.jpg?1734678948", + "priceUsd": "0.0045635" + }, + { + "chainId": 8453, + "address": "0x7002458B1DF59EccB57387bC79fFc7C29E22e6f7", + "name": "Origin Protocol", + "symbol": "OGN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", + "priceUsd": "0.057866" + }, + { + "chainId": 8453, + "address": "0xA99F6e6785Da0F5d6fB42495Fe424BCE029Eeb3E", + "name": "Pendle", + "symbol": "PENDLE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", + "priceUsd": "4.48" + }, + { + "chainId": 8453, + "address": "0xB4fDe59a779991bfB6a52253B51947828b982be3", + "name": "Pepe", + "symbol": "PEPE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1682922725", + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0x30c7235866872213F68cb1F08c37Cb9eCCB93452", + "name": "Prompt", + "symbol": "PROMPT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/55169/large/wayfinder.jpg?1744336900", + "priceUsd": "0.137157" + }, + { + "chainId": 8453, + "address": "0x1f73EAf55d696BFFA9b0EA16fa987B93b0f4d302", + "name": "Rocket Pool Protocol", + "symbol": "RPL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/2090/large/rocket_pool_%28RPL%29.png?1696503058", + "priceUsd": "37.709102377815825" + }, + { + "chainId": 8453, + "address": "0xFbB75A59193A3525a8825BeBe7D4b56899E2f7e1", + "name": "ResearchCoin", + "symbol": "RSC", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/28146/large/RH_BOTTLE_CLEAN_Aug_2024_1.png?1732742001", + "priceUsd": "0.447549" + }, + { + "chainId": 8453, + "address": "0xaB36452DbAC151bE02b16Ca17d8919826072f64a", + "name": "Reserve Rights", + "symbol": "RSR", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", + "priceUsd": "0.005826723370570488" + }, + { + "chainId": 8453, + "address": "0xC729777d0470F30612B1564Fd96E8Dd26f5814E3", + "name": "Sapien", + "symbol": "SAPIEN", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/68423/large/logo.png?1755710030", + "priceUsd": "0.152168" + }, + { + "chainId": 8453, + "address": "0x1C7a460413dD4e964f96D8dFC56E7223cE88CD85", + "name": "Seamlesss", + "symbol": "SEAM", + "decimals": 18, + "logoUrl": "https://basescan.org/token/images/seamless_32.png", + "priceUsd": "0.340919" + }, + { + "chainId": 8453, + "address": "0x662015EC830DF08C0FC45896FaB726542e8AC09E", + "name": "Status", + "symbol": "SNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", + "priceUsd": "0.029180593575659698" + }, + { + "chainId": 8453, + "address": "0x22e6966B799c4D5B13BE962E1D117b56327FDa66", + "name": "Synthetix Network Token", + "symbol": "SNX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", + "priceUsd": "1.087" + }, + { + "chainId": 8453, + "address": "0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C", + "name": "SPX6900", + "symbol": "SPX", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", + "priceUsd": "1.45" + }, + { + "chainId": 8453, + "address": "0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4", + "name": "Toshi", + "symbol": "TOSHI", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31126/large/Toshi_Logo_-_Circular.png?1721677476", + "priceUsd": "0.00098898" + }, + { + "chainId": 8453, + "address": "0x00000000A22C618fd6b4D7E9A335C4B96B189a38", + "name": "Towns", + "symbol": "TOWNS", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/55230/large/yImUvwK__400x400.png?1744857671", + "priceUsd": "0.01811908" + }, + { + "chainId": 8453, + "address": "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA", + "name": "USD Base Coin", + "symbol": "USDbC", + "decimals": 6, + "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", + "priceUsd": "0.999397" + }, + { + "chainId": 8453, + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", + "priceUsd": "0.999705" + }, + { + "chainId": 8453, + "address": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "0.999194" + }, + { + "chainId": 8453, + "address": "0xacfE6019Ed1A7Dc6f7B508C02d1b04ec88cC21bf", + "name": "Venice Token", + "symbol": "VVV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/54023/standard/Venice_Token_(1).png?1738017546", + "priceUsd": "2.41" + }, + { + "chainId": 8453, + "address": "0x489fe42C267fe0366B16b0c39e7AEEf977E841eF", + "name": "Wrapped Ampleforth", + "symbol": "WAMPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", + "priceUsd": "2.1096999193233255" + }, + { + "chainId": 8453, + "address": "0xA88594D404727625A9437C3f886C7643872296AE", + "name": "Moonwell", + "symbol": "WELL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26133/large/WELL.png?1696525221", + "priceUsd": "0.02312538" + }, + { + "chainId": 8453, + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://ethereum-optimism.github.io/data/WETH/logo.png", + "priceUsd": "4372.77" + }, + { + "chainId": 8453, + "address": "0xD7B99ffB8B2afc6fe013a17207cbe50f223aDc94", + "name": "XYO Network", + "symbol": "XYO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", + "priceUsd": "0.009040" + }, + { + "chainId": 8453, + "address": "0x9EaF8C1E34F05a589EDa6BAfdF391Cf6Ad3CB239", + "name": "yearn finance", + "symbol": "YFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", + "priceUsd": "5340.755750023404" + }, + { + "chainId": 8453, + "address": "0xf43eB8De897Fbc7F2502483B2Bef7Bb9EA179229", + "name": "Horizen", + "symbol": "ZEN", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/691/large/Horizen2.0-logo_icon-on-yellow_%281%29.png?1751696763", + "priceUsd": "11.62" + }, + { + "chainId": 8453, + "address": "0x1111111111166b7FE7bd91427724B487980aFc69", + "name": "Zora", + "symbol": "ZORA", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54693/large/zora.jpg?1741094751", + "priceUsd": "0.053145" + }, + { + "chainId": 8453, + "address": "0x6985884C4392D348587B19cb9eAAf157F13271cd", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": "2.34" + }, + { + "chainId": 8453, + "address": "0x3bB4445D30AC020a84c1b5A8A2C6248ebC9779D0", + "name": "0x Protocol Token", + "symbol": "ZRX", + "decimals": 18, + "logoUrl": "https://ethereum-optimism.github.io/data/ZRX/logo.png", + "priceUsd": "0.09119117231707215" + }, + { + "chainId": 42161, + "address": "0x6314C31A7a1652cE482cffe247E9CB7c3f4BB9aF", + "name": "1inch", + "symbol": "1INCH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", + "priceUsd": "0.2489588228471673" + }, + { + "chainId": 42161, + "address": "0xba5DdD1f9d7F570dc94a51479a000E3BCE967196", + "name": "Aave", + "symbol": "AAVE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", + "priceUsd": "276.06" + }, + { + "chainId": 42161, + "address": "0x53691596d1BCe8CEa565b84d4915e69e03d9C99d", + "name": "Across Protocol Token", + "symbol": "ACX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", + "priceUsd": "0.11094" + }, + { + "chainId": 42161, + "address": "0x377c1Fc73D4D0f5600cd943776CED07c2B9783cd", + "name": "Aevo", + "symbol": "AEVO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35893/standard/aevo.png", + "priceUsd": "0.09675610052146794" + }, + { + "chainId": 42161, + "address": "0xFA5Ed56A203466CbBC2430a43c66b9D8723528E7", + "name": "agEur", + "symbol": "agEUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", + "priceUsd": "1.16" + }, + { + "chainId": 42161, + "address": "0xb7910E8b16e63EFD51d5D1a093d56280012A3B9C", + "name": "Adventure Gold", + "symbol": "AGLD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", + "priceUsd": "0.5464" + }, + { + "chainId": 42161, + "address": "0xeC76E8fe6e2242e6c2117caA244B9e2DE1569923", + "name": "AIOZ Network", + "symbol": "AIOZ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", + "priceUsd": "0.2679156726233584" + }, + { + "chainId": 42161, + "address": "0xe7dcD50836d0A28c959c72D72122fEDB8E245A6C", + "name": "Aleph im", + "symbol": "ALEPH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", + "priceUsd": "0.0792471029790218" + }, + { + "chainId": 42161, + "address": "0xeF6124368c0B56556667e0de77eA008DfC0a71d1", + "name": "Alethea Artificial Liquid Intelligence", + "symbol": "ALI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", + "priceUsd": "0.005675368522712019" + }, + { + "chainId": 42161, + "address": "0xC9CBf102c73fb77Ec14f8B4C8bd88e050a6b2646", + "name": "Alpha Venture DAO", + "symbol": "ALPHA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", + "priceUsd": "0.014732268070836543" + }, + { + "chainId": 42161, + "address": "0x1bfc5d35bf0f7B9e15dc24c78b8C02dbC1e95447", + "name": "Ankr", + "symbol": "ANKR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", + "priceUsd": "0.013680940635142481" + }, + { + "chainId": 42161, + "address": "0x74885b4D524d497261259B38900f54e6dbAd2210", + "name": "ApeCoin", + "symbol": "APE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", + "priceUsd": "0.5603204598658061" + }, + { + "chainId": 42161, + "address": "0xF01dB12F50D0CDF5Fe360ae005b9c52F92CA7811", + "name": "API3", + "symbol": "API3", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13256/thumb/api3.jpg?1606751424", + "priceUsd": "0.8185828718931633" + }, + { + "chainId": 42161, + "address": "0x912CE59144191C1204E64559FE8253a0e49E6548", + "name": "Arbitrum", + "symbol": "ARB", + "decimals": 18, + "logoUrl": "https://arbitrum.foundation/logo.png", + "priceUsd": "0.418755" + }, + { + "chainId": 42161, + "address": "0xDac5094B7D59647626444a4F905060FCda4E656E", + "name": "Arkham", + "symbol": "ARKM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/30929/standard/Arkham_Logo_CG.png?1696529771", + "priceUsd": "0.5087351708583264" + }, + { + "chainId": 42161, + "address": "0xAC9Ac2C17cdFED4AbC80A53c5553388575714d03", + "name": "Automata", + "symbol": "ATA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", + "priceUsd": "0.0397" + }, + { + "chainId": 42161, + "address": "0xc7dEf82Ba77BAF30BbBc9b6162DC075b49092fb4", + "name": "Aethir Token", + "symbol": "ATH", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/36179/large/logogram_circle_dark_green_vb_green_(1).png?1718232706", + "priceUsd": "0.052432130189680765" + }, + { + "chainId": 42161, + "address": "0x23ee2343B892b1BB63503a4FAbc840E0e2C6810f", + "name": "Axelar", + "symbol": "AXL", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", + "priceUsd": "0.291207" + }, + { + "chainId": 42161, + "address": "0xe88998Fb579266628aF6a03e3821d5983e5D0089", + "name": "Axie Infinity", + "symbol": "AXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", + "priceUsd": "2.1266584308552745" + }, + { + "chainId": 42161, + "address": "0xBfa641051Ba0a0Ad1b0AcF549a89536A0D76472E", + "name": "Badger DAO", + "symbol": "BADGER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", + "priceUsd": "0.972151" + }, + { + "chainId": 42161, + "address": "0x040d1EdC9569d4Bab2D15287Dc5A4F10F56a56B8", + "name": "Balancer", + "symbol": "BAL", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", + "priceUsd": "1.14" + }, + { + "chainId": 42161, + "address": "0x3450687EF141dCd6110b77c2DC44B008616AeE75", + "name": "Basic Attention Token", + "symbol": "BAT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", + "priceUsd": "0.1571154704702344" + }, + { + "chainId": 42161, + "address": "0xa68Ec98D7ca870cF1Dd0b00EBbb7c4bF60A8e74d", + "name": "Biconomy", + "symbol": "BICO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", + "priceUsd": "0.090553" + }, + { + "chainId": 42161, + "address": "0x406C8dB506653D882295875F633bEC0bEb921C2A", + "name": "BitDAO", + "symbol": "BIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17627/thumb/rI_YptK8.png?1653983088", + "priceUsd": "2.5364775777228057" + }, + { + "chainId": 42161, + "address": "0xf7e17BA61973bcDB61f471eFb989E47d13bD565D", + "name": "HarryPotterObamaSonic10Inu", + "symbol": "BITCOIN", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", + "priceUsd": "0.0975789992870829" + }, + { + "chainId": 42161, + "address": "0xEf171a5BA71348eff16616fd692855c2Fe606EB2", + "name": "Blur", + "symbol": "BLUR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28453/large/blur.png?1670745921", + "priceUsd": "0.07161749917119595" + }, + { + "chainId": 42161, + "address": "0x7A24159672b83ED1b89467c9d6A99556bA06D073", + "name": "Bancor Network Token", + "symbol": "BNT", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", + "priceUsd": "0.6856" + }, + { + "chainId": 42161, + "address": "0x0D81E50bC677fa67341c44D7eaA9228DEE64A4e1", + "name": "BarnBridge", + "symbol": "BOND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", + "priceUsd": "0.154313" + }, + { + "chainId": 42161, + "address": "0x31190254504622cEFdFA55a7d3d272e6462629a2", + "name": "Binance USD", + "symbol": "BUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", + "priceUsd": "1.025531135070954" + }, + { + "chainId": 42161, + "address": "0xCdc343ebf71e38F003eD6c80171F5B8D7cF58860", + "name": "PancakeSwap", + "symbol": "CAKE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/12632/large/pancakeswap-cake-logo_%281%29.png?1696512440", + "priceUsd": "3.829" + }, + { + "chainId": 42161, + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "name": "Coinbase Wrapped BTC", + "symbol": "cbBTC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/40143/large/cbbtc.webp", + "priceUsd": "122687" + }, + { + "chainId": 42161, + "address": "0x1DEBd73E752bEaF79865Fd6446b0c970EaE7732f", + "name": "Coinbase Wrapped Staked ETH", + "symbol": "cbETH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/27008/large/cbeth.png", + "priceUsd": "4816.72" + }, + { + "chainId": 42161, + "address": "0x4E51aC49bC5e2d87e0EF713E9e5AB2D71EF4F336", + "name": "Celo native asset (Wormhole)", + "symbol": "CELO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", + "priceUsd": "0.3765" + }, + { + "chainId": 42161, + "address": "0x3a8B787f78D775AECFEEa15706D4221B40F345AB", + "name": "Celer Network", + "symbol": "CELR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4379/thumb/Celr.png?1554705437", + "priceUsd": "0.00767154" + }, + { + "chainId": 42161, + "address": "0x354A6dA3fcde098F8389cad84b0182725c6C91dE", + "name": "Compound", + "symbol": "COMP", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", + "priceUsd": "41.63" + }, + { + "chainId": 42161, + "address": "0x6FE14d3CC2f7bDdffBa5CdB3BBE7467dd81ea101", + "name": "COTI", + "symbol": "COTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2962/thumb/Coti.png?1559653863", + "priceUsd": "0.05108528424791468" + }, + { + "chainId": 42161, + "address": "0xcb8b5CD20BdCaea9a010aC1F8d835824F5C87A04", + "name": "CoW Protocol", + "symbol": "COW", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24384/large/CoW-token_logo.png?1719524382", + "priceUsd": "0.274709" + }, + { + "chainId": 42161, + "address": "0x69b937dB799a9BECC9E8A6F0a5d36eA3657273bf", + "name": "Covalent", + "symbol": "CQT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14168/thumb/covalent-cqt.png?1624545218", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0x8ea3156f834A0dfC78F1A5304fAC2CdA676F354C", + "name": "Cronos", + "symbol": "CRO", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", + "priceUsd": "0.1933605667352337" + }, + { + "chainId": 42161, + "address": "0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978", + "name": "Curve DAO Token", + "symbol": "CRV", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", + "priceUsd": "0.719583" + }, + { + "chainId": 42161, + "address": "0x319f865b287fCC10b30d8cE6144e8b6D1b476999", + "name": "Cartesi", + "symbol": "CTSI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", + "priceUsd": "0.07261" + }, + { + "chainId": 42161, + "address": "0x84F5c2cFba754E76DD5aE4fB369CfC920425E12b", + "name": "Cryptex Finance", + "symbol": "CTX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14932/thumb/glossy_icon_-_C200px.png?1619073171", + "priceUsd": "1.3272342177375096" + }, + { + "chainId": 42161, + "address": "0x9DfFB23CAd3322440bCcFF7aB1C58E781dDBF144", + "name": "Civic", + "symbol": "CVC", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", + "priceUsd": "0.08119748484973946" + }, + { + "chainId": 42161, + "address": "0xaAFcFD42c9954C6689ef1901e03db742520829c5", + "name": "Convex Finance", + "symbol": "CVX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", + "priceUsd": "3.298529250840107" + }, + { + "chainId": 42161, + "address": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + "name": "Dai Stablecoin", + "symbol": "DAI", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", + "priceUsd": "0.999426" + }, + { + "chainId": 42161, + "address": "0x3Be7cB2e9413Ef8F42b4A202a0114EB59b64e227", + "name": "DexTools", + "symbol": "DEXT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", + "priceUsd": "0.465878941863393" + }, + { + "chainId": 42161, + "address": "0xca642467C6Ebe58c13cB4A7091317f34E17ac05e", + "name": "DIA", + "symbol": "DIA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", + "priceUsd": "0.531682261965245" + }, + { + "chainId": 42161, + "address": "0xE3696a02b2C9557639E29d829E9C45EFa49aD47A", + "name": "district0x", + "symbol": "DNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/849/thumb/district0x.png?1547223762", + "priceUsd": "0.024045636188350344" + }, + { + "chainId": 42161, + "address": "0x4667cf53C4eDF659E402B733BEA42B18B68dd74c", + "name": "DeFi Pulse Index", + "symbol": "DPI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", + "priceUsd": "101.94157061883172" + }, + { + "chainId": 42161, + "address": "0x77b7787a09818502305C95d68A2571F090abb135", + "name": "Derive", + "symbol": "DRV", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", + "priceUsd": "0.03596978" + }, + { + "chainId": 42161, + "address": "0x51863cB90Ce5d6dA9663106F292fA27c8CC90c5a", + "name": "dYdX", + "symbol": "DYDX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", + "priceUsd": "0.07400739968866298" + }, + { + "chainId": 42161, + "address": "0x606C3e5075e5555e79Aa15F1E9FACB776F96C248", + "name": "EigenLayer", + "symbol": "EIGEN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37441/large/eigen.jpg?1728023974", + "priceUsd": "1.7696757385491755" + }, + { + "chainId": 42161, + "address": "0x3e4Cff6E50F37F731284A92d44AE943e17077fD4", + "name": "Dogelon Mars", + "symbol": "ELON", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14962/thumb/6GxcPRo3_400x400.jpg?1619157413", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0xdf8F0c63D9335A0AbD89F9F752d293A98EA977d8", + "name": "Ethena", + "symbol": "ENA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/36530/standard/ethena.png", + "priceUsd": "0.5565875400020969" + }, + { + "chainId": 42161, + "address": "0x7fa9549791EFc9030e1Ed3F25D18014163806758", + "name": "Enjin Coin", + "symbol": "ENJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1102/thumb/enjin-coin-logo.png?1547035078", + "priceUsd": "0.0746857623618561" + }, + { + "chainId": 42161, + "address": "0xfeA31d704DEb0975dA8e77Bf13E04239e70d7c28", + "name": "Ethereum Name Service", + "symbol": "ENS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", + "priceUsd": "20.675993532741565" + }, + { + "chainId": 42161, + "address": "0x2354c8e9Ea898c751F1A15Addeb048714D667f96", + "name": "Ethernity Chain", + "symbol": "ERN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", + "priceUsd": "0.08470155516475816" + }, + { + "chainId": 42161, + "address": "0x07D65C18CECbA423298c0aEB5d2BeDED4DFd5736", + "name": "Ether.fi", + "symbol": "ETHFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35958/standard/etherfi.jpeg", + "priceUsd": "1.6250180581990923" + }, + { + "chainId": 42161, + "address": "0x863708032B5c328e11aBcbC0DF9D79C71Fc52a48", + "name": "Euro Coin", + "symbol": "EURC", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", + "priceUsd": "1.1619401852007891" + }, + { + "chainId": 42161, + "address": "0x8553d254Cb6934b16F87D2e486b64BbD24C83C70", + "name": "Harvest Finance", + "symbol": "FARM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", + "priceUsd": "26.909293668650218" + }, + { + "chainId": 42161, + "address": "0x4BE87C766A7CE11D5Cc864b6C3Abb7457dCC4cC9", + "name": "Fetch ai", + "symbol": "FET", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + "priceUsd": "0.5283772641865987" + }, + { + "chainId": 42161, + "address": "0x849B40AB2469309117Ed1038c5A99894767C7282", + "name": "Stafi", + "symbol": "FIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", + "priceUsd": "0.08052380138181776" + }, + { + "chainId": 42161, + "address": "0xA8C25FdC09763A176353CC6a76882e05b4905FAe", + "name": "FLOKI", + "symbol": "FLOKI", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0x63806C056Fa458c548Fb416B15E358A9D685710A", + "name": "Flux", + "symbol": "FLUX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png", + "priceUsd": "0.17860945179289117" + }, + { + "chainId": 42161, + "address": "0x3A1429d50E0cBBc45c997aF600541Fe1cc3D2923", + "name": "Forta", + "symbol": "FORT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", + "priceUsd": "0.04361858" + }, + { + "chainId": 42161, + "address": "0xf929de51D91C77E42f5090069E0AD7A09e513c73", + "name": "ShapeShift FOX Token", + "symbol": "FOX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", + "priceUsd": "0.0237047" + }, + { + "chainId": 42161, + "address": "0x7468a5d8E02245B00E8C0217fCE021C70Bc51305", + "name": "Frax", + "symbol": "FRAX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", + "priceUsd": "0.9971658958023574" + }, + { + "chainId": 42161, + "address": "0xd42785D323e608B9E99fa542bd8b1000D4c2Df37", + "name": "Fantom", + "symbol": "FTM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", + "priceUsd": "0.27065749143232554" + }, + { + "chainId": 42161, + "address": "0xd9f9d2Ee2d3EFE420699079f16D9e924affFdEA4", + "name": "Frax Share", + "symbol": "FXS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", + "priceUsd": "2.124" + }, + { + "chainId": 42161, + "address": "0xc27E7325a6BEA1FcC06de7941473f5279bfd1182", + "name": "Galxe", + "symbol": "GAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", + "priceUsd": "0.5902255814769584" + }, + { + "chainId": 42161, + "address": "0x2A676eeAd159c4C8e8593471c6d666F02827FF8C", + "name": "GALA", + "symbol": "GALA", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/12493/standard/GALA-COINGECKO.png?1696512310", + "priceUsd": "0.015281502605895954" + }, + { + "chainId": 42161, + "address": "0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a", + "name": "GMX", + "symbol": "GMX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18323/large/arbit.png?1631532468", + "priceUsd": "13.78" + }, + { + "chainId": 42161, + "address": "0xa0b862F60edEf4452F25B4160F177db44DeB6Cf1", + "name": "Gnosis Token", + "symbol": "GNO", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", + "priceUsd": "147.22" + }, + { + "chainId": 42161, + "address": "0x9623063377AD1B27544C965cCd7342f7EA7e88C7", + "name": "The Graph", + "symbol": "GRT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", + "priceUsd": "0.080888" + }, + { + "chainId": 42161, + "address": "0x7f9a7DB853Ca816B9A138AEe3380Ef34c437dEe0", + "name": "Gitcoin", + "symbol": "GTC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", + "priceUsd": "0.28200434355576265" + }, + { + "chainId": 42161, + "address": "0x589d35656641d6aB57A545F08cf473eCD9B6D5F7", + "name": "GYEN", + "symbol": "GYEN", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", + "priceUsd": "0.00636203" + }, + { + "chainId": 42161, + "address": "0xd12Eeb0142D4Efe7Af82e4f29E5Af382615bcEeA", + "name": "Highstreet", + "symbol": "HIGH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", + "priceUsd": "0.4591132461391608" + }, + { + "chainId": 42161, + "address": "0x177F394A3eD18FAa85c1462Ae626438a70294EF7", + "name": "HOPR", + "symbol": "HOPR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14061/thumb/Shared_HOPR_logo_512px.png?1614073468", + "priceUsd": "0.04617580665610038" + }, + { + "chainId": 42161, + "address": "0x61cA9D186f6b9a793BC08F6C79fd35f205488673", + "name": "Illuvium", + "symbol": "ILV", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", + "priceUsd": "14.081873098293768" + }, + { + "chainId": 42161, + "address": "0x3cFD99593a7F035F717142095a3898e3Fca7783e", + "name": "Immutable X", + "symbol": "IMX", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17233/thumb/imx.png?1636691817", + "priceUsd": "0.6888" + }, + { + "chainId": 42161, + "address": "0x2A2053cb633CAD465B4A8975eD3d7f09DF608F80", + "name": "Injective", + "symbol": "INJ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", + "priceUsd": "12.204482065028753" + }, + { + "chainId": 42161, + "address": "0x25f05699548D3A0820b99f93c10c8BB573E27083", + "name": "JasmyCoin", + "symbol": "JASMY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", + "priceUsd": "0.012479264806524773" + }, + { + "chainId": 42161, + "address": "0x010700AB046Dd8e92b0e3587842080Df36364ed3", + "name": "Kinto", + "symbol": "K", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/54964/standard/k200.png?1742894885", + "priceUsd": "0.00625894700215694" + }, + { + "chainId": 42161, + "address": "0xf75eE6D319741057a82a88Eeff1DbAFAB7307b69", + "name": "KRYLL", + "symbol": "KRL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2807/thumb/krl.png?1547036979", + "priceUsd": "0.294653" + }, + { + "chainId": 42161, + "address": "0x3A18dcC9745eDcD1Ef33ecB93b0b6eBA5671e7Ca", + "name": "Kujira", + "symbol": "KUJI", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", + "priceUsd": "0.178702" + }, + { + "chainId": 42161, + "address": "0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60", + "name": "Lido DAO", + "symbol": "LDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", + "priceUsd": "1.15" + }, + { + "chainId": 42161, + "address": "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", + "name": "ChainLink Token", + "symbol": "LINK", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", + "priceUsd": "21.77" + }, + { + "chainId": 42161, + "address": "0x349fc93da004a63F3B1343361465981330A40B25", + "name": "Litentry", + "symbol": "LIT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", + "priceUsd": "0.3310357222962715" + }, + { + "chainId": 42161, + "address": "0x289ba1701C2F088cf0faf8B3705246331cB8A839", + "name": "Livepeer", + "symbol": "LPT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", + "priceUsd": "6.24" + }, + { + "chainId": 42161, + "address": "0xfb9E5D956D889D91a82737B9bFCDaC1DCE3e1449", + "name": "Liquity", + "symbol": "LQTY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", + "priceUsd": "0.718233" + }, + { + "chainId": 42161, + "address": "0x46d0cE7de6247b0A95f67b43B589b4041BaE7fbE", + "name": "LoopringCoin V2", + "symbol": "LRC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png", + "priceUsd": "0.084941" + }, + { + "chainId": 42161, + "address": "0x93b346b6BC2548dA6A1E7d98E9a421B42541425b", + "name": "Liquity USD", + "symbol": "LUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", + "priceUsd": "1" + }, + { + "chainId": 42161, + "address": "0x539bdE0d7Dbd336b79148AA742883198BBF60342", + "name": "MAGIC", + "symbol": "MAGIC", + "decimals": 18, + "logoUrl": "https://dynamic-assets.coinbase.com/30320a63f6038b944c9c0202fcb2392e6a1bd333814f74b4674774dd87f2d06d64fdd74c2f1ab4639917c75b749c323450408bec7a2737af8ae0c17871aa90de/asset_icons/98d278cda11639ed7449a0a3086cd2c83937ce71baf4ee43bb5b777423c00a75.png", + "priceUsd": "0.167251" + }, + { + "chainId": 42161, + "address": "0x442d24578A564EF628A65e6a7E3e7be2a165E231", + "name": "Decentraland", + "symbol": "MANA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", + "priceUsd": "0.3135602745851509" + }, + { + "chainId": 42161, + "address": "0x533A7B414CD1236815a5e09F1E97FC7d5c313739", + "name": "Mask Network", + "symbol": "MASK", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", + "priceUsd": "1.2488076059941424" + }, + { + "chainId": 42161, + "address": "0x99F40b01BA9C469193B360f72740E416B17Ac332", + "name": "MATH", + "symbol": "MATH", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", + "priceUsd": "0.082979" + }, + { + "chainId": 42161, + "address": "0x561877b6b3DD7651313794e5F2894B2F18bE0766", + "name": "Polygon", + "symbol": "MATIC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", + "priceUsd": "0.23802351749186065" + }, + { + "chainId": 42161, + "address": "0x7F728F3595db17B0B359f4FC47aE80FAd2e33769", + "name": "Metis", + "symbol": "METIS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", + "priceUsd": "12.79" + }, + { + "chainId": 42161, + "address": "0xB20A02dfFb172C474BC4bDa3fD6f4eE70C04daf2", + "name": "Magic Internet Money", + "symbol": "MIM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", + "priceUsd": "1.0003382585190026" + }, + { + "chainId": 42161, + "address": "0x2e9a6Df78E42a30712c10a9Dc4b1C8656f8F2879", + "name": "Maker", + "symbol": "MKR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png", + "priceUsd": "1575.3018493729548" + }, + { + "chainId": 42161, + "address": "0x8f5c1A99b1df736Ad685006Cb6ADCA7B7Ae4b514", + "name": "Melon", + "symbol": "MLN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", + "priceUsd": "7.78" + }, + { + "chainId": 42161, + "address": "0x9c1a1C7bA9c2602123FD7EF3eb41a769edf6C53A", + "name": "Mantle", + "symbol": "MNT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/30980/large/Mantle-Logo-mark.png?1739213200", + "priceUsd": "2.582618758162502" + }, + { + "chainId": 42161, + "address": "0x96c42662820F6Ea32f0A61A06a38a72B206aABaC", + "name": "Mog Coin", + "symbol": "MOG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/31059/large/MOG_LOGO_200x200.png", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0xE390C0B46bd723995BE02640E6F1e1c802F620AC", + "name": "Morpho Token", + "symbol": "MORPHO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/29837/large/Morpho-token-icon.png?1726771230", + "priceUsd": "1.721842542954761" + }, + { + "chainId": 42161, + "address": "0x29024832eC3baBF5074D4F46102aA988097f0Ca0", + "name": "Maple", + "symbol": "MPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14097/thumb/photo_2021-05-03_14.20.41.jpeg?1620022863", + "priceUsd": "0.7843683789726165" + }, + { + "chainId": 42161, + "address": "0x7b9b94aebe5E2039531af8E31045f377EcD9A39A", + "name": "Multichain", + "symbol": "MULTI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", + "priceUsd": "0.49799624438204476" + }, + { + "chainId": 42161, + "address": "0x5445972E76c5e4CEdD12B6e2BceF69133E15992F", + "name": "GensoKishi Metaverse", + "symbol": "MV", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", + "priceUsd": "0.00755254" + }, + { + "chainId": 42161, + "address": "0x91b468Fe3dce581D7a6cFE34189F1314b6862eD6", + "name": "MXC", + "symbol": "MXC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/4604/thumb/mxc.png?1655534336", + "priceUsd": "0.0013593289568151513" + }, + { + "chainId": 42161, + "address": "0x53236015A675fcB937485F1AE58040e4Fb920d5b", + "name": "PolySwarm", + "symbol": "NCT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", + "priceUsd": "0.020033226668842236" + }, + { + "chainId": 42161, + "address": "0xBE06ca305A5Cb49ABf6B1840da7c42690406177b", + "name": "NKN", + "symbol": "NKN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3375/thumb/nkn.png?1548329212", + "priceUsd": "0.025037864866809825" + }, + { + "chainId": 42161, + "address": "0x597701b32553b9fa473e21362D480b3a6B569711", + "name": "Numeraire", + "symbol": "NMR", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png", + "priceUsd": "15.781763374221558" + }, + { + "chainId": 42161, + "address": "0x933d31561e470478079FEB9A6Dd2691fAD8234DF", + "name": "Ocean Protocol", + "symbol": "OCEAN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", + "priceUsd": "0.26036604666238605" + }, + { + "chainId": 42161, + "address": "0x6FEb262FEb0f775B5312D2e009923f7f58AE423E", + "name": "Origin Protocol", + "symbol": "OGN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", + "priceUsd": "0.058036799074690455" + }, + { + "chainId": 42161, + "address": "0xd962C1895c46AC0378C502c207748b7061421e8e", + "name": "OMG Network", + "symbol": "OMG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/776/thumb/OMG_Network.jpg?1591167168", + "priceUsd": "0.1491374127630569" + }, + { + "chainId": 42161, + "address": "0xA2d52A05B8Bead5d824DF54Dd1AA63188B37A5E7", + "name": "Ondo Finance", + "symbol": "ONDO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656", + "priceUsd": "0.8942291824087522" + }, + { + "chainId": 42161, + "address": "0x1BDCC2075d5370293E248Cab0173eC3E551e6218", + "name": "Orion Protocol", + "symbol": "ORN", + "decimals": 8, + "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", + "priceUsd": "0.2636902598367738" + }, + { + "chainId": 42161, + "address": "0xfEb4DfC8C4Cf7Ed305bb08065D08eC6ee6728429", + "name": "PAX Gold", + "symbol": "PAXG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", + "priceUsd": "4062.2114812691334" + }, + { + "chainId": 42161, + "address": "0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8", + "name": "Pendle", + "symbol": "PENDLE", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", + "priceUsd": "4.48" + }, + { + "chainId": 42161, + "address": "0x35E6A59F786d9266c7961eA28c7b768B33959cbB", + "name": "Pepe", + "symbol": "PEPE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1682922725", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0x753D224bCf9AAFaCD81558c32341416df61D3DAC", + "name": "Perpetual Protocol", + "symbol": "PERP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", + "priceUsd": "0.279455" + }, + { + "chainId": 42161, + "address": "0xac7CE9F2794e01c0D27b096C52f592e343D77cbf", + "name": "Pirate Nation", + "symbol": "PIRATE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/38524/standard/_Pirate_Transparent_200x200.png", + "priceUsd": "0.019627200157602193" + }, + { + "chainId": 42161, + "address": "0x044d8e7F3A17751D521efEa8CCf9282268fE08CC", + "name": "Polygon Ecosystem Token", + "symbol": "POL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/32440/large/polygon.png?1698233684", + "priceUsd": "0.23792315034854336" + }, + { + "chainId": 42161, + "address": "0xeeeB5EaC2dB7A7Fc28134aA3248580d48b016b64", + "name": "Polkastarter", + "symbol": "POLS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", + "priceUsd": "0.1667781665226272" + }, + { + "chainId": 42161, + "address": "0xE12F29704F635F4A6E7Ae154838d21F9B33809e9", + "name": "Polymath", + "symbol": "POLY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", + "priceUsd": "0.035190147866426116" + }, + { + "chainId": 42161, + "address": "0xdA0a57B710768ae17941a9Fa33f8B720c8bD9ddD", + "name": "Marlin", + "symbol": "POND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/8903/thumb/POND_200x200.png?1622515451", + "priceUsd": "0.0076717" + }, + { + "chainId": 42161, + "address": "0x6380F3d0C1412a80EB00F49064DA30749DB991DE", + "name": "Portal", + "symbol": "PORTAL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35436/standard/portal.jpeg", + "priceUsd": "0.03734272443005163" + }, + { + "chainId": 42161, + "address": "0x4e91F2AF1ee0F84B529478f19794F5AFD423e4A6", + "name": "Power Ledger", + "symbol": "POWR", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", + "priceUsd": "0.14050192688292037" + }, + { + "chainId": 42161, + "address": "0x8d8e1b6ffc6832E8D2eF0DE8a3d957cAE7ac5067", + "name": "Prime", + "symbol": "PRIME", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/29053/large/PRIMELOGOOO.png?1676976222", + "priceUsd": "1.3200802798770386" + }, + { + "chainId": 42161, + "address": "0x82164a8B646401a8776F9dC5c8Cba35DcAf60Cd2", + "name": "PARSIQ", + "symbol": "PRQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", + "priceUsd": "0.007101478494831981" + }, + { + "chainId": 42161, + "address": "0xC7557C73e0eCa2E1BF7348bB6874Aee63C7eFF85", + "name": "Quant", + "symbol": "QNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/3370/thumb/5ZOu7brX_400x400.jpg?1612437252", + "priceUsd": "101.5877815649179" + }, + { + "chainId": 42161, + "address": "0x3c45038f4807c5bb72F6BC72c2A2B9c012155e49", + "name": "Radicle", + "symbol": "RAD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14013/thumb/radicle.png?1614402918", + "priceUsd": "0.6202724135303846" + }, + { + "chainId": 42161, + "address": "0xaeF5bbcbFa438519a5ea80B4c7181B4E78d419f2", + "name": "Rai Reflex Index", + "symbol": "RAI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", + "priceUsd": "4.93225958103794" + }, + { + "chainId": 42161, + "address": "0xCf78572A8fE97b2B9a4B9709f6a7D9a863c1b8E0", + "name": "Rarible", + "symbol": "RARI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", + "priceUsd": "0.8255872478802976" + }, + { + "chainId": 42161, + "address": "0x2E9AE8f178d5Ea81970C7799A377B3985cbC335F", + "name": "Rubic", + "symbol": "RBC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12629/thumb/200x200.png?1607952509", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0x9fA891e1dB0a6D1eEAC4B929b5AAE1011C79a204", + "name": "Republic Token", + "symbol": "REN", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", + "priceUsd": "0.007193" + }, + { + "chainId": 42161, + "address": "0x1Cb5bBc64e148C5b889E3c667B49edF78BB92171", + "name": "Request", + "symbol": "REQ", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1031/thumb/Request_icon_green.png?1643250951", + "priceUsd": "0.12841247415379142" + }, + { + "chainId": 42161, + "address": "0xef888bcA6AB6B1d26dbeC977C455388ecd794794", + "name": "Rari Governance Token", + "symbol": "RGT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12900/thumb/Rari_Logo_Transparent.png?1613978014", + "priceUsd": "0.050538" + }, + { + "chainId": 42161, + "address": "0xE575586566b02A16338c199c23cA6d295D794e66", + "name": "iExec RLC", + "symbol": "RLC", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/646/thumb/pL1VuXm.png?1604543202", + "priceUsd": "1.051653423013894" + }, + { + "chainId": 42161, + "address": "0xC8a4EeA31E9B6b61c406DF013DD4FEc76f21E279", + "name": "Render Token", + "symbol": "RNDR", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", + "priceUsd": "3.254940779380112" + }, + { + "chainId": 42161, + "address": "0xB766039cc6DB368759C1E56B79AFfE831d0Cc507", + "name": "Rocket Pool Protocol", + "symbol": "RPL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/2090/large/rocket_pool_%28RPL%29.png?1696503058", + "priceUsd": "4.84" + }, + { + "chainId": 42161, + "address": "0xCa5Ca9083702c56b481D1eec86F1776FDbd2e594", + "name": "Reserve Rights", + "symbol": "RSR", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", + "priceUsd": "0.00580656" + }, + { + "chainId": 42161, + "address": "0xd1318eb19DBF2647743c720ed35174efd64e3DAC", + "name": "The Sandbox", + "symbol": "SAND", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", + "priceUsd": "0.2627917517454947" + }, + { + "chainId": 42161, + "address": "0x1629c4112952a7a377cB9B8d7d8c903092f34B63", + "name": "Stader", + "symbol": "SD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", + "priceUsd": "0.4953647100681596" + }, + { + "chainId": 42161, + "address": "0x5033833c9fe8B9d3E09EEd2f73d2aaF7E3872fd1", + "name": "Shiba Inu", + "symbol": "SHIB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11939/thumb/shiba.png?1622619446", + "priceUsd": null + }, + { + "chainId": 42161, + "address": "0x4F9b7DEDD8865871dF65c5D26B1c2dD537267878", + "name": "SKALE", + "symbol": "SKL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/13245/thumb/SKALE_token_300x300.png?1606789574", + "priceUsd": "0.02366" + }, + { + "chainId": 42161, + "address": "0x707F635951193dDaFBB40971a0fCAAb8A6415160", + "name": "Status", + "symbol": "SNT", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", + "priceUsd": "0.021944592080815117" + }, + { + "chainId": 42161, + "address": "0xcBA56Cd8216FCBBF3fA6DF6137F3147cBcA37D60", + "name": "Synthetix Network Token", + "symbol": "SNX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", + "priceUsd": "1.085785653738852" + }, + { + "chainId": 42161, + "address": "0xb2BE52744a804Cc732d606817C2572C5A3B264e7", + "name": "Unisocks", + "symbol": "SOCKS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/10717/thumb/qFrcoiM.png?1582525244", + "priceUsd": "12102.353607339652" + }, + { + "chainId": 42161, + "address": "0xb74Da9FE2F96B9E0a5f4A3cf0b92dd2bEC617124", + "name": "SOL Wormhole ", + "symbol": "SOL", + "decimals": 9, + "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", + "priceUsd": "223.13400262442897" + }, + { + "chainId": 42161, + "address": "0x3E6648C5a70A150A88bCE65F4aD4d506Fe15d2AF", + "name": "Spell Token", + "symbol": "SPELL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", + "priceUsd": "0.00043446" + }, + { + "chainId": 42161, + "address": "0x53e70cc1d527b524A1C46Eaa892e4CB35d2ba901", + "name": "SPX6900", + "symbol": "SPX", + "decimals": 8, + "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", + "priceUsd": "1.4568236797059237" + }, + { + "chainId": 42161, + "address": "0x1337420dED5ADb9980CFc35f8f2B054ea86f8aB1", + "name": "SQD", + "symbol": "SQD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/37869/standard/New_Logo_SQD_Icon.png?1720048443", + "priceUsd": "0.167691" + }, + { + "chainId": 42161, + "address": "0xe018C7a3d175Fb0fE15D70Da2c874d3CA16313EC", + "name": "Stargate Finance", + "symbol": "STG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", + "priceUsd": "0.2041" + }, + { + "chainId": 42161, + "address": "0xE6320ebF209971b4F4696F7f0954b8457Aa2FCC2", + "name": "Storj Token", + "symbol": "STORJ", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", + "priceUsd": "0.22605763175238022" + }, + { + "chainId": 42161, + "address": "0x7f9cf5a2630a0d58567122217dF7609c26498956", + "name": "SuperFarm", + "symbol": "SUPER", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", + "priceUsd": "0.5636453010113891" + }, + { + "chainId": 42161, + "address": "0xA970AF1a584579B618be4d69aD6F73459D112F95", + "name": "Synth sUSD", + "symbol": "sUSD", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", + "priceUsd": "0.996573" + }, + { + "chainId": 42161, + "address": "0xd4d42F0b6DEF4CE0383636770eF773390d85c61A", + "name": "Sushi", + "symbol": "SUSHI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "priceUsd": "0.683853" + }, + { + "chainId": 42161, + "address": "0x2C96bE2612bec20fe2975C3ACFcbBe61a58f2571", + "name": "Swell", + "symbol": "SWELL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/28777/large/swell1.png?1727899715", + "priceUsd": "0.008353" + }, + { + "chainId": 42161, + "address": "0x1bCfc0B4eE1471674cd6A9F6B363A034375eAD84", + "name": "Synapse", + "symbol": "SYN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", + "priceUsd": "0.10881120711539255" + }, + { + "chainId": 42161, + "address": "0x0945Cae3ae47cb384b2d47BC448Dc6A9dEC21F55", + "name": "Threshold Network", + "symbol": "T", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/22228/thumb/nFPNiSbL_400x400.jpg?1641220340", + "priceUsd": "0.015217732390053801" + }, + { + "chainId": 42161, + "address": "0x7E2a1eDeE171C5B19E6c54D73752396C0A572594", + "name": "tBTC", + "symbol": "tBTC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/0x18084fbA666a33d37592fA2633fD49a74DD93a88/logo.png", + "priceUsd": "122314.50959016493" + }, + { + "chainId": 42161, + "address": "0xd58D345Fd9c82262E087d2D0607624B410D88242", + "name": "Tellor", + "symbol": "TRB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", + "priceUsd": "32.42" + }, + { + "chainId": 42161, + "address": "0xBfAE6fecD8124ba33cbB2180aAb0Fe4c03914A5A", + "name": "Tribe", + "symbol": "TRIBE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14575/thumb/tribe.PNG?1617487954", + "priceUsd": "0.6320000108740831" + }, + { + "chainId": 42161, + "address": "0x5C816d4582c857dcadb1bB1F62Ad6c9DEde4576a", + "name": "Turbo", + "symbol": "TURBO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/30117/large/TurboMark-QL_200.png?1708079597", + "priceUsd": "0.0035555052020918446" + }, + { + "chainId": 42161, + "address": "0xd693Ec944A85eeca4247eC1c3b130DCa9B0C3b22", + "name": "UMA Voting Token v1", + "symbol": "UMA", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828/logo.png", + "priceUsd": "1.2438919348954278" + }, + { + "chainId": 42161, + "address": "0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0", + "name": "Uniswap", + "symbol": "UNI", + "decimals": 18, + "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", + "priceUsd": "7.84" + }, + { + "chainId": 42161, + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "name": "USDCoin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.999705" + }, + { + "chainId": 42161, + "address": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + "name": "Bridged USDC", + "symbol": "USDC.e", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUsd": "0.999033" + }, + { + "chainId": 42161, + "address": "0x78df3a6044Ce3cB1905500345B967788b699dF8f", + "name": "Pax Dollar", + "symbol": "USDP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", + "priceUsd": "1.0055655527133194" + }, + { + "chainId": 42161, + "address": "0x6491c05A82219b8D1479057361ff1654749b876b", + "name": "USDS Stablecoin", + "symbol": "USDS", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/39926/large/usds.webp?1726666683", + "priceUsd": "0.999782" + }, + { + "chainId": 42161, + "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "name": "Tether USD", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", + "priceUsd": "1" + }, + { + "chainId": 42161, + "address": "0x7639AB8599f1b417CbE4ceD492fB30162140AbbB", + "name": "USUAL", + "symbol": "USUAL", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/51091/large/USUAL.jpg?1730035787", + "priceUsd": "0.0486659426791032" + }, + { + "chainId": 42161, + "address": "0x1c8Ec4DE3c2BFD3050695D89853EC6d78AE650bb", + "name": "Wrapped Ampleforth", + "symbol": "WAMPL", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", + "priceUsd": "2.1012409754645227" + }, + { + "chainId": 42161, + "address": "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + "name": "Wrapped BTC", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", + "priceUsd": "122508" + }, + { + "chainId": 42161, + "address": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 42161, + "address": "0x4D1d7134B87490AE5eEbdB22A5820d7d0E1980bf", + "name": "World Liberty Financial", + "symbol": "WLFI", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/50767/large/wlfi.png?1756438915", + "priceUsd": "0.1782" + }, + { + "chainId": 42161, + "address": "0xcAFcD85D8ca7Ad1e1C6F82F651fA15E33AEfD07b", + "name": "WOO Network", + "symbol": "WOO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", + "priceUsd": "0.066112" + }, + { + "chainId": 42161, + "address": "0x58BbC087e36Db40a84b22c1B93a042294deEAFEd", + "name": "Chain", + "symbol": "XCN", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", + "priceUsd": "0.010634058426173104" + }, + { + "chainId": 42161, + "address": "0xa05245Ade25cC1063EE50Cf7c083B4524c1C4302", + "name": "XSGD", + "symbol": "XSGD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", + "priceUsd": "0.7659771906624153" + }, + { + "chainId": 42161, + "address": "0x82e3A8F066a6989666b031d916c43672085b1582", + "name": "yearn finance", + "symbol": "YFI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", + "priceUsd": "5302.52" + }, + { + "chainId": 42161, + "address": "0x6DdBbcE7858D276678FC2B36123fD60547b88954", + "name": "Zetachain", + "symbol": "Zeta", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/26718/standard/Twitter_icon.png?1696525788", + "priceUsd": "0.1705" + }, + { + "chainId": 42161, + "address": "0x6985884C4392D348587B19cb9eAAf157F13271cd", + "name": "LayerZero", + "symbol": "ZRO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", + "priceUsd": "2.34" + }, + { + "chainId": 42161, + "address": "0xBD591Bd4DdB64b77B5f76Eab8f03d02519235Ae2", + "name": "0x Protocol Token", + "symbol": "ZRX", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png", + "priceUsd": "0.24747200628458066" + }, + { + "chainId": 81457, + "address": "0xb1a5700fA2358173Fe465e6eA4Ff52E36e88E2ad", + "name": "Blast", + "symbol": "BLAST", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/35494/standard/Blast.jpg?1719385662", + "priceUsd": "0.00197489" + }, + { + "chainId": 7777777, + "address": "0xCccCCccc7021b32EBb4e8C08314bD62F7c653EC4", + "name": "USD Coin (Bridged from Ethereum)", + "symbol": "USDzC", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/35218/large/USDC_Icon.png?1707908537", + "priceUsd": null + }, + { + "chainId": 1, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "name": "Polygon Ecosystem Token", + "symbol": "POL", + "decimals": 18, + "logoUrl": "https://static.debank.com/image/matic_token/logo_url/matic/6f5a6b6f0732a7a235131bd7804d357c.png", + "priceUsd": "0.239712" + }, + { + "chainId": 42161, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4345.25" + }, + { + "chainId": 324, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 8453, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 59144, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 34443, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 81457, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 1135, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 534352, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 480, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4360.59" + }, + { + "chainId": 57073, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4380.16" + }, + { + "chainId": 1868, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4382.82" + }, + { + "chainId": 130, + "address": "0x0000000000000000000000000000000000000000", + "name": "ETH", + "symbol": "ETH", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUsd": "4384.42" + }, + { + "chainId": 232, + "address": "0x0000000000000000000000000000000000000000", + "name": "GHO", + "symbol": "GHO", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/23508.png", + "priceUsd": "0.02897504" + }, + { + "chainId": 56, + "address": "0x0000000000000000000000000000000000000000", + "name": "BNB", + "symbol": "BNB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/825/small/binance-coin-logo.png?1547034615", + "priceUsd": "1273.94" + }, + { + "chainId": 999, + "address": "0x0000000000000000000000000000000000000000", + "name": "HYPE", + "symbol": "HYPE", + "decimals": 18, + "logoUrl": "https://static.debank.com/image/hyper_token/logo_url/hyper/0b3e288cfe418e9ce69eef4c96374583.png", + "priceUsd": "44.32" + }, + { + "chainId": 9745, + "address": "0x0000000000000000000000000000000000000000", + "name": "Plasma", + "symbol": "XPL", + "decimals": 18, + "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/36645.png", + "priceUsd": "0.758051" + }, + { + "chainId": 34268394551451, + "address": "So11111111111111111111111111111111111111112", + "name": "Wrapped SOL", + "symbol": "SOL", + "decimals": 9, + "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", + "priceUsd": "224.60070584181943" + }, + { + "chainId": 34268394551451, + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png", + "priceUsd": "0.9998037949521894" + }, + { + "chainId": 34268394551451, + "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "name": "USDT", + "symbol": "USDT", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB/logo.svg", + "priceUsd": "1.0002317501844211" + }, + { + "chainId": 34268394551451, + "address": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + "name": "Jupiter", + "symbol": "JUP", + "decimals": 6, + "logoUrl": "https://static.jup.ag/jup/icon.png", + "priceUsd": "0.4331084912112197" + }, + { + "chainId": 34268394551451, + "address": "6FrrzDk5mQARGc1TDYoyVnSyRdds1t4PbtohCD6p3tgG", + "name": "USX", + "symbol": "USX", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/Thomas-Solstice/usx-metadata/refs/heads/main/usx.png", + "priceUsd": "1.0003810144559233" + }, + { + "chainId": 34268394551451, + "address": "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", + "name": "Pump", + "symbol": "PUMP", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/bafkreibyb3hcn7gglvdqpmklfev3fut3eqv3kje54l3to3xzxxbgpt5wjm", + "priceUsd": "0.005613151300670042" + }, + { + "chainId": 34268394551451, + "address": "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + "name": "Ether (Portal)", + "symbol": "ETH", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs/logo.png", + "priceUsd": "4396.634831886667" + }, + { + "chainId": 34268394551451, + "address": "EWsfRP9yrxyt8xTSv28MV1Ldn7UPpXBLgWtZ4YWMpump", + "name": "SOLHolder", + "symbol": "SOLHolder", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/bafkreicssdcgqp4c24pqmbjbfqr766brx6ahn6xb4ei24ejzacj5k4xoqy", + "priceUsd": "0.0006378540496796011" + }, + { + "chainId": 34268394551451, + "address": "5LwseQRo8fsz4S3y7jbqqe5C7tZTz5PwhXNCHj13jLBi", + "name": "PESHI", + "symbol": "PESHI", + "decimals": 6, + "logoUrl": "https://bafkreidobd4eiplmvff42dnutldmwmjihkgbti6rpzuxz6p3c425e6qx6q.ipfs.nftstorage.link", + "priceUsd": "0.0000016193854176535999" + }, + { + "chainId": 34268394551451, + "address": "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", + "name": "USELESS COIN", + "symbol": "USELESS", + "decimals": 6, + "logoUrl": "https://i.ibb.co/fdGzcmbt/bafkreihsdoqkmpr5ryebaduoutyhj3nxco6wdp4s4743l2qrae4sz4hqrm.png", + "priceUsd": "0.41485619644127775" + }, + { + "chainId": 34268394551451, + "address": "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", + "name": "Wrapped BTC (Portal)", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh/logo.png", + "priceUsd": "123098.76014735346" + }, + { + "chainId": 34268394551451, + "address": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", + "name": "Bonk", + "symbol": "Bonk", + "decimals": 5, + "logoUrl": "https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I", + "priceUsd": "0.000019400918377283733" + }, + { + "chainId": 34268394551451, + "address": "5TfqNKZbn9AnNtzq8bbkyhKgcPGTfNDc9wNzFrTBpump", + "name": "Pumpfun Pepe", + "symbol": "PFP", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/bafkreigv6oeomhm7fpx4cbwe2dforkwlknhdyqs7y53searvm3v4er6w4a", + "priceUsd": "0.004479918768904373" + }, + { + "chainId": 34268394551451, + "address": "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "name": "Jito Staked SOL", + "symbol": "JitoSOL", + "decimals": 9, + "logoUrl": "https://storage.googleapis.com/token-metadata/JitoSOL-256.png", + "priceUsd": "277.0770076566333" + }, + { + "chainId": 34268394551451, + "address": "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", + "name": "Jupiter Perps", + "symbol": "JLP", + "decimals": 6, + "logoUrl": "https://static.jup.ag/jlp/icon.png", + "priceUsd": "5.82759745279217" + }, + { + "chainId": 34268394551451, + "address": "4NGbC4RRrUjS78ooSN53Up7gSg4dGrj6F6dxpMWHbonk", + "name": "Pandu Pandas", + "symbol": "PANDU", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/bafkreicplblomr55js3zlgztgg63w4jk6vdaxbchdsp5vrn7buv5gdkd2y", + "priceUsd": "0.0001572163356396279" + }, + { + "chainId": 34268394551451, + "address": "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", + "name": "Pudgy Penguins", + "symbol": "PENGU", + "decimals": 6, + "logoUrl": "https://arweave.net/BW67hICaKGd2_wamSB0IQq-x7Xwtmr2oJj1WnWGJRHU", + "priceUsd": "0.030977051157841096" + }, + { + "chainId": 34268394551451, + "address": "5UUH9RTDiSpq6HKS6bp4NdU9PNJpXRXuiw6ShBTBhgH2", + "name": "TROLL", + "symbol": "TROLL", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmWV8QgmH1gSw41yapzsFwkC8yPbzoSreAcJbzqfvo2sVq", + "priceUsd": "0.12627775321638382" + }, + { + "chainId": 34268394551451, + "address": "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", + "name": "Fartcoin", + "symbol": "Fartcoin", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmQr3Fz4h1etNsF7oLGMRHiCzhB5y9a7GjyodnF7zLHK1g", + "priceUsd": "0.6458651487875023" + }, + { + "chainId": 34268394551451, + "address": "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB", + "name": "World Liberty Financial USD", + "symbol": "USD1", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/worldliberty/usd1-metadata/refs/heads/main/logo.png", + "priceUsd": "0.9994202887759748" + }, + { + "chainId": 34268394551451, + "address": "J3NKxxXZcnNiMjKw9hYb2K4LUxgwB6t1FtPtQVsv3KFr", + "name": "SPX6900 (Wormhole)", + "symbol": "SPX", + "decimals": 8, + "logoUrl": "https://i.imgur.com/fLpAyY4.png", + "priceUsd": "1.4590083296363434" + }, + { + "chainId": 34268394551451, + "address": "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", + "name": "Jupiter Staked SOL", + "symbol": "JupSOL", + "decimals": 9, + "logoUrl": "https://static.jup.ag/jupSOL/icon.png", + "priceUsd": "256.1902508307915" + }, + { + "chainId": 34268394551451, + "address": "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", + "name": "Coinbase Wrapped BTC", + "symbol": "cbBTC", + "decimals": 8, + "logoUrl": "https://ipfs.io/ipfs/QmZ7L8yd5j36oXXydUiYFiFsRHbi3EdgC4RuFwvM7dcqge", + "priceUsd": "123114.68686288694" + }, + { + "chainId": 34268394551451, + "address": "Ey59PH7Z4BFU4HjyKnyMdWt5GGN76KazTAwQihoUXRnk", + "name": "Launch Coin on Believe", + "symbol": "LAUNCHCOIN", + "decimals": 9, + "logoUrl": "https://ipfs.io/ipfs/bafkreibeqt7fvgn2ubl4tha6sljnici2eus42dauxgtrdvfjf6m3vkdkoi", + "priceUsd": "0.11348470814836654" + }, + { + "chainId": 34268394551451, + "address": "6nR8wBnfsmXfcdDr1hovJKjvFQxNSidN6XFyfAFZpump", + "name": "GeorgePlaysClashRoyale", + "symbol": "Clash", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/bafybeidcnencwhveq7w56f6az47edmnuqfzk5qeecnjc77owwvnu3mlhne", + "priceUsd": "0.05129532870950466" + }, + { + "chainId": 34268394551451, + "address": "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", + "name": "PayPal USD", + "symbol": "PYUSD", + "decimals": 6, + "logoUrl": "https://424565.fs1.hubspotusercontent-na1.net/hubfs/424565/PYUSDLOGO.png", + "priceUsd": "0.9999237478615768" + }, + { + "chainId": 34268394551451, + "address": "DUSDt4AeLZHWYmcXnVGYdgAzjtzU5mXUVnTMdnSzAttM", + "name": "DUSD", + "symbol": "DUSD", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/standcoin/stdc_assets/refs/heads/main/images/dusd.png", + "priceUsd": "0.9999649342545918" + }, + { + "chainId": 34268394551451, + "address": "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", + "name": "OFFICIAL TRUMP", + "symbol": "TRUMP", + "decimals": 6, + "logoUrl": "https://arweave.net/VQrPjACwnQRmxdKBTqNwPiyo65x7LAT773t8Kd7YBzw", + "priceUsd": "7.551597277820382" + }, + { + "chainId": 34268394551451, + "address": "DvjbEsdca43oQcw2h3HW1CT7N3x5vRcr3QrvTUHnXvgV", + "name": "Doodles", + "symbol": "DOOD", + "decimals": 9, + "logoUrl": "https://arweave.net/OqHnpGf36DiprL5YIC4xJHG8zRsWFV2FsZcNTfVgAPg", + "priceUsd": "0.011947374001126955" + }, + { + "chainId": 34268394551451, + "address": "CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp", + "name": "Collector Crypt", + "symbol": "CARDS", + "decimals": 6, + "logoUrl": "https://gateway.irys.xyz/2wrjAk4pYAACF3siDHAxt7yJY7kHgt57TZAkbgecKHMR", + "priceUsd": "0.17551228609119013" + } + ], + "timestamp": 1760012681065, + "version": "1.0.0" +} \ No newline at end of file diff --git a/src/hooks/useSwapTokens.ts b/src/hooks/useSwapTokens.ts index fe9fecb3d..7f4d7cf56 100644 --- a/src/hooks/useSwapTokens.ts +++ b/src/hooks/useSwapTokens.ts @@ -1,7 +1,29 @@ import { useQuery } from "@tanstack/react-query"; import getApiEndpoint from "utils/serverless-api"; + +// Import cached tokens data +import cachedTokensData from "../data/swap-tokens.json"; +import { SwapToken } from "utils/serverless-api/types"; import { SwapTokensQuery } from "utils/serverless-api/prod/swap-tokens"; +type SwapTokensCache = { + tokens: SwapToken[]; + timestamp: number; + version: string; +}; + +function filterTokensByChainId( + tokens: SwapToken[], + chainId?: number | number[] +): SwapToken[] { + if (!chainId) { + return tokens; + } + + const chainIds = Array.isArray(chainId) ? chainId : [chainId]; + return tokens.filter((token) => chainIds.includes(token.chainId)); +} + export function useSwapTokens(query?: SwapTokensQuery) { return useQuery({ queryKey: ["swapTokens", query], @@ -9,5 +31,20 @@ export function useSwapTokens(query?: SwapTokensQuery) { const api = getApiEndpoint(); return await api.swapTokens(query); }, + // Use cached data as initial data for immediate loading + initialData: () => { + try { + const cache = cachedTokensData as SwapTokensCache; + const filteredTokens = filterTokensByChainId( + cache.tokens, + query?.chainId + ); + return filteredTokens; + } catch (error) { + console.warn("Failed to load cached swap tokens:", error); + return undefined; + } + }, + refetchInterval: 60_000, }); } From 5ebb81ad81bdcc462a224cc87f564c6627a812ef Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 9 Oct 2025 14:33:32 +0200 Subject: [PATCH 049/122] get swap chains statically Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 12 ++++----- src/utils/getSwapChains.ts | 26 +++++++++++++++++++ .../components/ConfirmationButton.tsx | 3 +-- 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 src/utils/getSwapChains.ts diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index d1ae8b874..7f88fe624 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { getConfig } from "utils/config"; -import { useSwapChains } from "./useSwapChains"; +import { getSwapChains } from "utils/getSwapChains"; import { useSwapTokens } from "./useSwapTokens"; export type LifiToken = { @@ -30,12 +30,14 @@ export type RouteFilterParams = { export default function useAvailableCrosschainRoutes( filterParams?: RouteFilterParams ) { - const swapChainsQuery = useSwapChains(); const swapTokensQuery = useSwapTokens(); return useQuery({ queryKey: ["availableCrosschainRoutes", filterParams], queryFn: async () => { + // Get chains statically instead of from API + const swapChains = getSwapChains(); + // 1) Build swap token map by chain const swapTokensByChain = (swapTokensQuery.data || []).reduce( (acc, token) => { @@ -88,9 +90,7 @@ export default function useAvailableCrosschainRoutes( ); // 3) Combine swap and bridge tokens, deduplicating by address - const chainIdsInSwap = new Set( - (swapChainsQuery.data || []).map((c) => c.chainId) - ); + const chainIdsInSwap = new Set(swapChains.map((c) => c.chainId)); const chainIdsInBridge = new Set( Object.keys(bridgeTokensByChain).map(Number) ); @@ -169,6 +169,6 @@ export default function useAvailableCrosschainRoutes( return combinedByChain; }, - enabled: swapChainsQuery.isSuccess && swapTokensQuery.isSuccess, + enabled: swapTokensQuery.isSuccess, }); } diff --git a/src/utils/getSwapChains.ts b/src/utils/getSwapChains.ts new file mode 100644 index 000000000..7a57c2691 --- /dev/null +++ b/src/utils/getSwapChains.ts @@ -0,0 +1,26 @@ +import mainnetChains from "../data/chains_1.json"; +import indirectChains from "../data/indirect_chains_1.json"; + +export type SwapChain = { + chainId: number; + name: string; + publicRpcUrl: string; + explorerUrl: string; + logoUrl: string; +}; + +/** + * Get swap chains data by combining mainnet and indirect chains + * This mimics the logic from api/swap/chains/index.ts for faster frontend access + */ +export function getSwapChains(): SwapChain[] { + const chains = mainnetChains; + + return [...chains, ...indirectChains].map((chain) => ({ + chainId: chain.chainId, + name: chain.name, + publicRpcUrl: chain.publicRpcUrl, + explorerUrl: chain.explorerUrl, + logoUrl: chain.logoUrl, + })); +} diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index a9a6e6d77..775e65f60 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -14,8 +14,7 @@ import { ReactComponent as Warning } from "assets/icons/warning_triangle.svg"; import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; -import { COLORS, formatUSDString, getConfig, isDefined } from "utils"; -import { useTokenConversion } from "hooks/useTokenConversion"; +import { COLORS, formatUSDString, isDefined } from "utils"; import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; From b889a8158e26bff98f3d39531ea6e31033ef4a73 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 9 Oct 2025 18:35:41 +0200 Subject: [PATCH 050/122] fixup Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/Modal.tsx | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 37bb17664..378091870 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -152,6 +152,9 @@ export default function ChainTokenSelectorModal({ if (tokenSearch === "") { return true; } + if (t.chainId !== selectedChain) { + return false; + } const keywords = [ t.symbol.toLowerCase().replaceAll(" ", ""), t.name.toLowerCase().replaceAll(" ", ""), @@ -493,19 +496,25 @@ const MobileLayout = ({ )} {/* All Chains Section */} - All Chains - {!displayedChains.all.length && chainSearch && ( - + + {displayedChains.all.length > 0 && ( + <> + All Chains + {displayedChains.all.map(({ chainId, isDisabled }) => ( + onChainSelect(Number(chainId))} + /> + ))} + )} - {displayedChains.all.map(({ chainId, isDisabled }) => ( - onChainSelect(Number(chainId))} - /> - ))} + + {!displayedChains.all.length && + !displayedChains.popular.length && + chainSearch && } ) : ( @@ -544,9 +553,7 @@ const MobileLayout = ({ )} {/* All Tokens Section */} - {!displayedTokens.all.length && tokenSearch && ( - - )} + {displayedTokens.all.length > 0 && ( <> All Tokens @@ -571,6 +578,9 @@ const MobileLayout = ({ ))} )} + {!displayedTokens.all.length && + !displayedTokens.popular.length && + tokenSearch && } )} @@ -638,19 +648,23 @@ const DesktopLayout = ({ )} {/* All Chains Section */} - All Chains - {!displayedChains.all.length && chainSearch && ( - + {displayedChains.all.length > 0 && ( + <> + All Chains + {displayedChains.all.map(({ chainId, isDisabled }) => ( + onChainSelect(Number(chainId))} + /> + ))} + )} - {displayedChains.all.map(({ chainId, isDisabled }) => ( - onChainSelect(Number(chainId))} - /> - ))} + {!displayedChains.all.length && + !displayedChains.popular.length && + chainSearch && } @@ -691,9 +705,6 @@ const DesktopLayout = ({ )} {/* All Tokens Section */} - {!displayedTokens.all.length && tokenSearch && ( - - )} {displayedTokens.all.length > 0 && ( <> All Tokens @@ -718,6 +729,9 @@ const DesktopLayout = ({ ))} )} + {!displayedTokens.all.length && + !displayedTokens.popular.length && + tokenSearch && } From a0687a80d1d4a653c1e61b78b4d67c894e4f762c Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 13 Oct 2025 13:45:29 +0200 Subject: [PATCH 051/122] switch for usd Signed-off-by: Gerhard Steenkamp --- src/components/GlobalStyles/GlobalStyles.tsx | 4 +- src/hooks/index.ts | 1 + src/hooks/useTokenInput.ts | 158 ++++++++++++++++++ src/utils/token.ts | 43 ++++- .../components/ChainTokenSelector/Modal.tsx | 55 ++---- .../ChainTokenSelector/SelectorButton.tsx | 25 +-- .../components/ConfirmationButton.tsx | 6 +- .../SwapAndBridge/components/InputForm.tsx | 132 +++++---------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 18 +- .../hooks/useValidateSwapAndBridge.ts | 6 +- 10 files changed, 277 insertions(+), 171 deletions(-) create mode 100644 src/hooks/useTokenInput.ts diff --git a/src/components/GlobalStyles/GlobalStyles.tsx b/src/components/GlobalStyles/GlobalStyles.tsx index bb4dc96bd..4cae697ac 100644 --- a/src/components/GlobalStyles/GlobalStyles.tsx +++ b/src/components/GlobalStyles/GlobalStyles.tsx @@ -107,7 +107,9 @@ const globalStyles = css` } button { border: none; - background-color: none; + background: none; + color: inherit; + cursor: pointer; } html, body { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index ae9e47b5d..2933bf102 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -17,3 +17,4 @@ export * from "./useWalletTrace"; export * from "./useQueue"; export * from "./useAmplitude"; export * from "./useRewardSummary"; +export * from "./useTokenInput"; diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts new file mode 100644 index 000000000..19a564a25 --- /dev/null +++ b/src/hooks/useTokenInput.ts @@ -0,0 +1,158 @@ +import { useCallback, useEffect, useState } from "react"; +import { BigNumber, utils } from "ethers"; +import { convertTokenToUSD, convertUSDToToken } from "utils"; +import { EnrichedToken } from "views/SwapAndBridge/components/ChainTokenSelector/Modal"; +import { formatUnitsWithMaxFractions } from "utils"; + +export type UnitType = "usd" | "token"; + +type UseTokenInputProps = { + token: EnrichedToken | null; + setAmount: (amount: BigNumber | null) => void; + expectedAmount: string | undefined; + shouldUpdate: boolean; + isUpdateLoading: boolean; +}; + +type UseTokenInputReturn = { + amountString: string; + setAmountString: (value: string) => void; + unit: UnitType; + convertedAmount: BigNumber | undefined; + toggleUnit: () => void; + handleInputChange: (value: string) => void; + handleBalanceClick: (amount: BigNumber, decimals: number) => void; +}; + +export function useTokenInput({ + token, + setAmount, + expectedAmount, + shouldUpdate, + isUpdateLoading, +}: UseTokenInputProps): UseTokenInputReturn { + const [amountString, setAmountString] = useState(""); + const [unit, setUnit] = useState("token"); + const [convertedAmount, setConvertedAmount] = useState(); + const [justTyped, setJustTyped] = useState(false); + + // Handle user input changes - propagate to parent + useEffect(() => { + if (!justTyped) { + return; + } + setJustTyped(false); + try { + if (!token) { + setAmount(null); + return; + } + if (unit === "token") { + const parsed = utils.parseUnits(amountString, token.decimals); + setAmount(parsed); + } else { + const tokenValue = convertUSDToToken(amountString, token); + setAmount(tokenValue); + } + } catch (e) { + setAmount(null); + } + }, [amountString, justTyped, token, unit, setAmount]); + + // Reset amount when token changes + useEffect(() => { + if (token) { + setAmountString(""); + setConvertedAmount(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token?.chainId, token?.symbol]); + + // Handle quote updates - only update the field that should receive the quote + useEffect(() => { + if (shouldUpdate && isUpdateLoading) { + setAmountString(""); + } + + if (expectedAmount && token && shouldUpdate) { + // TODO: handle converted amount + setAmountString( + formatUnitsWithMaxFractions(expectedAmount, token.decimals) + ); + } + }, [expectedAmount, isUpdateLoading, shouldUpdate, token]); + + // Set converted value for display + useEffect(() => { + if (!token || !amountString) return; + try { + if (unit === "token") { + // User typed token amount - convert to USD for display + const usdValue = convertTokenToUSD(amountString, token); + setConvertedAmount(usdValue); + } else { + // User typed USD amount - convert to token for display + const tokenValue = convertUSDToToken(amountString, token); + setConvertedAmount(tokenValue); + } + } catch (e) { + setConvertedAmount(undefined); + } + }, [token, amountString, unit]); + + // Toggle between token and USD units + const toggleUnit = useCallback(() => { + if (unit === "token") { + // Convert token amount to USD string for display + if (amountString && token && convertedAmount) { + try { + const a = utils.formatUnits(convertedAmount, 18); + setAmountString(a); + } catch (e) { + setAmountString("0"); + } finally { + setUnit("usd"); + } + } + } else { + // Convert USD amount to token string for display + if (amountString && token && convertedAmount) { + try { + const a = utils.formatUnits(convertedAmount, 18); + setAmountString(a); + } catch (e) { + setAmountString("0"); + } finally { + setUnit("token"); + } + } + } + }, [unit, amountString, token, convertedAmount]); + + // Handle input field changes + const handleInputChange = useCallback((value: string) => { + if (value === "" || /^\d*\.?\d*$/.test(value)) { + setJustTyped(true); + setAmountString(value); + } + }, []); + + // Handle balance selector click + const handleBalanceClick = useCallback( + (amount: BigNumber, decimals: number) => { + setAmount(amount); + setAmountString(formatUnitsWithMaxFractions(amount, decimals)); + }, + [setAmount] + ); + + return { + amountString, + setAmountString, + unit, + convertedAmount, + toggleUnit, + handleInputChange, + handleBalanceClick, + }; +} diff --git a/src/utils/token.ts b/src/utils/token.ts index 5a7df49d9..1300248fd 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -1,6 +1,15 @@ -import { ethers } from "ethers"; +import { BigNumber, ethers, utils } from "ethers"; +import { parseEther } from "ethers/lib/utils"; +import { LifiToken } from "hooks/useAvailableCrosschainRoutes"; -import { getProvider, ChainId, getConfig, getChainInfo } from "utils"; +import { + getProvider, + ChainId, + getConfig, + getChainInfo, + fixedPointAdjustment, + parseUnits, +} from "utils"; import { ERC20__factory } from "utils/typechain"; export async function getNativeBalance( @@ -75,3 +84,33 @@ export function getExplorerLinkForToken( ) { return `${getChainInfo(tokenChainId).explorerUrl}/address/${tokenAddress}`; } + +/** + * Converts a token amount to USD value + * @param tokenAmount - The token amount as a string (decimal format) + * @param token - The token object containing price and decimals + * @returns The USD value as a BigNumber (18 decimals) + */ +export function convertTokenToUSD( + tokenAmount: string, + token: LifiToken +): BigNumber { + const tokenScaled = parseUnits(tokenAmount, 18); + const priceScaled = parseUnits(token.priceUSD, 18); + return tokenScaled.mul(priceScaled).div(fixedPointAdjustment); +} + +/** + * Converts a USD amount to token amount + * @param usdAmount - The USD amount as a string (decimal format) + * @param token - The token object containing price and decimals + * @returns The token amount as a BigNumber + */ +export function convertUSDToToken( + usdAmount: string, + token: LifiToken +): BigNumber { + const usdScaled = parseUnits(usdAmount, 18); + const priceScaled = parseUnits(token.priceUSD, 18); + return usdScaled.mul(fixedPointAdjustment).div(priceScaled); +} diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 378091870..9868d6d70 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -1,5 +1,4 @@ import Modal from "components/Modal"; -import { EnrichedTokenSelect } from "./SelectorButton"; import styled from "@emotion/styled"; import { Searchbar } from "./Searchbar"; import TokenMask from "assets/mask/token-mask-corner.svg"; @@ -52,7 +51,7 @@ type DisplayedChains = { all: ChainData[]; }; -type EnrichedToken = LifiToken & { +export type EnrichedToken = LifiToken & { balance: BigNumber; balanceUsd: number; isReachable?: boolean; @@ -65,9 +64,9 @@ type DisplayedTokens = { }; type Props = { - onSelect: (token: EnrichedTokenSelect) => void; + onSelect: (token: EnrichedToken) => void; isOriginToken: boolean; - otherToken?: EnrichedTokenSelect | null; // The currently selected token on the other side + otherToken?: EnrichedToken | null; // The currently selected token on the other side displayModal: boolean; setDisplayModal: (displayModal: boolean) => void; }; @@ -331,7 +330,7 @@ const MobileModal = ({ displayedChains: DisplayedChains; displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; - onTokenSelect: (token: EnrichedTokenSelect) => void; + onTokenSelect: (token: EnrichedToken) => void; }) => { return ( void; - onTokenSelect: (token: EnrichedTokenSelect) => void; + onTokenSelect: (token: EnrichedToken) => void; }) => { return ( void; - onTokenSelect: (token: EnrichedTokenSelect) => void; + onTokenSelect: (token: EnrichedToken) => void; onModalClose: () => void; }) => { return ( @@ -536,15 +535,7 @@ const MobileLayout = ({ token={token} isSelected={false} onClick={() => { - onTokenSelect({ - chainId: token.chainId, - symbolUri: token.logoURI, - symbol: token.symbol, - address: token.address, - balance: token.balance, - priceUsd: parseUnits(token.priceUSD, 18), - decimals: token.decimals, - }); + onTokenSelect(token); onModalClose(); }} /> @@ -563,15 +554,7 @@ const MobileLayout = ({ token={token} isSelected={false} onClick={() => { - onTokenSelect({ - chainId: token.chainId, - symbolUri: token.logoURI, - symbol: token.symbol, - address: token.address, - balance: token.balance, - priceUsd: parseUnits(token.priceUSD, 18), - decimals: token.decimals, - }); + onTokenSelect(token); onModalClose(); }} /> @@ -609,7 +592,7 @@ const DesktopLayout = ({ displayedChains: DisplayedChains; displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; - onTokenSelect: (token: EnrichedTokenSelect) => void; + onTokenSelect: (token: EnrichedToken) => void; onModalClose: () => void; }) => { useHotkeys("esc", () => onModalClose()); @@ -688,15 +671,7 @@ const DesktopLayout = ({ token={token} isSelected={false} onClick={() => { - onTokenSelect({ - chainId: token.chainId, - symbolUri: token.logoURI, - symbol: token.symbol, - address: token.address, - balance: token.balance, - priceUsd: parseUnits(token.priceUSD, 18), - decimals: token.decimals, - }); + onTokenSelect(token); onModalClose(); }} /> @@ -714,15 +689,7 @@ const DesktopLayout = ({ token={token} isSelected={false} onClick={() => { - onTokenSelect({ - chainId: token.chainId, - symbolUri: token.logoURI, - symbol: token.symbol, - address: token.address, - balance: token.balance, - priceUsd: parseUnits(token.priceUSD, 18), - decimals: token.decimals, - }); + onTokenSelect(token); onModalClose(); }} /> diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index 8c0aeeb8f..e2ca69860 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -3,26 +3,13 @@ import { BigNumber } from "ethers"; import { useCallback, useEffect, useState } from "react"; import { COLORS, getChainInfo } from "utils"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; -import ChainTokenSelectorModal from "./Modal"; - -export type TokenSelect = { - chainId: number; - symbolUri: string; - symbol: string; - address: string; -}; - -export type EnrichedTokenSelect = TokenSelect & { - priceUsd: BigNumber; - balance: BigNumber; - decimals: number; -}; +import ChainTokenSelectorModal, { EnrichedToken } from "./Modal"; type Props = { - selectedToken: EnrichedTokenSelect | null; - onSelect?: (token: EnrichedTokenSelect) => void; + selectedToken: EnrichedToken | null; + onSelect?: (token: EnrichedToken) => void; isOriginToken: boolean; - otherToken?: EnrichedTokenSelect | null; // The currently selected token on the other side + otherToken?: EnrichedToken | null; // The currently selected token on the other side marginBottom?: string; className?: string; }; @@ -44,7 +31,7 @@ export default function SelectorButton({ }, [selectedToken]); const setSelectedToken = useCallback( - (token: EnrichedTokenSelect) => { + (token: EnrichedToken) => { onSelect?.(token); setDisplayModal(false); }, @@ -80,7 +67,7 @@ export default function SelectorButton({ <> setDisplayModal(true)}> - + diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 775e65f60..21ef9b3d8 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -15,7 +15,7 @@ import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { BigNumber } from "ethers"; import { COLORS, formatUSDString, isDefined } from "utils"; -import { EnrichedTokenSelect } from "./ChainTokenSelector/SelectorButton"; +import { EnrichedToken } from "./ChainTokenSelector/Modal"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; import { Tooltip } from "components/Tooltip"; @@ -33,8 +33,8 @@ export type BridgeButtonState = interface ConfirmationButtonProps extends ButtonHTMLAttributes { - inputToken: EnrichedTokenSelect | null; - outputToken: EnrichedTokenSelect | null; + inputToken: EnrichedToken | null; + outputToken: EnrichedToken | null; amount: BigNumber | null; swapQuote: SwapApprovalApiResponse | null; isQuoteLoading: boolean; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 4134260ff..8cc930440 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -1,13 +1,14 @@ -import { COLORS, formatUnitsWithMaxFractions, formatUSD } from "utils"; -import SelectorButton, { - EnrichedTokenSelect, -} from "./ChainTokenSelector/SelectorButton"; +import { COLORS, formatUSD, formatUSDString } from "utils"; +import SelectorButton from "./ChainTokenSelector/SelectorButton"; +import { EnrichedToken } from "./ChainTokenSelector/Modal"; import BalanceSelector from "./BalanceSelector"; import styled from "@emotion/styled"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { BigNumber, utils } from "ethers"; +import { useCallback } from "react"; +import { BigNumber } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; import { AmountInputError } from "views/Bridge/utils"; +import { useTokenInput } from "hooks"; +import { formatUnits } from "ethers/lib/utils"; export const InputForm = ({ inputToken, @@ -22,11 +23,11 @@ export const InputForm = ({ expectedInputAmount, validationError, }: { - inputToken: EnrichedTokenSelect | null; - setInputToken: (token: EnrichedTokenSelect | null) => void; + inputToken: EnrichedToken | null; + setInputToken: (token: EnrichedToken | null) => void; - outputToken: EnrichedTokenSelect | null; - setOutputToken: (token: EnrichedTokenSelect | null) => void; + outputToken: EnrichedToken | null; + setOutputToken: (token: EnrichedToken | null) => void; isQuoteLoading: boolean; expectedOutputAmount: string | undefined; @@ -101,8 +102,8 @@ const TokenInput = ({ otherToken, disabled, }: { - setToken: (token: EnrichedTokenSelect) => void; - token: EnrichedTokenSelect | null; + setToken: (token: EnrichedToken) => void; + token: EnrichedToken | null; setAmount: (amount: BigNumber | null) => void; isOrigin: boolean; expectedAmount: string | undefined; @@ -110,76 +111,36 @@ const TokenInput = ({ isUpdateLoading: boolean; insufficientInputBalance?: boolean; disabled?: boolean; - otherToken?: EnrichedTokenSelect | null; + otherToken?: EnrichedToken | null; }) => { - const [amountString, setAmountString] = useState(""); - const [justTyped, setJustTyped] = useState(false); - - // Handle user input changes - useEffect(() => { - if (!justTyped) { - return; - } - setJustTyped(false); - try { - if (!token) { - setAmount(null); - return; - } - const parsed = utils.parseUnits(amountString, token.decimals); - setAmount(parsed); - } catch (e) { - setAmount(null); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [amountString]); - - // Reset amount when token changes - useEffect(() => { - if (token) { - setAmountString(""); - } - }, [token]); - - // Handle quote updates - only update the field that should receive the quote - useEffect(() => { - if (shouldUpdate && isUpdateLoading) { - setAmountString(""); - } - - if (expectedAmount && token && shouldUpdate) { - setAmountString( - formatUnitsWithMaxFractions(expectedAmount, token.decimals) - ); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [expectedAmount, isUpdateLoading]); - - const estimatedUsdAmount = useMemo(() => { - try { - const amount = utils.parseUnits(amountString, token!.decimals); - if (!token) { - return null; - } - const priceAsNumeric = Number(utils.formatUnits(token.priceUsd, 18)); - const amountAsNumeric = Number(utils.formatUnits(amount, token.decimals)); - const estimatedUsdAmountNumeric = amountAsNumeric * priceAsNumeric; - const estimatedUsdAmount = utils.parseUnits( - estimatedUsdAmountNumeric.toString(), - 18 - ); - return formatUSD(estimatedUsdAmount); - } catch (e) { - return null; - } - }, [amountString, token]); + const { + amountString, + unit, + convertedAmount, + toggleUnit, + handleInputChange, + handleBalanceClick, + } = useTokenInput({ + token, + setAmount, + expectedAmount, + shouldUpdate, + isUpdateLoading, + }); const inputDisabled = (() => { if (disabled) return true; return Boolean(shouldUpdate && isUpdateLoading); })(); + const formattedConvertedAmount = (() => { + if (!convertedAmount) return "0.00"; + if (unit === "token") { + return "$" + formatUSD(convertedAmount); + } + return formatUnits(convertedAmount, 18) + token?.symbol; + })(); + return ( @@ -189,20 +150,16 @@ const TokenInput = ({ { - const value = e.target.value; - if (value === "" || /^\d*\.?\d*$/.test(value)) { - setJustTyped(true); - setAmountString(value); - } - }} + onChange={(e) => handleInputChange(e.target.value)} disabled={inputDisabled} error={insufficientInputBalance} /> - {" "} - Value: ${estimatedUsdAmount ?? "0.00"} + + {" "} + Value: {formattedConvertedAmount} + @@ -223,10 +180,7 @@ const TokenInput = ({ error={insufficientInputBalance} setAmount={(amount) => { if (amount) { - setAmount(amount); - setAmountString( - formatUnitsWithMaxFractions(amount, token.decimals) - ); + handleBalanceClick(amount, token.decimals); } }} /> @@ -255,6 +209,8 @@ const ValueRow = styled.div` } `; +const UnitToggleButton = styled.button``; + const TokenAmountStack = styled.div` display: flex; flex-direction: column; diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index e9a6c4984..9f846f1a1 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -3,7 +3,7 @@ import { BigNumber } from "ethers"; import { AmountInputError } from "../../Bridge/utils"; import useSwapQuote from "./useSwapQuote"; -import { EnrichedTokenSelect } from "../components/ChainTokenSelector/SelectorButton"; +import { EnrichedToken } from "../components/ChainTokenSelector/Modal"; import { useSwapApprovalAction, SwapApprovalData, @@ -13,10 +13,10 @@ import { BridgeButtonState } from "../components/ConfirmationButton"; import { useDebounce } from "@uidotdev/usehooks"; export type UseSwapAndBridgeReturn = { - inputToken: EnrichedTokenSelect | null; - outputToken: EnrichedTokenSelect | null; - setInputToken: (t: EnrichedTokenSelect | null) => void; - setOutputToken: (t: EnrichedTokenSelect | null) => void; + inputToken: EnrichedToken | null; + outputToken: EnrichedToken | null; + setInputToken: (t: EnrichedToken | null) => void; + setOutputToken: (t: EnrichedToken | null) => void; quickSwap: () => void; amount: BigNumber | null; @@ -47,12 +47,8 @@ export type UseSwapAndBridgeReturn = { }; export function useSwapAndBridge(): UseSwapAndBridgeReturn { - const [inputToken, setInputToken] = useState( - null - ); - const [outputToken, setOutputToken] = useState( - null - ); + const [inputToken, setInputToken] = useState(null); + const [outputToken, setOutputToken] = useState(null); const [amount, setAmount] = useState(null); const [isAmountOrigin, setIsAmountOrigin] = useState(true); diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts index 71ad0cbec..94d1d645a 100644 --- a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -3,7 +3,7 @@ import { BigNumber } from "ethers"; import axios from "axios"; import { AmountInputError } from "../../Bridge/utils"; -import { EnrichedTokenSelect } from "../components/ChainTokenSelector/SelectorButton"; +import { EnrichedToken } from "../components/ChainTokenSelector/Modal"; import { validationErrorTextMap } from "views/Bridge/components/AmountInput"; export type ValidationResult = { @@ -15,7 +15,7 @@ export type ValidationResult = { export function useValidateSwapAndBridge( amount: BigNumber | null, isAmountOrigin: boolean, - inputToken: EnrichedTokenSelect | null, + inputToken: EnrichedToken | null, error: any ): ValidationResult { const validation = useMemo(() => { @@ -54,7 +54,7 @@ export function useValidateSwapAndBridge( function getValidationErrorText(props: { validationError?: AmountInputError; - inputToken: EnrichedTokenSelect | null; + inputToken: EnrichedToken | null; }): string | undefined { if (!props.validationError) { return; From 1caac98600dbf6bfa0af77adc3a8676afaaed8b9 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 13 Oct 2025 13:53:40 +0200 Subject: [PATCH 052/122] show on output side Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 20 ++++++++++++++----- .../SwapAndBridge/components/InputForm.tsx | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 19a564a25..7202008dd 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -75,12 +75,22 @@ export function useTokenInput({ } if (expectedAmount && token && shouldUpdate) { - // TODO: handle converted amount - setAmountString( - formatUnitsWithMaxFractions(expectedAmount, token.decimals) - ); + if (unit === "token") { + // Display as token amount + setAmountString( + formatUnitsWithMaxFractions(expectedAmount, token.decimals) + ); + } else { + // Display as USD amount - convert token to USD + const tokenAmountFormatted = formatUnitsWithMaxFractions( + expectedAmount, + token.decimals + ); + const usdValue = convertTokenToUSD(tokenAmountFormatted, token); + setAmountString(utils.formatUnits(usdValue, 18)); + } } - }, [expectedAmount, isUpdateLoading, shouldUpdate, token]); + }, [expectedAmount, isUpdateLoading, shouldUpdate, token, unit]); // Set converted value for display useEffect(() => { diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 8cc930440..550a8f729 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -138,7 +138,7 @@ const TokenInput = ({ if (unit === "token") { return "$" + formatUSD(convertedAmount); } - return formatUnits(convertedAmount, 18) + token?.symbol; + return `${formatUnits(convertedAmount, 18)} ${token?.symbol}`; })(); return ( From ef7bc8367e9edc11989be7669a75bcbf1973d3a8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 13 Oct 2025 18:06:41 +0200 Subject: [PATCH 053/122] set default route on load & connect Signed-off-by: Gerhard Steenkamp --- src/hooks/useConnectionSVM.ts | 6 +- src/hooks/useEnrichedCrosschainBalances.ts | 7 +- .../SwapAndBridge/hooks/useDefaultRoute.ts | 100 ++++++++++++++++++ .../SwapAndBridge/hooks/useSwapAndBridge.ts | 58 ++++++++-- 4 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 src/views/SwapAndBridge/hooks/useDefaultRoute.ts diff --git a/src/hooks/useConnectionSVM.ts b/src/hooks/useConnectionSVM.ts index 38309b0ba..fa13417e7 100644 --- a/src/hooks/useConnectionSVM.ts +++ b/src/hooks/useConnectionSVM.ts @@ -73,7 +73,11 @@ export function useConnectionSVM() { ); return { - chainId: hubPoolChainId === 1 ? solana.chainId : solanaDevnet.chainId, + chainId: !connected + ? 0 + : hubPoolChainId === 1 + ? solana.chainId + : solanaDevnet.chainId, account: publicKey, select, state, diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts index a94451d50..b0bed59dd 100644 --- a/src/hooks/useEnrichedCrosschainBalances.ts +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -63,5 +63,10 @@ export function useEnrichedCrosschainBalances() { Array > ); - }, [availableCrosschainRoutes, tokenBalances]); + }, [ + availableCrosschainRoutes.data, + availableCrosschainRoutes.isLoading, + tokenBalances.data, + tokenBalances.isLoading, + ]); } diff --git a/src/views/SwapAndBridge/hooks/useDefaultRoute.ts b/src/views/SwapAndBridge/hooks/useDefaultRoute.ts new file mode 100644 index 000000000..bee60a5ce --- /dev/null +++ b/src/views/SwapAndBridge/hooks/useDefaultRoute.ts @@ -0,0 +1,100 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { useEnrichedCrosschainBalances } from "hooks/useEnrichedCrosschainBalances"; +import { CHAIN_IDs } from "utils"; +import { EnrichedToken } from "../components/ChainTokenSelector/Modal"; +import { useConnectionSVM } from "hooks/useConnectionSVM"; +import { usePrevious } from "@uidotdev/usehooks"; + +type DefaultRoute = { + inputToken: EnrichedToken | null; + outputToken: EnrichedToken | null; +}; + +export function useDefaultRoute(): DefaultRoute { + const [defaultInputToken, setDefaultInputToken] = + useState(null); + const [defaultOutputToken, setDefaultOutputToken] = + useState(null); + const [hasSetInitial, setHasSetInitial] = useState(false); + const [hasSetConnected, setHasSetConnected] = useState(false); + + const { isConnected: isConnectedEVM, chainId: chainIdEVM } = + useConnectionEVM(); + const { isConnected: isConnectedSVM, chainId: chainIdSVM } = + useConnectionSVM(); + const routeData = useEnrichedCrosschainBalances(); + + const anyConnected = isConnectedEVM || isConnectedSVM; + const previouslyConnected = usePrevious(anyConnected); + const chainId = chainIdEVM || chainIdSVM; + const hasRouteData = Object.keys(routeData).length ? true : false; + + const findUsdcToken = useCallback( + (targetChainId: number) => { + const tokensOnChain = routeData[targetChainId] || []; + return tokensOnChain.find( + (token) => token.symbol.toUpperCase() === "USDC" + ); + }, + [routeData] + ); + + // initial load + useEffect(() => { + // Wait for balances to be available + if (!hasRouteData || hasSetInitial) { + return; + } + // Wallet is not connected: Base -> Arbitrum + const inputToken = findUsdcToken(CHAIN_IDs.BASE); + const outputToken = findUsdcToken(CHAIN_IDs.ARBITRUM); + setDefaultInputToken(inputToken ?? null); + setDefaultOutputToken(outputToken ?? null); + setHasSetInitial(true); + }, [findUsdcToken, hasSetInitial, hasRouteData]); + + // connect wallet + useEffect(() => { + // Wait for balances to be available + if (!hasRouteData) { + return; + } + + // only first connection - also check hasSetConnected to prevent infinite loop + if (!previouslyConnected && anyConnected && chainId && !hasSetConnected) { + let inputToken: EnrichedToken | undefined; + let outputToken: EnrichedToken | undefined; + + if (chainId === CHAIN_IDs.ARBITRUM) { + // Special case: If on Arbitrum, use Arbitrum -> Base + inputToken = findUsdcToken(CHAIN_IDs.ARBITRUM); + outputToken = findUsdcToken(CHAIN_IDs.BASE); + } else { + // Use wallet's current network -> Arbitrum + inputToken = findUsdcToken(chainId); + outputToken = findUsdcToken(CHAIN_IDs.ARBITRUM); + } + + setDefaultInputToken(inputToken || null); + setDefaultOutputToken(outputToken || null); + setHasSetConnected(true); + } + }, [ + anyConnected, + hasRouteData, + chainId, + findUsdcToken, + previouslyConnected, + hasSetConnected, + ]); + + // Memoize the return value to prevent unnecessary re-renders + return useMemo( + () => ({ + inputToken: defaultInputToken, + outputToken: defaultOutputToken, + }), + [defaultInputToken, defaultOutputToken] + ); +} diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 9f846f1a1..22f9b7684 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { BigNumber } from "ethers"; import { AmountInputError } from "../../Bridge/utils"; @@ -11,6 +11,7 @@ import { import { useValidateSwapAndBridge } from "./useValidateSwapAndBridge"; import { BridgeButtonState } from "../components/ConfirmationButton"; import { useDebounce } from "@uidotdev/usehooks"; +import { useDefaultRoute } from "./useDefaultRoute"; export type UseSwapAndBridgeReturn = { inputToken: EnrichedToken | null; @@ -53,6 +54,34 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const [isAmountOrigin, setIsAmountOrigin] = useState(true); const debouncedAmount = useDebounce(amount, 300); + const defaultRoute = useDefaultRoute(); + + useEffect(() => { + if (defaultRoute.inputToken && defaultRoute.outputToken) { + setInputToken((prev) => { + // Only update if token is different (avoid unnecessary re-renders) + if ( + !prev || + prev.address !== defaultRoute.inputToken!.address || + prev.chainId !== defaultRoute.inputToken!.chainId + ) { + return defaultRoute.inputToken; + } + return prev; + }); + setOutputToken((prev) => { + // Only update if token is different (avoid unnecessary re-renders) + if ( + !prev || + prev.address !== defaultRoute.outputToken!.address || + prev.chainId !== defaultRoute.outputToken!.chainId + ) { + return defaultRoute.outputToken; + } + return prev; + }); + } + }, [defaultRoute]); const quickSwap = useCallback(() => { setInputToken((prevInput) => { @@ -130,7 +159,24 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { return buttonState === "loadingQuote" || buttonState === "submitting"; }, [buttonState]); - const buttonLabel = buttonLabels[buttonState]; + const buttonLabel = useMemo(() => buttonLabels[buttonState], [buttonState]); + + const buttonDisabled = useMemo( + () => + approvalAction.buttonDisabled || + !!validation.error || + !inputToken || + !outputToken || + !amount || + amount.lte(0), + [ + approvalAction.buttonDisabled, + validation.error, + inputToken, + outputToken, + amount, + ] + ); return { inputToken, @@ -154,13 +200,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { // Button state information buttonState, - buttonDisabled: - approvalAction.buttonDisabled || - !!validation.error || - !inputToken || - !outputToken || - !amount || - amount.lte(0), + buttonDisabled, buttonLoading, buttonLabel, From b78a0958a7dede88ca268267e1727dbc5f2d1cf4 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 12:08:22 +0200 Subject: [PATCH 054/122] fix max width Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index cc1d35d78..b38945703 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -41,7 +41,7 @@ export default function SwapAndBridge() { }, [onConfirm, inputToken, outputToken]); return ( - + Date: Tue, 14 Oct 2025 12:26:17 +0200 Subject: [PATCH 055/122] fix converted amount scaling issue Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 6 +++--- src/utils/format.ts | 4 ++-- src/utils/token.ts | 12 ++++++------ src/views/SwapAndBridge/components/InputForm.tsx | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 7202008dd..4e79c1623 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -87,7 +87,7 @@ export function useTokenInput({ token.decimals ); const usdValue = convertTokenToUSD(tokenAmountFormatted, token); - setAmountString(utils.formatUnits(usdValue, 18)); + setAmountString(utils.formatUnits(usdValue, token.decimals)); } } }, [expectedAmount, isUpdateLoading, shouldUpdate, token, unit]); @@ -116,7 +116,7 @@ export function useTokenInput({ // Convert token amount to USD string for display if (amountString && token && convertedAmount) { try { - const a = utils.formatUnits(convertedAmount, 18); + const a = utils.formatUnits(convertedAmount, token.decimals); setAmountString(a); } catch (e) { setAmountString("0"); @@ -128,7 +128,7 @@ export function useTokenInput({ // Convert USD amount to token string for display if (amountString && token && convertedAmount) { try { - const a = utils.formatUnits(convertedAmount, 18); + const a = utils.formatUnits(convertedAmount, token.decimals); setAmountString(a); } catch (e) { setAmountString("0"); diff --git a/src/utils/format.ts b/src/utils/format.ts index e4d29c475..76f98d9d0 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -295,8 +295,8 @@ export function humanReadableNumber(num: number, decimals = 0): string { * @returns A string formatted as USD. A number with 2 decimal places. * @note USD only has 2 decimal places of precision, so this will round to the nearest cent. */ -export function formatUSD(value: BigNumberish): string { - const formattedString = ethers.utils.formatUnits(value, 18); +export function formatUSD(value: BigNumberish, decimals = 18): string { + const formattedString = ethers.utils.formatUnits(value, decimals); return numeral(Number(formattedString).toFixed(2)).format("0,0.00"); } diff --git a/src/utils/token.ts b/src/utils/token.ts index 1300248fd..e5a711c0d 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -95,9 +95,9 @@ export function convertTokenToUSD( tokenAmount: string, token: LifiToken ): BigNumber { - const tokenScaled = parseUnits(tokenAmount, 18); - const priceScaled = parseUnits(token.priceUSD, 18); - return tokenScaled.mul(priceScaled).div(fixedPointAdjustment); + const tokenScaled = parseUnits(tokenAmount, token.decimals); + const priceScaled = parseUnits(token.priceUSD, token.decimals); + return tokenScaled.mul(priceScaled).div(parseUnits("1", token.decimals)); } /** @@ -110,7 +110,7 @@ export function convertUSDToToken( usdAmount: string, token: LifiToken ): BigNumber { - const usdScaled = parseUnits(usdAmount, 18); - const priceScaled = parseUnits(token.priceUSD, 18); - return usdScaled.mul(fixedPointAdjustment).div(priceScaled); + const usdScaled = parseUnits(usdAmount, token.decimals); + const priceScaled = parseUnits(token.priceUSD, token.decimals); + return usdScaled.mul(parseUnits("1", token.decimals)).div(priceScaled); } diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 550a8f729..397140191 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -136,9 +136,9 @@ const TokenInput = ({ const formattedConvertedAmount = (() => { if (!convertedAmount) return "0.00"; if (unit === "token") { - return "$" + formatUSD(convertedAmount); + return "$" + formatUSD(convertedAmount, token?.decimals); } - return `${formatUnits(convertedAmount, 18)} ${token?.symbol}`; + return `${formatUnits(convertedAmount, token?.decimals)} ${token?.symbol}`; })(); return ( From 554f7c031dcda42fdec40239a89247531fd9dc63 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 12:38:18 +0200 Subject: [PATCH 056/122] fixup Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 4e79c1623..f100f79ec 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -94,7 +94,10 @@ export function useTokenInput({ // Set converted value for display useEffect(() => { - if (!token || !amountString) return; + if (!token || !amountString) { + setConvertedAmount(undefined); + return; + } try { if (unit === "token") { // User typed token amount - convert to USD for display From 81000997c0f0fce543645ac2df4e116d15e351aa Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 12:40:33 +0200 Subject: [PATCH 057/122] import global css vars from figma Signed-off-by: Gerhard Steenkamp --- src/components/GlobalStyles/GlobalStyles.tsx | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/components/GlobalStyles/GlobalStyles.tsx b/src/components/GlobalStyles/GlobalStyles.tsx index 4cae697ac..b5261d354 100644 --- a/src/components/GlobalStyles/GlobalStyles.tsx +++ b/src/components/GlobalStyles/GlobalStyles.tsx @@ -84,6 +84,88 @@ const variables = css` --tints-shades-white-100: #e0f3ff; --tints-shades-white-200: #9daab2; + /* Color tokens (from Figma) */ + --base-aqua: var(--shades-aqua-aqua-300); + --base-bright-gray: var(--shades-neutrals-neutral-100); + --base-dark-gray: var(--shades-neutrals-neutral-800); + --functional-blue: #47a8ff; + --functional-red: #ff6166; + --functional-yellow: #ff9500; + --shades-aqua-aqua-100: #bdfced; + --shades-aqua-aqua-200: #98fbe4; + --shades-aqua-aqua-300: #6cf9d8; + --shades-aqua-aqua-400: #66e5c7; + --shades-aqua-aqua-500: #59bca6; + --shades-aqua-aqua-600: #4d9385; + --shades-aqua-aqua-700: #406b65; + --shades-aqua-aqua-800: #3a5754; + --shades-aqua-aqua-900: #334244; + --shades-neutrals-neutral-000: #ffffff; + --shades-neutrals-neutral-100: #e0f3ff; + --shades-neutrals-neutral-200: #cedfeb; + --shades-neutrals-neutral-300: #aab8c2; + --shades-neutrals-neutral-400: #869099; + --shades-neutrals-neutral-500: #636970; + --shades-neutrals-neutral-600: #51555c; + --shades-neutrals-neutral-700: #3f4247; + --shades-neutrals-neutral-800: #2d2e33; + --shades-neutrals-neutral-850: #34353b; + --shades-neutrals-neutral-900: #202024; + --transparency-aqua-aqua-10: #6cf9d81a; + --transparency-aqua-aqua-20: #6cf9d833; + --transparency-aqua-aqua-30: #6cf9d84d; + --transparency-aqua-aqua-40: #6cf9d866; + --transparency-aqua-aqua-5: #6cf9d80d; + --transparency-aqua-aqua-50: #6cf9d880; + --transparency-aqua-aqua-60: #6cf9d899; + --transparency-aqua-aqua-70: #6cf9d8b2; + --transparency-aqua-aqua-80: #6cf9d8cc; + --transparency-aqua-aqua-90: #6cf9d8e5; + --transparency-bright-gray-bright-gray-10: #e0f3ff1a; + --transparency-bright-gray-bright-gray-20: #e0f3ff33; + --transparency-bright-gray-bright-gray-30: #e0f3ff4d; + --transparency-bright-gray-bright-gray-40: #e0f3ff66; + --transparency-bright-gray-bright-gray-5: #e0f3ff0d; + --transparency-bright-gray-bright-gray-50: #e0f3ff80; + --transparency-bright-gray-bright-gray-60: #e0f3ff99; + --transparency-bright-gray-bright-gray-70: #e0f3ffb2; + --transparency-bright-gray-bright-gray-80: #e0f3ffcc; + --transparency-bright-gray-bright-gray-90: #e0f3ffe5; + --transparency-dark-gray-dark-gray-10: #2d2e331a; + --transparency-dark-gray-dark-gray-20: #2d2e3333; + --transparency-dark-gray-dark-gray-30: #2d2e334d; + --transparency-dark-gray-dark-gray-40: #2d2e3366; + --transparency-dark-gray-dark-gray-5: #2d2e330d; + --transparency-dark-gray-dark-gray-50: #2d2e3380; + --transparency-dark-gray-dark-gray-60: #2d2e3399; + --transparency-dark-gray-dark-gray-70: #2d2e33b2; + --transparency-dark-gray-dark-gray-80: #2d2e33cc; + --transparency-dark-gray-dark-gray-90: #2d2e33e5; + + /* Spacing tokens (from Figma) */ + --corner-radius-none: 0px; + --corner-radius-3x-small: 4px; + --corner-radius-2x-small: 6px; + --corner-radius-x-small: 8px; + --corner-radius-small: 12px; + --corner-radius-medium: 16px; + --corner-radius-large: 24px; + --corner-radius-x-large: 32px; + --corner-radius-2x-large: 40px; + --corner-radius-round: 999px; + --spacing-none: 0px; + --spacing-3x-small: 4px; + --spacing-2x-small: 6px; + --spacing-x-small: 8px; + --spacing-small: 12px; + --spacing-medium: 16px; + --spacing-large: 24px; + --spacing-x-large: 32px; + --spacing-2x-large: 40px; + --spacing-3x-large: 64px; + --spacing-4x-large: 80px; + --spacing-5x-large: 120px; + /* Old variables kept until refactored */ --color-primary: var(--color-interface-aqua); --color-gray: var(--color-neutrals-black-800); From 565910f73fb976336f06b6a92d7acedf3402b1f5 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 12:58:09 +0200 Subject: [PATCH 058/122] fix padding, hover styles for converter Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/components/InputForm.tsx | 12 +++++++++++- src/views/SwapAndBridge/index.tsx | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 397140191..8c99459d3 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -209,7 +209,17 @@ const ValueRow = styled.div` } `; -const UnitToggleButton = styled.button``; +const UnitToggleButton = styled.button` + color: var(--color-neutrals-light-200); + + &:hover:not(:disabled) { + color: var(--color-interface-white); + } + + svg { + color: inherit; + } +`; const TokenAmountStack = styled.div` display: flex; diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index b38945703..cc88dac3c 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -80,7 +80,7 @@ const Wrapper = styled.div` display: flex; flex-direction: column; - gap: 16px; + gap: 8px; align-items: center; justify-content: center; From f29bec82302c31bc748de61f7386e3cb3fb6cc68 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 13:02:49 +0200 Subject: [PATCH 059/122] clean up Signed-off-by: Gerhard Steenkamp --- src/utils/token.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/token.ts b/src/utils/token.ts index e5a711c0d..104985701 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -1,5 +1,4 @@ -import { BigNumber, ethers, utils } from "ethers"; -import { parseEther } from "ethers/lib/utils"; +import { BigNumber, ethers } from "ethers"; import { LifiToken } from "hooks/useAvailableCrosschainRoutes"; import { @@ -7,7 +6,6 @@ import { ChainId, getConfig, getChainInfo, - fixedPointAdjustment, parseUnits, } from "utils"; import { ERC20__factory } from "utils/typechain"; From 72d5363969f8e996ed77fbfe1ddb1c0d2a0d7bf8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 13:15:23 +0200 Subject: [PATCH 060/122] reset amounts if set to 0 Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 48 ++++++++++++++----- src/views/SwapAndBridge/hooks/useSwapQuote.ts | 5 +- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index f100f79ec..2166aaed9 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -47,11 +47,26 @@ export function useTokenInput({ setAmount(null); return; } + // If the input is empty or effectively zero, set amount to null + if (!amountString || !Number(amountString)) { + setAmount(null); + return; + } if (unit === "token") { const parsed = utils.parseUnits(amountString, token.decimals); + // If parsed amount is zero or negative, set to null + if (parsed.lte(0)) { + setAmount(null); + return; + } setAmount(parsed); } else { const tokenValue = convertUSDToToken(amountString, token); + // If converted value is zero or negative, set to null + if (tokenValue.lte(0)) { + setAmount(null); + return; + } setAmount(tokenValue); } } catch (e) { @@ -74,20 +89,27 @@ export function useTokenInput({ setAmountString(""); } - if (expectedAmount && token && shouldUpdate) { - if (unit === "token") { - // Display as token amount - setAmountString( - formatUnitsWithMaxFractions(expectedAmount, token.decimals) - ); + if (shouldUpdate && token) { + // Clear the field when there's no expected amount and not loading + if (!expectedAmount && !isUpdateLoading) { + setAmountString(""); } else { - // Display as USD amount - convert token to USD - const tokenAmountFormatted = formatUnitsWithMaxFractions( - expectedAmount, - token.decimals - ); - const usdValue = convertTokenToUSD(tokenAmountFormatted, token); - setAmountString(utils.formatUnits(usdValue, token.decimals)); + if (expectedAmount) { + if (unit === "token") { + // Display as token amount + setAmountString( + formatUnitsWithMaxFractions(expectedAmount, token.decimals) + ); + } else { + // Display as USD amount - convert token to USD + const tokenAmountFormatted = formatUnitsWithMaxFractions( + expectedAmount, + token.decimals + ); + const usdValue = convertTokenToUSD(tokenAmountFormatted, token); + setAmountString(utils.formatUnits(usdValue, token.decimals)); + } + } } } }, [expectedAmount, isUpdateLoading, shouldUpdate, token, unit]); diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index e84854424..be11387ef 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -43,7 +43,10 @@ const useSwapQuote = ({ depositor, recipient, ], - queryFn: async (): Promise => { + queryFn: async (): Promise => { + if (Number(amount) <= 0) { + return undefined; + } if (!origin || !destination || !amount || !depositor) { throw new Error("Missing required swap quote parameters"); } From 65326b1b6baa5a75c0d746085d409559d95a7e06 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 15:03:42 +0200 Subject: [PATCH 061/122] fix arrow icon Signed-off-by: Gerhard Steenkamp --- src/assets/icons/arrow-down.svg | 3 +++ src/views/SwapAndBridge/components/InputForm.tsx | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 src/assets/icons/arrow-down.svg diff --git a/src/assets/icons/arrow-down.svg b/src/assets/icons/arrow-down.svg new file mode 100644 index 000000000..f9f930a98 --- /dev/null +++ b/src/assets/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 8c99459d3..365186336 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -1,4 +1,4 @@ -import { COLORS, formatUSD, formatUSDString } from "utils"; +import { COLORS, formatUSD } from "utils"; import SelectorButton from "./ChainTokenSelector/SelectorButton"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; import BalanceSelector from "./BalanceSelector"; @@ -6,6 +6,7 @@ import styled from "@emotion/styled"; import { useCallback } from "react"; import { BigNumber } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; +import { ReactComponent as ArrowDown } from "assets/icons/arrow-down.svg"; import { AmountInputError } from "views/Bridge/utils"; import { useTokenInput } from "hooks"; import { formatUnits } from "ethers/lib/utils"; @@ -70,7 +71,7 @@ export const InputForm = ({ disabled={!outputToken || !outputToken} /> - + Date: Tue, 14 Oct 2025 15:33:28 +0200 Subject: [PATCH 062/122] add all font sizes Signed-off-by: Gerhard Steenkamp --- public/fonts/Barlow-ExtraBold.woff2 | Bin 0 -> 38720 bytes public/fonts/Barlow-ExtraLight.woff2 | Bin 0 -> 36644 bytes public/fonts/Barlow-Light.woff2 | Bin 0 -> 36816 bytes public/fonts/Barlow-SemiBold.woff2 | Bin 0 -> 38400 bytes public/fonts/Barlow-Thin.woff2 | Bin 0 -> 36368 bytes src/components/GlobalStyles/GlobalStyles.tsx | 50 +++++++++++++++++++ 6 files changed, 50 insertions(+) create mode 100644 public/fonts/Barlow-ExtraBold.woff2 create mode 100644 public/fonts/Barlow-ExtraLight.woff2 create mode 100644 public/fonts/Barlow-Light.woff2 create mode 100644 public/fonts/Barlow-SemiBold.woff2 create mode 100644 public/fonts/Barlow-Thin.woff2 diff --git a/public/fonts/Barlow-ExtraBold.woff2 b/public/fonts/Barlow-ExtraBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6174634d1c736948e0a344a044d6d6e531a0084d GIT binary patch literal 38720 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ℭ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%0Deg&GRa~>XYe6yP`-cbXQ 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 E02R3Os{jB1 literal 0 HcmV?d00001 diff --git a/public/fonts/Barlow-Light.woff2 b/public/fonts/Barlow-Light.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5dd45ebd9e5310c106860ad6c59f1fd2ab11faa7 GIT binary patch literal 36816 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 U|H%IX`fm;>ckXXWpBU_20Z9I<&Hw-a literal 0 HcmV?d00001 diff --git a/public/fonts/Barlow-SemiBold.woff2 b/public/fonts/Barlow-SemiBold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..52681ea89c1e3ba370e8c877d82b3588c1c53008 GIT binary patch literal 38400 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�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 Date: Tue, 14 Oct 2025 15:57:47 +0200 Subject: [PATCH 063/122] fix hover style Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/components/ConfirmationButton.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 21ef9b3d8..dd6864cc3 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -470,13 +470,11 @@ const StyledButton = styled.button<{ color: ${({ aqua }) => (aqua ? "#2D2E33" : "#E0F3FF")}; &:not(:disabled):hover { - ${({ aqua }) => + box-shadow: ${({ aqua }) => aqua - ? `background: rgba(108, 249, 216, 0.1);` + ? `0 0 10px 0 var(--Transparency-Bright-Gray-bright-gray-50, rgba(224, 243, 255, 0.50)) inset, 0 0 4px 2px var(--Transparency-Aqua-aqua-20, rgba(108, 249, 216, 0.20)), 0 2px 12px 1px var(--Transparency-Aqua-aqua-20, rgba(108, 249, 216, 0.20)), 0 4px 24px 2px var(--Transparency-Aqua-aqua-20, rgba(108, 249, 216, 0.20))` : ` - box-shadow: 0 0 16px 0 ${COLORS.aqua}; - background: ${COLORS.aqua}; - `} + `}; } &:not(:disabled):focus { From 0ff33f726b42afe46c6254a2957dca83c294e3d1 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 16:02:54 +0200 Subject: [PATCH 064/122] hide summary when button expanded Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index dd6864cc3..6ba13862e 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -92,17 +92,19 @@ const ExpandableLabelSection: React.FC< Fast & Secure - - - - {fee} - - - - - + {!expanded && ( + + + + {fee} + + + + + + )} ); From 45d10d368fd7c25e24b88c41cf8f3a69abb46cc6 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 14 Oct 2025 16:13:29 +0200 Subject: [PATCH 065/122] fix balance animation Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/components/BalanceSelector.tsx | 7 ++++--- .../components/ChainTokenSelector/SelectorButton.tsx | 1 - src/views/SwapAndBridge/components/InputForm.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index 093625c4b..b5a96eb4f 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -12,7 +12,7 @@ type BalanceSelectorProps = { error?: boolean; }; -export default function BalanceSelector({ +export function BalanceSelector({ balance, decimals, setAmount, @@ -64,7 +64,7 @@ export default function BalanceSelector({ type: "spring", stiffness: 400, damping: 28, - delay: index * 0.07, + delay: (percentages.length - 1 - index) * 0.07, }} whileHover={{ scale: 1.05, @@ -120,7 +120,8 @@ const PillsContainer = styled.div` align-items: center; gap: var(--spacing); position: absolute; - right: calc(100% + (var(--spacing) * 2)); + right: 100%; + padding-right: calc(var(--spacing) * 2); .pill { display: flex; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index e2ca69860..6f4832386 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -1,5 +1,4 @@ import styled from "@emotion/styled"; -import { BigNumber } from "ethers"; import { useCallback, useEffect, useState } from "react"; import { COLORS, getChainInfo } from "utils"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 365186336..ead0da39a 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -1,7 +1,7 @@ import { COLORS, formatUSD } from "utils"; import SelectorButton from "./ChainTokenSelector/SelectorButton"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; -import BalanceSelector from "./BalanceSelector"; +import { BalanceSelector } from "./BalanceSelector"; import styled from "@emotion/styled"; import { useCallback } from "react"; import { BigNumber } from "ethers"; From a536dfca486788ce7ee8a7ac7e978a01b95e3a75 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 15 Oct 2025 13:48:06 +0200 Subject: [PATCH 066/122] fix confirmation button states Signed-off-by: Gerhard Steenkamp --- src/views/Bridge/components/AmountInput.tsx | 5 ++ src/views/Bridge/utils.ts | 3 + .../components/ChainTokenSelector/Modal.tsx | 16 ++--- .../ChainTokenSelector/Searchbar.tsx | 2 +- .../ChainTokenSelector/SelectorButton.tsx | 16 ++--- .../components/ConfirmationButton.tsx | 59 +++++++++---------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 39 +++++++++--- .../hooks/useValidateSwapAndBridge.ts | 28 +++++++-- src/views/SwapAndBridge/index.tsx | 16 +---- 9 files changed, 110 insertions(+), 74 deletions(-) diff --git a/src/views/Bridge/components/AmountInput.tsx b/src/views/Bridge/components/AmountInput.tsx index 7a2b55677..fc983e7ff 100644 --- a/src/views/Bridge/components/AmountInput.tsx +++ b/src/views/Bridge/components/AmountInput.tsx @@ -20,6 +20,11 @@ export const validationErrorTextMap: Record = { "Price impact is too high. Check back later when liquidity is restored.", [AmountInputError.SWAP_QUOTE_UNAVAILABLE]: "Swap quote temporarily unavailable. Please try again later.", + [AmountInputError.NO_INPUT_TOKEN_SELECTED]: + "Select an input token to continue", + [AmountInputError.NO_OUTPUT_TOKEN_SELECTED]: + "Select an output token to continue", + [AmountInputError.NO_AMOUNT_ENTERED]: "Enter an amount to continue", }; type Props = { diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index 8b5a3897d..4f8d8bf49 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -53,6 +53,9 @@ export enum AmountInputError { AMOUNT_TOO_LOW = "amountTooLow", PRICE_IMPACT_TOO_HIGH = "priceImpactTooHigh", SWAP_QUOTE_UNAVAILABLE = "swapQuoteUnavailable", + NO_INPUT_TOKEN_SELECTED = "noInputTokenSelected", + NO_OUTPUT_TOKEN_SELECTED = "noOutputTokenSelected", + NO_AMOUNT_ENTERED = "noAmountEntered", } const config = getConfig(); const enabledRoutes = config.getEnabledRoutes(); diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 9868d6d70..cccf745d0 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -937,7 +937,7 @@ const BackButton = styled.button` height: 32px; border: none; background: transparent; - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); cursor: pointer; border-radius: 6px; transition: background 0.2s ease-in-out; @@ -955,7 +955,7 @@ const BackButton = styled.button` const Title = styled.div` overflow: hidden; - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); font-family: Barlow; font-size: 20px; font-style: normal; @@ -1036,7 +1036,7 @@ const ChainItemImage = styled.img` const ChainItemName = styled.div` overflow: hidden; - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); text-overflow: ellipsis; /* Body/Medium */ font-family: Barlow; @@ -1065,7 +1065,7 @@ const TokenNameSymbolWrapper = styled.div` const TokenName = styled.div` overflow: hidden; - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); /* Body/Medium */ font-family: Barlow; font-size: 16px; @@ -1081,7 +1081,7 @@ const TokenName = styled.div` const TokenSymbol = styled.div` overflow: hidden; - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); text-overflow: ellipsis; /* Body/X Small */ font-family: Barlow; @@ -1106,7 +1106,7 @@ const TokenBalanceStack = styled.div` `; const TokenBalance = styled.div` - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); /* Body/Small */ font-family: Barlow; font-size: 14px; @@ -1116,7 +1116,7 @@ const TokenBalance = styled.div` `; const TokenBalanceUsd = styled.div` - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); /* Body/X Small */ font-family: Barlow; font-size: 12px; @@ -1127,7 +1127,7 @@ const TokenBalanceUsd = styled.div` `; const SectionHeader = styled.div` - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); font-size: 14px; font-weight: 400; line-height: 130%; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx index b387971d5..570aedcd0 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -81,7 +81,7 @@ const StyledProductIcon = styled(ProductIcon)` const Input = styled.input` overflow: hidden; - color: var(--Base-bright-gray, #e0f3ff); + color: var(--base-bright-gray, #e0f3ff); text-overflow: ellipsis; &::placeholder { diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index 6f4832386..b43f5f313 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -90,7 +90,7 @@ export default function SelectorButton({ ); } -const Wrapper = styled.div` +const Wrapper = styled.button` --height: 48px; --padding: 8px; height: var(--height); @@ -133,6 +133,7 @@ const NamesStack = styled.div` padding-inline: var(--padding); white-space: nowrap; height: 100%; + min-width: 72px; flex-grow: 1; @@ -141,11 +142,10 @@ const NamesStack = styled.div` `; const TokenName = styled.div` - font-size: 16px; - line-height: 16px; - + font-size: 14px; + line-height: 100%; font-weight: 600; - color: #e0f3ff; + color: var(--base-bright-gray, #e0f3ff); `; const SelectTokenName = styled(TokenName)` @@ -154,10 +154,10 @@ const SelectTokenName = styled(TokenName)` `; const ChainName = styled.div` - font-size: 12px; - line-height: 12px; + font-size: 14px; font-weight: 400; - color: #e0f3ff; + line-height: 100%; + color: var(--base-bright-gray, #e0f3ff); opacity: 0.5; `; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 6ba13862e..c6005eb56 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -38,7 +38,7 @@ interface ConfirmationButtonProps amount: BigNumber | null; swapQuote: SwapApprovalApiResponse | null; isQuoteLoading: boolean; - onConfirm?: () => void; + onConfirm?: () => Promise; validationError?: AmountInputError; validationWarning?: AmountInputError; validationErrorFormatted?: string; @@ -76,7 +76,9 @@ const ExpandableLabelSection: React.FC< }) => { // Render state-specific content let content: React.ReactNode = null; - if (validationError && validationErrorFormatted) { + + // Show validation messages for all non-ready states + if (state !== "readyToConfirm" && validationErrorFormatted) { content = ( <> @@ -85,30 +87,30 @@ const ExpandableLabelSection: React.FC< ); - } else if (hasQuote) { + } else if (state === "readyToConfirm" && hasQuote) { + // Only show quote details when ready to confirm content = ( <> Fast & Secure - {!expanded && ( - - - - {fee} - - - - - - )} + + + + {fee} + + + + + ); } else { + // Default state - show Across V4 branding content = ( <> @@ -139,9 +141,7 @@ const ExpandableLabelSection: React.FC< > {content} - - {expanded && children} - + {children} ); @@ -159,7 +159,7 @@ const ButtonCore: React.FC<{ @@ -244,7 +244,8 @@ export const ConfirmationButton: React.FC = ({ }; }, [swapQuote, inputToken, outputToken, amount]); - const clickHandler = onConfirm; + // When notConnected, make button clickable so it can open wallet modal + const isButtonDisabled = state === "notConnected" ? false : buttonDisabled; // Render unified group driven by state const content = ( @@ -334,9 +335,9 @@ export const ConfirmationButton: React.FC = ({ state={state} label={buttonLabel} loading={buttonLoading} - disabled={buttonDisabled} + disabled={isButtonDisabled} fullHeight={state !== "readyToConfirm"} - onClick={clickHandler} + onClick={onConfirm} /> @@ -344,7 +345,7 @@ export const ConfirmationButton: React.FC = ({ return ( @@ -364,18 +365,16 @@ const ValidationText = styled.div` `; // Styled components -const Container = styled(motion.div)<{ state: BridgeButtonState }>` - background: ${({ state }) => - state === "validationError" - ? COLORS["grey-400-5"] - : "rgba(108, 249, 216, 0.1)"}; +const Container = styled(motion.div)<{ disabled: boolean }>` + background: ${({ disabled }) => + disabled ? COLORS["grey-400-5"] : "rgba(108, 249, 216, 0.1)"}; border-radius: 24px; display: flex; flex-direction: column; padding: 8px 12px 12px 12px; width: 100%; overflow: hidden; - gap: ${({ state }) => (state === "readyToConfirm" ? "8px" : "0")}; + gap: ${({ disabled }) => (disabled ? "0px" : "8px")}; `; const ExpandableLabelButton = styled.button` diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 22f9b7684..89514b294 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -12,6 +12,8 @@ import { useValidateSwapAndBridge } from "./useValidateSwapAndBridge"; import { BridgeButtonState } from "../components/ConfirmationButton"; import { useDebounce } from "@uidotdev/usehooks"; import { useDefaultRoute } from "./useDefaultRoute"; +import { useConnection } from "hooks"; +import { useHistory } from "react-router-dom"; export type UseSwapAndBridgeReturn = { inputToken: EnrichedToken | null; @@ -44,7 +46,7 @@ export type UseSwapAndBridgeReturn = { isConnected: boolean; isWrongNetwork: boolean; isSubmitting: boolean; - onConfirm: () => Promise; + onConfirm: () => Promise; }; export function useSwapAndBridge(): UseSwapAndBridgeReturn { @@ -55,6 +57,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const debouncedAmount = useDebounce(amount, 300); const defaultRoute = useDefaultRoute(); + const { connect } = useConnection(); + const history = useHistory(); useEffect(() => { if (defaultRoute.inputToken && defaultRoute.outputToken) { @@ -120,6 +124,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { amount, isAmountOrigin, inputToken, + outputToken, error ); @@ -132,9 +137,29 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { }, [swapQuote]); const onConfirm = useCallback(async () => { + // If not connected, open the wallet connection modal + if (!approvalAction.isConnected) { + connect({ trackSection: "bridgeForm" }); + return; + } + + // Otherwise, proceed with the transaction const txHash = await approvalAction.buttonActionHandler(); - return txHash as string; - }, [approvalAction]); + // Only navigate if we got a transaction hash (not empty string from wallet connection) + if (txHash) { + history.push( + `/bridge/${txHash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` + ); + } + }, [ + approvalAction, + connect, + history, + inputToken?.chainId, + inputToken?.symbol, + outputToken?.chainId, + outputToken?.symbol, + ]); // Button state logic const buttonState: BridgeButtonState = useMemo(() => { @@ -214,11 +239,11 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const buttonLabels: Record = { notConnected: "Connect Wallet", - awaitingTokenSelection: "Select a token", - awaitingAmountInput: "Enter an amount", + awaitingTokenSelection: "Confirm Swap", + awaitingAmountInput: "Confirm Swap", readyToConfirm: "Confirm Swap", submitting: "Confirming...", - wrongNetwork: "Switch network and confirm transaction", - loadingQuote: "Finalizing quote", + wrongNetwork: "Confirm Swap", + loadingQuote: "Finalizing quote...", validationError: "Confirm Swap", }; diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts index 94d1d645a..d69bd4a23 100644 --- a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -16,17 +16,31 @@ export function useValidateSwapAndBridge( amount: BigNumber | null, isAmountOrigin: boolean, inputToken: EnrichedToken | null, + outputToken: EnrichedToken | null, error: any ): ValidationResult { const validation = useMemo(() => { let errorType: AmountInputError | undefined = undefined; - // invalid amount (allow empty/no amount without error) - if (amount && amount.lte(0)) { + + // Check if input token is selected + if (!inputToken) { + errorType = AmountInputError.NO_INPUT_TOKEN_SELECTED; + } + // Check if output token is selected + else if (!outputToken) { + errorType = AmountInputError.NO_OUTPUT_TOKEN_SELECTED; + } + // Check if amount is entered + else if (!amount || amount.isZero()) { + errorType = AmountInputError.NO_AMOUNT_ENTERED; + } + // invalid amount + else if (amount.lte(0)) { errorType = AmountInputError.INVALID; } // balance check for origin-side inputs - if (!errorType && isAmountOrigin && inputToken?.balance) { - if (amount && amount.gt(inputToken.balance)) { + else if (isAmountOrigin && inputToken?.balance) { + if (amount.gt(inputToken.balance)) { errorType = AmountInputError.INSUFFICIENT_BALANCE; } } @@ -45,9 +59,10 @@ export function useValidateSwapAndBridge( errorFormatted: getValidationErrorText({ validationError: errorType, inputToken, + outputToken, }), }; - }, [amount, isAmountOrigin, inputToken, error]); + }, [amount, isAmountOrigin, inputToken, outputToken, error]); return validation; } @@ -55,12 +70,13 @@ export function useValidateSwapAndBridge( function getValidationErrorText(props: { validationError?: AmountInputError; inputToken: EnrichedToken | null; + outputToken: EnrichedToken | null; }): string | undefined { if (!props.validationError) { return; } return validationErrorTextMap[props.validationError]?.replace( "[INPUT_TOKEN]", - props.inputToken!.symbol + props.inputToken?.symbol ?? "" ); } diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index cc88dac3c..10b5e438b 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -1,9 +1,7 @@ import { LayoutV2 } from "components"; import styled from "@emotion/styled"; -import { useCallback } from "react"; import { InputForm } from "./components/InputForm"; import ConfirmationButton from "./components/ConfirmationButton"; -import { useHistory } from "react-router-dom"; import { useSwapAndBridge } from "./hooks/useSwapAndBridge"; export default function SwapAndBridge() { @@ -20,7 +18,6 @@ export default function SwapAndBridge() { isQuoteLoading, expectedInputAmount, expectedOutputAmount, - onConfirm, validationError, validationWarning, validationErrorFormatted, @@ -28,17 +25,8 @@ export default function SwapAndBridge() { buttonDisabled, buttonLoading, buttonLabel, + onConfirm, } = useSwapAndBridge(); - const history = useHistory(); - - // Handle confirmation (placeholder for now) - const handleConfirm = useCallback(async () => { - const txHash = await onConfirm(); - history.push( - `/bridge/${txHash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onConfirm, inputToken, outputToken]); return ( @@ -62,7 +50,7 @@ export default function SwapAndBridge() { amount={amount} swapQuote={swapQuote || null} isQuoteLoading={isQuoteLoading} - onConfirm={handleConfirm} + onConfirm={onConfirm} validationError={validationError} validationWarning={validationWarning} validationErrorFormatted={validationErrorFormatted} From 9448102ec790fa424c911e6a029b6a8d5b8164c3 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 15 Oct 2025 13:59:40 +0200 Subject: [PATCH 067/122] fixup Signed-off-by: Gerhard Steenkamp --- src/components/Modal/Modal.styles.ts | 4 ++-- src/components/Modal/Modal.tsx | 3 ++- .../SwapAndBridge/components/ChainTokenSelector/Modal.tsx | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 8992cbbe0..1ffc4b32b 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -184,9 +184,9 @@ export const ModalHeader = styled.div` width: 100%; `; -export const ModalContent = styled.div` +export const ModalContent = styled.div<{ noScroll?: boolean }>` flex: 1; - overflow: hidden; + overflow: ${({ noScroll }) => (noScroll ? "hidden" : "hidden scroll")}; padding: var(--padding-modal-content); min-height: 0; width: 100%; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 5f49f71c1..8bbbbb04f 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -39,7 +39,7 @@ export type ModalProps = { topYOffset?: number; "data-cy"?: string; bottomYOffset?: number; - + noScroll?: boolean; children?: React.ReactNode; footer?: React.ReactNode; titleBorder?: boolean; @@ -63,6 +63,7 @@ const Modal = ({ className, "data-cy": dataCy, titleBorder = false, + noScroll = false, }: ModalProps) => { const verticalLocation: ModalDirection | undefined = typeof _verticalLocation === "string" diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index cccf745d0..c1f4fc404 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -361,6 +361,7 @@ const MobileModal = ({ width={400} height={600} titleBorder + noScroll > Date: Wed, 15 Oct 2025 16:07:38 +0200 Subject: [PATCH 068/122] use bridge-and-swap route Signed-off-by: Gerhard Steenkamp --- src/Routes.tsx | 5 ++++- src/components/BreadcrumbV2/BreadcrumbV2.tsx | 7 +++++-- src/utils/amplitude.ts | 4 ++-- src/views/Bridge/hooks/useBridgeAction/factory.ts | 2 +- src/views/DepositStatus/components/Breadcrumb.tsx | 3 +++ src/views/SwapAndBridge/hooks/useSwapAndBridge.ts | 2 +- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Routes.tsx b/src/Routes.tsx index 99ef012b7..748f8e9df 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -145,7 +145,10 @@ const Routes: React.FC = () => { }} /> - + { const { ancestorRoutes, currentRoute } = useBreadcrumb(); - const definedAncestors = - onlyRootAncestor && ancestorRoutes.length > 0 + const definedAncestors = customAncestorRoutes + ? customAncestorRoutes + : onlyRootAncestor && ancestorRoutes.length > 0 ? [ancestorRoutes[0]] : ancestorRoutes; diff --git a/src/utils/amplitude.ts b/src/utils/amplitude.ts index 206fa5d55..5caf5c64c 100644 --- a/src/utils/amplitude.ts +++ b/src/utils/amplitude.ts @@ -70,8 +70,8 @@ export function getPageValue() { const path = getSanitizedPathname(); // Check if the path is a deposit status page. We know that the - // deposit status page will always have a path that starts with /bridge/0x{tx hash} - if (/\/bridge\/0x[0-9a-fA-F]+/.test(path)) { + // deposit status page will always have a path that starts with /bridge-and-swap/0x{tx hash} + if (/\/bridge-and-swap\/0x[0-9a-fA-F]+/.test(path)) { return "depositStatusPage"; } diff --git a/src/views/Bridge/hooks/useBridgeAction/factory.ts b/src/views/Bridge/hooks/useBridgeAction/factory.ts index c67fdd39c..0a6fd06e9 100644 --- a/src/views/Bridge/hooks/useBridgeAction/factory.ts +++ b/src/views/Bridge/hooks/useBridgeAction/factory.ts @@ -174,7 +174,7 @@ export function createBridgeActionHook(strategy: BridgeActionStrategy) { statusPageSearchParams.set("integrator", existingIntegrator); } history.push( - `/bridge/${txHash}?${statusPageSearchParams}`, + `/bridge-and-swap/${txHash}?${statusPageSearchParams}`, // This state is stored in session storage and therefore persist // after a refresh of the deposit status page. { fromBridgePagePayload } diff --git a/src/views/DepositStatus/components/Breadcrumb.tsx b/src/views/DepositStatus/components/Breadcrumb.tsx index fff83d47e..e4d4230d7 100644 --- a/src/views/DepositStatus/components/Breadcrumb.tsx +++ b/src/views/DepositStatus/components/Breadcrumb.tsx @@ -11,6 +11,9 @@ type Props = { export function Breadcrumb({ depositTxHash }: Props) { return ( {shortenString(depositTxHash, "..", 4)} diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 89514b294..be97ed3df 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -148,7 +148,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { // Only navigate if we got a transaction hash (not empty string from wallet connection) if (txHash) { history.push( - `/bridge/${txHash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` + `/bridge-and-swap/${txHash}?originChainId=${inputToken?.chainId}&destinationChainId=${outputToken?.chainId}&inputTokenSymbol=${inputToken?.symbol}&outputTokenSymbol=${outputToken?.symbol}&referrer=` ); } }, [ From 88bcfbd77b83fcebd2d1ade346001ad988d7a656 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 15 Oct 2025 16:12:18 +0200 Subject: [PATCH 069/122] redirect from old bridge route Signed-off-by: Gerhard Steenkamp --- src/Routes.tsx | 12 ++++++++++++ vercel.json | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Routes.tsx b/src/Routes.tsx index 748f8e9df..37c82718e 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -149,6 +149,18 @@ const Routes: React.FC = () => { path="/bridge-and-swap/:depositTxHash" component={DepositStatus} /> + + ( + + )} + /> Date: Wed, 15 Oct 2025 17:08:52 +0200 Subject: [PATCH 070/122] fix all chains token sorting Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 123 +++--------------- src/hooks/useEnrichedCrosschainBalances.ts | 47 ++++--- .../components/ChainTokenSelector/Modal.tsx | 58 ++++----- 3 files changed, 67 insertions(+), 161 deletions(-) diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 7f88fe624..644652e47 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -1,6 +1,4 @@ import { useQuery } from "@tanstack/react-query"; -import { getConfig } from "utils/config"; -import { getSwapChains } from "utils/getSwapChains"; import { useSwapTokens } from "./useSwapTokens"; export type LifiToken = { @@ -35,11 +33,8 @@ export default function useAvailableCrosschainRoutes( return useQuery({ queryKey: ["availableCrosschainRoutes", filterParams], queryFn: async () => { - // Get chains statically instead of from API - const swapChains = getSwapChains(); - - // 1) Build swap token map by chain - const swapTokensByChain = (swapTokensQuery.data || []).reduce( + // Build token map by chain from API tokens + const tokensByChain = (swapTokensQuery.data || []).reduce( (acc, token) => { const mapped: LifiToken = { chainId: token.chainId, @@ -61,113 +56,27 @@ export default function useAvailableCrosschainRoutes( {} as Record> ); - // 2) Build bridge token map by origin chain from generated routes - const config = getConfig(); - const enabledRoutes = config.getEnabledRoutes(); - const bridgeOriginChains = Array.from( - new Set(enabledRoutes.map((r) => r.fromChain)) - ); - - const bridgeTokensByChain = bridgeOriginChains.reduce( - (acc, fromChainId) => { - const reachable = config.filterReachableTokens(fromChainId); - const lifiTokens: LifiToken[] = reachable.map((t) => ({ - chainId: fromChainId, - address: t.address, - name: t.name, - symbol: t.displaySymbol || t.symbol, - decimals: t.decimals, - logoURI: t.logoURI || "", - // We do not have price data from the routes; default to 0 - priceUSD: "0", - coinKey: t.symbol, - routeSource: "bridge", - })); - acc[fromChainId] = lifiTokens; - return acc; - }, - {} as Record> - ); - - // 3) Combine swap and bridge tokens, deduplicating by address - const chainIdsInSwap = new Set(swapChains.map((c) => c.chainId)); - const chainIdsInBridge = new Set( - Object.keys(bridgeTokensByChain).map(Number) - ); - const chainIds = Array.from( - new Set([...chainIdsInSwap, ...chainIdsInBridge]) - ); - - const combinedByChain: Record> = {}; - for (const chainId of chainIds) { - const swapTokens = swapTokensByChain[chainId] || []; - const bridgeTokens = bridgeTokensByChain[chainId] || []; - - // Deduplicate by address (case-insensitive), preferring swap tokens for price data - const tokenMap = new Map(); - - // Add bridge tokens first - bridgeTokens.forEach((token) => { - tokenMap.set(token.address.toLowerCase(), token); - }); - - // Add swap tokens, overriding bridge tokens if same address (swap has price data) - swapTokens.forEach((token) => { - tokenMap.set(token.address.toLowerCase(), token); - }); - - combinedByChain[chainId] = Array.from(tokenMap.values()); - } - - // 4) Apply route filtering if filterParams are provided + // Apply route filtering if filterParams are provided if (filterParams?.inputToken || filterParams?.outputToken) { - const config = getConfig(); const otherToken = filterParams.inputToken || filterParams.outputToken; - const isFilteringForInput = !!filterParams.inputToken; - - // Mark tokens as reachable/unreachable based on route validation - for (const chainId of Object.keys(combinedByChain)) { - combinedByChain[Number(chainId)] = combinedByChain[ - Number(chainId) - ].map((token) => { - const fromChain = isFilteringForInput - ? Number(chainId) - : otherToken!.chainId; - const toChain = isFilteringForInput - ? otherToken!.chainId - : Number(chainId); - const fromTokenSymbol = isFilteringForInput - ? token.symbol - : otherToken!.symbol; - const toTokenSymbol = isFilteringForInput - ? otherToken!.symbol - : token.symbol; - let isReachable = false; - - // For same chain, not reachable (no swaps allowed on same chain) - if (fromChain === toChain) { - isReachable = false; - } else { - // For different chains, check if there's an explicit bridge route - const bridgeRoutes = config.filterRoutes({ - fromChain, - toChain, - fromTokenSymbol, - toTokenSymbol, - }); - isReachable = bridgeRoutes.length > 0; + // Mark tokens as unreachable if they're on the same chain as the filter token + for (const chainId of Object.keys(tokensByChain)) { + tokensByChain[Number(chainId)] = tokensByChain[Number(chainId)].map( + (token) => { + // For same chain, not reachable (no swaps allowed on same chain) + const isReachable = Number(chainId) !== otherToken!.chainId; + + return { + ...token, + isReachable, + }; } - - return { - ...token, - isReachable, - }; - }); + ); } } - return combinedByChain; + return tokensByChain; }, enabled: swapTokensQuery.isSuccess, }); diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts index b0bed59dd..0fe06e843 100644 --- a/src/hooks/useEnrichedCrosschainBalances.ts +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -23,30 +23,29 @@ export function useEnrichedCrosschainBalances() { ); const tokens = availableCrosschainRoutes.data![Number(chainId)]; - const enrichedTokens = tokens - .map((t) => { - const balance = balancesForChain?.balances.find((b) => - compareAddressesSimple(b.address, t.address) - ); - return { - ...t, - balance: balance?.balance - ? BigNumber.from(balance.balance) - : BigNumber.from(0), - balanceUsd: - balance?.balance && t - ? Number( - utils.formatUnits( - BigNumber.from(balance.balance), - t.decimals - ) - ) * Number(t.priceUSD) - : 0, - }; - }) - // TODO: consider removing - // Filter out tokens that don't have a logoURI - .filter((t) => t.logoURI !== undefined); + const enrichedTokens = tokens.map((t) => { + const balance = balancesForChain?.balances.find((b) => + compareAddressesSimple(b.address, t.address) + ); + return { + ...t, + balance: balance?.balance + ? BigNumber.from(balance.balance) + : BigNumber.from(0), + balanceUsd: + balance?.balance && t + ? Number( + utils.formatUnits( + BigNumber.from(balance.balance), + t.decimals + ) + ) * Number(t.priceUSD) + : 0, + }; + }); + // // TODO: consider removing + // // Filter out tokens that don't have a logoURI + // .filter((t) => t.logoURI !== undefined); // Sort high to low balanceUsd const sortedByBalance = enrichedTokens.sort( diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index c1f4fc404..7cfcf662d 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -78,21 +78,9 @@ export default function ChainTokenSelectorModal({ onSelect, otherToken, }: Props) { - const balances = useEnrichedCrosschainBalances(); + const crossChainRoutes = useEnrichedCrosschainBalances(); const { isMobile } = useCurrentBreakpoint(); - const crossChainRoutes = useAvailableCrosschainRoutes( - otherToken - ? { - [isOriginToken ? "outputToken" : "inputToken"]: { - chainId: otherToken.chainId, - address: otherToken.address, - symbol: otherToken.symbol, - }, - } - : undefined - ); - const [selectedChain, setSelectedChain] = useState( popularChains[0] ); @@ -110,16 +98,16 @@ export default function ChainTokenSelectorModal({ }, [displayModal]); const displayedTokens = useMemo(() => { - let tokens = selectedChain ? (balances[selectedChain] ?? []) : []; + let tokens = selectedChain ? (crossChainRoutes[selectedChain] ?? []) : []; if (tokens.length === 0 && selectedChain === null) { - tokens = Object.values(balances).flatMap((t) => t); + tokens = Object.values(crossChainRoutes).flatMap((t) => t); } // Enrich tokens with reachability information from the hook const enrichedTokens = tokens.map((token) => { // Find the corresponding token in crossChainRoutes to check isReachable - const routeToken = crossChainRoutes.data?.[token.chainId]?.find( + const routeToken = crossChainRoutes?.[token.chainId]?.find( (rt) => rt.address.toLowerCase() === token.address.toLowerCase() ); @@ -151,7 +139,8 @@ export default function ChainTokenSelectorModal({ if (tokenSearch === "") { return true; } - if (t.chainId !== selectedChain) { + // When a specific chain is selected, only show tokens from that chain + if (selectedChain !== null && t.chainId !== selectedChain) { return false; } const keywords = [ @@ -164,14 +153,6 @@ export default function ChainTokenSelectorModal({ ); }); - // Separate popular tokens from all tokens - const popularTokensList = filteredTokens.filter((token) => - popularTokens.includes(token.symbol) - ); - const allTokensList = filteredTokens.filter( - (token) => !popularTokens.includes(token.symbol) - ); - // Sort function that prioritizes tokens with balance, then by balance amount, then alphabetically const sortTokens = (tokens: EnrichedToken[]) => { return tokens.sort((a, b) => { @@ -204,18 +185,35 @@ export default function ChainTokenSelectorModal({ }); }; + // When "all chains" is selected, don't separate popular tokens + if (selectedChain === null) { + const sortedAllTokens = sortTokens(filteredTokens); + return { + popular: [], // No popular section for all chains + all: sortedAllTokens, + }; + } + + // When a specific chain is selected, separate popular tokens from all tokens + const popularTokensList = filteredTokens.filter((token) => + popularTokens.includes(token.symbol) + ); + const allTokensList = filteredTokens.filter( + (token) => !popularTokens.includes(token.symbol) + ); + // Sort both sections const sortedPopularTokens = sortTokens(popularTokensList); const sortedAllTokens = sortTokens(allTokensList); return { - popular: sortedPopularTokens.slice(0, 50), // Limit to 50 popular tokens - all: sortedAllTokens.slice(0, 50), // Limit to 50 all tokens + popular: sortedPopularTokens, + all: sortedAllTokens, }; - }, [selectedChain, balances, tokenSearch, crossChainRoutes.data]); + }, [selectedChain, crossChainRoutes, tokenSearch]); const displayedChains = useMemo(() => { - const chainsWithDisabledState = Object.keys(crossChainRoutes.data || {}) + const chainsWithDisabledState = Object.keys(crossChainRoutes || {}) .map((chainId) => getChainInfo(Number(chainId))) .filter((chainInfo) => { // TODO: check why we are filtering out Boba? @@ -260,7 +258,7 @@ export default function ChainTokenSelectorModal({ popular: popularChainsData, all: allChainsData, } as DisplayedChains; - }, [chainSearch, crossChainRoutes.data, otherToken]); + }, [chainSearch, crossChainRoutes, otherToken]); return isMobile ? ( Date: Fri, 17 Oct 2025 18:16:33 +0200 Subject: [PATCH 071/122] remove unreachable logic Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 21 ---------- .../components/ChainTokenSelector/Modal.tsx | 39 ++----------------- 2 files changed, 4 insertions(+), 56 deletions(-) diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 644652e47..434ff72fe 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -11,7 +11,6 @@ export type LifiToken = { coinKey: string; logoURI: string; routeSource: "bridge" | "swap"; - isReachable?: boolean; // Added to mark if token is reachable from the other token }; export type TokenInfo = { @@ -56,26 +55,6 @@ export default function useAvailableCrosschainRoutes( {} as Record> ); - // Apply route filtering if filterParams are provided - if (filterParams?.inputToken || filterParams?.outputToken) { - const otherToken = filterParams.inputToken || filterParams.outputToken; - - // Mark tokens as unreachable if they're on the same chain as the filter token - for (const chainId of Object.keys(tokensByChain)) { - tokensByChain[Number(chainId)] = tokensByChain[Number(chainId)].map( - (token) => { - // For same chain, not reachable (no swaps allowed on same chain) - const isReachable = Number(chainId) !== otherToken!.chainId; - - return { - ...token, - isReachable, - }; - } - ); - } - } - return tokensByChain; }, enabled: swapTokensQuery.isSuccess, diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 7cfcf662d..750058cf5 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -54,7 +54,6 @@ type DisplayedChains = { export type EnrichedToken = LifiToken & { balance: BigNumber; balanceUsd: number; - isReachable?: boolean; routeSource: "bridge" | "swap"; }; @@ -104,32 +103,15 @@ export default function ChainTokenSelectorModal({ tokens = Object.values(crossChainRoutes).flatMap((t) => t); } - // Enrich tokens with reachability information from the hook + // Enrich tokens with route source information const enrichedTokens = tokens.map((token) => { - // Find the corresponding token in crossChainRoutes to check isReachable + // Find the corresponding token in crossChainRoutes to get route source const routeToken = crossChainRoutes?.[token.chainId]?.find( (rt) => rt.address.toLowerCase() === token.address.toLowerCase() ); - // Determine if token should be disabled based on new requirements: - // Only disable tokens if the token is NOT a swap token AND it is not reachable via a bridge route - let shouldDisable = false; - if (routeToken) { - // If it's a swap token, never disable it - if (routeToken.routeSource === "swap") { - shouldDisable = false; - } else { - // If it's not a swap token, disable it only if it's not reachable via bridge - shouldDisable = routeToken.isReachable === false; - } - } else { - // If no route token found, disable it (not available for any routes) - shouldDisable = true; - } - return { ...token, - isReachable: !shouldDisable, routeSource: routeToken?.routeSource || "bridge", // Default to bridge if not found }; }); @@ -156,15 +138,7 @@ export default function ChainTokenSelectorModal({ // Sort function that prioritizes tokens with balance, then by balance amount, then alphabetically const sortTokens = (tokens: EnrichedToken[]) => { return tokens.sort((a, b) => { - // First, sort by disabled status - disabled tokens go to bottom - const aDisabled = a.isReachable === false; - const bDisabled = b.isReachable === false; - - if (aDisabled !== bDisabled) { - return aDisabled ? 1 : -1; - } - - // Then sort by balance - tokens with balance go to top + // Sort by balance - tokens with balance go to top const aHasBalance = a.balance.gt(0) && a.balanceUsd > 0.01; const bHasBalance = b.balance.gt(0) && b.balanceUsd > 0.01; @@ -758,14 +732,9 @@ const TokenEntry = ({ onClick: () => void; }) => { const hasBalance = token.balance.gt(0) && token.balanceUsd > 0.01; - const isDisabled = token.isReachable === false; return ( - + {token.name} From c1be7a99a7d952c8d48ab9eea461a380ea646ffe Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 17 Oct 2025 18:21:36 +0200 Subject: [PATCH 072/122] hide summary when expanded Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index c6005eb56..63bf575a4 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -95,17 +95,19 @@ const ExpandableLabelSection: React.FC< Fast & Secure - - - - {fee} - - - - - + {!expanded && ( + + + + {fee} + + + + + + )} ); @@ -218,7 +220,6 @@ export const ConfirmationButton: React.FC = ({ }; } - // Get fees from the top-level fees object (new structure) const bridgeFeesUsd = swapQuote.fees.relayerTotal.amountUsd; const gasFeeUsd = ( Number(swapQuote.fees.originGas.amountUsd) + From 2c48ed57f819cfb103cc581f7c866f4938ae17f1 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 17 Oct 2025 18:54:42 +0200 Subject: [PATCH 073/122] fix animation Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 102 +++++++++--------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 63bf575a4..bf942d684 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -251,7 +251,7 @@ export const ConfirmationButton: React.FC = ({ // Render unified group driven by state const content = ( <> - + = ({ validationErrorFormatted={validationErrorFormatted} hasQuote={!!swapQuote} > - {expanded && state === "readyToConfirm" ? ( - - - - - Route - - - - {displayValues.route} - - - - - - {displayValues.estimatedTime} - - - - - Net Fee - - - - - {displayValues.netFee} - - + + + + + Route + + + + {displayValues.route} + + + + + + {displayValues.estimatedTime} + + + + + Net Fee + + + + + {displayValues.netFee} + + + + Bridge Fee + + {displayValues.bridgeFee} + + + + Gas Fee + {displayValues.gasFee} + + {isDefined(displayValues.swapFee) && ( - Bridge Fee + Swap Fee - {displayValues.bridgeFee} + {displayValues.swapFee} - - Gas Fee - - {displayValues.gasFee} - - - {isDefined(displayValues.swapFee) && ( - - Swap Fee - - {displayValues.swapFee} - - - )} - - - ) : null} + )} + + From a1df6cc03cb974fc3b0123cff7529200b77a9db4 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 17 Oct 2025 21:00:12 +0200 Subject: [PATCH 074/122] fixup Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 274 ++++++++---------- src/views/SwapAndBridge/index.tsx | 2 +- 2 files changed, 125 insertions(+), 151 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index bf942d684..bb0ef6f53 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -127,25 +127,45 @@ const ExpandableLabelSection: React.FC< } return ( - - + - - {content} - - {children} - - + {content} + + + {expanded && ( + + {children} + + )} + + ); }; @@ -165,23 +185,13 @@ const ButtonCore: React.FC<{ loading={loading} fullHeight={fullHeight} > - - - - {loading && } - {state === "notConnected" && ( - - )} - {label} - - - + + {loading && } + {state === "notConnected" && ( + + )} + {label} + ); @@ -250,105 +260,80 @@ export const ConfirmationButton: React.FC = ({ // Render unified group driven by state const content = ( - <> - - - setExpanded((e) => !e)} - visible={true} - state={state} - validationError={validationError} - validationWarning={validationWarning} - validationErrorFormatted={validationErrorFormatted} - hasQuote={!!swapQuote} - > - - - - - Route - - - - {displayValues.route} - - - - - - {displayValues.estimatedTime} - - - - - Net Fee - - - - - {displayValues.netFee} - - - - Bridge Fee - - {displayValues.bridgeFee} - - - - Gas Fee - {displayValues.gasFee} - - {isDefined(displayValues.swapFee) && ( - - Swap Fee - - {displayValues.swapFee} - - - )} - - - - - - - - - + + setExpanded((e) => !e)} + visible={true} + state={state} + validationError={validationError} + validationWarning={validationWarning} + validationErrorFormatted={validationErrorFormatted} + hasQuote={!!swapQuote} + > + + + + + Route + + + + {displayValues.route} + + + + + + {displayValues.estimatedTime} + + + + + Net Fee + + + + + {displayValues.netFee} + + + + Bridge Fee + {displayValues.bridgeFee} + + + Gas Fee + {displayValues.gasFee} + + {isDefined(displayValues.swapFee) && ( + + Swap Fee + {displayValues.swapFee} + + )} + + + + + ); - return ( - - {content} - - ); + return {content}; }; const ValidationText = styled.div` @@ -371,7 +356,6 @@ const Container = styled(motion.div)<{ disabled: boolean }>` padding: 8px 12px 12px 12px; width: 100%; overflow: hidden; - gap: ${({ disabled }) => (disabled ? "0px" : "8px")}; `; const ExpandableLabelButton = styled.button` @@ -380,6 +364,7 @@ const ExpandableLabelButton = styled.button` justify-content: space-between; width: 100%; padding: 8px; + padding-bottom: 16px; background: transparent; border: none; cursor: pointer; @@ -433,22 +418,13 @@ const StyledChevronDown = styled(ChevronDownIcon)<{ expanded: boolean }>` width: 20px; height: 20px; margin-left: 12px; - transition: transform 0.3s ease; + transition: transform 0.2s ease; cursor: pointer; color: #e0f3ff; transform: ${({ expanded }) => expanded ? "rotate(180deg)" : "rotate(0deg)"}; `; -const ExpandableContent = styled.div<{ expanded: boolean }>` - overflow: hidden; - transition: - max-height 0.3s ease, - margin-top 0.3s ease; - max-height: ${({ expanded }) => (expanded ? "500px" : "0")}; - margin-top: ${({ expanded }) => (expanded ? "8px" : "0")}; -`; - const StyledButton = styled.button<{ aqua?: boolean; loading?: boolean; @@ -459,7 +435,11 @@ const StyledButton = styled.button<{ border-radius: 12px; font-weight: 600; font-size: 16px; - transition: all 0.3s ease; + transition: + background 0.3s ease, + color 0.3s ease, + box-shadow 0.3s ease, + opacity 0.3s ease; border: none; cursor: pointer; @@ -509,10 +489,6 @@ const StyledLoadingIcon = styled(LoadingIcon)` } `; -const ButtonContainer = styled.div<{ expanded: boolean }>` - flex: 0 0 auto; -`; - const ExpandedDetails = styled.div` color: #e0f3ff; font-size: 14px; @@ -559,5 +535,3 @@ const FeeBreakdownLabel = styled.span` const FeeBreakdownValue = styled.span` color: #e0f3ff; `; - -export default ConfirmationButton; diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 10b5e438b..e1e4d1eba 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -1,7 +1,7 @@ import { LayoutV2 } from "components"; import styled from "@emotion/styled"; import { InputForm } from "./components/InputForm"; -import ConfirmationButton from "./components/ConfirmationButton"; +import { ConfirmationButton } from "./components/ConfirmationButton"; import { useSwapAndBridge } from "./hooks/useSwapAndBridge"; export default function SwapAndBridge() { From 065a1b025074686df62de77442e6c7f68bf18a80 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 20 Oct 2025 13:01:57 +0200 Subject: [PATCH 075/122] fix icon, tab index, scroll behaviour Signed-off-by: Gerhard Steenkamp --- src/assets/icons/checkmark-circle-filled.svg | 3 + src/components/Modal/Modal.styles.ts | 2 +- src/components/Modal/Modal.tsx | 2 +- .../components/ChainTokenSelector/Modal.tsx | 55 ++++++++++++++----- .../ChainTokenSelector/Searchbar.tsx | 42 +++++++------- .../components/ConfirmationButton.tsx | 4 +- .../SwapAndBridge/components/InputForm.tsx | 19 ++++++- 7 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 src/assets/icons/checkmark-circle-filled.svg diff --git a/src/assets/icons/checkmark-circle-filled.svg b/src/assets/icons/checkmark-circle-filled.svg new file mode 100644 index 000000000..36a6867ec --- /dev/null +++ b/src/assets/icons/checkmark-circle-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 1ffc4b32b..c3d39c424 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -186,7 +186,7 @@ export const ModalHeader = styled.div` export const ModalContent = styled.div<{ noScroll?: boolean }>` flex: 1; - overflow: ${({ noScroll }) => (noScroll ? "hidden" : "hidden scroll")}; + overflow: ${({ noScroll }) => (noScroll ? "clip" : "hidden scroll")}; padding: var(--padding-modal-content); min-height: 0; width: 100%; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 8bbbbb04f..39658a23e 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -178,7 +178,7 @@ const Modal = ({ {titleBorder && } - {children} + {children} {footer && {footer}} diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 750058cf5..dcd40fda1 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -2,9 +2,7 @@ import Modal from "components/Modal"; import styled from "@emotion/styled"; import { Searchbar } from "./Searchbar"; import TokenMask from "assets/mask/token-mask-corner.svg"; -import useAvailableCrosschainRoutes, { - LifiToken, -} from "hooks/useAvailableCrosschainRoutes"; +import { LifiToken } from "hooks/useAvailableCrosschainRoutes"; import { CHAIN_IDs, ChainInfo, @@ -15,8 +13,8 @@ import { parseUnits, TOKEN_SYMBOLS_MAP, } from "utils"; -import { useMemo, useState, useEffect } from "react"; -import { ReactComponent as CheckmarkCircle } from "assets/icons/checkmark-circle.svg"; +import { useMemo, useState, useEffect, useRef } from "react"; +import { ReactComponent as CheckmarkCircleFilled } from "assets/icons/checkmark-circle-filled.svg"; import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; import { ReactComponent as SearchResults } from "assets/icons/search_results.svg"; import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; @@ -391,6 +389,7 @@ const DesktopModal = ({ width={1100} height={800} titleBorder + noScroll > void; onModalClose: () => void; }) => { + const chainSearchInputRef = useRef(null); + const tokenSearchInputRef = useRef(null); + + // Focus chain search input when modal opens or when navigating back to chain step + useEffect(() => { + if (mobileStep === "chain") { + chainSearchInputRef.current?.focus(); + } else if (mobileStep === "token") { + tokenSearchInputRef.current?.focus(); + } + }, [mobileStep]); + return ( {mobileStep === "chain" ? ( // Step 1: Chain Selection void; onModalClose: () => void; }) => { + const chainSearchInputRef = useRef(null); + const tokenSearchInputRef = useRef(null); useHotkeys("esc", () => onModalClose()); + + // Focus chain search input when component mounts + useEffect(() => { + chainSearchInputRef.current?.focus(); + }, []); + + function handleSelectChain(chainId: number | null): void { + onChainSelect(chainId); + tokenSearchInputRef.current?.focus(); + } + return ( onChainSelect(null)} + onClick={() => handleSelectChain(null)} /> {/* Popular Chains Section */} @@ -594,10 +620,10 @@ const DesktopLayout = ({ {displayedChains.popular.map(({ chainId, isDisabled }) => ( onChainSelect(Number(chainId))} + onClick={() => handleSelectChain(chainId)} /> ))} @@ -610,10 +636,10 @@ const DesktopLayout = ({ {displayedChains.all.map(({ chainId, isDisabled }) => ( onChainSelect(Number(chainId))} + onClick={() => handleSelectChain(chainId)} /> ))} @@ -629,6 +655,7 @@ const DesktopLayout = ({ inputProps={{ tabIndex: 3, }} + ref={tokenSearchInputRef} searchTopic="Token" search={tokenSearch} setSearch={setTokenSearch} @@ -800,9 +827,7 @@ const SearchBarStyled = styled(Searchbar)` const TokenItemImageWrapper = styled.div` width: 32px; height: 32px; - flex-shrink: 0; - position: relative; `; @@ -1014,7 +1039,7 @@ const ChainItemName = styled.div` line-height: 130%; /* 20.8px */ `; -const ChainItemCheckmark = styled(CheckmarkCircle)` +const ChainItemCheckmark = styled(CheckmarkCircleFilled)` width: 20px; height: 20px; margin-left: auto; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx index 570aedcd0..d9ab65d0d 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Searchbar.tsx @@ -12,28 +12,26 @@ type Props = { inputProps?: React.ComponentPropsWithoutRef<"input">; }; -export const Searchbar = ({ - searchTopic, - search, - setSearch, - className, - inputProps, -}: Props) => { - return ( - - - setSearch(e.target.value)} - {...inputProps} - /> - {search ? setSearch("")} /> :
} - - ); -}; +export const Searchbar = React.forwardRef( + ({ searchTopic, search, setSearch, className, inputProps }, ref) => { + Searchbar.displayName = "Searchbar"; + return ( + + + setSearch(e.target.value)} + {...inputProps} + /> + {search ? setSearch("")} /> :
} + + ); + } +); const Wrapper = styled.div` display: flex; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index bb0ef6f53..b38e59c8b 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -260,7 +260,7 @@ export const ConfirmationButton: React.FC = ({ // Render unified group driven by state const content = ( - + <> = ({ fullHeight={state !== "readyToConfirm"} onClick={onConfirm} /> - + ); return {content}; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index ead0da39a..0d4e09ab1 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -3,7 +3,7 @@ import SelectorButton from "./ChainTokenSelector/SelectorButton"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; import { BalanceSelector } from "./BalanceSelector"; import styled from "@emotion/styled"; -import { useCallback } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { BigNumber } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; import { ReactComponent as ArrowDown } from "assets/icons/arrow-down.svg"; @@ -114,6 +114,9 @@ const TokenInput = ({ disabled?: boolean; otherToken?: EnrichedToken | null; }) => { + const amountInputRef = useRef(null); + const hasAutoFocusedRef = useRef(false); + const { amountString, unit, @@ -134,6 +137,19 @@ const TokenInput = ({ return Boolean(shouldUpdate && isUpdateLoading); })(); + useEffect(() => { + // Focus origin token amount input when it first becomes enabled + if ( + isOrigin && + !inputDisabled && + !hasAutoFocusedRef.current && + amountInputRef.current + ) { + amountInputRef.current.focus(); + hasAutoFocusedRef.current = true; + } + }, [isOrigin, inputDisabled]); + const formattedConvertedAmount = (() => { if (!convertedAmount) return "0.00"; if (unit === "token") { @@ -149,6 +165,7 @@ const TokenInput = ({ {isOrigin ? "From" : "To"} handleInputChange(e.target.value)} From a1e1af8c3cefe5e802c38bcc4e3d0d1a6a44ea73 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 20 Oct 2025 21:01:21 +0200 Subject: [PATCH 076/122] set fallback token image. add api error component Signed-off-by: Gerhard Steenkamp --- src/assets/token-logos/fallback.svg | 31 +++++++++ src/components/AmountInput/AmountInput.tsx | 19 ++++-- .../DepositsTable/cells/AssetCell.tsx | 10 ++- .../DepositsTable/cells/RewardsCell.tsx | 3 +- src/components/TokenImage/TokenImage.tsx | 26 ++++++++ src/components/TokenImage/index.ts | 1 + src/components/index.ts | 1 + src/hooks/useSwapQuote.ts | 3 +- src/utils/errors.ts | 64 +++++++++++++++++++ src/views/Bridge/components/TokenFee.tsx | 8 +-- src/views/Bridge/components/TokenSelector.tsx | 8 +-- .../Rewards/components/ChainLogoOverlap.tsx | 10 ++- .../Rewards/components/OverviewSection.tsx | 4 +- .../Rewards/components/RewardProgramCard.tsx | 4 +- .../components/ChainTokenSelector/Modal.tsx | 6 +- .../ChainTokenSelector/SelectorButton.tsx | 9 +-- .../SwapAndBridge/components/InputForm.tsx | 4 ++ .../SwapAndBridge/components/QuoteWarning.tsx | 54 ++++++++++++++++ .../SwapAndBridge/hooks/useSwapAndBridge.ts | 10 +++ .../hooks/useValidateSwapAndBridge.ts | 12 +++- src/views/SwapAndBridge/index.tsx | 2 + 21 files changed, 255 insertions(+), 34 deletions(-) create mode 100644 src/assets/token-logos/fallback.svg create mode 100644 src/components/TokenImage/TokenImage.tsx create mode 100644 src/components/TokenImage/index.ts create mode 100644 src/views/SwapAndBridge/components/QuoteWarning.tsx diff --git a/src/assets/token-logos/fallback.svg b/src/assets/token-logos/fallback.svg new file mode 100644 index 000000000..82cde45e5 --- /dev/null +++ b/src/assets/token-logos/fallback.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/AmountInput/AmountInput.tsx b/src/components/AmountInput/AmountInput.tsx index 7c45b8e71..a424345e3 100644 --- a/src/components/AmountInput/AmountInput.tsx +++ b/src/components/AmountInput/AmountInput.tsx @@ -7,6 +7,7 @@ import { IconPair } from "components/IconPair"; import { Text } from "components/Text"; import { Tooltip } from "components/Tooltip"; import { Input, InputGroup } from "components/Input"; +import { TokenImage } from "components/TokenImage"; import { useTokenConversion } from "hooks/useTokenConversion"; import { QUERIESV2, @@ -81,13 +82,23 @@ export function AmountInput({ token.logoURIs?.length === 2 ? ( } - RightIcon={} + LeftIcon={ + + } + RightIcon={ + + } iconSize={16} /> ) : ( - + ) ) : null} + } RightIcon={ - {`${rightTokenSymbol} + } iconSize={24} /> @@ -66,7 +70,7 @@ const StyledAssetCell = styled(BaseCell)` } `; -const TokenIconImg = styled.img` +const TokenIconImg = styled(TokenImage)` width: 32px; height: 32px; diff --git a/src/components/DepositsTable/cells/RewardsCell.tsx b/src/components/DepositsTable/cells/RewardsCell.tsx index 7904f21cc..acabe9c19 100644 --- a/src/components/DepositsTable/cells/RewardsCell.tsx +++ b/src/components/DepositsTable/cells/RewardsCell.tsx @@ -1,6 +1,7 @@ import styled from "@emotion/styled"; import { Text } from "components/Text"; +import { TokenImage } from "components/TokenImage"; import { Deposit } from "hooks/useDeposits"; import { @@ -22,7 +23,7 @@ export function RewardsCell({ deposit, width }: Props) { {deposit.rewards && rewardToken ? ( <> - {rewardToken.symbol} + {formatUnitsWithMaxFractions( deposit.rewards.amount, diff --git a/src/components/TokenImage/TokenImage.tsx b/src/components/TokenImage/TokenImage.tsx new file mode 100644 index 000000000..e4b5f6927 --- /dev/null +++ b/src/components/TokenImage/TokenImage.tsx @@ -0,0 +1,26 @@ +import React, { useState, ImgHTMLAttributes } from "react"; +import fallbackLogo from "assets/token-logos/fallback.svg"; + +type TokenImageProps = ImgHTMLAttributes & { + src?: string; + alt: string; +}; + +/** + * TokenImage component that handles missing or broken token logo URLs + * Falls back to a default logo if the URL is missing or the image fails to load + */ +export function TokenImage({ src, alt, ...props }: TokenImageProps) { + const [imageError, setImageError] = useState(false); + + const imageSrc = !src || imageError ? fallbackLogo : src; + + return ( + {alt} setImageError(true)} + /> + ); +} diff --git a/src/components/TokenImage/index.ts b/src/components/TokenImage/index.ts new file mode 100644 index 000000000..85c1399a8 --- /dev/null +++ b/src/components/TokenImage/index.ts @@ -0,0 +1 @@ +export * from "./TokenImage"; diff --git a/src/components/index.ts b/src/components/index.ts index 9d8d74ed6..3d90df9f8 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -16,3 +16,4 @@ export * from "./Button"; export * from "./Badge"; export * from "./ExternalLink"; export * from "./ErrorBoundary"; +export * from "./TokenImage"; diff --git a/src/hooks/useSwapQuote.ts b/src/hooks/useSwapQuote.ts index 63e5e0464..03490b2dc 100644 --- a/src/hooks/useSwapQuote.ts +++ b/src/hooks/useSwapQuote.ts @@ -63,7 +63,8 @@ export function useSwapQuoteQuery(params: SwapQuoteQueryKeyParams) { }); }, enabled: !!params.swapTokenSymbol, - refetchInterval: 5_000, + retry: 3, + refetchInterval: 10_000, }); } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 5e38607b7..c2628323e 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,3 +1,4 @@ +import axios, { AxiosError } from "axios"; import { ChainId } from "./constants"; export class UnsupportedChainIdError extends Error { @@ -58,3 +59,66 @@ export class InsufficientBalanceError extends Error { this.message = "Insufficient balance."; } } + +export type AcrossApiErrorResponse = { + code?: string; + message?: string; +}; + +export function getQuoteWarningMessage(error: Error | null): string | null { + if (!error || !axios.isAxiosError(error)) { + return null; + } + + const axiosError = error as AxiosError; + + const errorData = axiosError.response?.data; + if (!errorData?.code) { + return null; + } + + const code = errorData.code; + const message = errorData.message; + + // Upstream swap provider errors - show user-friendly messages + switch (code) { + case "SWAP_LIQUIDITY_INSUFFICIENT": + return "Insufficient liquidity available for this swap. Try a smaller amount or different tokens."; + + case "SWAP_QUOTE_UNAVAILABLE": + return message?.includes("No possible route") + ? "No route found for this token pair. Try selecting different tokens." + : "Unable to get a quote at this time. Please try again."; + + case "SWAP_TYPE_NOT_GUARANTEED": + return "This trade type cannot be guaranteed on this route. Try a different amount or token pair."; + + case "AMOUNT_TOO_LOW": + return "Amount is too low to cover bridge and swap fees. Try increasing the amount."; + + case "AMOUNT_TOO_HIGH": + return "Amount exceeds the maximum deposit limit. Try a smaller amount."; + + case "ROUTE_NOT_ENABLED": + return "This route is currently unavailable. Try different chains or tokens."; + + case "INVALID_PARAM": + // Show the specific message for invalid params if available + if (message) { + return message; + } + return "Invalid parameters. Please check your input and try again."; + + // Upstream service errors - be more generic + case "UPSTREAM_HTTP_ERROR": + case "UPSTREAM_RPC_ERROR": + case "UPSTREAM_GATEWAY_TIMEOUT": + case "UNEXPECTED_ERROR": + case "SIMULATION_ERROR": + case "ABI_ENCODING_ERROR": + case "INVALID_METHOD": + case "MISSING_PARAM": + default: + return "Oops, something went wrong. Please try again."; + } +} diff --git a/src/views/Bridge/components/TokenFee.tsx b/src/views/Bridge/components/TokenFee.tsx index 837f0cc09..c29c27f61 100644 --- a/src/views/Bridge/components/TokenFee.tsx +++ b/src/views/Bridge/components/TokenFee.tsx @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react"; import { Text, TextColor } from "components/Text"; -import { LoadingSkeleton } from "components"; +import { LoadingSkeleton, TokenImage } from "components"; import { formatUnitsWithMaxFractions, getExplorerLinkForToken, @@ -73,13 +73,13 @@ const TokenFee = ({ exit={{ y: "-100%" }} transition={{ duration: 0.2 }} > - + )} ) : ( - + )} ); @@ -96,7 +96,7 @@ const Wrapper = styled.div<{ invertDirection?: boolean }>` gap: 8px; `; -const TokenSymbol = styled.img` +const TokenSymbol = styled(TokenImage)` width: 16px; height: 16px; `; diff --git a/src/views/Bridge/components/TokenSelector.tsx b/src/views/Bridge/components/TokenSelector.tsx index 32c32679e..4e1598e92 100644 --- a/src/views/Bridge/components/TokenSelector.tsx +++ b/src/views/Bridge/components/TokenSelector.tsx @@ -2,7 +2,7 @@ import styled from "@emotion/styled"; import { useMemo } from "react"; import { ReactComponent as LinkExternalIcon } from "assets/icons/arrow-up-right-boxed.svg"; -import { Selector } from "components"; +import { Selector, TokenImage } from "components"; import { Text } from "components/Text"; import { TokenInfo, getTokenForChain, tokenList } from "utils"; @@ -125,7 +125,7 @@ export function TokenSelector({ }, element: ( - + {t.name} @@ -160,7 +160,7 @@ export function TokenSelector({ }))} displayElement={ - + {tokenToDisplay.displaySymbol || tokenToDisplay.symbol.toUpperCase()} @@ -192,7 +192,7 @@ const CoinIconTextWrapper = styled.div` gap: 12px; `; -const CoinIcon = styled.img` +const CoinIcon = styled(TokenImage)` width: 24px; height: 24px; `; diff --git a/src/views/Rewards/components/ChainLogoOverlap.tsx b/src/views/Rewards/components/ChainLogoOverlap.tsx index 492f7bba3..2db670bc4 100644 --- a/src/views/Rewards/components/ChainLogoOverlap.tsx +++ b/src/views/Rewards/components/ChainLogoOverlap.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; import { Tooltip } from "components/Tooltip"; +import { TokenImage } from "components/TokenImage"; import { COLORS, getChainInfo, rewardProgramTypes } from "utils"; import { useRewardProgramCard } from "../hooks/useRewardProgramCard"; @@ -14,7 +15,9 @@ const ChainLogoOverlap = ({ program }: { program: rewardProgramTypes }) => { } const tooltipTitle = `${rewardTokenSymbol} Reward eligible routes`; - const tooltipIcon = ; + const tooltipIcon = ( + + ); return ( { ))} @@ -53,7 +57,7 @@ const ChainOverlapWrapper = styled(Tooltip)` align-items: flex-start; `; -const ChainLogo = styled.img<{ zIndex: number }>` +const ChainLogo = styled(TokenImage)<{ zIndex: number }>` height: 18px; width: 18px; border-radius: 50%; @@ -62,7 +66,7 @@ const ChainLogo = styled.img<{ zIndex: number }>` margin-left: -10px; `; -const ChainTooltipIcon = styled.img` +const ChainTooltipIcon = styled(TokenImage)` width: 16px; height: 16px; diff --git a/src/views/Rewards/components/OverviewSection.tsx b/src/views/Rewards/components/OverviewSection.tsx index 271adc0c6..2d1720748 100644 --- a/src/views/Rewards/components/OverviewSection.tsx +++ b/src/views/Rewards/components/OverviewSection.tsx @@ -12,7 +12,7 @@ import { } from "utils"; import GenericOverviewCard from "./GenericOverviewCard"; import { useRewardProgramCard } from "../hooks/useRewardProgramCard"; -import { Text } from "components"; +import { Text, TokenImage } from "components"; import { BigNumber } from "ethers"; import { useHistory } from "react-router-dom"; import ChainLogoOverlap from "./ChainLogoOverlap"; @@ -213,7 +213,7 @@ const LogoContainer = styled.div<{ primaryColor: string; smallLogo?: boolean }>` 0px 2px 6px 0px rgba(0, 0, 0, 0.08); `; -const Logo = styled.img<{ smallLogo: boolean }>` +const Logo = styled(TokenImage)<{ smallLogo: boolean }>` height: ${({ smallLogo }) => (smallLogo ? 16 : 24)}px; width: ${({ smallLogo }) => (smallLogo ? 16 : 24)}px; `; diff --git a/src/views/Rewards/components/RewardProgramCard.tsx b/src/views/Rewards/components/RewardProgramCard.tsx index 042e4f6dd..31aa49dd1 100644 --- a/src/views/Rewards/components/RewardProgramCard.tsx +++ b/src/views/Rewards/components/RewardProgramCard.tsx @@ -5,7 +5,7 @@ import { formatUnitsWithMaxFractions, rewardProgramTypes, } from "utils"; -import { Text } from "components"; +import { Text, TokenImage } from "components"; import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; import { useRewardProgramCard } from "../hooks/useRewardProgramCard"; import { Link } from "react-router-dom"; @@ -72,7 +72,7 @@ const Wrapper = styled.div<{ primaryColor: string; backgroundUrl: string }>` } `; -const Logo = styled.img` +const Logo = styled(TokenImage)` height: 24px; width: 24px; `; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index dcd40fda1..173db56c6 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -21,7 +21,7 @@ import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; import { useEnrichedCrosschainBalances } from "hooks/useEnrichedCrosschainBalances"; import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; -import { Text } from "components"; +import { Text, TokenImage } from "components"; import { useHotkeys } from "react-hotkeys-hook"; const popularChains = [ @@ -831,7 +831,7 @@ const TokenItemImageWrapper = styled.div` position: relative; `; -const TokenItemTokenImage = styled.img` +const TokenItemTokenImage = styled(TokenImage)` width: 100%; height: 100%; @@ -846,7 +846,7 @@ const TokenItemTokenImage = styled.img` mask-position: center; `; -const TokenItemChainImage = styled.img` +const TokenItemChainImage = styled(TokenImage)` width: 12px; height: 12px; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index b43f5f313..b21abf423 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -2,6 +2,7 @@ import styled from "@emotion/styled"; import { useCallback, useEffect, useState } from "react"; import { COLORS, getChainInfo } from "utils"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; +import { TokenImage } from "components/TokenImage"; import ChainTokenSelectorModal, { EnrichedToken } from "./Modal"; type Props = { @@ -66,8 +67,8 @@ export default function SelectorButton({ <> setDisplayModal(true)}> - - + + @@ -169,7 +170,7 @@ const TokenStack = styled.div` flex-grow: 0; `; -const TokenImg = styled.img` +const TokenImg = styled(TokenImage)` border-radius: 50%; position: absolute; top: var(--padding); @@ -179,7 +180,7 @@ const TokenImg = styled.img` z-index: 1; `; -const ChainImg = styled.img` +const ChainImg = styled(TokenImage)` border-radius: 50%; border: 1px solid transparent; background: ${COLORS["grey-600"]}; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 0d4e09ab1..f7e729423 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -2,6 +2,7 @@ import { COLORS, formatUSD } from "utils"; import SelectorButton from "./ChainTokenSelector/SelectorButton"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; import { BalanceSelector } from "./BalanceSelector"; +import { QuoteWarning } from "./QuoteWarning"; import styled from "@emotion/styled"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { BigNumber } from "ethers"; @@ -23,6 +24,7 @@ export const InputForm = ({ expectedOutputAmount, expectedInputAmount, validationError, + quoteWarningMessage, }: { inputToken: EnrichedToken | null; setInputToken: (token: EnrichedToken | null) => void; @@ -39,6 +41,7 @@ export const InputForm = ({ isAmountOrigin: boolean; setIsAmountOrigin: (isAmountOrigin: boolean) => void; validationError: AmountInputError | undefined; + quoteWarningMessage: string | null; }) => { const quickSwap = useCallback(() => { const origin = inputToken; @@ -87,6 +90,7 @@ export const InputForm = ({ otherToken={inputToken} disabled={!outputToken || !outputToken} /> + {/* */} ); }; diff --git a/src/views/SwapAndBridge/components/QuoteWarning.tsx b/src/views/SwapAndBridge/components/QuoteWarning.tsx new file mode 100644 index 000000000..d87a20405 --- /dev/null +++ b/src/views/SwapAndBridge/components/QuoteWarning.tsx @@ -0,0 +1,54 @@ +import styled from "@emotion/styled"; +import { COLORS } from "utils"; +import { ReactComponent as WarningTriangle } from "assets/icons/warning_triangle.svg"; + +type QuoteWarningProps = { + message: string | null; +}; + +export const QuoteWarning = ({ message }: QuoteWarningProps) => { + if (!message) { + return null; + } + + return ( + + + + + {message} + + ); +}; + +const WarningContainer = styled.div` + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border-radius: 16px; + background: ${COLORS["black-700"]}; + border: 1px solid ${COLORS.warning}; + width: 100%; +`; + +const AlertIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: ${COLORS.warning}; + margin-top: 2px; + + svg { + color: inherit; + } +`; + +const WarningText = styled.p` + color: ${COLORS["light-200"]}; + font-size: 14px; + font-weight: 400; + line-height: 150%; + margin: 0; +`; diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index be97ed3df..4489f596d 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -14,6 +14,7 @@ import { useDebounce } from "@uidotdev/usehooks"; import { useDefaultRoute } from "./useDefaultRoute"; import { useConnection } from "hooks"; import { useHistory } from "react-router-dom"; +import { getQuoteWarningMessage } from "utils"; export type UseSwapAndBridgeReturn = { inputToken: EnrichedToken | null; @@ -47,6 +48,8 @@ export type UseSwapAndBridgeReturn = { isWrongNetwork: boolean; isSubmitting: boolean; onConfirm: () => Promise; + quoteError: Error | null; + quoteWarningMessage: string | null; }; export function useSwapAndBridge(): UseSwapAndBridgeReturn { @@ -203,6 +206,11 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { ] ); + const quoteWarningMessage = useMemo( + () => getQuoteWarningMessage(error), + [error] + ); + return { inputToken, outputToken, @@ -234,6 +242,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { isWrongNetwork: approvalAction.isWrongNetwork, isSubmitting: approvalAction.isButtonActionLoading, onConfirm, + quoteError: error, + quoteWarningMessage, }; } diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts index d69bd4a23..f63a95119 100644 --- a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -1,11 +1,16 @@ import { useMemo } from "react"; import { BigNumber } from "ethers"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { AmountInputError } from "../../Bridge/utils"; import { EnrichedToken } from "../components/ChainTokenSelector/Modal"; import { validationErrorTextMap } from "views/Bridge/components/AmountInput"; +type AcrossApiErrorResponse = { + code?: string; + message?: string; +}; + export type ValidationResult = { error?: AmountInputError; warn?: AmountInputError; @@ -17,7 +22,7 @@ export function useValidateSwapAndBridge( isAmountOrigin: boolean, inputToken: EnrichedToken | null, outputToken: EnrichedToken | null, - error: any + error: Error | null ): ValidationResult { const validation = useMemo(() => { let errorType: AmountInputError | undefined = undefined; @@ -46,7 +51,8 @@ export function useValidateSwapAndBridge( } // backend availability if (!errorType && error && axios.isAxiosError(error)) { - const code = (error.response?.data as any)?.code as string | undefined; + const axiosError = error as AxiosError; + const code = axiosError.response?.data?.code; if (code === "AMOUNT_TOO_LOW") { errorType = AmountInputError.AMOUNT_TOO_LOW; } else if (code === "SWAP_QUOTE_UNAVAILABLE") { diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index e1e4d1eba..25a9c2d85 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -26,6 +26,7 @@ export default function SwapAndBridge() { buttonLoading, buttonLabel, onConfirm, + quoteWarningMessage, } = useSwapAndBridge(); return ( @@ -43,6 +44,7 @@ export default function SwapAndBridge() { expectedOutputAmount={expectedOutputAmount} expectedInputAmount={expectedInputAmount} validationError={validationError} + quoteWarningMessage={quoteWarningMessage} /> Date: Mon, 20 Oct 2025 21:30:20 +0200 Subject: [PATCH 077/122] fix math Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 8 ++- src/utils/token.ts | 49 ++++++++++++++++--- .../SwapAndBridge/components/InputForm.tsx | 4 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 2166aaed9..9364f6ebe 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -107,7 +107,8 @@ export function useTokenInput({ token.decimals ); const usdValue = convertTokenToUSD(tokenAmountFormatted, token); - setAmountString(utils.formatUnits(usdValue, token.decimals)); + // convertTokenToUSD returns in 18 decimal precision + setAmountString(utils.formatUnits(usdValue, 18)); } } } @@ -131,6 +132,7 @@ export function useTokenInput({ setConvertedAmount(tokenValue); } } catch (e) { + // getting an underflow error here setConvertedAmount(undefined); } }, [token, amountString, unit]); @@ -141,7 +143,8 @@ export function useTokenInput({ // Convert token amount to USD string for display if (amountString && token && convertedAmount) { try { - const a = utils.formatUnits(convertedAmount, token.decimals); + // convertedAmount is USD value in 18 decimals + const a = utils.formatUnits(convertedAmount, 18); setAmountString(a); } catch (e) { setAmountString("0"); @@ -153,6 +156,7 @@ export function useTokenInput({ // Convert USD amount to token string for display if (amountString && token && convertedAmount) { try { + // convertedAmount is token value in token's native decimals const a = utils.formatUnits(convertedAmount, token.decimals); setAmountString(a); } catch (e) { diff --git a/src/utils/token.ts b/src/utils/token.ts index 104985701..675b156ff 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -83,6 +83,26 @@ export function getExplorerLinkForToken( return `${getChainInfo(tokenChainId).explorerUrl}/address/${tokenAddress}`; } +// Standard precision for intermediate calculations (matches Ethereum wei) +const PRECISION = 18; + +/** + * Limits a decimal string to a maximum number of decimal places without losing precision + * @param value - The decimal string to limit + * @param maxDecimals - Maximum number of decimal places + * @returns The limited string + */ +function limitDecimals(value: string, maxDecimals: number): string { + const parts = value.split("."); + if (parts.length === 1) { + return value; // No decimal point + } + if (parts[1].length <= maxDecimals) { + return value; // Within limits + } + return parts[0] + "." + parts[1].substring(0, maxDecimals); +} + /** * Converts a token amount to USD value * @param tokenAmount - The token amount as a string (decimal format) @@ -93,22 +113,37 @@ export function convertTokenToUSD( tokenAmount: string, token: LifiToken ): BigNumber { - const tokenScaled = parseUnits(tokenAmount, token.decimals); - const priceScaled = parseUnits(token.priceUSD, token.decimals); - return tokenScaled.mul(priceScaled).div(parseUnits("1", token.decimals)); + // Use 18 decimals for maximum precision in calculations + const normalizedAmount = limitDecimals(tokenAmount, PRECISION); + const tokenScaled = parseUnits(normalizedAmount, PRECISION); + const priceScaled = parseUnits(token.priceUSD, PRECISION); + return tokenScaled.mul(priceScaled).div(parseUnits("1", PRECISION)); } /** * Converts a USD amount to token amount * @param usdAmount - The USD amount as a string (decimal format) * @param token - The token object containing price and decimals - * @returns The token amount as a BigNumber + * @returns The token amount as a BigNumber (in token's native decimals) */ export function convertUSDToToken( usdAmount: string, token: LifiToken ): BigNumber { - const usdScaled = parseUnits(usdAmount, token.decimals); - const priceScaled = parseUnits(token.priceUSD, token.decimals); - return usdScaled.mul(parseUnits("1", token.decimals)).div(priceScaled); + // Use 18 decimals for maximum precision in calculations + const normalizedAmount = limitDecimals(usdAmount, PRECISION); + const usdScaled = parseUnits(normalizedAmount, PRECISION); + const priceScaled = parseUnits(token.priceUSD, PRECISION); + const result18Dec = usdScaled + .mul(parseUnits("1", PRECISION)) + .div(priceScaled); + + // Convert from 18 decimals to token's native decimals + const decimalDiff = PRECISION - token.decimals; + if (decimalDiff > 0) { + return result18Dec.div(BigNumber.from(10).pow(decimalDiff)); + } else if (decimalDiff < 0) { + return result18Dec.mul(BigNumber.from(10).pow(-decimalDiff)); + } + return result18Dec; } diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index f7e729423..e1c79db22 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -157,8 +157,10 @@ const TokenInput = ({ const formattedConvertedAmount = (() => { if (!convertedAmount) return "0.00"; if (unit === "token") { - return "$" + formatUSD(convertedAmount, token?.decimals); + // convertTokenToUSD returns in 18 decimal precision + return "$" + formatUSD(convertedAmount); } + // convertUSDToToken returns in token's native decimals return `${formatUnits(convertedAmount, token?.decimals)} ${token?.symbol}`; })(); From 589e0321bb53f7b54720934ca390974d35f203fc Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 20 Oct 2025 21:39:20 +0200 Subject: [PATCH 078/122] switch both input units simultaneously Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 13 +++++++++++-- .../SwapAndBridge/components/InputForm.tsx | 19 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 9364f6ebe..f3a3d0533 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -12,6 +12,9 @@ type UseTokenInputProps = { expectedAmount: string | undefined; shouldUpdate: boolean; isUpdateLoading: boolean; + // Optional: Allow unit state to be controlled from parent + unit?: UnitType; + setUnit?: (unit: UnitType) => void; }; type UseTokenInputReturn = { @@ -30,12 +33,18 @@ export function useTokenInput({ expectedAmount, shouldUpdate, isUpdateLoading, + unit: externalUnit, + setUnit: externalSetUnit, }: UseTokenInputProps): UseTokenInputReturn { const [amountString, setAmountString] = useState(""); - const [unit, setUnit] = useState("token"); + const [internalUnit, setInternalUnit] = useState("token"); const [convertedAmount, setConvertedAmount] = useState(); const [justTyped, setJustTyped] = useState(false); + // Use external unit if provided, otherwise use internal state + const unit = externalUnit ?? internalUnit; + const setUnit = externalSetUnit ?? setInternalUnit; + // Handle user input changes - propagate to parent useEffect(() => { if (!justTyped) { @@ -166,7 +175,7 @@ export function useTokenInput({ } } } - }, [unit, amountString, token, convertedAmount]); + }, [unit, amountString, token, convertedAmount, setUnit]); // Handle input field changes const handleInputChange = useCallback((value: string) => { diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index e1c79db22..6801d9bb3 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -4,12 +4,12 @@ import { EnrichedToken } from "./ChainTokenSelector/Modal"; import { BalanceSelector } from "./BalanceSelector"; import { QuoteWarning } from "./QuoteWarning"; import styled from "@emotion/styled"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { BigNumber } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; import { ReactComponent as ArrowDown } from "assets/icons/arrow-down.svg"; import { AmountInputError } from "views/Bridge/utils"; -import { useTokenInput } from "hooks"; +import { useTokenInput, UnitType } from "hooks"; import { formatUnits } from "ethers/lib/utils"; export const InputForm = ({ @@ -43,6 +43,9 @@ export const InputForm = ({ validationError: AmountInputError | undefined; quoteWarningMessage: string | null; }) => { + // Shared unit state for both inputs + const [unit, setUnit] = useState("token"); + const quickSwap = useCallback(() => { const origin = inputToken; const destination = outputToken; @@ -72,6 +75,8 @@ export const InputForm = ({ } otherToken={outputToken} disabled={!outputToken || !outputToken} + unit={unit} + setUnit={setUnit} /> @@ -89,6 +94,8 @@ export const InputForm = ({ isUpdateLoading={isQuoteLoading} otherToken={inputToken} disabled={!outputToken || !outputToken} + unit={unit} + setUnit={setUnit} /> {/* */} @@ -106,6 +113,8 @@ const TokenInput = ({ insufficientInputBalance = false, otherToken, disabled, + unit, + setUnit, }: { setToken: (token: EnrichedToken) => void; token: EnrichedToken | null; @@ -117,13 +126,15 @@ const TokenInput = ({ insufficientInputBalance?: boolean; disabled?: boolean; otherToken?: EnrichedToken | null; + unit: UnitType; + setUnit: (unit: UnitType) => void; }) => { const amountInputRef = useRef(null); const hasAutoFocusedRef = useRef(false); const { amountString, - unit, + unit: hookUnit, convertedAmount, toggleUnit, handleInputChange, @@ -134,6 +145,8 @@ const TokenInput = ({ expectedAmount, shouldUpdate, isUpdateLoading, + unit, + setUnit, }); const inputDisabled = (() => { From eae872e4d84fc8e063d5eb86aea1e41b9ea62f34 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 20 Oct 2025 22:26:13 +0200 Subject: [PATCH 079/122] add prefix if usd unit Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/components/InputForm.tsx | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 6801d9bb3..f07b04569 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -183,14 +183,23 @@ const TokenInput = ({ {isOrigin ? "From" : "To"} - handleInputChange(e.target.value)} - disabled={inputDisabled} error={insufficientInputBalance} - /> + > + handleInputChange(e.target.value)} + disabled={inputDisabled} + error={insufficientInputBalance} + /> + @@ -275,27 +284,60 @@ const TokenAmountInputTitle = styled.div` line-height: 130%; `; -const TokenAmountInput = styled.input<{ +const TokenAmountInputWrapper = styled.div<{ + showPrefix: boolean; value: string; error: boolean; }>` - font-family: Barlow; + display: flex; + align-items: center; + width: 100%; + position: relative; + font-size: 48px; font-weight: 300; line-height: 120%; letter-spacing: -1.92px; - width: 100%; + color: ${({ value, error }) => error ? COLORS.error : value ? COLORS.aqua : COLORS["light-200"]}; + &:focus-within { + font-size: 48px; + } + + ${({ showPrefix }) => + showPrefix && + ` + &::before { + content: "$"; + margin-right: 4px; + flex-shrink: 0; + font-size: 48px; + font-weight: 300; + line-height: 120%; + letter-spacing: -1.92px; + } + `} +`; + +const TokenAmountInput = styled.input<{ + value: string; + error: boolean; +}>` + width: 100%; outline: none; border: none; background: transparent; - + font: inherit; + font-size: inherit; + color: ${({ value, error }) => + error ? COLORS.error : value ? COLORS.aqua : COLORS["light-200"]}; flex-shrink: 0; &:focus { font-size: 48px; + outline: none; } `; From b1e13391958386b075bab652d3909cd40c9cb41e Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 21 Oct 2025 14:11:59 +0200 Subject: [PATCH 080/122] fix swap quote refetch and response parsing logic Signed-off-by: Gerhard Steenkamp --- .../serverless-api/mocked/swap-approval.ts | 84 +++++++++++++++++++ .../serverless-api/prod/swap-approval.ts | 55 ++++++++++++ .../components/ConfirmationButton.tsx | 4 +- src/views/SwapAndBridge/hooks/useSwapQuote.ts | 21 +++-- 4 files changed, 151 insertions(+), 13 deletions(-) diff --git a/src/utils/serverless-api/mocked/swap-approval.ts b/src/utils/serverless-api/mocked/swap-approval.ts index c1136e388..6b3ef6f2b 100644 --- a/src/utils/serverless-api/mocked/swap-approval.ts +++ b/src/utils/serverless-api/mocked/swap-approval.ts @@ -93,5 +93,89 @@ export async function swapApprovalApiCall( maxFeePerGas: BigNumber.from("0"), maxPriorityFeePerGas: BigNumber.from("0"), }, + fees: { + total: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: "0", + token: { + decimals: outputToken?.decimals ?? 18, + symbol: outputToken?.symbol ?? "UNKNOWN", + address: params.outputToken, + name: outputToken?.symbol ?? "UNKNOWN", + chainId: params.destinationChainId, + }, + }, + originGas: { + amount: BigNumber.from("0"), + amountUsd: "0", + token: { + chainId: params.originChainId, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + symbol: "ETH", + }, + }, + destinationGas: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: "0", + token: { + chainId: params.destinationChainId, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + symbol: "ETH", + }, + }, + relayerCapital: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: "0", + token: { + decimals: outputToken?.decimals ?? 18, + symbol: outputToken?.symbol ?? "UNKNOWN", + address: params.outputToken, + name: outputToken?.symbol ?? "UNKNOWN", + chainId: params.destinationChainId, + }, + }, + lpFee: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: "0", + token: { + decimals: outputToken?.decimals ?? 18, + symbol: outputToken?.symbol ?? "UNKNOWN", + address: params.outputToken, + name: outputToken?.symbol ?? "UNKNOWN", + chainId: params.destinationChainId, + }, + }, + relayerTotal: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: "0", + token: { + decimals: outputToken?.decimals ?? 18, + symbol: outputToken?.symbol ?? "UNKNOWN", + address: params.outputToken, + name: outputToken?.symbol ?? "UNKNOWN", + chainId: params.destinationChainId, + }, + }, + app: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: "0", + token: { + decimals: outputToken?.decimals ?? 18, + symbol: outputToken?.symbol ?? "UNKNOWN", + address: params.outputToken, + name: outputToken?.symbol ?? "UNKNOWN", + chainId: params.destinationChainId, + }, + }, + swap: undefined, + }, }; } diff --git a/src/utils/serverless-api/prod/swap-approval.ts b/src/utils/serverless-api/prod/swap-approval.ts index 49cd441b1..fa6b09a8b 100644 --- a/src/utils/serverless-api/prod/swap-approval.ts +++ b/src/utils/serverless-api/prod/swap-approval.ts @@ -320,5 +320,60 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { ? BigNumber.from(result.swapTx.maxPriorityFeePerGas) : undefined, }, + fees: { + total: { + amount: BigNumber.from(result.fees.total.amount), + amountUsd: result.fees.total.amountUsd, + pct: result.fees.total.pct, + token: result.fees.total.token, + }, + originGas: { + amount: BigNumber.from(result.fees.originGas.amount), + amountUsd: result.fees.originGas.amountUsd, + token: result.fees.originGas.token, + }, + destinationGas: { + amount: BigNumber.from(result.fees.destinationGas.amount), + amountUsd: result.fees.destinationGas.amountUsd, + pct: result.fees.destinationGas.pct, + token: result.fees.destinationGas.token, + }, + relayerCapital: { + amount: BigNumber.from(result.fees.relayerCapital.amount), + amountUsd: result.fees.relayerCapital.amountUsd, + pct: result.fees.relayerCapital.pct, + token: result.fees.relayerCapital.token, + }, + lpFee: { + amount: BigNumber.from(result.fees.lpFee.amount), + amountUsd: result.fees.lpFee.amountUsd, + pct: result.fees.lpFee.pct, + token: result.fees.lpFee.token, + }, + relayerTotal: { + amount: BigNumber.from(result.fees.relayerTotal.amount), + amountUsd: result.fees.relayerTotal.amountUsd, + pct: result.fees.relayerTotal.pct, + token: result.fees.relayerTotal.token, + }, + app: { + amount: BigNumber.from(result.fees.app.amount), + amountUsd: result.fees.app.amountUsd, + pct: result.fees.app.pct, + token: result.fees.app.token, + }, + swap: result.fees.swap + ? { + amount: BigNumber.from(result.fees.swap.amount), + amountUsd: result.fees.swap.amountUsd, + pct: result.fees.swap.pct, + token: result.fees.swap.token, + } + : undefined, + }, }; } + +export type SwapApprovalApiCallReturnType = Awaited< + ReturnType +>; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index b38e59c8b..760f71268 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -19,7 +19,7 @@ import { EnrichedToken } from "./ChainTokenSelector/Modal"; import styled from "@emotion/styled"; import { AmountInputError } from "../../Bridge/utils"; import { Tooltip } from "components/Tooltip"; -import { SwapApprovalApiResponse } from "utils/serverless-api/prod/swap-approval"; +import { SwapApprovalApiCallReturnType } from "utils/serverless-api/prod/swap-approval"; export type BridgeButtonState = | "notConnected" @@ -36,7 +36,7 @@ interface ConfirmationButtonProps inputToken: EnrichedToken | null; outputToken: EnrichedToken | null; amount: BigNumber | null; - swapQuote: SwapApprovalApiResponse | null; + swapQuote: SwapApprovalApiCallReturnType | null; isQuoteLoading: boolean; onConfirm?: () => Promise; validationError?: AmountInputError; diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index be11387ef..289a15ab4 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -1,12 +1,11 @@ import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; import { BigNumber } from "ethers"; import { useConnection } from "hooks"; -import { vercelApiBaseUrl } from "utils"; import { SwapApiToken, + swapApprovalApiCall, + SwapApprovalApiCallReturnType, SwapApprovalApiQueryParams, - SwapApprovalApiResponse, } from "utils/serverless-api/prod/swap-approval"; type SwapQuoteParams = { @@ -43,7 +42,7 @@ const useSwapQuote = ({ depositor, recipient, ], - queryFn: async (): Promise => { + queryFn: async (): Promise => { if (Number(amount) <= 0) { return undefined; } @@ -66,17 +65,17 @@ const useSwapQuote = ({ ...(refundAddress ? { refundAddress } : {}), }; - const { data } = await axios.get( - `${vercelApiBaseUrl}/api/swap/approval`, - { - params, - } - ); + const data = await swapApprovalApiCall(params); return data; }, enabled: !!origin?.address && !!destination?.address && !!amount && !!depositor, - refetchInterval: 5_000, + retry: 2, + + refetchInterval(query) { + // only refetch if data + return query.state.status === "success" ? 10_000 : false; + }, }); return { data, isLoading, error }; From 454f64d43c23e5d408ec62d0bb43d1e4e762036d Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 21 Oct 2025 22:04:49 +0200 Subject: [PATCH 081/122] fix tab index Signed-off-by: Gerhard Steenkamp --- src/components/Modal/Modal.tsx | 4 +- .../components/ChainTokenSelector/Modal.tsx | 43 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 39658a23e..917b4a9a4 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -44,6 +44,7 @@ export type ModalProps = { footer?: React.ReactNode; titleBorder?: boolean; className?: string; + closeButtonTabIndex?: number; }; const Modal = ({ @@ -64,6 +65,7 @@ const Modal = ({ "data-cy": dataCy, titleBorder = false, noScroll = false, + closeButtonTabIndex = 999999, // should default to being last }: ModalProps) => { const verticalLocation: ModalDirection | undefined = typeof _verticalLocation === "string" @@ -169,7 +171,7 @@ const Modal = ({ )} externalModalExitHandler()} > diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 173db56c6..9ef5ff40e 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -390,6 +390,7 @@ const DesktopModal = ({ height={800} titleBorder noScroll + closeButtonTabIndex={99999} > handleSelectChain(null)} + tabIndex={2} /> {/* Popular Chains Section */} {displayedChains.popular.length > 0 && ( <> Popular Chains - {displayedChains.popular.map(({ chainId, isDisabled }) => ( + {displayedChains.popular.map(({ chainId, isDisabled }, index) => ( handleSelectChain(chainId)} + tabIndex={3 + index} /> ))} @@ -633,13 +649,14 @@ const DesktopLayout = ({ {displayedChains.all.length > 0 && ( <> All Chains - {displayedChains.all.map(({ chainId, isDisabled }) => ( + {displayedChains.all.map(({ chainId, isDisabled }, index) => ( handleSelectChain(chainId)} + tabIndex={3 + displayedChains.popular.length + index} /> ))} @@ -653,7 +670,7 @@ const DesktopLayout = ({ 0 && ( <> Popular Tokens - {displayedTokens.popular.map((token) => ( + {displayedTokens.popular.map((token, index) => ( ))} @@ -683,7 +701,7 @@ const DesktopLayout = ({ {displayedTokens.all.length > 0 && ( <> All Tokens - {displayedTokens.all.map((token) => ( + {displayedTokens.all.map((token, index) => ( ))} @@ -724,11 +743,13 @@ const ChainEntry = ({ isSelected, onClick, isDisabled = false, + tabIndex, }: { chainId: number | null; isSelected: boolean; onClick: () => void; isDisabled?: boolean; + tabIndex?: number; }) => { const chainInfo = chainId ? getChainInfo(chainId) @@ -741,6 +762,7 @@ const ChainEntry = ({ isSelected={isSelected} isDisabled={isDisabled} onClick={isDisabled ? undefined : onClick} + tabIndex={tabIndex} > {chainInfo.name} @@ -753,15 +775,22 @@ const TokenEntry = ({ token, isSelected, onClick, + tabIndex, }: { token: LifiToken & { balanceUsd: number; balance: BigNumber }; isSelected: boolean; onClick: () => void; + tabIndex?: number; }) => { const hasBalance = token.balance.gt(0) && token.balanceUsd > 0.01; return ( - + {token.name} From 727389aef72b3c3fe4a1dcff8374a96ca5bf8a5f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 22 Oct 2025 16:11:15 +0200 Subject: [PATCH 082/122] edit recipient address Signed-off-by: Gerhard Steenkamp --- src/assets/icons/pencil.svg | 3 + src/components/Modal/Modal.styles.ts | 2 +- src/views/Bridge/Bridge.tsx | 17 +-- .../Bridge/components/ChangeAccountModal.tsx | 144 +++++++++++------- .../components/ChainTokenSelector/Modal.tsx | 2 +- .../SwapAndBridge/components/InputForm.tsx | 61 +++++++- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 70 ++++++++- src/views/SwapAndBridge/hooks/useSwapQuote.ts | 6 +- src/views/SwapAndBridge/index.tsx | 10 ++ 9 files changed, 235 insertions(+), 80 deletions(-) create mode 100644 src/assets/icons/pencil.svg diff --git a/src/assets/icons/pencil.svg b/src/assets/icons/pencil.svg new file mode 100644 index 000000000..78c27820d --- /dev/null +++ b/src/assets/icons/pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index c3d39c424..073437731 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -95,7 +95,7 @@ const minimumMargin = 32; export const ModalContentWrapper = styled.div` --padding-modal-content: ${({ padding }) => padding === "normal" ? "24px" : "16px"}; - height: ${({ height, topYOffset }) => + max-height: ${({ height, topYOffset }) => height ? `min(calc(100svh - ${minimumMargin * 2}px - ${topYOffset ?? 0}px), ${height}px)` : "calc(100svh - 64px)"}; diff --git a/src/views/Bridge/Bridge.tsx b/src/views/Bridge/Bridge.tsx index b6e03c545..948146487 100644 --- a/src/views/Bridge/Bridge.tsx +++ b/src/views/Bridge/Bridge.tsx @@ -4,12 +4,10 @@ import { LayoutV2 } from "components"; import { Wrapper } from "./Bridge.styles"; import Breadcrumb from "./components/Breadcrumb"; import BridgeForm from "./components/BridgeForm"; -import ChangeAccountModal from "./components/ChangeAccountModal"; import { useBridge } from "./hooks/useBridge"; -import { getEcosystem } from "utils"; const Bridge = () => { - const [displayChangeAccount, setDisplayChangeAccount] = useState(false); + const [_, setDisplayChangeAccount] = useState(false); const { selectedRoute, @@ -33,8 +31,6 @@ const Bridge = () => { universalSwapQuote, toAccountEVM, toAccountSVM, - handleChangeToAddressEVM, - handleChangeToAddressSVM, handleChangeAmountInput, handleClickMaxBalance, handleSelectInputToken, @@ -45,19 +41,8 @@ const Bridge = () => { isQuoteLoading, } = useBridge(); - const destinationChainEcosystem = getEcosystem(selectedRoute.toChain); - return ( <> - setDisplayChangeAccount(false)} - currentAccountEVM={toAccountEVM?.address} - currentAccountSVM={toAccountSVM?.address} - onChangeAccountEVM={handleChangeToAddressEVM} - onChangeAccountSVM={handleChangeToAddressSVM} - destinationChainEcosystem={destinationChainEcosystem} - /> diff --git a/src/views/Bridge/components/ChangeAccountModal.tsx b/src/views/Bridge/components/ChangeAccountModal.tsx index f48f5617a..414f0c40b 100644 --- a/src/views/Bridge/components/ChangeAccountModal.tsx +++ b/src/views/Bridge/components/ChangeAccountModal.tsx @@ -7,13 +7,14 @@ import { Modal, Text } from "components"; import { PrimaryButton, UnstyledButton } from "components/Button"; import { Input, InputGroup } from "components/Input"; import { ReactComponent as CrossIcon } from "assets/icons/cross.svg"; +import { ReactComponent as PencilIcon } from "assets/icons/pencil.svg"; + import { ampli } from "ampli"; import { useAmplitude } from "hooks"; import { useDisallowList } from "hooks/useDisallowList"; +import { shortenAddress } from "utils"; type ChangeAccountModalProps = { - displayModal: boolean; - onCloseModal: () => void; currentAccountEVM?: string; currentAccountSVM?: string; onChangeAccountEVM: (account: string) => void; @@ -22,25 +23,27 @@ type ChangeAccountModalProps = { }; const ChangeAccountModal = ({ - displayModal, - onCloseModal, currentAccountEVM, currentAccountSVM, onChangeAccountEVM, onChangeAccountSVM, destinationChainEcosystem, }: ChangeAccountModalProps) => { - const [userInput, setUserInput] = useState( + const currentRecipient = destinationChainEcosystem === "evm" ? currentAccountEVM || "" - : currentAccountSVM || "" - ); + : currentAccountSVM || ""; + + const [displayModal, setDisplayModal] = useState(false); + const [userInput, setUserInput] = useState(currentRecipient); const [validInput, setValidInput] = useState(false); const { isBlocked, isLoading } = useDisallowList(userInput); const { addToAmpliQueue } = useAmplitude(); + const onCloseModal = () => setDisplayModal(false); + useEffect(() => { if (displayModal) { setUserInput( @@ -105,56 +108,93 @@ const ChangeAccountModal = ({ const validationLevel = !validInput && !!userInput ? "error" : "valid"; return ( - - - - - setUserInput(t.target.value)} - /> - - - - - - - - Cancel - - - - - Save - - - - - - Note that only{" "} - - {destinationChainEcosystem === "evm" ? "Ethereum" : "Solana"} - {" "} - addresses are valid. - - - + <> + setDisplayModal(true)}> + {shortenAddress(currentRecipient, "..", 4)} + + + + + + + Wallet Address + + setUserInput(t.target.value)} + /> + + + + + + + + Cancel + + + + + Save + + + + + + Note that only{" "} + + {destinationChainEcosystem === "evm" ? "Ethereum" : "Solana"} + {" "} + addresses are valid. + + + + ); }; export default ChangeAccountModal; +const Trigger = styled.button` + color: var(--base-bright-gray, #e0f3ff); + + /* Body/Small */ + font-family: Barlow; + font-size: 14px; + font-style: normal; + font-weight: 400; + + display: inline-flex; + align-items: center; + gap: 4px; + opacity: 0.5; + + &:hover { + opacity: 1; + } +`; + +const SubHeading = styled.div` + color: var(--base-bright-gray, #e0f3ff); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 130%; + opacity: 0.5; +`; + const InnerWrapper = styled.div` display: flex; flex-direction: column; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 9ef5ff40e..24400c2c1 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -583,7 +583,7 @@ const DesktopLayout = ({ }) => { const chainSearchInputRef = useRef(null); const tokenSearchInputRef = useRef(null); - useHotkeys("esc", () => onModalClose()); + useHotkeys("esc", () => onModalClose(), { enableOnFormTags: true }); // Focus chain search input when component mounts useEffect(() => { diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index f07b04569..9802f2e94 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -11,6 +11,7 @@ import { ReactComponent as ArrowDown } from "assets/icons/arrow-down.svg"; import { AmountInputError } from "views/Bridge/utils"; import { useTokenInput, UnitType } from "hooks"; import { formatUnits } from "ethers/lib/utils"; +import ChangeAccountModal from "views/Bridge/components/ChangeAccountModal"; export const InputForm = ({ inputToken, @@ -25,6 +26,11 @@ export const InputForm = ({ expectedInputAmount, validationError, quoteWarningMessage, + toAccountEVM, + toAccountSVM, + handleChangeToAddressEVM, + handleChangeToAddressSVM, + destinationChainEcosystem, }: { inputToken: EnrichedToken | null; setInputToken: (token: EnrichedToken | null) => void; @@ -42,6 +48,12 @@ export const InputForm = ({ setIsAmountOrigin: (isAmountOrigin: boolean) => void; validationError: AmountInputError | undefined; quoteWarningMessage: string | null; + + toAccountEVM?: { address: string }; + toAccountSVM?: { address: string }; + handleChangeToAddressEVM: (account: string) => void; + handleChangeToAddressSVM: (account: string) => void; + destinationChainEcosystem: "evm" | "svm"; }) => { // Shared unit state for both inputs const [unit, setUnit] = useState("token"); @@ -77,6 +89,11 @@ export const InputForm = ({ disabled={!outputToken || !outputToken} unit={unit} setUnit={setUnit} + toAccountEVM={toAccountEVM} + toAccountSVM={toAccountSVM} + handleChangeToAddressEVM={handleChangeToAddressEVM} + handleChangeToAddressSVM={handleChangeToAddressSVM} + destinationChainEcosystem={destinationChainEcosystem} /> @@ -96,6 +113,11 @@ export const InputForm = ({ disabled={!outputToken || !outputToken} unit={unit} setUnit={setUnit} + toAccountEVM={toAccountEVM} + toAccountSVM={toAccountSVM} + handleChangeToAddressEVM={handleChangeToAddressEVM} + handleChangeToAddressSVM={handleChangeToAddressSVM} + destinationChainEcosystem={destinationChainEcosystem} /> {/* */} @@ -115,6 +137,11 @@ const TokenInput = ({ disabled, unit, setUnit, + toAccountEVM, + toAccountSVM, + handleChangeToAddressEVM, + handleChangeToAddressSVM, + destinationChainEcosystem, }: { setToken: (token: EnrichedToken) => void; token: EnrichedToken | null; @@ -128,13 +155,17 @@ const TokenInput = ({ otherToken?: EnrichedToken | null; unit: UnitType; setUnit: (unit: UnitType) => void; + toAccountEVM?: { address: string }; + toAccountSVM?: { address: string }; + handleChangeToAddressEVM: (account: string) => void; + handleChangeToAddressSVM: (account: string) => void; + destinationChainEcosystem: "evm" | "svm"; }) => { const amountInputRef = useRef(null); const hasAutoFocusedRef = useRef(false); const { amountString, - unit: hookUnit, convertedAmount, toggleUnit, handleInputChange, @@ -182,6 +213,17 @@ const TokenInput = ({ {isOrigin ? "From" : "To"} + {!isOrigin && ( + <> + + + )} ["toAccountEVM"]; + toAccountSVM: ReturnType["toAccountSVM"]; + handleChangeToAddressEVM: (account: string) => void; + handleChangeToAddressSVM: (account: string) => void; + destinationChainEcosystem: "evm" | "svm"; + // Legacy properties isConnected: boolean; isWrongNetwork: boolean; @@ -60,9 +69,43 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const debouncedAmount = useDebounce(amount, 300); const defaultRoute = useDefaultRoute(); - const { connect } = useConnection(); + const history = useHistory(); + const { + isConnected: isConnectedEVM, + chainId: walletChainIdEVM, + account: accountEVM, + connect: connectEVM, + } = useConnectionEVM(); + const { + isConnected: isConnectedSVM, + account: accountSVM, + connect: connectSVM, + } = useConnectionSVM(); + + const { + toAccountEVM, + toAccountSVM, + handleChangeToAddressEVM, + handleChangeToAddressSVM, + } = useToAccount(outputToken?.chainId); + + const originChainEcosystem = inputToken?.chainId + ? getEcosystem(inputToken?.chainId) + : "evm"; + const destinationChainEcosystem = outputToken?.chainId + ? getEcosystem(outputToken?.chainId) + : "evm"; + + const depositor = + originChainEcosystem === "evm" ? accountEVM : accountSVM?.toBase58(); + const recipient = + destinationChainEcosystem === "evm" ? toAccountEVM : toAccountSVM; + + console.log("depositor", depositor); + console.log("recipient", recipient?.address); + useEffect(() => { if (defaultRoute.inputToken && defaultRoute.outputToken) { setInputToken((prev) => { @@ -108,6 +151,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { destination: outputToken ? outputToken : null, amount: debouncedAmount, isInputAmount: isAmountOrigin, + depositor, + recipient: recipient?.address, }); const approvalData: SwapApprovalData | undefined = useMemo(() => { @@ -142,8 +187,12 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const onConfirm = useCallback(async () => { // If not connected, open the wallet connection modal if (!approvalAction.isConnected) { - connect({ trackSection: "bridgeForm" }); - return; + if (originChainEcosystem === "evm") { + connectEVM({ trackSection: "bridgeForm" }); + return; + } else { + connectSVM({ trackSection: "bridgeForm" }); + } } // Otherwise, proceed with the transaction @@ -156,10 +205,12 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { } }, [ approvalAction, - connect, + connectEVM, + connectSVM, history, inputToken?.chainId, inputToken?.symbol, + originChainEcosystem, outputToken?.chainId, outputToken?.symbol, ]); @@ -237,6 +288,13 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { buttonLoading, buttonLabel, + // Account management + toAccountEVM, + toAccountSVM, + handleChangeToAddressEVM, + handleChangeToAddressSVM, + destinationChainEcosystem, + // Legacy properties isConnected: approvalAction.isConnected, isWrongNetwork: approvalAction.isWrongNetwork, diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index 289a15ab4..06b18b786 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -1,6 +1,5 @@ import { useQuery } from "@tanstack/react-query"; import { BigNumber } from "ethers"; -import { useConnection } from "hooks"; import { SwapApiToken, swapApprovalApiCall, @@ -13,7 +12,8 @@ type SwapQuoteParams = { destination: SwapApiToken | null; amount: BigNumber | null; isInputAmount: boolean; - recipient?: string; + depositor: string | undefined; + recipient: string | undefined; integratorId?: string; refundAddress?: string; refundOnOrigin?: boolean; @@ -28,10 +28,10 @@ const useSwapQuote = ({ recipient, integratorId, refundAddress, + depositor, refundOnOrigin = true, slippageTolerance = 1, }: SwapQuoteParams) => { - const { account: depositor } = useConnection(); const { data, isLoading, error } = useQuery({ queryKey: [ "swap-quote", diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 25a9c2d85..daaeacc46 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -27,6 +27,11 @@ export default function SwapAndBridge() { buttonLabel, onConfirm, quoteWarningMessage, + toAccountEVM, + toAccountSVM, + handleChangeToAddressEVM, + handleChangeToAddressSVM, + destinationChainEcosystem, } = useSwapAndBridge(); return ( @@ -45,6 +50,11 @@ export default function SwapAndBridge() { expectedInputAmount={expectedInputAmount} validationError={validationError} quoteWarningMessage={quoteWarningMessage} + toAccountEVM={toAccountEVM} + toAccountSVM={toAccountSVM} + handleChangeToAddressEVM={handleChangeToAddressEVM} + handleChangeToAddressSVM={handleChangeToAddressSVM} + destinationChainEcosystem={destinationChainEcosystem} /> Date: Thu, 23 Oct 2025 12:22:07 +0200 Subject: [PATCH 083/122] refactor and style change account modal Signed-off-by: Gerhard Steenkamp --- src/components/Input/index.tsx | 25 ++- .../Bridge/components/ChangeAccountModal.tsx | 193 ++++++++---------- src/views/Bridge/hooks/useToAccount.ts | 14 ++ .../components/ConfirmationButton.tsx | 1 + .../SwapAndBridge/components/InputForm.tsx | 45 +--- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 44 +--- src/views/SwapAndBridge/index.tsx | 12 +- 7 files changed, 142 insertions(+), 192 deletions(-) diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 0bc7cd940..9acb03c10 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { forwardRef } from "react"; import { COLORS, QUERIESV2 } from "utils"; @@ -21,12 +22,14 @@ export const colorMap = { }, }; -export function Input({ - validationLevel, - ...props -}: IValidInput & React.InputHTMLAttributes) { - return ; -} +export const Input = forwardRef< + HTMLInputElement, + IValidInput & React.ComponentPropsWithoutRef<"input"> +>(({ validationLevel, ...props }, ref) => { + return ; +}); + +Input.displayName = "Input"; export function InputGroup({ validationLevel, @@ -38,6 +41,7 @@ export function InputGroup({ const StyledInput = styled.input` font-weight: 400; font-size: 18px; + font-size: 16px; line-height: 26px; color: ${({ validationLevel }) => colorMap[validationLevel].text}; background: none; @@ -49,7 +53,6 @@ const StyledInput = styled.input` &:focus { outline: 0; - font-size: 18px; } &::placeholder { @@ -82,6 +85,14 @@ const InputGroupWrapper = styled.div` gap: 8px; width: 100%; + &:has(:focus-visible) { + border-color: ${COLORS.aqua}; + } + + &:focus-within { + border-color: ${COLORS.aqua}; + } + @media ${QUERIESV2.sm.andDown} { padding: 6px 12px 6px 24px; } diff --git a/src/views/Bridge/components/ChangeAccountModal.tsx b/src/views/Bridge/components/ChangeAccountModal.tsx index 414f0c40b..cfffd1fb9 100644 --- a/src/views/Bridge/components/ChangeAccountModal.tsx +++ b/src/views/Bridge/components/ChangeAccountModal.tsx @@ -1,10 +1,10 @@ import styled from "@emotion/styled"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ethers } from "ethers"; import { isAddress as isSvmAddress } from "@solana/kit"; import { Modal, Text } from "components"; -import { PrimaryButton, UnstyledButton } from "components/Button"; +import { PrimaryButton } from "components/Button"; import { Input, InputGroup } from "components/Input"; import { ReactComponent as CrossIcon } from "assets/icons/cross.svg"; import { ReactComponent as PencilIcon } from "assets/icons/pencil.svg"; @@ -12,33 +12,31 @@ import { ReactComponent as PencilIcon } from "assets/icons/pencil.svg"; import { ampli } from "ampli"; import { useAmplitude } from "hooks"; import { useDisallowList } from "hooks/useDisallowList"; -import { shortenAddress } from "utils"; +import { COLORS, shortenAddress } from "utils"; +import { useHotkeys } from "react-hotkeys-hook"; +import { ToAccountManagement } from "../hooks/useToAccount"; type ChangeAccountModalProps = { - currentAccountEVM?: string; - currentAccountSVM?: string; - onChangeAccountEVM: (account: string) => void; - onChangeAccountSVM: (account: string) => void; + toAccountManagement: ToAccountManagement; destinationChainEcosystem: "evm" | "svm"; }; -const ChangeAccountModal = ({ - currentAccountEVM, - currentAccountSVM, - onChangeAccountEVM, - onChangeAccountSVM, +export const ChangeAccountModal = ({ + toAccountManagement, destinationChainEcosystem, }: ChangeAccountModalProps) => { - const currentRecipient = - destinationChainEcosystem === "evm" - ? currentAccountEVM || "" - : currentAccountSVM || ""; - + const { + currentRecipientAccount, + handleChangeToAddressEVM, + handleChangeToAddressSVM, + defaultRecipientAccount, + } = toAccountManagement; + const inputRef = useRef(null); const [displayModal, setDisplayModal] = useState(false); - const [userInput, setUserInput] = useState(currentRecipient); + const [userInput, setUserInput] = useState(currentRecipientAccount ?? ""); const [validInput, setValidInput] = useState(false); - const { isBlocked, isLoading } = useDisallowList(userInput); + const { isBlocked, isLoading } = useDisallowList(userInput ?? ""); const { addToAmpliQueue } = useAmplitude(); @@ -46,18 +44,10 @@ const ChangeAccountModal = ({ useEffect(() => { if (displayModal) { - setUserInput( - destinationChainEcosystem === "evm" - ? currentAccountEVM || "" - : currentAccountSVM || "" - ); + inputRef.current?.focus(); + setUserInput(currentRecipientAccount ?? ""); } - }, [ - currentAccountEVM, - currentAccountSVM, - displayModal, - destinationChainEcosystem, - ]); + }, [displayModal, currentRecipientAccount]); useEffect(() => { if (isLoading) { @@ -72,21 +62,14 @@ const ChangeAccountModal = ({ ? isValidAddressEVM : isValidAddressSVM ); - }, [ - currentAccountEVM, - currentAccountSVM, - userInput, - isBlocked, - isLoading, - destinationChainEcosystem, - ]); + }, [userInput, isBlocked, isLoading, destinationChainEcosystem]); const handleClickSave = () => { if (validInput || userInput === "") { if (destinationChainEcosystem === "evm") { - onChangeAccountEVM(userInput); + handleChangeToAddressEVM(userInput); } else { - onChangeAccountSVM(userInput); + handleChangeToAddressSVM(userInput); } addToAmpliQueue(() => { ampli.toAccountChanged({ @@ -107,10 +90,16 @@ const ChangeAccountModal = ({ const validationLevel = !validInput && !!userInput ? "error" : "valid"; + useHotkeys("esc", () => onCloseModal(), { enableOnFormTags: true }); + + if (!currentRecipientAccount) { + return; + } + return ( <> setDisplayModal(true)}> - {shortenAddress(currentRecipient, "..", 4)} + {shortenAddress(currentRecipientAccount, "..", 4)} @@ -119,53 +108,66 @@ const ChangeAccountModal = ({ exitModalHandler={handleClickCancel} verticalLocation="middle" isOpen={displayModal} - width={550} + width={480} exitOnOutsideClick titleBorder > - + Wallet Address - - setUserInput(t.target.value)} - /> - - - - - - - - Cancel - - - setUserInput(defaultRecipientAccount ?? "")} > - - Save - - - - - + Reset to Default + + )} + + + + setUserInput(t.target.value)} + /> + + + + + Note that only{" "} - + {destinationChainEcosystem === "evm" ? "Ethereum" : "Solana"} - {" "} + {" "} addresses are valid. - + + + + + Save + + + ); }; -export default ChangeAccountModal; +const ResetButton = styled.button` + color: ${COLORS.aqua}; + padding-block: 0px; +`; + +const RowSpaced = styled.div` + width: 100%; + display: flex; + justify-content: space-between; +`; const Trigger = styled.button` color: var(--base-bright-gray, #e0f3ff); @@ -187,27 +189,27 @@ const Trigger = styled.button` `; const SubHeading = styled.div` - color: var(--base-bright-gray, #e0f3ff); + color: var(--shades-Neutrals-neutral-400, #869099); font-size: 14px; font-style: normal; font-weight: 400; line-height: 130%; - opacity: 0.5; `; -const InnerWrapper = styled.div` - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 24px; - width: 100%; +const Warning = styled.span` + color: var(--shades-Neutrals-neutral-400, #869099); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 130%; + margin-top: 4px; + margin-bottom: 12px; `; const Wrapper = styled.div` display: flex; flex-direction: column; - gap: 24px; + gap: 12px; justify-content: flex-start; align-items: center; @@ -234,8 +236,8 @@ const StyledCrossIcon = styled(CrossIcon)` flex-shrink: 0; `; -const UnderlinedText = styled.span` - text-decoration: underline; +const BoldText = styled.span` + font-weight: 500; color: #e0f3ff; `; @@ -248,27 +250,6 @@ const ButtonWrapper = styled.div` width: 100%; `; -const CancelButton = styled(UnstyledButton)` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - width: 100%; - height: 64px; - - border: 1px solid #9daab3; - border-radius: 12px; - background: transparent; - - cursor: pointer; - - &:hover { - color: #e0f3ff; - border-color: #e0f3ff; - } -`; - const SaveButton = styled(PrimaryButton)` display: flex; flex-direction: row; diff --git a/src/views/Bridge/hooks/useToAccount.ts b/src/views/Bridge/hooks/useToAccount.ts index f978fd766..f2aaef8ad 100644 --- a/src/views/Bridge/hooks/useToAccount.ts +++ b/src/views/Bridge/hooks/useToAccount.ts @@ -10,6 +10,8 @@ export type ToAccount = { is7702Delegate: boolean; }; +export type ToAccountManagement = ReturnType; + export function useToAccount(toChainId?: number) { const [customToAddressEVM, setCustomToAddressEVM] = useState< string | undefined @@ -110,7 +112,19 @@ export function useToAccount(toChainId?: number) { setCustomToAddressSVM(address); }, []); + const defaultRecipientAccount = useMemo(() => { + return isDestinationSVM + ? connectedAccountSVM?.toBase58() + : connectedAccountEVM; + }, [connectedAccountEVM, connectedAccountSVM, isDestinationSVM]); + + const currentRecipientAccount = useMemo(() => { + return isDestinationSVM ? toAccountSVM?.address : toAccountEVM?.address; + }, [isDestinationSVM, toAccountEVM?.address, toAccountSVM?.address]); + return { + currentRecipientAccount, + defaultRecipientAccount, toAccountEVM, toAccountSVM, handleChangeToAddressEVM, diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 760f71268..e489af8e0 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -39,6 +39,7 @@ interface ConfirmationButtonProps swapQuote: SwapApprovalApiCallReturnType | null; isQuoteLoading: boolean; onConfirm?: () => Promise; + quoteWarningMessage: string | null; validationError?: AmountInputError; validationWarning?: AmountInputError; validationErrorFormatted?: string; diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 9802f2e94..5fd1a8634 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -2,16 +2,16 @@ import { COLORS, formatUSD } from "utils"; import SelectorButton from "./ChainTokenSelector/SelectorButton"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; import { BalanceSelector } from "./BalanceSelector"; -import { QuoteWarning } from "./QuoteWarning"; import styled from "@emotion/styled"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { BigNumber } from "ethers"; import { ReactComponent as ArrowsCross } from "assets/icons/arrows-cross.svg"; import { ReactComponent as ArrowDown } from "assets/icons/arrow-down.svg"; import { AmountInputError } from "views/Bridge/utils"; import { useTokenInput, UnitType } from "hooks"; import { formatUnits } from "ethers/lib/utils"; -import ChangeAccountModal from "views/Bridge/components/ChangeAccountModal"; +import { ChangeAccountModal } from "views/Bridge/components/ChangeAccountModal"; +import { ToAccountManagement } from "views/Bridge/hooks/useToAccount"; export const InputForm = ({ inputToken, @@ -25,11 +25,7 @@ export const InputForm = ({ expectedOutputAmount, expectedInputAmount, validationError, - quoteWarningMessage, - toAccountEVM, - toAccountSVM, - handleChangeToAddressEVM, - handleChangeToAddressSVM, + toAccountManagement, destinationChainEcosystem, }: { inputToken: EnrichedToken | null; @@ -47,12 +43,7 @@ export const InputForm = ({ isAmountOrigin: boolean; setIsAmountOrigin: (isAmountOrigin: boolean) => void; validationError: AmountInputError | undefined; - quoteWarningMessage: string | null; - - toAccountEVM?: { address: string }; - toAccountSVM?: { address: string }; - handleChangeToAddressEVM: (account: string) => void; - handleChangeToAddressSVM: (account: string) => void; + toAccountManagement: ToAccountManagement; destinationChainEcosystem: "evm" | "svm"; }) => { // Shared unit state for both inputs @@ -89,10 +80,7 @@ export const InputForm = ({ disabled={!outputToken || !outputToken} unit={unit} setUnit={setUnit} - toAccountEVM={toAccountEVM} - toAccountSVM={toAccountSVM} - handleChangeToAddressEVM={handleChangeToAddressEVM} - handleChangeToAddressSVM={handleChangeToAddressSVM} + toAccountManagement={toAccountManagement} destinationChainEcosystem={destinationChainEcosystem} /> @@ -113,13 +101,9 @@ export const InputForm = ({ disabled={!outputToken || !outputToken} unit={unit} setUnit={setUnit} - toAccountEVM={toAccountEVM} - toAccountSVM={toAccountSVM} - handleChangeToAddressEVM={handleChangeToAddressEVM} - handleChangeToAddressSVM={handleChangeToAddressSVM} + toAccountManagement={toAccountManagement} destinationChainEcosystem={destinationChainEcosystem} /> - {/* */} ); }; @@ -137,10 +121,7 @@ const TokenInput = ({ disabled, unit, setUnit, - toAccountEVM, - toAccountSVM, - handleChangeToAddressEVM, - handleChangeToAddressSVM, + toAccountManagement, destinationChainEcosystem, }: { setToken: (token: EnrichedToken) => void; @@ -155,10 +136,7 @@ const TokenInput = ({ otherToken?: EnrichedToken | null; unit: UnitType; setUnit: (unit: UnitType) => void; - toAccountEVM?: { address: string }; - toAccountSVM?: { address: string }; - handleChangeToAddressEVM: (account: string) => void; - handleChangeToAddressSVM: (account: string) => void; + toAccountManagement: ToAccountManagement; destinationChainEcosystem: "evm" | "svm"; }) => { const amountInputRef = useRef(null); @@ -216,10 +194,7 @@ const TokenInput = ({ {!isOrigin && ( <> diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index d1c6b7f32..0f65a8557 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -29,12 +29,13 @@ export type UseSwapAndBridgeReturn = { setAmount: (a: BigNumber | null) => void; isAmountOrigin: boolean; setIsAmountOrigin: (v: boolean) => void; - + // route swapQuote: ReturnType["data"]; isQuoteLoading: boolean; expectedInputAmount?: string; expectedOutputAmount?: string; - + destinationChainEcosystem: "svm" | "evm"; + // validation validationError?: AmountInputError; validationWarning?: AmountInputError; validationErrorFormatted?: string | undefined; @@ -46,11 +47,7 @@ export type UseSwapAndBridgeReturn = { buttonLabel: string; // Account management - toAccountEVM: ReturnType["toAccountEVM"]; - toAccountSVM: ReturnType["toAccountSVM"]; - handleChangeToAddressEVM: (account: string) => void; - handleChangeToAddressSVM: (account: string) => void; - destinationChainEcosystem: "evm" | "svm"; + toAccountManagement: ReturnType; // Legacy properties isConnected: boolean; @@ -72,24 +69,10 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const history = useHistory(); - const { - isConnected: isConnectedEVM, - chainId: walletChainIdEVM, - account: accountEVM, - connect: connectEVM, - } = useConnectionEVM(); - const { - isConnected: isConnectedSVM, - account: accountSVM, - connect: connectSVM, - } = useConnectionSVM(); + const { account: accountEVM, connect: connectEVM } = useConnectionEVM(); + const { account: accountSVM, connect: connectSVM } = useConnectionSVM(); - const { - toAccountEVM, - toAccountSVM, - handleChangeToAddressEVM, - handleChangeToAddressSVM, - } = useToAccount(outputToken?.chainId); + const toAccountManagement = useToAccount(outputToken?.chainId); const originChainEcosystem = inputToken?.chainId ? getEcosystem(inputToken?.chainId) @@ -100,11 +83,6 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const depositor = originChainEcosystem === "evm" ? accountEVM : accountSVM?.toBase58(); - const recipient = - destinationChainEcosystem === "evm" ? toAccountEVM : toAccountSVM; - - console.log("depositor", depositor); - console.log("recipient", recipient?.address); useEffect(() => { if (defaultRoute.inputToken && defaultRoute.outputToken) { @@ -152,7 +130,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { amount: debouncedAmount, isInputAmount: isAmountOrigin, depositor, - recipient: recipient?.address, + recipient: toAccountManagement.currentRecipientAccount, }); const approvalData: SwapApprovalData | undefined = useMemo(() => { @@ -289,12 +267,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { buttonLabel, // Account management - toAccountEVM, - toAccountSVM, - handleChangeToAddressEVM, - handleChangeToAddressSVM, + toAccountManagement, destinationChainEcosystem, - // Legacy properties isConnected: approvalAction.isConnected, isWrongNetwork: approvalAction.isWrongNetwork, diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index daaeacc46..31f2cb9da 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -27,11 +27,8 @@ export default function SwapAndBridge() { buttonLabel, onConfirm, quoteWarningMessage, - toAccountEVM, - toAccountSVM, - handleChangeToAddressEVM, - handleChangeToAddressSVM, destinationChainEcosystem, + toAccountManagement, } = useSwapAndBridge(); return ( @@ -49,12 +46,8 @@ export default function SwapAndBridge() { expectedOutputAmount={expectedOutputAmount} expectedInputAmount={expectedInputAmount} validationError={validationError} - quoteWarningMessage={quoteWarningMessage} - toAccountEVM={toAccountEVM} - toAccountSVM={toAccountSVM} - handleChangeToAddressEVM={handleChangeToAddressEVM} - handleChangeToAddressSVM={handleChangeToAddressSVM} destinationChainEcosystem={destinationChainEcosystem} + toAccountManagement={toAccountManagement} /> Date: Thu, 23 Oct 2025 12:24:08 +0200 Subject: [PATCH 084/122] fixup Signed-off-by: Gerhard Steenkamp --- src/views/Bridge/components/ChangeAccountModal.tsx | 1 + src/views/SwapAndBridge/components/InputForm.tsx | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/views/Bridge/components/ChangeAccountModal.tsx b/src/views/Bridge/components/ChangeAccountModal.tsx index cfffd1fb9..9490fc667 100644 --- a/src/views/Bridge/components/ChangeAccountModal.tsx +++ b/src/views/Bridge/components/ChangeAccountModal.tsx @@ -126,6 +126,7 @@ export const ChangeAccountModal = ({ Date: Thu, 23 Oct 2025 12:50:39 +0200 Subject: [PATCH 085/122] better quote errors Signed-off-by: Gerhard Steenkamp --- src/utils/errors.ts | 13 +++++++++---- .../components/ConfirmationButton.tsx | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/utils/errors.ts b/src/utils/errors.ts index c2628323e..2f2c9078d 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -103,11 +103,16 @@ export function getQuoteWarningMessage(error: Error | null): string | null { return "This route is currently unavailable. Try different chains or tokens."; case "INVALID_PARAM": - // Show the specific message for invalid params if available - if (message) { - return message; + // return "Invalid parameters. Please check your input and try again."; + if (!message) { + return "Unable to get a quote at this time. Please try again."; } - return "Invalid parameters. Please check your input and try again."; + if ( + message?.includes("doesn't have enough funds to support this deposit") + ) { + return "Amount too high. Try a smaller amount."; + } + return message; // Upstream service errors - be more generic case "UPSTREAM_HTTP_ERROR": diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index e489af8e0..64689cdbf 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -63,6 +63,7 @@ const ExpandableLabelSection: React.FC< validationError?: AmountInputError; validationWarning?: AmountInputError; validationErrorFormatted?: string; + quoteWarningMessage?: string | null; }> > = ({ fee, @@ -74,12 +75,23 @@ const ExpandableLabelSection: React.FC< hasQuote, validationError, validationErrorFormatted, + quoteWarningMessage, }) => { // Render state-specific content let content: React.ReactNode = null; // Show validation messages for all non-ready states - if (state !== "readyToConfirm" && validationErrorFormatted) { + if (quoteWarningMessage) { + // Show quote warning message when ready to confirm but there's a warning + content = ( + <> + + + {quoteWarningMessage} + + + ); + } else if (state !== "readyToConfirm" && validationErrorFormatted) { content = ( <> @@ -202,6 +214,7 @@ export const ConfirmationButton: React.FC = ({ amount, swapQuote, onConfirm, + quoteWarningMessage, validationError, validationWarning, validationErrorFormatted, @@ -272,6 +285,7 @@ export const ConfirmationButton: React.FC = ({ validationError={validationError} validationWarning={validationWarning} validationErrorFormatted={validationErrorFormatted} + quoteWarningMessage={quoteWarningMessage} hasQuote={!!swapQuote} > From 9d2415acefb0b726c0f59446ec56498fddda42cf Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 23 Oct 2025 13:15:08 +0200 Subject: [PATCH 086/122] add native balance support Signed-off-by: Gerhard Steenkamp --- api/user-token-balances.ts | 179 --------------------- api/user-token-balances/_service.ts | 231 ++++++++++++++++++++++++++++ api/user-token-balances/index.ts | 45 ++++++ 3 files changed, 276 insertions(+), 179 deletions(-) delete mode 100644 api/user-token-balances.ts create mode 100644 api/user-token-balances/_service.ts create mode 100644 api/user-token-balances/index.ts diff --git a/api/user-token-balances.ts b/api/user-token-balances.ts deleted file mode 100644 index 61b6b9881..000000000 --- a/api/user-token-balances.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { VercelResponse } from "@vercel/node"; -import { assert, Infer, type } from "superstruct"; -import { TypedVercelRequest } from "./_types"; -import { getLogger, handleErrorCondition, validAddress } from "./_utils"; -import { getAlchemyRpcFromConfigJson } from "./_providers"; -import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; -import { BigNumber } from "ethers"; - -const UserTokenBalancesQueryParamsSchema = type({ - account: validAddress(), -}); - -type UserTokenBalancesQueryParams = Infer< - typeof UserTokenBalancesQueryParamsSchema ->; - -const fetchTokenBalancesForChain = async ( - chainId: number, - account: string -): Promise<{ - chainId: number; - balances: Array<{ address: string; balance: string }>; -}> => { - const logger = getLogger(); - const rpcUrl = getAlchemyRpcFromConfigJson(chainId); - - if (!rpcUrl) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "No Alchemy RPC URL found for chain, returning empty balances", - chainId, - }); - return { - chainId, - balances: [], - }; - } - - try { - const requestBody = { - jsonrpc: "2.0", - id: 1, - method: "alchemy_getTokenBalances", - params: [account], - }; - - logger.debug({ - at: "fetchTokenBalancesForChain", - message: "Making request to Alchemy API", - chainId, - account, - rpcUrl, - }); - - const response = await fetch(rpcUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "HTTP error from Alchemy API, returning empty balances", - chainId, - status: response.status, - statusText: response.statusText, - }); - return { - chainId, - balances: [], - }; - } - - const data = await response.json(); - - logger.debug({ - at: "fetchTokenBalancesForChain", - message: "Received response from Alchemy API", - chainId, - responseData: data, - }); - - // Validate the response structure - if (!data || !data.result || !data.result.tokenBalances) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "Invalid response from Alchemy API, returning empty balances", - chainId, - responseData: data, - }); - return { - chainId, - balances: [], - }; - } - - const balances = ( - data.result.tokenBalances as { - contractAddress: string; - tokenBalance: string; - }[] - ) - .filter((t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0)) - .map((t) => ({ - address: t.contractAddress, - balance: BigNumber.from(t.tokenBalance).toString(), - })); - - return { - chainId, - balances, - }; - } catch (error) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: - "Error fetching token balances from Alchemy API, returning empty balances", - chainId, - error: error instanceof Error ? error.message : String(error), - }); - return { - chainId, - balances: [], - }; - } -}; - -const handler = async ( - request: TypedVercelRequest, - response: VercelResponse -) => { - const logger = getLogger(); - - try { - const { query } = request; - assert(query, UserTokenBalancesQueryParamsSchema); - const { account } = query; - - // Get all available chain IDs that have Alchemy RPC URLs - const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs) - .sort((a, b) => a - b) - .filter((chainId) => !!getAlchemyRpcFromConfigJson(chainId)); - - // Fetch balances for all chains in parallel - const balancePromises = chainIdsAvailable.map((chainId) => - fetchTokenBalancesForChain(chainId, account) - ); - - const chainBalances = await Promise.all(balancePromises); - - const responseData = { - account, - balances: chainBalances.map(({ chainId, balances }) => ({ - chainId: chainId.toString(), - balances, - })), - }; - - logger.debug({ - at: "UserTokenBalances", - message: "Response data", - responseJson: responseData, - }); - - // Cache for 3 minutes - response.setHeader( - "Cache-Control", - "s-maxage=180, stale-while-revalidate=60" - ); - response.status(200).json(responseData); - } catch (error: unknown) { - return handleErrorCondition("user-token-balances", response, logger, error); - } -}; - -export default handler; diff --git a/api/user-token-balances/_service.ts b/api/user-token-balances/_service.ts new file mode 100644 index 000000000..d088d7e53 --- /dev/null +++ b/api/user-token-balances/_service.ts @@ -0,0 +1,231 @@ +import { BigNumber, ethers } from "ethers"; +import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; +import { getLogger } from "../_utils"; +import { getAlchemyRpcFromConfigJson } from "../_providers"; + +const logger = getLogger(); + +const fetchNativeBalance = async ( + chainId: number, + account: string, + rpcUrl: string +): Promise => { + try { + const requestBody = { + jsonrpc: "2.0", + id: 1, + method: "eth_getBalance", + params: [account, "latest"], + }; + + logger.debug({ + at: "fetchNativeBalance", + message: "Fetching native balance", + chainId, + account, + }); + + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + logger.warn({ + at: "fetchNativeBalance", + message: "HTTP error fetching native balance", + chainId, + status: response.status, + statusText: response.statusText, + }); + return null; + } + + const data = await response.json(); + + if (!data || !data.result) { + logger.warn({ + at: "fetchNativeBalance", + message: "Invalid response for native balance", + chainId, + responseData: data, + }); + return null; + } + + return BigNumber.from(data.result).toString(); + } catch (error) { + logger.warn({ + at: "fetchNativeBalance", + message: "Error fetching native balance", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } +}; + +export const fetchTokenBalancesForChain = async ( + chainId: number, + account: string +): Promise<{ + chainId: number; + balances: Array<{ address: string; balance: string }>; +}> => { + const rpcUrl = getAlchemyRpcFromConfigJson(chainId); + + if (!rpcUrl) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "No Alchemy RPC URL found for chain, returning empty balances", + chainId, + }); + return { + chainId, + balances: [], + }; + } + + try { + // Fetch both ERC20 and native balances in parallel + const [erc20Response, nativeBalance] = await Promise.all([ + fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "alchemy_getTokenBalances", + params: [account], // TODO: explicitly add token addresses for each chain + }), + }), + fetchNativeBalance(chainId, account, rpcUrl), + ]); + + logger.debug({ + at: "fetchTokenBalancesForChain", + message: "Making request to Alchemy API for ERC20 tokens", + chainId, + account, + rpcUrl, + }); + + if (!erc20Response.ok) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "HTTP error from Alchemy API, returning empty balances", + chainId, + status: erc20Response.status, + statusText: erc20Response.statusText, + }); + return { + chainId, + balances: [], + }; + } + + const data = await erc20Response.json(); + + logger.debug({ + at: "fetchTokenBalancesForChain", + message: "Received response from Alchemy API", + chainId, + responseData: data, + }); + + // Validate the response structure + if (!data || !data.result || !data.result.tokenBalances) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "Invalid response from Alchemy API, returning empty balances", + chainId, + responseData: data, + }); + return { + chainId, + balances: [], + }; + } + + const erc20Balances = ( + data.result.tokenBalances as { + contractAddress: string; + tokenBalance: string; + }[] + ) + .filter((t) => { + if (!t.tokenBalance) return false; + try { + const balance = BigNumber.from(t.tokenBalance); + // Filter out zero balances and MaxUint256 (Alchemy sometimes borks and returns MaxUint256) + return balance.gt(0) && balance.lt(ethers.constants.MaxUint256); + } catch (error) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "Invalid token balance value", + chainId, + tokenAddress: t.contractAddress, + tokenBalance: t.tokenBalance, + }); + return false; + } + }) + .map((t) => ({ + address: t.contractAddress, + balance: BigNumber.from(t.tokenBalance).toString(), + })); + + // Add native balance if it exists and is greater than 0 + const balances = [...erc20Balances]; + if (nativeBalance && BigNumber.from(nativeBalance).gt(0)) { + balances.unshift({ + address: ethers.constants.AddressZero, + balance: nativeBalance, + }); + } + + return { + chainId, + balances, + }; + } catch (error) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: + "Error fetching token balances from Alchemy API, returning empty balances", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return { + chainId, + balances: [], + }; + } +}; + +export const handleUserTokenBalances = async (account: string) => { + // Get all available chain IDs that have Alchemy RPC URLs + const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs) + .sort((a, b) => a - b) + .filter((chainId) => !!getAlchemyRpcFromConfigJson(chainId)); + + // Fetch balances for all chains in parallel + const balancePromises = chainIdsAvailable.map((chainId) => + fetchTokenBalancesForChain(chainId, account) + ); + + const chainBalances = await Promise.all(balancePromises); + + return { + account, + balances: chainBalances.map(({ chainId, balances }) => ({ + chainId: chainId.toString(), + balances, + })), + }; +}; diff --git a/api/user-token-balances/index.ts b/api/user-token-balances/index.ts new file mode 100644 index 000000000..4f3dc7338 --- /dev/null +++ b/api/user-token-balances/index.ts @@ -0,0 +1,45 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, type } from "superstruct"; +import { TypedVercelRequest } from "../_types"; +import { getLogger, handleErrorCondition, validAddress } from "../_utils"; +import { handleUserTokenBalances } from "./_service"; + +const UserTokenBalancesQueryParamsSchema = type({ + account: validAddress(), +}); + +type UserTokenBalancesQueryParams = Infer< + typeof UserTokenBalancesQueryParamsSchema +>; + +const handler = async ( + request: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + + try { + const { query } = request; + assert(query, UserTokenBalancesQueryParamsSchema); + const { account } = query; + + const responseData = await handleUserTokenBalances(account); + + logger.debug({ + at: "UserTokenBalances", + message: "Response data", + responseJson: responseData, + }); + + // Cache for 3 minutes + response.setHeader( + "Cache-Control", + "s-maxage=180, stale-while-revalidate=60" + ); + response.status(200).json(responseData); + } catch (error: unknown) { + return handleErrorCondition("user-token-balances", response, logger, error); + } +}; + +export default handler; From af3e60610e1a4faee4c11de0d694f1e1228d78f0 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 23 Oct 2025 15:43:41 +0200 Subject: [PATCH 087/122] refactor quoteError logic Signed-off-by: Gerhard Steenkamp --- .../Bridge/components/ChangeAccountModal.tsx | 1 + .../components/ConfirmationButton.tsx | 5 +-- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 21 +++++++------ .../hooks/useValidateSwapAndBridge.ts | 31 +++++++------------ 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/views/Bridge/components/ChangeAccountModal.tsx b/src/views/Bridge/components/ChangeAccountModal.tsx index 9490fc667..6fe80f7ff 100644 --- a/src/views/Bridge/components/ChangeAccountModal.tsx +++ b/src/views/Bridge/components/ChangeAccountModal.tsx @@ -160,6 +160,7 @@ export const ChangeAccountModal = ({ }; const ResetButton = styled.button` + font-size: 14px; color: ${COLORS.aqua}; padding-block: 0px; `; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 64689cdbf..fc8479425 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -29,7 +29,8 @@ export type BridgeButtonState = | "submitting" | "wrongNetwork" | "loadingQuote" - | "validationError"; + | "validationError" + | "quoteError"; interface ConfirmationButtonProps extends ButtonHTMLAttributes { @@ -81,7 +82,7 @@ const ExpandableLabelSection: React.FC< let content: React.ReactNode = null; // Show validation messages for all non-ready states - if (quoteWarningMessage) { + if (quoteWarningMessage && state === "quoteError") { // Show quote warning message when ready to confirm but there's a warning content = ( <> diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 0f65a8557..d7fdc2e38 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -123,7 +123,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const { data: swapQuote, isLoading: isQuoteLoading, - error, + error: quoteError, } = useSwapQuote({ origin: inputToken ? inputToken : null, destination: outputToken ? outputToken : null, @@ -150,8 +150,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { amount, isAmountOrigin, inputToken, - outputToken, - error + outputToken ); const expectedInputAmount = useMemo(() => { @@ -196,6 +195,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { // Button state logic const buttonState: BridgeButtonState = useMemo(() => { if (isQuoteLoading) return "loadingQuote"; + if (quoteError) return "quoteError"; if (!approvalAction.isConnected) return "notConnected"; if (approvalAction.isButtonActionLoading) return "submitting"; if (!inputToken || !outputToken) return "awaitingTokenSelection"; @@ -203,12 +203,13 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { if (validation.error) return "validationError"; return "readyToConfirm"; }, [ - approvalAction.isButtonActionLoading, + isQuoteLoading, + quoteError, approvalAction.isConnected, + approvalAction.isButtonActionLoading, inputToken, outputToken, amount, - isQuoteLoading, validation.error, ]); @@ -235,10 +236,9 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { ] ); - const quoteWarningMessage = useMemo( - () => getQuoteWarningMessage(error), - [error] - ); + const quoteWarningMessage = useMemo(() => { + return getQuoteWarningMessage(quoteError); + }, [quoteError]); return { inputToken, @@ -274,7 +274,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { isWrongNetwork: approvalAction.isWrongNetwork, isSubmitting: approvalAction.isButtonActionLoading, onConfirm, - quoteError: error, + quoteError, quoteWarningMessage, }; } @@ -284,6 +284,7 @@ const buttonLabels: Record = { awaitingTokenSelection: "Confirm Swap", awaitingAmountInput: "Confirm Swap", readyToConfirm: "Confirm Swap", + quoteError: "Confirm Swap", submitting: "Confirming...", wrongNetwork: "Confirm Swap", loadingQuote: "Finalizing quote...", diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts index f63a95119..f9376482a 100644 --- a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -1,16 +1,10 @@ import { useMemo } from "react"; import { BigNumber } from "ethers"; -import axios, { AxiosError } from "axios"; import { AmountInputError } from "../../Bridge/utils"; import { EnrichedToken } from "../components/ChainTokenSelector/Modal"; import { validationErrorTextMap } from "views/Bridge/components/AmountInput"; -type AcrossApiErrorResponse = { - code?: string; - message?: string; -}; - export type ValidationResult = { error?: AmountInputError; warn?: AmountInputError; @@ -21,8 +15,7 @@ export function useValidateSwapAndBridge( amount: BigNumber | null, isAmountOrigin: boolean, inputToken: EnrichedToken | null, - outputToken: EnrichedToken | null, - error: Error | null + outputToken: EnrichedToken | null ): ValidationResult { const validation = useMemo(() => { let errorType: AmountInputError | undefined = undefined; @@ -49,16 +42,16 @@ export function useValidateSwapAndBridge( errorType = AmountInputError.INSUFFICIENT_BALANCE; } } - // backend availability - if (!errorType && error && axios.isAxiosError(error)) { - const axiosError = error as AxiosError; - const code = axiosError.response?.data?.code; - if (code === "AMOUNT_TOO_LOW") { - errorType = AmountInputError.AMOUNT_TOO_LOW; - } else if (code === "SWAP_QUOTE_UNAVAILABLE") { - errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; - } - } + // // backend availability + // if (!errorType && error && axios.isAxiosError(error)) { + // const axiosError = error as AxiosError; + // const code = axiosError.response?.data?.code; + // if (code === "AMOUNT_TOO_LOW") { + // errorType = AmountInputError.AMOUNT_TOO_LOW; + // } else if (code === "SWAP_QUOTE_UNAVAILABLE") { + // errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; + // } + // } return { error: errorType, warn: undefined as AmountInputError | undefined, @@ -68,7 +61,7 @@ export function useValidateSwapAndBridge( outputToken, }), }; - }, [amount, isAmountOrigin, inputToken, outputToken, error]); + }, [amount, isAmountOrigin, inputToken, outputToken]); return validation; } From 5a43e32c660f25e1bddea4115c20147db8aaf488 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 16:00:48 +0200 Subject: [PATCH 088/122] specify tokens, add solana support to balance endpoint Signed-off-by: Gerhard Steenkamp --- api/_balance.ts | 2 +- api/user-token-balances/_service.ts | 223 ++- src/data/swap-tokens.json | 2201 +++++++++++++++------------ 3 files changed, 1393 insertions(+), 1033 deletions(-) diff --git a/api/_balance.ts b/api/_balance.ts index a2fbc1107..dcf68f788 100644 --- a/api/_balance.ts +++ b/api/_balance.ts @@ -33,7 +33,7 @@ export async function getBalance( ); } -async function getSvmBalance( +export async function getSvmBalance( chainId: string | number, account: string, token: string diff --git a/api/user-token-balances/_service.ts b/api/user-token-balances/_service.ts index d088d7e53..3c89705ba 100644 --- a/api/user-token-balances/_service.ts +++ b/api/user-token-balances/_service.ts @@ -1,23 +1,74 @@ import { BigNumber, ethers } from "ethers"; import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; +import * as sdk from "@across-protocol/sdk"; import { getLogger } from "../_utils"; import { getAlchemyRpcFromConfigJson } from "../_providers"; +import { isSvmAddress } from "../_address"; +import { getSvmBalance } from "../_balance"; +import { fetchSwapTokensData, SwapToken } from "../swap/tokens/_service"; const logger = getLogger(); +async function getSwapTokens(): Promise { + try { + return await fetchSwapTokensData(); + } catch (error) { + logger.warn({ + at: "getSwapTokens", + message: "Failed to fetch swap tokens", + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} + +function getEvmChainIds(): number[] { + return Object.values(MAINNET_CHAIN_IDs) + .filter((chainId) => !sdk.utils.chainIsSvm(chainId)) + .filter((chainId) => !!getAlchemyRpcFromConfigJson(chainId)) + .sort((a, b) => a - b); +} + +function getSvmChainIds(): number[] { + return Object.values(MAINNET_CHAIN_IDs) + .filter((chainId) => sdk.utils.chainIsSvm(chainId)) + .sort((a, b) => a - b); +} + +function getTokenAddressesForChain( + swapTokens: SwapToken[], + chainId: number +): string[] { + const tokens = swapTokens + .filter((token) => token.chainId === chainId) + .map((token) => token.address); + + // Remove duplicates and filter out native token (AddressZero) since we fetch it separately + return Array.from( + new Set(tokens.filter((addr) => addr !== ethers.constants.AddressZero)) + ); +} + +function getSvmTokenAddressesForChain( + swapTokens: SwapToken[], + chainId: number +): string[] { + const tokens = swapTokens + .filter((token) => token.chainId === chainId) + .map((token) => token.address); + + // Remove duplicates and filter out native token (zero address) since we fetch it separately + return Array.from( + new Set(tokens.filter((addr) => addr !== sdk.constants.ZERO_ADDRESS)) + ); +} + const fetchNativeBalance = async ( chainId: number, account: string, rpcUrl: string ): Promise => { try { - const requestBody = { - jsonrpc: "2.0", - id: 1, - method: "eth_getBalance", - params: [account, "latest"], - }; - logger.debug({ at: "fetchNativeBalance", message: "Fetching native balance", @@ -30,7 +81,12 @@ const fetchNativeBalance = async ( headers: { "Content-Type": "application/json", }, - body: JSON.stringify(requestBody), + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "eth_getBalance", + params: [account, "latest"], + }), }); if (!response.ok) { @@ -68,9 +124,76 @@ const fetchNativeBalance = async ( } }; +const fetchSolanaTokenBalances = async ( + chainId: number, + account: string, + tokenAddresses: string[] +): Promise<{ + chainId: number; + balances: Array<{ address: string; balance: string }>; +}> => { + try { + logger.debug({ + at: "fetchSolanaTokenBalances", + message: "Fetching Solana token balances", + chainId, + account, + tokenAddressCount: tokenAddresses.length, + }); + + // Include native SOL balance (zero address) + const allTokenAddresses = [sdk.constants.ZERO_ADDRESS, ...tokenAddresses]; + + // Fetch balances for all tokens in parallel + const balancePromises = allTokenAddresses.map(async (tokenAddress) => { + try { + const balance = await getSvmBalance(chainId, account, tokenAddress); + return { + address: tokenAddress, + balance: balance.toString(), + }; + } catch (error) { + logger.warn({ + at: "fetchSolanaTokenBalances", + message: "Error fetching balance for token", + chainId, + tokenAddress, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } + }); + + const results = await Promise.all(balancePromises); + + // Filter out null results and zero balances + const balances = results.filter( + (result): result is { address: string; balance: string } => + result !== null && BigNumber.from(result.balance).gt(0) + ); + + return { + chainId, + balances, + }; + } catch (error) { + logger.warn({ + at: "fetchSolanaTokenBalances", + message: "Error fetching Solana token balances, returning empty balances", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return { + chainId, + balances: [], + }; + } +}; + export const fetchTokenBalancesForChain = async ( chainId: number, - account: string + account: string, + tokenAddresses: string[] ): Promise<{ chainId: number; balances: Array<{ address: string; balance: string }>; @@ -101,7 +224,7 @@ export const fetchTokenBalancesForChain = async ( jsonrpc: "2.0", id: 1, method: "alchemy_getTokenBalances", - params: [account], // TODO: explicitly add token addresses for each chain + params: [account, tokenAddresses], }), }), fetchNativeBalance(chainId, account, rpcUrl), @@ -209,15 +332,81 @@ export const fetchTokenBalancesForChain = async ( }; export const handleUserTokenBalances = async (account: string) => { - // Get all available chain IDs that have Alchemy RPC URLs - const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs) - .sort((a, b) => a - b) - .filter((chainId) => !!getAlchemyRpcFromConfigJson(chainId)); + // Check if the account is a Solana address + const isSolanaAddress = isSvmAddress(account); + + if (isSolanaAddress) { + // For SVM addresses, fetch all SVM chain balances + logger.debug({ + at: "handleUserTokenBalances", + message: "Detected SVM address, fetching SVM balances", + account, + }); + + const svmChainIds = getSvmChainIds(); + + // Fetch swap tokens to get the list of token addresses for each chain + const swapTokens = await getSwapTokens(); + + logger.debug({ + at: "handleUserTokenBalances", + message: "Fetched swap tokens for SVM", + tokenCount: swapTokens.length, + }); + + // Fetch balances for all SVM chains in parallel + const balancePromises = svmChainIds.map((chainId) => { + const tokenAddresses = getSvmTokenAddressesForChain(swapTokens, chainId); + logger.debug({ + at: "handleUserTokenBalances", + message: "Token addresses for SVM chain", + chainId, + tokenAddressCount: tokenAddresses.length, + }); + return fetchSolanaTokenBalances(chainId, account, tokenAddresses); + }); + + const chainBalances = await Promise.all(balancePromises); + + return { + account, + balances: chainBalances.map(({ chainId, balances }) => ({ + chainId: chainId.toString(), + balances, + })), + }; + } + + // For EVM addresses, fetch all EVM chain balances + logger.debug({ + at: "handleUserTokenBalances", + message: "Detected EVM address, fetching EVM balances", + account, + }); + + // Get all available EVM chain IDs that have Alchemy RPC URLs + const chainIdsAvailable = getEvmChainIds(); + + // Fetch swap tokens to get the list of token addresses for each chain + const swapTokens = await getSwapTokens(); + + logger.debug({ + at: "handleUserTokenBalances", + message: "Fetched swap tokens", + tokenCount: swapTokens.length, + }); // Fetch balances for all chains in parallel - const balancePromises = chainIdsAvailable.map((chainId) => - fetchTokenBalancesForChain(chainId, account) - ); + const balancePromises = chainIdsAvailable.map((chainId) => { + const tokenAddresses = getTokenAddressesForChain(swapTokens, chainId); + logger.debug({ + at: "handleUserTokenBalances", + message: "Token addresses for chain", + chainId, + tokenAddressCount: tokenAddresses.length, + }); + return fetchTokenBalancesForChain(chainId, account, tokenAddresses); + }); const chainBalances = await Promise.all(balancePromises); diff --git a/src/data/swap-tokens.json b/src/data/swap-tokens.json index e7d0aea55..8dad11b4e 100644 --- a/src/data/swap-tokens.json +++ b/src/data/swap-tokens.json @@ -7,7 +7,7 @@ "symbol": "1INCH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", - "priceUsd": "0.24835" + "priceUsd": "0.181256" }, { "chainId": 1, @@ -16,7 +16,7 @@ "symbol": "A8", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/39170/standard/A8_Token-04_200x200.png?1720798300", - "priceUsd": "0.08491677302193065" + "priceUsd": null }, { "chainId": 1, @@ -25,7 +25,7 @@ "symbol": "AAVE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", - "priceUsd": "276.06" + "priceUsd": "236.62" }, { "chainId": 1, @@ -34,7 +34,7 @@ "symbol": "ABT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2341/thumb/arcblock.png?1547036543", - "priceUsd": "0.6021527578836832" + "priceUsd": null }, { "chainId": 1, @@ -43,7 +43,7 @@ "symbol": "ACH", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/12390/thumb/ACH_%281%29.png?1599691266", - "priceUsd": "0.018528970984471187" + "priceUsd": null }, { "chainId": 1, @@ -52,7 +52,7 @@ "symbol": "ACX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", - "priceUsd": "0.111262" + "priceUsd": "0.071962" }, { "chainId": 1, @@ -61,7 +61,7 @@ "symbol": "ADX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", - "priceUsd": null + "priceUsd": "0.11783361110879194" }, { "chainId": 1, @@ -70,7 +70,7 @@ "symbol": "AERGO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4490/thumb/aergo.png?1647696770", - "priceUsd": "0.12097457985117842" + "priceUsd": null }, { "chainId": 1, @@ -79,7 +79,7 @@ "symbol": "AEVO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35893/standard/aevo.png", - "priceUsd": "0.096988" + "priceUsd": "0.066092" }, { "chainId": 1, @@ -88,7 +88,7 @@ "symbol": "agEUR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", - "priceUsd": "1.1635785053489096" + "priceUsd": "1.17" }, { "chainId": 1, @@ -97,7 +97,7 @@ "symbol": "AGLD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", - "priceUsd": "0.6450181756960583" + "priceUsd": "0.6171718228097985" }, { "chainId": 1, @@ -106,7 +106,7 @@ "symbol": "AIOZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", - "priceUsd": "0.269038" + "priceUsd": "0.197563" }, { "chainId": 1, @@ -115,7 +115,7 @@ "symbol": "ALCX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14113/thumb/Alchemix.png?1614409874", - "priceUsd": "8.75" + "priceUsd": "7.68" }, { "chainId": 1, @@ -124,7 +124,7 @@ "symbol": "ALEPH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", - "priceUsd": "0.06990075260438454" + "priceUsd": "0.06419819501317577" }, { "chainId": 1, @@ -133,7 +133,7 @@ "symbol": "ALI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", - "priceUsd": "0.005723599473239193" + "priceUsd": "0.004086497830883674" }, { "chainId": 1, @@ -142,7 +142,7 @@ "symbol": "ALICE", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", - "priceUsd": "0.3825432255973373" + "priceUsd": "0.3288120182166636" }, { "chainId": 1, @@ -151,7 +151,7 @@ "symbol": "ALPHA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", - "priceUsd": "0.01461347" + "priceUsd": "0.009853618643575719" }, { "chainId": 1, @@ -160,7 +160,7 @@ "symbol": "ALT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/34608/standard/Logomark_200x200.png", - "priceUsd": "0.02778276" + "priceUsd": "0.019757" }, { "chainId": 1, @@ -169,7 +169,7 @@ "symbol": "AMP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12409/thumb/amp-200x200.png?1599625397", - "priceUsd": "0.00299801" + "priceUsd": "0.00263972" }, { "chainId": 1, @@ -178,7 +178,7 @@ "symbol": "ANKR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", - "priceUsd": "0.013846381285394373" + "priceUsd": "0.010265479498987028" }, { "chainId": 1, @@ -187,7 +187,7 @@ "symbol": "ANT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/681/thumb/JelZ58cv_400x400.png?1601449653", - "priceUsd": "0.49893083327521964" + "priceUsd": "0.47482451670962345" }, { "chainId": 1, @@ -196,7 +196,7 @@ "symbol": "APE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", - "priceUsd": "0.5624223258266647" + "priceUsd": "0.442565" }, { "chainId": 1, @@ -205,7 +205,7 @@ "symbol": "API3", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13256/thumb/api3.jpg?1606751424", - "priceUsd": "0.823883" + "priceUsd": "0.688422" }, { "chainId": 1, @@ -214,7 +214,7 @@ "symbol": "APU", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/35986/large/200x200.png?1710308147", - "priceUsd": "0.00013068" + "priceUsd": null }, { "chainId": 1, @@ -223,7 +223,7 @@ "symbol": "ARB", "decimals": 18, "logoUrl": "https://arbitrum.foundation/logo.png", - "priceUsd": "0.418755" + "priceUsd": "0.332923" }, { "chainId": 1, @@ -232,7 +232,7 @@ "symbol": "ARKM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/30929/standard/Arkham_Logo_CG.png?1696529771", - "priceUsd": "0.38184467298899505" + "priceUsd": "0.34863415698207295" }, { "chainId": 1, @@ -241,7 +241,7 @@ "symbol": "ARPA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", - "priceUsd": "0.009285973685280075" + "priceUsd": "0.013011234437794785" }, { "chainId": 1, @@ -250,7 +250,7 @@ "symbol": "ASH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15714/thumb/omnPqaTY.png?1622820503", - "priceUsd": "0.9247673787550553" + "priceUsd": "0.7407360380001773" }, { "chainId": 1, @@ -259,7 +259,7 @@ "symbol": "ASM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11605/thumb/gpvrlkSq_400x400_%281%29.jpg?1591775789", - "priceUsd": "0.06466856607775083" + "priceUsd": "0.01432904" }, { "chainId": 1, @@ -268,7 +268,7 @@ "symbol": "AST", "decimals": 4, "logoUrl": "https://assets.coingecko.com/coins/images/1019/thumb/Airswap.png?1630903484", - "priceUsd": "0.029853760156180194" + "priceUsd": "0.026353682441322236" }, { "chainId": 1, @@ -277,7 +277,7 @@ "symbol": "ATA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", - "priceUsd": "0.040714414720865906" + "priceUsd": "0.030819723895733654" }, { "chainId": 1, @@ -286,7 +286,7 @@ "symbol": "ATH", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/36179/large/logogram_circle_dark_green_vb_green_(1).png?1718232706", - "priceUsd": "0.052467" + "priceUsd": "0.03011729" }, { "chainId": 1, @@ -295,7 +295,7 @@ "symbol": "AUCTION", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13860/thumb/1_KtgpRIJzuwfHe0Rl0avP_g.jpeg?1612412025", - "priceUsd": "8.154566945593762" + "priceUsd": null }, { "chainId": 1, @@ -304,7 +304,7 @@ "symbol": "AUDIO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12913/thumb/AudiusCoinLogo_2x.png?1603425727", - "priceUsd": "0.054061721237164004" + "priceUsd": "0.04140526" }, { "chainId": 1, @@ -322,7 +322,7 @@ "symbol": "AXL", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", - "priceUsd": "0.2929734557331972" + "priceUsd": "0.18970002045216927" }, { "chainId": 1, @@ -331,7 +331,7 @@ "symbol": "AXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", - "priceUsd": "2.11" + "priceUsd": "1.61" }, { "chainId": 1, @@ -340,7 +340,7 @@ "symbol": "BADGER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", - "priceUsd": "0.97573" + "priceUsd": "0.830417" }, { "chainId": 1, @@ -349,7 +349,7 @@ "symbol": "BAL", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", - "priceUsd": "1.14" + "priceUsd": "1.034" }, { "chainId": 1, @@ -358,7 +358,7 @@ "symbol": "BAND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9545/thumb/band-protocol.png?1568730326", - "priceUsd": "0.6758955604053999" + "priceUsd": "0.5475757187080764" }, { "chainId": 1, @@ -367,7 +367,7 @@ "symbol": "BAT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", - "priceUsd": "0.1559633056201925" + "priceUsd": "0.168896" }, { "chainId": 1, @@ -376,7 +376,7 @@ "symbol": "BEAM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/32417/standard/chain-logo.png?1698114384", - "priceUsd": "0.00874505" + "priceUsd": null }, { "chainId": 1, @@ -385,7 +385,7 @@ "symbol": "BICO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", - "priceUsd": "0.09065698071630358" + "priceUsd": "0.06535592485080026" }, { "chainId": 1, @@ -394,7 +394,7 @@ "symbol": "BIGTIME", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/32251/standard/-6136155493475923781_121.jpg?1696998691", - "priceUsd": "0.047348411285928906" + "priceUsd": "0.034735639950209556" }, { "chainId": 1, @@ -403,7 +403,7 @@ "symbol": "BIO", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/53022/large/bio.jpg?1735011002", - "priceUsd": "0.119872" + "priceUsd": "0.089941" }, { "chainId": 1, @@ -412,7 +412,7 @@ "symbol": "BIT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17627/thumb/rI_YptK8.png?1653983088", - "priceUsd": "2.550847669016368" + "priceUsd": "1.69" }, { "chainId": 1, @@ -421,7 +421,7 @@ "symbol": "BITCOIN", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", - "priceUsd": "0.096997" + "priceUsd": "0.064113" }, { "chainId": 1, @@ -430,7 +430,7 @@ "symbol": "BLUR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28453/large/blur.png?1670745921", - "priceUsd": "0.0870963325585314" + "priceUsd": "0.07043845155023096" }, { "chainId": 1, @@ -439,7 +439,7 @@ "symbol": "BLZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2848/thumb/ColorIcon_3x.png?1622516510", - "priceUsd": "0.030750981687807866" + "priceUsd": "0.02445198964729434" }, { "chainId": 1, @@ -448,7 +448,7 @@ "symbol": "BNT", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", - "priceUsd": "0.684579" + "priceUsd": "0.621654" }, { "chainId": 1, @@ -457,7 +457,7 @@ "symbol": "BOBA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20285/thumb/BOBA.png?1636811576", - "priceUsd": "0.08686545841487457" + "priceUsd": "0.06611297783392729" }, { "chainId": 1, @@ -466,7 +466,7 @@ "symbol": "BOND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", - "priceUsd": "0.15236644952732825" + "priceUsd": "0.13820539860376949" }, { "chainId": 1, @@ -475,7 +475,7 @@ "symbol": "BTRST", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18100/thumb/braintrust.PNG?1630475394", - "priceUsd": "0.3867799487884984" + "priceUsd": "0.4163225123616608" }, { "chainId": 1, @@ -484,7 +484,7 @@ "symbol": "BUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", - "priceUsd": "0.9899099170252645" + "priceUsd": "0.996965" }, { "chainId": 1, @@ -502,7 +502,7 @@ "symbol": "CAKE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/12632/large/pancakeswap-cake-logo_%281%29.png?1696512440", - "priceUsd": "3.81" + "priceUsd": "2.72" }, { "chainId": 1, @@ -511,7 +511,7 @@ "symbol": "cbBTC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/40143/standard/cbbtc.webp", - "priceUsd": null + "priceUsd": "115346" }, { "chainId": 1, @@ -520,7 +520,7 @@ "symbol": "cbETH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/27008/large/cbeth.png", - "priceUsd": "4816.72" + "priceUsd": "4577.84" }, { "chainId": 1, @@ -529,7 +529,7 @@ "symbol": "CELO", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", - "priceUsd": "0.42952459484694416" + "priceUsd": "0.2803471390763023" }, { "chainId": 1, @@ -538,7 +538,7 @@ "symbol": "CELR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4379/thumb/Celr.png?1554705437", - "priceUsd": "0.0077831751546029015" + "priceUsd": "0.005787315951236763" }, { "chainId": 1, @@ -547,7 +547,7 @@ "symbol": "CHR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/5000/thumb/Chromia.png?1559038018", - "priceUsd": "0.08327870294975556" + "priceUsd": "0.07588975554529749" }, { "chainId": 1, @@ -556,7 +556,7 @@ "symbol": "CHZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8834/thumb/Chiliz.png?1561970540", - "priceUsd": "0.04046281519762788" + "priceUsd": "0.033057851314099294" }, { "chainId": 1, @@ -565,7 +565,7 @@ "symbol": "CLV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15278/thumb/clover.png?1645084454", - "priceUsd": "0.021274833674050836" + "priceUsd": "0.07416575360535407" }, { "chainId": 1, @@ -574,7 +574,7 @@ "symbol": "COMP", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", - "priceUsd": "41.59113915910497" + "priceUsd": "38.06" }, { "chainId": 1, @@ -583,7 +583,7 @@ "symbol": "CORN", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54471/large/corn.jpg?1739933588", - "priceUsd": "0.096562" + "priceUsd": null }, { "chainId": 1, @@ -592,7 +592,7 @@ "symbol": "COTI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2962/thumb/Coti.png?1559653863", - "priceUsd": "0.05172350426311411" + "priceUsd": "0.035405160776626546" }, { "chainId": 1, @@ -601,7 +601,7 @@ "symbol": "COVAL", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/588/thumb/coval-logo.png?1599493950", - "priceUsd": "0.0006174723528966431" + "priceUsd": "0.0005187511296123356" }, { "chainId": 1, @@ -610,7 +610,7 @@ "symbol": "COW", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24384/large/CoW-token_logo.png?1719524382", - "priceUsd": "0.275109" + "priceUsd": "0.239954" }, { "chainId": 1, @@ -619,7 +619,7 @@ "symbol": "CPOOL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19252/large/photo_2022-08-31_12.45.02.jpeg?1696518697", - "priceUsd": "0.124843" + "priceUsd": "0.089267" }, { "chainId": 1, @@ -628,7 +628,7 @@ "symbol": "CQT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14168/thumb/covalent-cqt.png?1624545218", - "priceUsd": "0.0012093983234396837" + "priceUsd": "0.001237090204263367" }, { "chainId": 1, @@ -637,7 +637,7 @@ "symbol": "CRO", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", - "priceUsd": "0.192116" + "priceUsd": "0.158544" }, { "chainId": 1, @@ -646,7 +646,7 @@ "symbol": "CRPT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1901/thumb/crypt.png?1547036205", - "priceUsd": "0.0006950436675444071" + "priceUsd": null }, { "chainId": 1, @@ -655,7 +655,7 @@ "symbol": "CRV", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", - "priceUsd": "0.719583" + "priceUsd": "0.567487" }, { "chainId": 1, @@ -664,7 +664,7 @@ "symbol": "CTSI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", - "priceUsd": "0.08272383644510108" + "priceUsd": "0.07898759317050742" }, { "chainId": 1, @@ -673,7 +673,7 @@ "symbol": "CTX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14932/thumb/glossy_icon_-_C200px.png?1619073171", - "priceUsd": "1.3129731831764002" + "priceUsd": "1.2665918771446754" }, { "chainId": 1, @@ -682,7 +682,7 @@ "symbol": "CUBE", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/10687/thumb/CUBE_icon.png?1617026861", - "priceUsd": "0.30914320431849696" + "priceUsd": "0.25577027904299027" }, { "chainId": 1, @@ -691,7 +691,7 @@ "symbol": "CVC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", - "priceUsd": "0.07239176819765296" + "priceUsd": "0.05775781654568151" }, { "chainId": 1, @@ -700,7 +700,7 @@ "symbol": "CVX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", - "priceUsd": "3.29" + "priceUsd": "2.33" }, { "chainId": 1, @@ -709,7 +709,7 @@ "symbol": "CXT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/39177/large/CXT_Ticker.png?1720829918", - "priceUsd": "0.02199355566421097" + "priceUsd": null }, { "chainId": 1, @@ -718,7 +718,7 @@ "symbol": "DAI", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", - "priceUsd": "0.999769" + "priceUsd": "0.999565" }, { "chainId": 1, @@ -727,7 +727,7 @@ "symbol": "DAR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/19837/thumb/dar.png?1636014223", - "priceUsd": "0.04350571850144363" + "priceUsd": null }, { "chainId": 1, @@ -736,7 +736,7 @@ "symbol": "DDX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13453/thumb/ddx_logo.png?1608741641", - "priceUsd": "0.060304550074008534" + "priceUsd": "0.04049329577324592" }, { "chainId": 1, @@ -745,7 +745,7 @@ "symbol": "DENT", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/1152/thumb/gLCEA2G.png?1604543239", - "priceUsd": "0.0006067585333378868" + "priceUsd": null }, { "chainId": 1, @@ -754,7 +754,7 @@ "symbol": "DEXT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", - "priceUsd": "0.468019" + "priceUsd": "0.358769" }, { "chainId": 1, @@ -763,7 +763,7 @@ "symbol": "DIA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", - "priceUsd": "0.5350519303665007" + "priceUsd": "0.626901" }, { "chainId": 1, @@ -772,7 +772,7 @@ "symbol": "DNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/849/thumb/district0x.png?1547223762", - "priceUsd": "0.023283129709207685" + "priceUsd": "0.020593935650337562" }, { "chainId": 1, @@ -781,7 +781,7 @@ "symbol": "DPI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", - "priceUsd": "101.97356153284423" + "priceUsd": "88.17" }, { "chainId": 1, @@ -790,7 +790,7 @@ "symbol": "DREP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14578/thumb/KotgsCgS_400x400.jpg?1617094445", - "priceUsd": "0.00129017" + "priceUsd": null }, { "chainId": 1, @@ -799,7 +799,7 @@ "symbol": "DRV", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", - "priceUsd": "0.039509073309452726" + "priceUsd": "0.0509993805221497" }, { "chainId": 1, @@ -808,7 +808,7 @@ "symbol": "DYDX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", - "priceUsd": "0.5405683524036875" + "priceUsd": "0.06613942906284138" }, { "chainId": 1, @@ -817,7 +817,7 @@ "symbol": "DYP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13480/thumb/DYP_Logo_Symbol-8.png?1655809066", - "priceUsd": "0.004826650033929355" + "priceUsd": "0.00475904855250053" }, { "chainId": 1, @@ -826,7 +826,7 @@ "symbol": "EIGEN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/37441/large/eigen.jpg?1728023974", - "priceUsd": "1.77" + "priceUsd": "1.12" }, { "chainId": 1, @@ -835,7 +835,7 @@ "symbol": "ELA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2780/thumb/Elastos.png?1597048112", - "priceUsd": "1.76" + "priceUsd": null }, { "chainId": 1, @@ -853,7 +853,7 @@ "symbol": "ENA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/36530/standard/ethena.png", - "priceUsd": "0.557757" + "priceUsd": "0.496997" }, { "chainId": 1, @@ -862,7 +862,7 @@ "symbol": "ENJ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1102/thumb/enjin-coin-logo.png?1547035078", - "priceUsd": "0.07409959001948155" + "priceUsd": "0.060616426084634425" }, { "chainId": 1, @@ -871,7 +871,7 @@ "symbol": "ENS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", - "priceUsd": "20.7" + "priceUsd": "16.05" }, { "chainId": 1, @@ -880,7 +880,7 @@ "symbol": "ERA", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54475/large/Token_Logo.png?1749676251", - "priceUsd": "0.504864607580487" + "priceUsd": null }, { "chainId": 1, @@ -889,7 +889,7 @@ "symbol": "ERN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", - "priceUsd": "0.084402" + "priceUsd": "0.07284502992633199" }, { "chainId": 1, @@ -898,7 +898,7 @@ "symbol": "ETHFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35958/standard/etherfi.jpeg", - "priceUsd": "1.63" + "priceUsd": "1.022" }, { "chainId": 1, @@ -907,7 +907,7 @@ "symbol": "EUL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26149/thumb/YCvKDfl8_400x400.jpeg?1656041509", - "priceUsd": "10.1" + "priceUsd": "8.31" }, { "chainId": 1, @@ -916,7 +916,7 @@ "symbol": "EURC", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", - "priceUsd": "1.1616257172492535" + "priceUsd": "1.16" }, { "chainId": 1, @@ -952,7 +952,7 @@ "symbol": "FARM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", - "priceUsd": "26.858663627223446" + "priceUsd": "22.78277073689968" }, { "chainId": 1, @@ -961,7 +961,7 @@ "symbol": "FET", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", - "priceUsd": "0.528389" + "priceUsd": "0.273586" }, { "chainId": 1, @@ -970,7 +970,7 @@ "symbol": "FIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", - "priceUsd": "0.08054002912988648" + "priceUsd": "0.07089587603240945" }, { "chainId": 1, @@ -988,7 +988,7 @@ "symbol": "FLUX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png", - "priceUsd": "0.1924176625030025" + "priceUsd": "0.1182395373794088" }, { "chainId": 1, @@ -997,7 +997,7 @@ "symbol": "FORT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", - "priceUsd": "0.044691842757873426" + "priceUsd": "0.03674244" }, { "chainId": 1, @@ -1006,7 +1006,7 @@ "symbol": "FORTH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14917/thumb/photo_2021-04-22_00.00.03.jpeg?1619020835", - "priceUsd": "2.619398450138765" + "priceUsd": "2.35" }, { "chainId": 1, @@ -1015,7 +1015,7 @@ "symbol": "FOX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", - "priceUsd": null + "priceUsd": "0.01902824" }, { "chainId": 1, @@ -1024,7 +1024,7 @@ "symbol": "FRAX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", - "priceUsd": "0.997483" + "priceUsd": "0.996225" }, { "chainId": 1, @@ -1033,7 +1033,7 @@ "symbol": "FTM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", - "priceUsd": "0.2722528135568368" + "priceUsd": "0.1637" }, { "chainId": 1, @@ -1042,7 +1042,7 @@ "symbol": "FX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8186/thumb/47271330_590071468072434_707260356350705664_n.jpg?1556096683", - "priceUsd": "0.083957" + "priceUsd": "0.068993" }, { "chainId": 1, @@ -1051,7 +1051,7 @@ "symbol": "FXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", - "priceUsd": "2.13" + "priceUsd": "1.48" }, { "chainId": 1, @@ -1060,7 +1060,7 @@ "symbol": "G", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/39200/large/gravity.jpg?1721020647", - "priceUsd": "0.00969701" + "priceUsd": "0.00745761" }, { "chainId": 1, @@ -1069,7 +1069,7 @@ "symbol": "GAL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", - "priceUsd": "0.5782186618036622" + "priceUsd": "0.42042540979169246" }, { "chainId": 1, @@ -1078,7 +1078,7 @@ "symbol": "GALA", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/12493/standard/GALA-COINGECKO.png?1696512310", - "priceUsd": "0.01525303" + "priceUsd": "0.01130316" }, { "chainId": 1, @@ -1087,7 +1087,7 @@ "symbol": "GFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19081/thumb/GOLDFINCH.png?1634369662", - "priceUsd": "0.4738967034334735" + "priceUsd": null }, { "chainId": 1, @@ -1096,7 +1096,7 @@ "symbol": "GHST", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", - "priceUsd": "0.37858344359672713" + "priceUsd": "0.3297574201906172" }, { "chainId": 1, @@ -1105,7 +1105,7 @@ "symbol": "GLM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/542/thumb/Golem_Submark_Positive_RGB.png?1606392013", - "priceUsd": "0.21798478278697406" + "priceUsd": "0.184404569275224" }, { "chainId": 1, @@ -1114,7 +1114,7 @@ "symbol": "GNO", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", - "priceUsd": "147.07" + "priceUsd": "138.26" }, { "chainId": 1, @@ -1123,7 +1123,7 @@ "symbol": "GODS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17139/thumb/10631.png?1635718182", - "priceUsd": "0.11146553704706548" + "priceUsd": "0.08848919670090254" }, { "chainId": 1, @@ -1132,7 +1132,7 @@ "symbol": "GRT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", - "priceUsd": "0.0807830069851891" + "priceUsd": "0.065303" }, { "chainId": 1, @@ -1141,7 +1141,7 @@ "symbol": "GTC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", - "priceUsd": "0.2704470962395942" + "priceUsd": "0.21006877421940467" }, { "chainId": 1, @@ -1150,7 +1150,7 @@ "symbol": "GUSD", "decimals": 2, "logoUrl": "https://assets.coingecko.com/coins/images/5992/thumb/gemini-dollar-gusd.png?1536745278", - "priceUsd": "0.9817744881585663" + "priceUsd": "0.993898782945412" }, { "chainId": 1, @@ -1159,7 +1159,7 @@ "symbol": "GYEN", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", - "priceUsd": "0.006394246618249068" + "priceUsd": "0.006487760726933" }, { "chainId": 1, @@ -1168,7 +1168,7 @@ "symbol": "HFT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26136/large/hashflow-icon-cmc.png", - "priceUsd": "0.07607860664961957" + "priceUsd": "0.05219791402605047" }, { "chainId": 1, @@ -1177,7 +1177,7 @@ "symbol": "HIGH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", - "priceUsd": "0.45631568495537816" + "priceUsd": "0.31049324118794175" }, { "chainId": 1, @@ -1186,7 +1186,7 @@ "symbol": "HOPR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14061/thumb/Shared_HOPR_logo_512px.png?1614073468", - "priceUsd": "0.0790552237984121" + "priceUsd": "0.07444470820661181" }, { "chainId": 1, @@ -1195,7 +1195,7 @@ "symbol": "IDEX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2565/thumb/logomark-purple-286x286.png?1638362736", - "priceUsd": "0.0243306535722624" + "priceUsd": "0.019836240745421476" }, { "chainId": 1, @@ -1204,7 +1204,7 @@ "symbol": "ILV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", - "priceUsd": "14.02" + "priceUsd": "11.77" }, { "chainId": 1, @@ -1213,7 +1213,7 @@ "symbol": "IMX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17233/thumb/imx.png?1636691817", - "priceUsd": "0.688854" + "priceUsd": "0.541966" }, { "chainId": 1, @@ -1222,7 +1222,7 @@ "symbol": "INDEX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12729/thumb/index.png?1634894321", - "priceUsd": "1.0125325512507388" + "priceUsd": "0.8019950630386816" }, { "chainId": 1, @@ -1231,7 +1231,7 @@ "symbol": "INJ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", - "priceUsd": "12.22" + "priceUsd": "8.77" }, { "chainId": 1, @@ -1240,7 +1240,7 @@ "symbol": "INV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14205/thumb/inverse_finance.jpg?1614921871", - "priceUsd": "36.83" + "priceUsd": "32.89" }, { "chainId": 1, @@ -1249,7 +1249,7 @@ "symbol": "IOTX", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", - "priceUsd": "0.023460682934752897" + "priceUsd": "0.01169996" }, { "chainId": 1, @@ -1267,7 +1267,7 @@ "symbol": "JASMY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", - "priceUsd": "0.01241309" + "priceUsd": "0.01044631" }, { "chainId": 1, @@ -1276,7 +1276,7 @@ "symbol": "JUP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/10351/thumb/logo512.png?1632480932", - "priceUsd": "0.0009570124316578048" + "priceUsd": null }, { "chainId": 1, @@ -1285,7 +1285,7 @@ "symbol": "KEEP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3373/thumb/IuNzUb5b_400x400.jpg?1589526336", - "priceUsd": "0.07196560259015167" + "priceUsd": "0.06300548429693778" }, { "chainId": 1, @@ -1294,7 +1294,7 @@ "symbol": "KERNEL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54326/large/Kernel_token_logo_2x.png?1739827205", - "priceUsd": "0.2171433123141692" + "priceUsd": "0.1920206048032526" }, { "chainId": 1, @@ -1303,7 +1303,7 @@ "symbol": "KEY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2034/thumb/selfkey.png?1548608934", - "priceUsd": "0.0002413168419798915" + "priceUsd": null }, { "chainId": 1, @@ -1312,7 +1312,7 @@ "symbol": "KNC", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdd974D5C2e2928deA5F71b9825b8b646686BD200/logo.png", - "priceUsd": "0.32736813856347" + "priceUsd": "0.31276907835888296" }, { "chainId": 1, @@ -1321,7 +1321,7 @@ "symbol": "KP3R", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12966/thumb/kp3r_logo.jpg?1607057458", - "priceUsd": "4.43857845013025" + "priceUsd": "3.9292456961952413" }, { "chainId": 1, @@ -1330,7 +1330,7 @@ "symbol": "KRL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2807/thumb/krl.png?1547036979", - "priceUsd": "0.29397641553826764" + "priceUsd": "0.26156854579669014" }, { "chainId": 1, @@ -1339,7 +1339,7 @@ "symbol": "KUJI", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", - "priceUsd": "0.178702" + "priceUsd": "0.109968" }, { "chainId": 1, @@ -1348,7 +1348,7 @@ "symbol": "L3", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/37768/large/Square.png", - "priceUsd": "0.02944509640772015" + "priceUsd": "0.023238557300703983" }, { "chainId": 1, @@ -1357,7 +1357,7 @@ "symbol": "LA", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/36510.png", - "priceUsd": "0.365682" + "priceUsd": null }, { "chainId": 1, @@ -1366,7 +1366,7 @@ "symbol": "LCX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9985/thumb/zRPSu_0o_400x400.jpg?1574327008", - "priceUsd": "0.131983" + "priceUsd": "0.109807" }, { "chainId": 1, @@ -1375,7 +1375,7 @@ "symbol": "LDO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", - "priceUsd": "1.15" + "priceUsd": "0.952765" }, { "chainId": 1, @@ -1384,7 +1384,7 @@ "symbol": "LINK", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", - "priceUsd": "21.77" + "priceUsd": "18.74" }, { "chainId": 1, @@ -1393,7 +1393,7 @@ "symbol": "LIT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", - "priceUsd": "0.407593130417842" + "priceUsd": "0.38131218828086233" }, { "chainId": 1, @@ -1402,7 +1402,7 @@ "symbol": "LOKA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22572/thumb/loka_64pix.png?1642643271", - "priceUsd": "0.15290724774348907" + "priceUsd": "0.13688165359935062" }, { "chainId": 1, @@ -1411,7 +1411,7 @@ "symbol": "LOOM", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0/logo.png", - "priceUsd": "0.4629528254851496" + "priceUsd": "0.00495121" }, { "chainId": 1, @@ -1420,7 +1420,7 @@ "symbol": "LPT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", - "priceUsd": "6.202553939778055" + "priceUsd": "5.16565292452629" }, { "chainId": 1, @@ -1429,7 +1429,7 @@ "symbol": "LQTY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", - "priceUsd": "0.721197" + "priceUsd": "0.526478" }, { "chainId": 1, @@ -1438,7 +1438,7 @@ "symbol": "LRC", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png", - "priceUsd": "0.0848570920963512" + "priceUsd": "0.07011760321465224" }, { "chainId": 1, @@ -1447,7 +1447,7 @@ "symbol": "LRDS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/34775/standard/LRDS_PNG.png", - "priceUsd": "0.107052" + "priceUsd": null }, { "chainId": 1, @@ -1456,7 +1456,7 @@ "symbol": "LSETH", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/28848/large/LsETH-receipt-token-circle.png?1696527824", - "priceUsd": "4714.320645775342" + "priceUsd": "4466.93" }, { "chainId": 1, @@ -1465,7 +1465,7 @@ "symbol": "LSK", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/385/large/Lisk_logo.png?1722338450", - "priceUsd": "0.28087277175401854" + "priceUsd": "0.22061185941191072" }, { "chainId": 1, @@ -1474,7 +1474,7 @@ "symbol": "LUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", - "priceUsd": "1.0047396192340776" + "priceUsd": "1.006282529260867" }, { "chainId": 1, @@ -1483,7 +1483,7 @@ "symbol": "MANA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", - "priceUsd": "0.313836" + "priceUsd": "0.245618" }, { "chainId": 1, @@ -1492,7 +1492,7 @@ "symbol": "MASK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", - "priceUsd": "1.2474156598581578" + "priceUsd": "0.884186" }, { "chainId": 1, @@ -1501,7 +1501,7 @@ "symbol": "MATH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", - "priceUsd": "0.08366075498752316" + "priceUsd": "0.06931347839457945" }, { "chainId": 1, @@ -1510,7 +1510,7 @@ "symbol": "MATIC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", - "priceUsd": "0.23810539436048594" + "priceUsd": "0.2019089696" }, { "chainId": 1, @@ -1519,7 +1519,7 @@ "symbol": "MC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19304/thumb/Db4XqML.png?1634972154", - "priceUsd": "0.11317889058382306" + "priceUsd": null }, { "chainId": 1, @@ -1528,7 +1528,7 @@ "symbol": "MCO2", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14414/thumb/ENtxnThA_400x400.jpg?1615948522", - "priceUsd": "0.16624398173852997" + "priceUsd": "0.17427768187230014" }, { "chainId": 1, @@ -1537,7 +1537,7 @@ "symbol": "MDT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2441/thumb/mdt_logo.png?1569813574", - "priceUsd": "0.020457521838596948" + "priceUsd": null }, { "chainId": 1, @@ -1546,7 +1546,7 @@ "symbol": "MEME", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/32528/large/memecoin_(2).png", - "priceUsd": "0.0023939440579224596" + "priceUsd": "0.0016781822330587718" }, { "chainId": 1, @@ -1555,7 +1555,7 @@ "symbol": "METIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", - "priceUsd": "12.77" + "priceUsd": "10.4" }, { "chainId": 1, @@ -1564,7 +1564,7 @@ "symbol": "MIM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", - "priceUsd": "1.0039587746240246" + "priceUsd": "1.010831007165445" }, { "chainId": 1, @@ -1573,7 +1573,7 @@ "symbol": "MIR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13295/thumb/mirror_logo_transparent.png?1611554658", - "priceUsd": "0.011270136488385001" + "priceUsd": "0.0103762479749909" }, { "chainId": 1, @@ -1582,7 +1582,7 @@ "symbol": "MKR", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png", - "priceUsd": "1573.44" + "priceUsd": "1438.25" }, { "chainId": 1, @@ -1591,7 +1591,7 @@ "symbol": "MLN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", - "priceUsd": "7.7460223309395335" + "priceUsd": "7.675690967591945" }, { "chainId": 1, @@ -1600,7 +1600,7 @@ "symbol": "MNT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/30980/large/Mantle-Logo-mark.png?1739213200", - "priceUsd": "2.51" + "priceUsd": "1.67" }, { "chainId": 1, @@ -1618,7 +1618,7 @@ "symbol": "MONA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13298/thumb/monavale_logo.jpg?1607232721", - "priceUsd": "68.6147426671038" + "priceUsd": "69.58636188235134" }, { "chainId": 1, @@ -1627,7 +1627,7 @@ "symbol": "MORPHO", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/29837/large/Morpho-token-icon.png?1726771230", - "priceUsd": "1.72" + "priceUsd": "2.01" }, { "chainId": 1, @@ -1636,7 +1636,7 @@ "symbol": "MOVE", "decimals": 8, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/32452.png", - "priceUsd": "0.10849967087568456" + "priceUsd": null }, { "chainId": 1, @@ -1645,7 +1645,7 @@ "symbol": "MPL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14097/thumb/photo_2021-05-03_14.20.41.jpeg?1620022863", - "priceUsd": "1.0011836814271482" + "priceUsd": "0.9544465326983481" }, { "chainId": 1, @@ -1654,7 +1654,7 @@ "symbol": "MTL", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/763/thumb/Metal.png?1592195010", - "priceUsd": "0.43632525192703936" + "priceUsd": null }, { "chainId": 1, @@ -1663,7 +1663,7 @@ "symbol": "MULTI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", - "priceUsd": "0.49623364215086785" + "priceUsd": "0.4429381153963063" }, { "chainId": 1, @@ -1672,7 +1672,7 @@ "symbol": "MUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11576/thumb/mStable_USD.png?1595591803", - "priceUsd": "0.9812716083616795" + "priceUsd": "1.0132269136125978" }, { "chainId": 1, @@ -1681,7 +1681,7 @@ "symbol": "MUSE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13230/thumb/muse_logo.png?1606460453", - "priceUsd": "8.758471004350765" + "priceUsd": "8.34" }, { "chainId": 1, @@ -1690,7 +1690,7 @@ "symbol": "MV", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", - "priceUsd": "0.00752695" + "priceUsd": "0.006271996856211323" }, { "chainId": 1, @@ -1699,7 +1699,7 @@ "symbol": "MXC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4604/thumb/mxc.png?1655534336", - "priceUsd": "0.00133558610551269" + "priceUsd": "0.0013341333123090893" }, { "chainId": 1, @@ -1708,7 +1708,7 @@ "symbol": "NCT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", - "priceUsd": "0.02426522278959423" + "priceUsd": "0.021090053661694183" }, { "chainId": 1, @@ -1717,7 +1717,7 @@ "symbol": "Neiro", "decimals": 9, "logoUrl": "https://coin-images.coingecko.com/coins/images/39488/large/neiro.jpg?1731449567", - "priceUsd": "0.0002701" + "priceUsd": "0.00020404" }, { "chainId": 1, @@ -1726,7 +1726,7 @@ "symbol": "NEWT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/66819/large/newton.jpg?1750642513", - "priceUsd": "0.195415" + "priceUsd": null }, { "chainId": 1, @@ -1735,7 +1735,7 @@ "symbol": "NKN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3375/thumb/nkn.png?1548329212", - "priceUsd": "0.02482653136836031" + "priceUsd": "0.02023199763268341" }, { "chainId": 1, @@ -1744,7 +1744,7 @@ "symbol": "NMR", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png", - "priceUsd": "15.65" + "priceUsd": "12.93" }, { "chainId": 1, @@ -1753,7 +1753,7 @@ "symbol": "NU", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3318/thumb/photo1198982838879365035.jpg?1547037916", - "priceUsd": "0.0582474623958541" + "priceUsd": "0.05263336856121454" }, { "chainId": 1, @@ -1762,7 +1762,7 @@ "symbol": "OCEAN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", - "priceUsd": "0.252995" + "priceUsd": "0.313738" }, { "chainId": 1, @@ -1771,7 +1771,7 @@ "symbol": "OGN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", - "priceUsd": "0.057906" + "priceUsd": "0.04892204" }, { "chainId": 1, @@ -1780,7 +1780,7 @@ "symbol": "OMG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/776/thumb/OMG_Network.jpg?1591167168", - "priceUsd": "0.1473258357496226" + "priceUsd": "0.12483098645680554" }, { "chainId": 1, @@ -1789,7 +1789,7 @@ "symbol": "OMNI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/36465/standard/Symbol-Color.png?1711511095", - "priceUsd": "4.117278777562853" + "priceUsd": null }, { "chainId": 1, @@ -1798,7 +1798,7 @@ "symbol": "ONDO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656", - "priceUsd": "0.893449" + "priceUsd": "0.759366" }, { "chainId": 1, @@ -1816,7 +1816,7 @@ "symbol": "ORN", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", - "priceUsd": "0.26345618305624396" + "priceUsd": "0.13426778491727676" }, { "chainId": 1, @@ -1825,7 +1825,7 @@ "symbol": "OXT", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4575f41308EC1483f3d399aa9a2826d74Da13Deb/logo.png", - "priceUsd": "0.05053889030415258" + "priceUsd": "0.04478699630276651" }, { "chainId": 1, @@ -1834,7 +1834,7 @@ "symbol": "PAX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1601/thumb/pax.png?1547035800", - "priceUsd": "0.0017497906655058052" + "priceUsd": "0.0019436077418054048" }, { "chainId": 1, @@ -1843,7 +1843,7 @@ "symbol": "PAXG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", - "priceUsd": "4057.02" + "priceUsd": "3936.55" }, { "chainId": 1, @@ -1852,7 +1852,7 @@ "symbol": "PDA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14316/standard/PDA-symbol.png?1710234068", - "priceUsd": "0.0044866604939796054" + "priceUsd": null }, { "chainId": 1, @@ -1861,7 +1861,7 @@ "symbol": "PENDLE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", - "priceUsd": "4.48" + "priceUsd": "3.35" }, { "chainId": 1, @@ -1879,7 +1879,7 @@ "symbol": "PERP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", - "priceUsd": "0.28029" + "priceUsd": "0.227382" }, { "chainId": 1, @@ -1888,7 +1888,7 @@ "symbol": "PIRATE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/38524/standard/_Pirate_Transparent_200x200.png", - "priceUsd": "0.01974249" + "priceUsd": "0.01253611706287983" }, { "chainId": 1, @@ -1897,7 +1897,7 @@ "symbol": "PLU", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1241/thumb/pluton.png?1548331624", - "priceUsd": "0.4207111768843342" + "priceUsd": "0.33510433197280065" }, { "chainId": 1, @@ -1906,7 +1906,7 @@ "symbol": "POL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/32440/large/polygon.png?1698233684", - "priceUsd": "0.238289" + "priceUsd": "0.20257" }, { "chainId": 1, @@ -1915,7 +1915,7 @@ "symbol": "POLS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", - "priceUsd": "0.1658678025888471" + "priceUsd": "0.145125" }, { "chainId": 1, @@ -1924,7 +1924,7 @@ "symbol": "POLY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", - "priceUsd": "0.07416653780725273" + "priceUsd": "0.03553332" }, { "chainId": 1, @@ -1933,7 +1933,7 @@ "symbol": "POND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8903/thumb/POND_200x200.png?1622515451", - "priceUsd": "0.00768808934441551" + "priceUsd": "0.005961719669198544" }, { "chainId": 1, @@ -1942,7 +1942,7 @@ "symbol": "PORTAL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35436/standard/portal.jpeg", - "priceUsd": "0.037255619818756504" + "priceUsd": "0.026655504210165163" }, { "chainId": 1, @@ -1951,7 +1951,7 @@ "symbol": "POWR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", - "priceUsd": "0.1407286763918426" + "priceUsd": "0.11944224167555147" }, { "chainId": 1, @@ -1960,7 +1960,7 @@ "symbol": "PRIME", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/29053/large/PRIMELOGOOO.png?1676976222", - "priceUsd": "1.32" + "priceUsd": "1.13" }, { "chainId": 1, @@ -1969,7 +1969,7 @@ "symbol": "PRO", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/869/thumb/propy.png?1548332100", - "priceUsd": "0.702793" + "priceUsd": "0.619861" }, { "chainId": 1, @@ -1978,7 +1978,7 @@ "symbol": "PROVE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/67905/large/succinct-logo.png?1754228574", - "priceUsd": "0.764369823768134" + "priceUsd": "0.7920847414342348" }, { "chainId": 1, @@ -1987,7 +1987,7 @@ "symbol": "PRQ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", - "priceUsd": "0.04791819009420432" + "priceUsd": "0.04694718902529" }, { "chainId": 1, @@ -1996,7 +1996,7 @@ "symbol": "PSTAKE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/23931/thumb/PSTAKE_Dark.png?1645709930", - "priceUsd": "0.032128646831839934" + "priceUsd": "0.028401124610658256" }, { "chainId": 1, @@ -2005,7 +2005,7 @@ "symbol": "PUFFER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/50630/large/puffer.jpg?1728545297", - "priceUsd": "0.15212479917503655" + "priceUsd": "0.129784" }, { "chainId": 1, @@ -2014,7 +2014,7 @@ "symbol": "PYUSD", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/31212/large/PYUSD_Logo_%282%29.png?1691458314", - "priceUsd": "0.99982" + "priceUsd": "1" }, { "chainId": 1, @@ -2023,7 +2023,7 @@ "symbol": "QNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3370/thumb/5ZOu7brX_400x400.jpg?1612437252", - "priceUsd": "101.79" + "priceUsd": "82.66" }, { "chainId": 1, @@ -2032,7 +2032,7 @@ "symbol": "QRDO", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/17541/thumb/qrdo.png?1630637735", - "priceUsd": "0.00034751978050065" + "priceUsd": null }, { "chainId": 1, @@ -2041,7 +2041,7 @@ "symbol": "QSP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1219/thumb/0_E0kZjb4dG4hUnoDD_.png?1604815917", - "priceUsd": "0.0010119653917936282" + "priceUsd": "0.0013099122838606057" }, { "chainId": 1, @@ -2050,7 +2050,7 @@ "symbol": "QUICK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13970/thumb/1_pOU6pBMEmiL-ZJVb0CYRjQ.png?1613386659", - "priceUsd": "22.86629961711038" + "priceUsd": "19.21455813267734" }, { "chainId": 1, @@ -2059,7 +2059,7 @@ "symbol": "RAD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14013/thumb/radicle.png?1614402918", - "priceUsd": "0.619813" + "priceUsd": "0.508758" }, { "chainId": 1, @@ -2068,7 +2068,7 @@ "symbol": "RAI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", - "priceUsd": "4.9331629264933525" + "priceUsd": "4.674693475837152" }, { "chainId": 1, @@ -2077,7 +2077,7 @@ "symbol": "RARE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17753/thumb/RARE.jpg?1629220534", - "priceUsd": "0.049153759507244106" + "priceUsd": "0.035889535475395806" }, { "chainId": 1, @@ -2086,7 +2086,7 @@ "symbol": "RARI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", - "priceUsd": "0.8234048885917141" + "priceUsd": "0.5377319959829038" }, { "chainId": 1, @@ -2095,7 +2095,7 @@ "symbol": "RBC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12629/thumb/200x200.png?1607952509", - "priceUsd": "0.003771143534073512" + "priceUsd": "0.0035950994179944273" }, { "chainId": 1, @@ -2104,7 +2104,7 @@ "symbol": "RBN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15823/thumb/RBN_64x64.png?1633529723", - "priceUsd": "0.12036" + "priceUsd": null }, { "chainId": 1, @@ -2113,7 +2113,7 @@ "symbol": "RED", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/53640/large/RedStone_Logo_New_White.png?1740640919", - "priceUsd": "0.471204" + "priceUsd": "0.359934" }, { "chainId": 1, @@ -2122,7 +2122,7 @@ "symbol": "REN", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", - "priceUsd": "0.007042263184421685" + "priceUsd": "0.006079422731760229" }, { "chainId": 1, @@ -2131,7 +2131,7 @@ "symbol": "REP", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1985365e9f78359a9B6AD760e32412f4a445E862/logo.png", - "priceUsd": "1.145405351695479" + "priceUsd": "2.3151133927350513" }, { "chainId": 1, @@ -2140,7 +2140,7 @@ "symbol": "REPv2", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x221657776846890989a759BA2973e427DfF5C9bB/logo.png", - "priceUsd": "1.161385273043465" + "priceUsd": "2.13" }, { "chainId": 1, @@ -2149,7 +2149,7 @@ "symbol": "REQ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1031/thumb/Request_icon_green.png?1643250951", - "priceUsd": "0.12918514167848896" + "priceUsd": "0.12352520063610414" }, { "chainId": 1, @@ -2158,7 +2158,7 @@ "symbol": "REVV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", - "priceUsd": "0.02096099219292354" + "priceUsd": "0.02073797174419581" }, { "chainId": 1, @@ -2167,7 +2167,7 @@ "symbol": "REZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/37327/standard/renzo_200x200.png?1714025012", - "priceUsd": "0.0140748" + "priceUsd": "0.01030212" }, { "chainId": 1, @@ -2176,7 +2176,7 @@ "symbol": "RGT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12900/thumb/Rari_Logo_Transparent.png?1613978014", - "priceUsd": "4.38412770723148" + "priceUsd": "4.1198474186244365" }, { "chainId": 1, @@ -2185,7 +2185,7 @@ "symbol": "RLC", "decimals": 9, "logoUrl": "https://assets.coingecko.com/coins/images/646/thumb/pL1VuXm.png?1604543202", - "priceUsd": "1.0467243114425002" + "priceUsd": "0.8960746031674413" }, { "chainId": 1, @@ -2194,7 +2194,7 @@ "symbol": "RLUSD", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/39651/large/RLUSD_200x200_(1).png?1727376633", - "priceUsd": "0.999476" + "priceUsd": "1" }, { "chainId": 1, @@ -2203,7 +2203,7 @@ "symbol": "RLY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12843/thumb/image.png?1611212077", - "priceUsd": "0.0009478678742264927" + "priceUsd": "0.0008978422685106603" }, { "chainId": 1, @@ -2212,7 +2212,7 @@ "symbol": "RNDR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", - "priceUsd": "3.25" + "priceUsd": "2.56" }, { "chainId": 1, @@ -2221,7 +2221,7 @@ "symbol": "ROOK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13005/thumb/keeper_dao_logo.jpg?1604316506", - "priceUsd": "16.62577043964562" + "priceUsd": "0.3257657501253583" }, { "chainId": 1, @@ -2230,7 +2230,7 @@ "symbol": "RPL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/2090/large/rocket_pool_%28RPL%29.png?1696503058", - "priceUsd": "4.834628228805114" + "priceUsd": "3.49" }, { "chainId": 1, @@ -2239,7 +2239,7 @@ "symbol": "RSR", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", - "priceUsd": "0.00582109" + "priceUsd": "0.0054959" }, { "chainId": 1, @@ -2248,7 +2248,7 @@ "symbol": "SAFE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/27032/standard/Artboard_1_copy_8circle-1.png?1696526084", - "priceUsd": "0.357523" + "priceUsd": "0.257938" }, { "chainId": 1, @@ -2257,7 +2257,7 @@ "symbol": "SAND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", - "priceUsd": "0.262065" + "priceUsd": "0.21716" }, { "chainId": 1, @@ -2266,7 +2266,7 @@ "symbol": "SD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", - "priceUsd": "0.4928079141534901" + "priceUsd": "0.49145181130876164" }, { "chainId": 1, @@ -2284,7 +2284,7 @@ "symbol": "SHPING", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2588/thumb/r_yabKKi_400x400.jpg?1639470164", - "priceUsd": "0.004139983917264716" + "priceUsd": null }, { "chainId": 1, @@ -2293,7 +2293,7 @@ "symbol": "SKL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13245/thumb/SKALE_token_300x300.png?1606789574", - "priceUsd": "0.02373514752710204" + "priceUsd": "0.0195379905207809" }, { "chainId": 1, @@ -2302,7 +2302,7 @@ "symbol": "SKY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/39925/large/sky.jpg?1724827980", - "priceUsd": "0.066406" + "priceUsd": "0.059301" }, { "chainId": 1, @@ -2311,7 +2311,7 @@ "symbol": "SLP", "decimals": 0, "logoUrl": "https://assets.coingecko.com/coins/images/10366/thumb/SLP.png?1578640057", - "priceUsd": "0.0015687414522509533" + "priceUsd": null }, { "chainId": 1, @@ -2320,7 +2320,7 @@ "symbol": "SNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", - "priceUsd": "0.021898746567394167" + "priceUsd": "0.01837347" }, { "chainId": 1, @@ -2329,7 +2329,7 @@ "symbol": "SNX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", - "priceUsd": "1.087" + "priceUsd": "1.17" }, { "chainId": 1, @@ -2338,7 +2338,7 @@ "symbol": "SOCKS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/10717/thumb/qFrcoiM.png?1582525244", - "priceUsd": "12071.871166689514" + "priceUsd": "11199.557905819836" }, { "chainId": 1, @@ -2347,7 +2347,7 @@ "symbol": "SOL", "decimals": 9, "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", - "priceUsd": "223.82158850406506" + "priceUsd": "202.58" }, { "chainId": 1, @@ -2356,7 +2356,7 @@ "symbol": "SPELL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", - "priceUsd": "0.00043460246896488155" + "priceUsd": "0.00036413696225908127" }, { "chainId": 1, @@ -2365,7 +2365,7 @@ "symbol": "SPK", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/38637/large/Spark-Logomark-RGB.png?1744878896", - "priceUsd": "0.04685871" + "priceUsd": "0.03878427" }, { "chainId": 1, @@ -2374,7 +2374,7 @@ "symbol": "SPX", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", - "priceUsd": "1.45" + "priceUsd": "1.1" }, { "chainId": 1, @@ -2383,7 +2383,7 @@ "symbol": "STG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", - "priceUsd": "0.203163" + "priceUsd": "0.152935" }, { "chainId": 1, @@ -2392,7 +2392,7 @@ "symbol": "STORJ", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", - "priceUsd": "0.2253795522833661" + "priceUsd": "0.18155479725013227" }, { "chainId": 1, @@ -2401,7 +2401,7 @@ "symbol": "STRK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26433/standard/starknet.png?1696525507", - "priceUsd": "0.156915" + "priceUsd": null }, { "chainId": 1, @@ -2410,7 +2410,7 @@ "symbol": "STX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1230/thumb/stox-token.png?1547035256", - "priceUsd": "0.00065308" + "priceUsd": "0.0024873408839254417" }, { "chainId": 1, @@ -2419,7 +2419,7 @@ "symbol": "SUKU", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11969/thumb/UmfW5S6f_400x400.jpg?1596602238", - "priceUsd": null + "priceUsd": "0.020444435276121366" }, { "chainId": 1, @@ -2428,7 +2428,7 @@ "symbol": "SUPER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", - "priceUsd": "0.564243" + "priceUsd": "0.40647" }, { "chainId": 1, @@ -2437,7 +2437,7 @@ "symbol": "sUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", - "priceUsd": "0.996658" + "priceUsd": "0.98886" }, { "chainId": 1, @@ -2446,7 +2446,7 @@ "symbol": "SUSHI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", - "priceUsd": "0.686039" + "priceUsd": "0.542971" }, { "chainId": 1, @@ -2455,7 +2455,7 @@ "symbol": "SWELL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/28777/large/swell1.png?1727899715", - "priceUsd": "0.00801714707941713" + "priceUsd": "0.006329748898632404" }, { "chainId": 1, @@ -2464,7 +2464,7 @@ "symbol": "SWFTC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/2346/thumb/SWFTCoin.jpg?1618392022", - "priceUsd": "0.00829267" + "priceUsd": null }, { "chainId": 1, @@ -2473,7 +2473,7 @@ "symbol": "SXP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9368/thumb/swipe.png?1566792311", - "priceUsd": "0.07637650836772185" + "priceUsd": "0.04206069606480924" }, { "chainId": 1, @@ -2482,7 +2482,7 @@ "symbol": "SXT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/55424/large/sxt-token_circle.jpg?1745935919", - "priceUsd": "0.06838082143034907" + "priceUsd": "0.055945" }, { "chainId": 1, @@ -2500,7 +2500,7 @@ "symbol": "SYN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", - "priceUsd": "0.10894392420379893" + "priceUsd": "0.07951615194469983" }, { "chainId": 1, @@ -2509,7 +2509,7 @@ "symbol": "SYRUP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/51232/standard/IMG_7420.png?1730831572", - "priceUsd": "0.394949" + "priceUsd": "0.399912" }, { "chainId": 1, @@ -2518,7 +2518,7 @@ "symbol": "T", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22228/thumb/nFPNiSbL_400x400.jpg?1641220340", - "priceUsd": "0.01511999" + "priceUsd": "0.01311314" }, { "chainId": 1, @@ -2527,7 +2527,7 @@ "symbol": "tBTC", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/0x18084fbA666a33d37592fA2633fD49a74DD93a88/logo.png", - "priceUsd": "122133" + "priceUsd": "115172" }, { "chainId": 1, @@ -2536,7 +2536,7 @@ "symbol": "TERM", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/38142/large/terms.png?1716630303", - "priceUsd": "0.4385490642962406" + "priceUsd": null }, { "chainId": 1, @@ -2545,7 +2545,7 @@ "symbol": "TIME", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/604/thumb/time-32x32.png?1627130666", - "priceUsd": "10.22" + "priceUsd": null }, { "chainId": 1, @@ -2554,7 +2554,7 @@ "symbol": "TLM", "decimals": 4, "logoUrl": "https://assets.coingecko.com/coins/images/14676/thumb/kY-C4o7RThfWrDQsLCAG4q4clZhBDDfJQVhWUEKxXAzyQYMj4Jmq1zmFwpRqxhAJFPOa0AsW_PTSshoPuMnXNwq3rU7Imp15QimXTjlXMx0nC088mt1rIwRs75GnLLugWjSllxgzvQ9YrP4tBgclK4_rb17hjnusGj_c0u2fx0AvVokjSNB-v2poTj0xT9BZRCbzRE3-lF1.jpg?1617700061", - "priceUsd": "0.004928335519167" + "priceUsd": null }, { "chainId": 1, @@ -2563,7 +2563,7 @@ "symbol": "TOKE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17495/thumb/tokemak-avatar-200px-black.png?1628131614", - "priceUsd": "0.235122" + "priceUsd": "0.227537" }, { "chainId": 1, @@ -2572,7 +2572,7 @@ "symbol": "TOKEN", "decimals": 9, "logoUrl": "https://coin-images.coingecko.com/coins/images/32507/large/MAIN_TokenFi_logo_icon.png?1698918427", - "priceUsd": "0.01213972" + "priceUsd": null }, { "chainId": 1, @@ -2581,7 +2581,7 @@ "symbol": "TONE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2325/thumb/tec.png?1547036538", - "priceUsd": "0.0007407287127434519" + "priceUsd": "0.0006788645448811032" }, { "chainId": 1, @@ -2590,7 +2590,7 @@ "symbol": "TRAC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1877/thumb/TRAC.jpg?1635134367", - "priceUsd": "0.488233" + "priceUsd": "0.731595" }, { "chainId": 1, @@ -2599,7 +2599,7 @@ "symbol": "TRB", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", - "priceUsd": "32.37765888806222" + "priceUsd": "27.4856222443088" }, { "chainId": 1, @@ -2608,7 +2608,7 @@ "symbol": "TREE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/67664/large/TREE_logo.png?1753601041", - "priceUsd": "0.2434068837048001" + "priceUsd": "0.185287" }, { "chainId": 1, @@ -2617,7 +2617,7 @@ "symbol": "TRIBE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14575/thumb/tribe.PNG?1617487954", - "priceUsd": "0.6305304192551023" + "priceUsd": "0.5921269796843415" }, { "chainId": 1, @@ -2626,7 +2626,7 @@ "symbol": "TRU", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/13180/thumb/truefi_glyph_color.png?1617610941", - "priceUsd": "0.0267452" + "priceUsd": "0.019226449905275896" }, { "chainId": 1, @@ -2635,7 +2635,7 @@ "symbol": "TURBO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/30117/large/TurboMark-QL_200.png?1708079597", - "priceUsd": "0.00355453" + "priceUsd": "0.00248698" }, { "chainId": 1, @@ -2644,7 +2644,7 @@ "symbol": "TVK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13330/thumb/virtua_original.png?1656043619", - "priceUsd": "0.025295534498726888" + "priceUsd": "0.016695171727937008" }, { "chainId": 1, @@ -2662,7 +2662,7 @@ "symbol": "UNFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13152/thumb/logo-2.png?1605748967", - "priceUsd": "0.144828696111073" + "priceUsd": null }, { "chainId": 1, @@ -2671,7 +2671,7 @@ "symbol": "UNI", "decimals": 18, "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", - "priceUsd": "7.84" + "priceUsd": "6.62" }, { "chainId": 1, @@ -2689,7 +2689,7 @@ "symbol": "USD1", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54977/large/USD1_1000x1000_transparent.png?1749297002", - "priceUsd": "0.999627" + "priceUsd": "1.001" }, { "chainId": 1, @@ -2698,7 +2698,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.999705" + "priceUsd": "0.999806" }, { "chainId": 1, @@ -2707,7 +2707,7 @@ "symbol": "USDG", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/51281/large/GDN_USDG_Token_200x200.png", - "priceUsd": "0.9816562571382551" + "priceUsd": "0.9998" }, { "chainId": 1, @@ -2716,7 +2716,7 @@ "symbol": "USDP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", - "priceUsd": "1.5569929672028677" + "priceUsd": "1.4958398305196505" }, { "chainId": 1, @@ -2725,7 +2725,7 @@ "symbol": "USDQ", "decimals": 6, "logoUrl": "https://coin-images.coingecko.com/coins/images/51852/large/USDQ_1000px_Color.png?1732071232", - "priceUsd": "0.999799" + "priceUsd": "0.999759" }, { "chainId": 1, @@ -2734,7 +2734,7 @@ "symbol": "USDR", "decimals": 6, "logoUrl": "https://coin-images.coingecko.com/coins/images/53721/large/stablrusd-logo.png?1737126629", - "priceUsd": "0.999648" + "priceUsd": "0.999857" }, { "chainId": 1, @@ -2743,7 +2743,7 @@ "symbol": "USDS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/39926/large/usds.webp?1726666683", - "priceUsd": "0.999736" + "priceUsd": "1" }, { "chainId": 1, @@ -2761,7 +2761,7 @@ "symbol": "USUAL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/51091/large/USUAL.jpg?1730035787", - "priceUsd": "0.04850135" + "priceUsd": "0.03239731" }, { "chainId": 1, @@ -2770,7 +2770,7 @@ "symbol": "VANRY", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/33466/large/apple-touch-icon.png?1701942541", - "priceUsd": "0.0257143" + "priceUsd": null }, { "chainId": 1, @@ -2779,7 +2779,7 @@ "symbol": "VGX", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/794/thumb/Voyager-vgx.png?1575693595", - "priceUsd": "0.7756022866139018" + "priceUsd": null }, { "chainId": 1, @@ -2788,7 +2788,7 @@ "symbol": "WAMPL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", - "priceUsd": "2.070203016662237" + "priceUsd": "1.8731895961007101" }, { "chainId": 1, @@ -2797,7 +2797,7 @@ "symbol": "WANLOG", "decimals": 12, "logoUrl": "https://assets.kraken.com/marketing/web/icons-uni-webp/s_anlog.webp?i=kds", - "priceUsd": "0.0010479955980391838" + "priceUsd": "0.0013502602547598025" }, { "chainId": 1, @@ -2806,7 +2806,7 @@ "symbol": "WBTC", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", - "priceUsd": "122699" + "priceUsd": "115331" }, { "chainId": 1, @@ -2815,7 +2815,16 @@ "symbol": "WCFG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17106/thumb/WCFG.jpg?1626266462", - "priceUsd": "0.34796245117239233" + "priceUsd": "0.3207170182356214" + }, + { + "chainId": 1, + "address": "0xeF4461891DfB3AC8572cCf7C794664A8DD927945", + "name": "WalletConnect Token", + "symbol": "WCT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/50390/large/wc-token1.png?1727569464", + "priceUsd": null }, { "chainId": 1, @@ -2824,7 +2833,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 1, @@ -2833,7 +2842,7 @@ "symbol": "WLFI", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/50767/large/wlfi.png?1756438915", - "priceUsd": "0.178407" + "priceUsd": "0.147078" }, { "chainId": 1, @@ -2842,7 +2851,7 @@ "symbol": "WOO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", - "priceUsd": "0.066112" + "priceUsd": "0.0422311" }, { "chainId": 1, @@ -2851,7 +2860,7 @@ "symbol": "XCN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", - "priceUsd": "0.01066584" + "priceUsd": "0.00892673" }, { "chainId": 1, @@ -2860,7 +2869,7 @@ "symbol": "XSGD", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", - "priceUsd": "0.9610249026701526" + "priceUsd": "0.771149" }, { "chainId": 1, @@ -2869,7 +2878,7 @@ "symbol": "XYO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", - "priceUsd": "0.00881705" + "priceUsd": "0.00750409" }, { "chainId": 1, @@ -2878,7 +2887,7 @@ "symbol": "YFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", - "priceUsd": "5320.34" + "priceUsd": "4860.52" }, { "chainId": 1, @@ -2887,7 +2896,7 @@ "symbol": "YFII", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11902/thumb/YFII-logo.78631676.png?1598677348", - "priceUsd": "75.88501013667818" + "priceUsd": "68.72105457223097" }, { "chainId": 1, @@ -2896,7 +2905,7 @@ "symbol": "YGG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17358/thumb/le1nzlO6_400x400.jpg?1632465691", - "priceUsd": "0.161404" + "priceUsd": "0.136176" }, { "chainId": 1, @@ -2905,7 +2914,7 @@ "symbol": "Zeta", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26718/standard/Twitter_icon.png?1696525788", - "priceUsd": "0.17037924273459923" + "priceUsd": "0.1259938236911512" }, { "chainId": 1, @@ -2914,7 +2923,7 @@ "symbol": "ZRO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", - "priceUsd": "2.34" + "priceUsd": "1.74" }, { "chainId": 1, @@ -2923,7 +2932,7 @@ "symbol": "ZRX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png", - "priceUsd": "0.24666301528626908" + "priceUsd": "0.19736884481075476" }, { "chainId": 10, @@ -2932,7 +2941,7 @@ "symbol": "AAVE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", - "priceUsd": "276.06" + "priceUsd": "236.62" }, { "chainId": 10, @@ -2941,7 +2950,7 @@ "symbol": "BUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", - "priceUsd": "1.3663341169185133" + "priceUsd": "0.9718526511696358" }, { "chainId": 10, @@ -2950,7 +2959,7 @@ "symbol": "CELO", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", - "priceUsd": "0.3250550212080028" + "priceUsd": null }, { "chainId": 10, @@ -2959,7 +2968,7 @@ "symbol": "CYBER", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/31274/large/token.png?1715826754", - "priceUsd": "1.52" + "priceUsd": "1.6968235741417501" }, { "chainId": 10, @@ -2968,7 +2977,7 @@ "symbol": "DRV", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", - "priceUsd": "0.03596978" + "priceUsd": "0.04364813" }, { "chainId": 10, @@ -2977,7 +2986,7 @@ "symbol": "KUJI", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", - "priceUsd": "0.16075783074532105" + "priceUsd": null }, { "chainId": 10, @@ -2986,7 +2995,7 @@ "symbol": "OP", "decimals": 18, "logoUrl": "https://ethereum-optimism.github.io/data/OP/logo.png", - "priceUsd": "0.700864" + "priceUsd": "0.460609" }, { "chainId": 10, @@ -2995,7 +3004,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", - "priceUsd": "0.999705" + "priceUsd": "0.999806" }, { "chainId": 10, @@ -3004,7 +3013,7 @@ "symbol": "USDC.e", "decimals": 6, "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", - "priceUsd": "0.999447" + "priceUsd": "0.99957" }, { "chainId": 10, @@ -3013,16 +3022,16 @@ "symbol": "VELO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12538/standard/Logo_200x_200.png?1696512350", - "priceUsd": "0.04545114" + "priceUsd": "0.0342343" }, { "chainId": 10, "address": "0xeF4461891DfB3AC8572cCf7C794664A8DD927945", - "name": "WalletConnect", + "name": "WalletConnect Token", "symbol": "WCT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/50390/large/wc-token1.png?1727569464", - "priceUsd": "0.251522" + "priceUsd": null }, { "chainId": 10, @@ -3031,7 +3040,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 10, @@ -3040,7 +3049,7 @@ "symbol": "WLD", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/31069/large/worldcoin.jpeg?1696529903", - "priceUsd": "1.21" + "priceUsd": "0.921462" }, { "chainId": 10, @@ -3049,7 +3058,7 @@ "symbol": "ZRO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", - "priceUsd": "2.34" + "priceUsd": "1.74" }, { "chainId": 56, @@ -3058,7 +3067,7 @@ "symbol": "1INCH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", - "priceUsd": "0.248156" + "priceUsd": "0.179057" }, { "chainId": 56, @@ -3067,7 +3076,7 @@ "symbol": "AAVE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", - "priceUsd": "276.06" + "priceUsd": "236.62" }, { "chainId": 56, @@ -3076,7 +3085,7 @@ "symbol": "ACH", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/12390/thumb/ACH_%281%29.png?1599691266", - "priceUsd": "0.01847294" + "priceUsd": null }, { "chainId": 56, @@ -3085,7 +3094,7 @@ "symbol": "ADX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", - "priceUsd": "0.10140869664084146" + "priceUsd": null }, { "chainId": 56, @@ -3094,7 +3103,7 @@ "symbol": "agEUR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", - "priceUsd": "1.1351829697860463" + "priceUsd": "1.17" }, { "chainId": 56, @@ -3103,7 +3112,7 @@ "symbol": "AIOZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", - "priceUsd": "0.269038" + "priceUsd": null }, { "chainId": 56, @@ -3112,7 +3121,7 @@ "symbol": "ALEPH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", - "priceUsd": "0.064669" + "priceUsd": null }, { "chainId": 56, @@ -3121,7 +3130,7 @@ "symbol": "ALICE", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", - "priceUsd": "0.3723838300657885" + "priceUsd": "0.316133" }, { "chainId": 56, @@ -3130,7 +3139,7 @@ "symbol": "ALPHA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", - "priceUsd": "0.014495105590404275" + "priceUsd": null }, { "chainId": 56, @@ -3139,7 +3148,7 @@ "symbol": "ANKR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", - "priceUsd": "0.01366072" + "priceUsd": "0.010227534573640123" }, { "chainId": 56, @@ -3148,7 +3157,7 @@ "symbol": "ARPA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", - "priceUsd": "0.02054787" + "priceUsd": null }, { "chainId": 56, @@ -3157,7 +3166,7 @@ "symbol": "ATA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", - "priceUsd": "0.039703888909643666" + "priceUsd": null }, { "chainId": 56, @@ -3166,7 +3175,7 @@ "symbol": "AXL", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", - "priceUsd": "0.2871530110598361" + "priceUsd": null }, { "chainId": 56, @@ -3175,7 +3184,7 @@ "symbol": "AXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", - "priceUsd": "2.11" + "priceUsd": "1.5960292388960664" }, { "chainId": 56, @@ -3184,7 +3193,7 @@ "symbol": "BLZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2848/thumb/ColorIcon_3x.png?1622516510", - "priceUsd": "0.03070011" + "priceUsd": null }, { "chainId": 56, @@ -3193,7 +3202,7 @@ "symbol": "BUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", - "priceUsd": "1.025" + "priceUsd": "1.001" }, { "chainId": 56, @@ -3202,7 +3211,7 @@ "symbol": "C98", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17117/thumb/logo.png?1626412904", - "priceUsd": null + "priceUsd": "0.03784488" }, { "chainId": 56, @@ -3211,7 +3220,7 @@ "symbol": "CHR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/5000/thumb/Chromia.png?1559038018", - "priceUsd": "0.08382" + "priceUsd": null }, { "chainId": 56, @@ -3220,7 +3229,7 @@ "symbol": "CLV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15278/thumb/clover.png?1645084454", - "priceUsd": "0.015005887353143784" + "priceUsd": null }, { "chainId": 56, @@ -3229,7 +3238,7 @@ "symbol": "COMP", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", - "priceUsd": "41.24120729375521" + "priceUsd": "37.949222198140426" }, { "chainId": 56, @@ -3238,7 +3247,7 @@ "symbol": "COVAL", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/588/thumb/coval-logo.png?1599493950", - "priceUsd": "0.00064329" + "priceUsd": "0.00053861" }, { "chainId": 56, @@ -3247,7 +3256,7 @@ "symbol": "CTSI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", - "priceUsd": "0.07261" + "priceUsd": null }, { "chainId": 56, @@ -3256,7 +3265,7 @@ "symbol": "DAI", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", - "priceUsd": "0.999392" + "priceUsd": "0.999726" }, { "chainId": 56, @@ -3265,7 +3274,7 @@ "symbol": "DAR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/19837/thumb/dar.png?1636014223", - "priceUsd": "0.08443555353020463" + "priceUsd": null }, { "chainId": 56, @@ -3274,7 +3283,7 @@ "symbol": "DEXT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", - "priceUsd": "0.467603" + "priceUsd": null }, { "chainId": 56, @@ -3283,7 +3292,7 @@ "symbol": "DIA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", - "priceUsd": "0.531795" + "priceUsd": null }, { "chainId": 56, @@ -3292,7 +3301,7 @@ "symbol": "DREP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14578/thumb/KotgsCgS_400x400.jpg?1617094445", - "priceUsd": "0.00129017" + "priceUsd": null }, { "chainId": 56, @@ -3301,7 +3310,7 @@ "symbol": "DYP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13480/thumb/DYP_Logo_Symbol-8.png?1655809066", - "priceUsd": "0.00482137" + "priceUsd": "0.04183781324758531" }, { "chainId": 56, @@ -3319,7 +3328,7 @@ "symbol": "FARM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", - "priceUsd": "26.5" + "priceUsd": null }, { "chainId": 56, @@ -3328,7 +3337,7 @@ "symbol": "FET", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", - "priceUsd": "0.528389" + "priceUsd": null }, { "chainId": 56, @@ -3346,7 +3355,7 @@ "symbol": "FRAX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", - "priceUsd": "1.0049182173515283" + "priceUsd": "0.996545351756993" }, { "chainId": 56, @@ -3355,7 +3364,7 @@ "symbol": "FTM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", - "priceUsd": "0.28903552563851953" + "priceUsd": "0.19762641849511575" }, { "chainId": 56, @@ -3364,7 +3373,7 @@ "symbol": "FXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", - "priceUsd": "2.12" + "priceUsd": "1.4339240907619055" }, { "chainId": 56, @@ -3373,7 +3382,7 @@ "symbol": "GAL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", - "priceUsd": "0.6165" + "priceUsd": "0.4753261714257818" }, { "chainId": 56, @@ -3382,7 +3391,7 @@ "symbol": "HFT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26136/large/hashflow-icon-cmc.png", - "priceUsd": "0.07057965602448696" + "priceUsd": "0.05068831766762676" }, { "chainId": 56, @@ -3391,7 +3400,7 @@ "symbol": "HIGH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", - "priceUsd": "0.45628092069831305" + "priceUsd": "0.3020565614236082" }, { "chainId": 56, @@ -3400,7 +3409,7 @@ "symbol": "INJ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", - "priceUsd": "12.22" + "priceUsd": "8.77" }, { "chainId": 56, @@ -3409,7 +3418,7 @@ "symbol": "JUP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/10351/thumb/logo512.png?1632480932", - "priceUsd": "0.00089806" + "priceUsd": null }, { "chainId": 56, @@ -3418,7 +3427,7 @@ "symbol": "KUJI", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", - "priceUsd": "0.178702" + "priceUsd": null }, { "chainId": 56, @@ -3427,7 +3436,7 @@ "symbol": "LINK", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", - "priceUsd": "21.77" + "priceUsd": "18.74" }, { "chainId": 56, @@ -3436,7 +3445,7 @@ "symbol": "MASK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", - "priceUsd": "1.25" + "priceUsd": "0.8841532133256067" }, { "chainId": 56, @@ -3445,7 +3454,7 @@ "symbol": "MATH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", - "priceUsd": "0.082979" + "priceUsd": null }, { "chainId": 56, @@ -3454,7 +3463,7 @@ "symbol": "MATIC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", - "priceUsd": "0.23384472184469995" + "priceUsd": "0.19976120558763635" }, { "chainId": 56, @@ -3463,7 +3472,7 @@ "symbol": "MC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19304/thumb/Db4XqML.png?1634972154", - "priceUsd": "0.106562" + "priceUsd": null }, { "chainId": 56, @@ -3472,7 +3481,7 @@ "symbol": "METIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", - "priceUsd": "0.010838862909664453" + "priceUsd": "0.007322991530894919" }, { "chainId": 56, @@ -3481,7 +3490,7 @@ "symbol": "MIM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", - "priceUsd": "1.0107794374125223" + "priceUsd": "1.010831007165445" }, { "chainId": 56, @@ -3490,7 +3499,7 @@ "symbol": "MIR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13295/thumb/mirror_logo_transparent.png?1611554658", - "priceUsd": "0.01173196" + "priceUsd": null }, { "chainId": 56, @@ -3499,7 +3508,7 @@ "symbol": "MULTI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", - "priceUsd": "0.05004492060438173" + "priceUsd": null }, { "chainId": 56, @@ -3508,7 +3517,7 @@ "symbol": "PERP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", - "priceUsd": "0.13016145807432467" + "priceUsd": null }, { "chainId": 56, @@ -3517,7 +3526,7 @@ "symbol": "POLS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", - "priceUsd": "0.164847" + "priceUsd": null }, { "chainId": 56, @@ -3526,7 +3535,7 @@ "symbol": "PRQ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", - "priceUsd": "0.00709433" + "priceUsd": null }, { "chainId": 56, @@ -3535,7 +3544,7 @@ "symbol": "PSTAKE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/23931/thumb/PSTAKE_Dark.png?1645709930", - "priceUsd": "0.015506478778893242" + "priceUsd": null }, { "chainId": 56, @@ -3544,7 +3553,7 @@ "symbol": "REVV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", - "priceUsd": "0.00104648" + "priceUsd": "0.0023739091322783463" }, { "chainId": 56, @@ -3553,7 +3562,7 @@ "symbol": "SD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", - "priceUsd": "0.497549" + "priceUsd": null }, { "chainId": 56, @@ -3562,7 +3571,7 @@ "symbol": "SOL", "decimals": 9, "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", - "priceUsd": "219.93824557760686" + "priceUsd": null }, { "chainId": 56, @@ -3571,7 +3580,7 @@ "symbol": "STG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", - "priceUsd": "0.20219556602945252" + "priceUsd": "0.15216242202354593" }, { "chainId": 56, @@ -3580,7 +3589,7 @@ "symbol": "SUPER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", - "priceUsd": "0.35208615538562527" + "priceUsd": null }, { "chainId": 56, @@ -3589,7 +3598,7 @@ "symbol": "SUSHI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", - "priceUsd": "0.6794621380930607" + "priceUsd": "0.5401949610224372" }, { "chainId": 56, @@ -3598,7 +3607,7 @@ "symbol": "SWFTC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2346/thumb/SWFTCoin.jpg?1618392022", - "priceUsd": "0.008268848283498451" + "priceUsd": null }, { "chainId": 56, @@ -3607,7 +3616,7 @@ "symbol": "SXP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9368/thumb/swipe.png?1566792311", - "priceUsd": "0.020228734327283678" + "priceUsd": "0.017285225247155245" }, { "chainId": 56, @@ -3616,7 +3625,7 @@ "symbol": "SYN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", - "priceUsd": "0.108305" + "priceUsd": null }, { "chainId": 56, @@ -3625,7 +3634,7 @@ "symbol": "TIME", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/604/thumb/time-32x32.png?1627130666", - "priceUsd": "10.22" + "priceUsd": null }, { "chainId": 56, @@ -3634,7 +3643,7 @@ "symbol": "TLM", "decimals": 4, "logoUrl": "https://assets.coingecko.com/coins/images/14676/thumb/kY-C4o7RThfWrDQsLCAG4q4clZhBDDfJQVhWUEKxXAzyQYMj4Jmq1zmFwpRqxhAJFPOa0AsW_PTSshoPuMnXNwq3rU7Imp15QimXTjlXMx0nC088mt1rIwRs75GnLLugWjSllxgzvQ9YrP4tBgclK4_rb17hjnusGj_c0u2fx0AvVokjSNB-v2poTj0xT9BZRCbzRE3-lF1.jpg?1617700061", - "priceUsd": "0.004193545947459356" + "priceUsd": null }, { "chainId": 56, @@ -3643,7 +3652,7 @@ "symbol": "UNFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13152/thumb/logo-2.png?1605748967", - "priceUsd": "0.171649" + "priceUsd": null }, { "chainId": 56, @@ -3652,7 +3661,7 @@ "symbol": "UNI", "decimals": 18, "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", - "priceUsd": "7.84" + "priceUsd": "6.62" }, { "chainId": 56, @@ -3670,7 +3679,7 @@ "symbol": "USDC", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.999709" + "priceUsd": "0.999973" }, { "chainId": 56, @@ -3679,7 +3688,7 @@ "symbol": "USDT", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", - "priceUsd": "0.999968" + "priceUsd": "0.999876" }, { "chainId": 56, @@ -3688,7 +3697,7 @@ "symbol": "WBNB", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/smartchain/assets/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c/logo.png", - "priceUsd": "1281.22" + "priceUsd": "1143.97" }, { "chainId": 56, @@ -3697,7 +3706,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4373.98" + "priceUsd": "4158.32" }, { "chainId": 56, @@ -3706,7 +3715,7 @@ "symbol": "WOO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", - "priceUsd": "0.06561889846517358" + "priceUsd": "0.04194953789091194" }, { "chainId": 56, @@ -3715,7 +3724,7 @@ "symbol": "XCN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", - "priceUsd": "0.01066584" + "priceUsd": null }, { "chainId": 56, @@ -3724,7 +3733,7 @@ "symbol": "ZRO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", - "priceUsd": "2.3437558551854827" + "priceUsd": "1.73" }, { "chainId": 130, @@ -4185,15 +4194,6 @@ "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", "priceUsd": null }, - { - "chainId": 130, - "address": "0xBbE97f3522101e5B6976cBf77376047097BA837F", - "name": "BONK", - "symbol": "BONK", - "decimals": 5, - "logoUrl": "https://assets.coingecko.com/coins/images/28600/standard/bonk.jpg?1696527587", - "priceUsd": null - }, { "chainId": 130, "address": "0x6A4a359C7453F5892392FCb8eAB7A9A100986B71", @@ -4291,7 +4291,7 @@ "symbol": "COMP", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", - "priceUsd": "36.57273628569128" + "priceUsd": null }, { "chainId": 130, @@ -4426,7 +4426,7 @@ "symbol": "DAI", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", - "priceUsd": "1.0005967057868332" + "priceUsd": "0.9986733463611019" }, { "chainId": 130, @@ -4570,7 +4570,7 @@ "symbol": "ENS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", - "priceUsd": "39.92084867155041" + "priceUsd": "37.69887197394634" }, { "chainId": 130, @@ -4950,15 +4950,6 @@ "logoUrl": "https://assets.coingecko.com/coins/images/10351/thumb/logo512.png?1632480932", "priceUsd": null }, - { - "chainId": 130, - "address": "0xbe51A5e8FA434F09663e8fB4CCe79d0B2381Afad", - "name": "Jupiter", - "symbol": "JUP", - "decimals": 6, - "logoUrl": "https://assets.coingecko.com/coins/images/34188/standard/jup.png?1704266489", - "priceUsd": "0.431489" - }, { "chainId": 130, "address": "0x05DBd720fc26F732c8d42Ea89BD7F442EA6AFE80", @@ -5047,7 +5038,7 @@ "symbol": "LINK", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", - "priceUsd": "21.769495205100565" + "priceUsd": "18.49208705696888" }, { "chainId": 130, @@ -5922,15 +5913,6 @@ "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", "priceUsd": null }, - { - "chainId": 130, - "address": "0xbdE8A5331E8Ac4831cf8ea9e42e229219EafaB97", - "name": "Solana", - "symbol": "SOL", - "decimals": 9, - "logoUrl": "https://coin-images.coingecko.com/coins/images/54680/large/base.png?1749023388", - "priceUsd": "223.57749727520465" - }, { "chainId": 130, "address": "0x739316C7bc4A39Eb39dcFa1b181b64abc17fEF7F", @@ -6208,7 +6190,7 @@ "symbol": "UNI", "decimals": 18, "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", - "priceUsd": "7.84" + "priceUsd": "6.62" }, { "chainId": 130, @@ -6226,7 +6208,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.999705" + "priceUsd": "0.999806" }, { "chainId": 130, @@ -6280,7 +6262,7 @@ "symbol": "USDT", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", - "priceUsd": "1.0004915320963803" + "priceUsd": null }, { "chainId": 130, @@ -6325,7 +6307,7 @@ "symbol": "WBTC", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", - "priceUsd": "122667.62208327817" + "priceUsd": null }, { "chainId": 130, @@ -6343,16 +6325,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.813939" - }, - { - "chainId": 130, - "address": "0x97Fadb3D000b953360FD011e173F12cDDB5d70Fa", - "name": "dogwifhat", - "symbol": "WIF", - "decimals": 6, - "logoUrl": "https://assets.coingecko.com/coins/images/33566/standard/dogwifhat.jpg?1702499428", - "priceUsd": "0.706488" + "priceUsd": "4156.59053785" }, { "chainId": 130, @@ -6451,7 +6424,7 @@ "symbol": "1INCH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", - "priceUsd": "0.25067227441032397" + "priceUsd": "0.17756647232350692" }, { "chainId": 137, @@ -6460,7 +6433,7 @@ "symbol": "AAVE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", - "priceUsd": "276.06" + "priceUsd": "236.62" }, { "chainId": 137, @@ -6469,7 +6442,7 @@ "symbol": "ACX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", - "priceUsd": "0.11094" + "priceUsd": "0.071639" }, { "chainId": 137, @@ -6478,7 +6451,7 @@ "symbol": "ADX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/847/thumb/Ambire_AdEx_Symbol_color.png?1655432540", - "priceUsd": "0.1011" + "priceUsd": "0.1209" }, { "chainId": 137, @@ -6487,7 +6460,7 @@ "symbol": "agEUR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", - "priceUsd": "1.1786811136663522" + "priceUsd": "1.17" }, { "chainId": 137, @@ -6496,7 +6469,7 @@ "symbol": "AGLD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", - "priceUsd": "0.08371763155072778" + "priceUsd": "0.06774285897016526" }, { "chainId": 137, @@ -6505,7 +6478,7 @@ "symbol": "AIOZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", - "priceUsd": "0.04440327700379732" + "priceUsd": "0.034302845483768885" }, { "chainId": 137, @@ -6514,7 +6487,7 @@ "symbol": "ALCX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14113/thumb/Alchemix.png?1614409874", - "priceUsd": "8.792443727" + "priceUsd": "8.40379478885658" }, { "chainId": 137, @@ -6523,7 +6496,7 @@ "symbol": "ALEPH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", - "priceUsd": "0.05846729649268761" + "priceUsd": "0.0799587429137528" }, { "chainId": 137, @@ -6532,7 +6505,7 @@ "symbol": "ALI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", - "priceUsd": "0.005661474156285133" + "priceUsd": "0.004068033281508833" }, { "chainId": 137, @@ -6541,7 +6514,7 @@ "symbol": "ALICE", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/14375/thumb/alice_logo.jpg?1615782968", - "priceUsd": "0.2123637158893901" + "priceUsd": "0.18164734347850084" }, { "chainId": 137, @@ -6550,7 +6523,7 @@ "symbol": "ALPHA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", - "priceUsd": "0.01747722995" + "priceUsd": "0.011431386352945648" }, { "chainId": 137, @@ -6559,7 +6532,7 @@ "symbol": "AMP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12409/thumb/amp-200x200.png?1599625397", - "priceUsd": "0.0032294516702799127" + "priceUsd": "0.002470905414093286" }, { "chainId": 137, @@ -6568,7 +6541,7 @@ "symbol": "ANKR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", - "priceUsd": "0.013521341208785894" + "priceUsd": "0.010491512893713764" }, { "chainId": 137, @@ -6586,7 +6559,7 @@ "symbol": "APE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", - "priceUsd": "0.5576761180501775" + "priceUsd": "0.44589012866198735" }, { "chainId": 137, @@ -6604,7 +6577,7 @@ "symbol": "ARPA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8506/thumb/9u0a23XY_400x400.jpg?1559027357", - "priceUsd": "0.02054787" + "priceUsd": "0.020823218151527528" }, { "chainId": 137, @@ -6613,7 +6586,7 @@ "symbol": "AST", "decimals": 4, "logoUrl": "https://assets.coingecko.com/coins/images/1019/thumb/Airswap.png?1630903484", - "priceUsd": "0.46794954741623995" + "priceUsd": "0.4002651385813952" }, { "chainId": 137, @@ -6622,7 +6595,7 @@ "symbol": "ATA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", - "priceUsd": "0.04045548251217933" + "priceUsd": "0.03239444560446025" }, { "chainId": 137, @@ -6631,7 +6604,7 @@ "symbol": "AUDIO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12913/thumb/AudiusCoinLogo_2x.png?1603425727", - "priceUsd": "0.06563845546516388" + "priceUsd": "0.05614448313519359" }, { "chainId": 137, @@ -6640,7 +6613,7 @@ "symbol": "AXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", - "priceUsd": "2.3073277786729967" + "priceUsd": "1.720736653908279" }, { "chainId": 137, @@ -6649,7 +6622,7 @@ "symbol": "BADGER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", - "priceUsd": "1.5686363921976594" + "priceUsd": "1.447186424806225" }, { "chainId": 137, @@ -6658,7 +6631,7 @@ "symbol": "BAL", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", - "priceUsd": "1.137762798245673" + "priceUsd": "1.0367879475402833" }, { "chainId": 137, @@ -6667,7 +6640,7 @@ "symbol": "BAND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9545/thumb/band-protocol.png?1568730326", - "priceUsd": "1.328108957568383" + "priceUsd": "0.42812998781204226" }, { "chainId": 137, @@ -6676,7 +6649,7 @@ "symbol": "BAT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", - "priceUsd": "0.14918109526611817" + "priceUsd": "0.1413957217039092" }, { "chainId": 137, @@ -6685,7 +6658,7 @@ "symbol": "BICO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", - "priceUsd": "0.07759396775347024" + "priceUsd": "0.05354652630611442" }, { "chainId": 137, @@ -6712,7 +6685,7 @@ "symbol": "BNT", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", - "priceUsd": "0.5821143765115562" + "priceUsd": "0.5172821541521775" }, { "chainId": 137, @@ -6721,7 +6694,7 @@ "symbol": "BOBA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20285/thumb/BOBA.png?1636811576", - "priceUsd": "0.02297442390389694" + "priceUsd": "0.019651394084032162" }, { "chainId": 137, @@ -6739,7 +6712,7 @@ "symbol": "BUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", - "priceUsd": "0.9939931103245507" + "priceUsd": "1.0083947681522372" }, { "chainId": 137, @@ -6766,7 +6739,7 @@ "symbol": "CHZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8834/thumb/Chiliz.png?1561970540", - "priceUsd": "0.037329726663262994" + "priceUsd": "0.03036524650362244" }, { "chainId": 137, @@ -6775,7 +6748,7 @@ "symbol": "COMP", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", - "priceUsd": "39.946063251753124" + "priceUsd": "37.04097778418656" }, { "chainId": 137, @@ -6802,7 +6775,7 @@ "symbol": "CRO", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", - "priceUsd": "0.1661370674868661" + "priceUsd": "0.16706312209635973" }, { "chainId": 137, @@ -6811,7 +6784,7 @@ "symbol": "CRV", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", - "priceUsd": "0.719583" + "priceUsd": "0.567487" }, { "chainId": 137, @@ -6820,7 +6793,7 @@ "symbol": "CTSI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", - "priceUsd": "0.07261" + "priceUsd": "0.08669057951006942" }, { "chainId": 137, @@ -6838,7 +6811,7 @@ "symbol": "CUBE", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/10687/thumb/CUBE_icon.png?1617026861", - "priceUsd": "0.311835" + "priceUsd": "0.2583589504605532" }, { "chainId": 137, @@ -6847,7 +6820,7 @@ "symbol": "CVC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", - "priceUsd": "0.080971" + "priceUsd": "0.061702" }, { "chainId": 137, @@ -6856,7 +6829,7 @@ "symbol": "CVX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", - "priceUsd": "1.8323999579291146" + "priceUsd": "2.2664458839220027" }, { "chainId": 137, @@ -6865,7 +6838,7 @@ "symbol": "DAI", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", - "priceUsd": "0.999733" + "priceUsd": "0.999856" }, { "chainId": 137, @@ -6901,7 +6874,7 @@ "symbol": "DPI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", - "priceUsd": "100.65052882009066" + "priceUsd": "86.95095051062147" }, { "chainId": 137, @@ -6910,7 +6883,7 @@ "symbol": "DYDX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", - "priceUsd": "0.05677536969824203" + "priceUsd": "0.06516847199526213" }, { "chainId": 137, @@ -6937,7 +6910,7 @@ "symbol": "ENS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", - "priceUsd": "20.6595799873757" + "priceUsd": "16.03926805102688" }, { "chainId": 137, @@ -6946,7 +6919,7 @@ "symbol": "ERN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", - "priceUsd": "0.09578829672260285" + "priceUsd": "0.06911425517275929" }, { "chainId": 137, @@ -6955,7 +6928,7 @@ "symbol": "EURC", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", - "priceUsd": "2856.655882274905" + "priceUsd": "2.0434170362334663" }, { "chainId": 137, @@ -6964,7 +6937,7 @@ "symbol": "FARM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", - "priceUsd": "235803391453.96518" + "priceUsd": "201696695037.78818" }, { "chainId": 137, @@ -6973,7 +6946,7 @@ "symbol": "FET", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", - "priceUsd": "0.533765725858233" + "priceUsd": "0.22402535069704058" }, { "chainId": 137, @@ -6982,7 +6955,7 @@ "symbol": "FIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", - "priceUsd": "0.14041786603509518" + "priceUsd": "0.12010776998967262" }, { "chainId": 137, @@ -7000,7 +6973,7 @@ "symbol": "FORT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", - "priceUsd": "0.0654886814323961" + "priceUsd": "0.057796753476667496" }, { "chainId": 137, @@ -7009,7 +6982,7 @@ "symbol": "FORTH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14917/thumb/photo_2021-04-22_00.00.03.jpeg?1619020835", - "priceUsd": "23.93197328208239" + "priceUsd": "20.470443138944503" }, { "chainId": 137, @@ -7018,7 +6991,7 @@ "symbol": "FOX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", - "priceUsd": "0.024032618501119315" + "priceUsd": "0.01893487106933752" }, { "chainId": 137, @@ -7027,7 +7000,7 @@ "symbol": "FRAX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", - "priceUsd": "0.9735439365263314" + "priceUsd": "0.9854719212878693" }, { "chainId": 137, @@ -7036,7 +7009,7 @@ "symbol": "FTM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", - "priceUsd": "0.2813189250932908" + "priceUsd": "0.1722543889179826" }, { "chainId": 137, @@ -7045,7 +7018,7 @@ "symbol": "FXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", - "priceUsd": "2.2337826456011975" + "priceUsd": "1.4317263661126398" }, { "chainId": 137, @@ -7054,7 +7027,7 @@ "symbol": "GHST", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", - "priceUsd": "0.37751" + "priceUsd": "0.326572" }, { "chainId": 137, @@ -7063,7 +7036,7 @@ "symbol": "GLM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/542/thumb/Golem_Submark_Positive_RGB.png?1606392013", - "priceUsd": "0.22328354370396006" + "priceUsd": "0.1823329012628519" }, { "chainId": 137, @@ -7072,7 +7045,7 @@ "symbol": "GNO", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", - "priceUsd": "182.05435161122313" + "priceUsd": "31.37293591346084" }, { "chainId": 137, @@ -7090,7 +7063,7 @@ "symbol": "GRT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", - "priceUsd": "0.07989707848246687" + "priceUsd": "0.064578138554371" }, { "chainId": 137, @@ -7099,7 +7072,7 @@ "symbol": "GTC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", - "priceUsd": "0.3381839367" + "priceUsd": "0.26852865691884537" }, { "chainId": 137, @@ -7108,7 +7081,7 @@ "symbol": "GUSD", "decimals": 2, "logoUrl": "https://assets.coingecko.com/coins/images/5992/thumb/gemini-dollar-gusd.png?1536745278", - "priceUsd": "0.9487705752187483" + "priceUsd": "0.8115400215016917" }, { "chainId": 137, @@ -7117,7 +7090,7 @@ "symbol": "GYEN", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", - "priceUsd": "0.002696942443761723" + "priceUsd": "0.002915011281925254" }, { "chainId": 137, @@ -7135,7 +7108,7 @@ "symbol": "IDEX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2565/thumb/logomark-purple-286x286.png?1638362736", - "priceUsd": "0.021582127459347844" + "priceUsd": "0.019040712732429875" }, { "chainId": 137, @@ -7144,7 +7117,7 @@ "symbol": "ILV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", - "priceUsd": "20.204750806525652" + "priceUsd": "17.37582064636093" }, { "chainId": 137, @@ -7162,7 +7135,7 @@ "symbol": "INDEX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12729/thumb/index.png?1634894321", - "priceUsd": "1.012" + "priceUsd": "0.795945" }, { "chainId": 137, @@ -7189,7 +7162,7 @@ "symbol": "IOTX", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", - "priceUsd": "0.02934062641124858" + "priceUsd": "0.007037952945048426" }, { "chainId": 137, @@ -7198,7 +7171,7 @@ "symbol": "JASMY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", - "priceUsd": "0.012673890407730974" + "priceUsd": "0.010796739840791119" }, { "chainId": 137, @@ -7243,7 +7216,7 @@ "symbol": "LDO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", - "priceUsd": "1.15" + "priceUsd": "0.952765" }, { "chainId": 137, @@ -7252,7 +7225,7 @@ "symbol": "LINK", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", - "priceUsd": "21.77" + "priceUsd": "18.74" }, { "chainId": 137, @@ -7261,7 +7234,7 @@ "symbol": "LIT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", - "priceUsd": "5.582301013228283" + "priceUsd": "4.7748747722911835" }, { "chainId": 137, @@ -7288,7 +7261,7 @@ "symbol": "LPT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", - "priceUsd": "0.002134056805326236" + "priceUsd": "0.001825385979409179" }, { "chainId": 137, @@ -7297,7 +7270,7 @@ "symbol": "LQTY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", - "priceUsd": "0.01801536009269893" + "priceUsd": "0.01540961123675108" }, { "chainId": 137, @@ -7315,7 +7288,7 @@ "symbol": "LUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", - "priceUsd": "1" + "priceUsd": "0.9721139653466119" }, { "chainId": 137, @@ -7324,7 +7297,7 @@ "symbol": "MANA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", - "priceUsd": "0.30852407916468383" + "priceUsd": "0.2425826823235643" }, { "chainId": 137, @@ -7333,7 +7306,7 @@ "symbol": "MASK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", - "priceUsd": "1.242368505130622" + "priceUsd": "0.8733742813192864" }, { "chainId": 137, @@ -7360,7 +7333,7 @@ "symbol": "MCO2", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14414/thumb/ENtxnThA_400x400.jpg?1615948522", - "priceUsd": "0.16794996452261213" + "priceUsd": "0.18639650739157765" }, { "chainId": 137, @@ -7369,7 +7342,7 @@ "symbol": "METIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", - "priceUsd": "327272.3171370884" + "priceUsd": "279935.51889518264" }, { "chainId": 137, @@ -7396,7 +7369,7 @@ "symbol": "MKR", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png", - "priceUsd": "1588.5852189147936" + "priceUsd": "1404.1462154396602" }, { "chainId": 137, @@ -7405,7 +7378,7 @@ "symbol": "MLN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", - "priceUsd": "9.719215562502487" + "priceUsd": "7.84" }, { "chainId": 137, @@ -7414,7 +7387,7 @@ "symbol": "MONA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13298/thumb/monavale_logo.jpg?1607232721", - "priceUsd": "69.37502697042436" + "priceUsd": "69.20095779611593" }, { "chainId": 137, @@ -7423,7 +7396,7 @@ "symbol": "MV", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", - "priceUsd": "0.007571744848345652" + "priceUsd": "0.006074619331288136" }, { "chainId": 137, @@ -7432,7 +7405,7 @@ "symbol": "NCT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", - "priceUsd": "0.02050158" + "priceUsd": "0.01661166" }, { "chainId": 137, @@ -7450,7 +7423,7 @@ "symbol": "OCEAN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", - "priceUsd": "0.2437154250124526" + "priceUsd": "0.3167745497775078" }, { "chainId": 137, @@ -7477,7 +7450,7 @@ "symbol": "ORN", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", - "priceUsd": "0.1519682246131855" + "priceUsd": "0.12998748010468592" }, { "chainId": 137, @@ -7486,7 +7459,7 @@ "symbol": "OXT", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4575f41308EC1483f3d399aa9a2826d74Da13Deb/logo.png", - "priceUsd": "4955950.58026542" + "priceUsd": "0.02499294098715514" }, { "chainId": 137, @@ -7495,7 +7468,7 @@ "symbol": "PAXG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", - "priceUsd": "4055.03754680343" + "priceUsd": "3950.3792472556" }, { "chainId": 137, @@ -7522,7 +7495,7 @@ "symbol": "PLU", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1241/thumb/pluton.png?1548331624", - "priceUsd": "67.76933568240494" + "priceUsd": "57.96715198948287" }, { "chainId": 137, @@ -7531,7 +7504,7 @@ "symbol": "POLS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", - "priceUsd": "133.71606618040283" + "priceUsd": "114.37532113403321" }, { "chainId": 137, @@ -7540,7 +7513,7 @@ "symbol": "POLY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", - "priceUsd": "0.2169076673179811" + "priceUsd": "0.10986123318940401" }, { "chainId": 137, @@ -7558,7 +7531,7 @@ "symbol": "POWR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", - "priceUsd": "105.6802273292315" + "priceUsd": "90.39459717571293" }, { "chainId": 137, @@ -7576,7 +7549,7 @@ "symbol": "PRQ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", - "priceUsd": "78353034158176720" + "priceUsd": "67020020104216456" }, { "chainId": 137, @@ -7594,7 +7567,7 @@ "symbol": "QUICK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13970/thumb/1_pOU6pBMEmiL-ZJVb0CYRjQ.png?1613386659", - "priceUsd": "23.531103099950304" + "priceUsd": "19.520135296833764" }, { "chainId": 137, @@ -7612,7 +7585,7 @@ "symbol": "RAI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", - "priceUsd": "3.625711250113875" + "priceUsd": "4.28337248660846" }, { "chainId": 137, @@ -7621,7 +7594,7 @@ "symbol": "RARI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", - "priceUsd": "0.8924905136483643" + "priceUsd": "1.2311817533888176" }, { "chainId": 137, @@ -7630,7 +7603,7 @@ "symbol": "RBC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12629/thumb/200x200.png?1607952509", - "priceUsd": "0.010866470034968772" + "priceUsd": "0.009294739483033438" }, { "chainId": 137, @@ -7639,7 +7612,7 @@ "symbol": "REN", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", - "priceUsd": "3175614299.350026" + "priceUsd": "0.7502608223319054" }, { "chainId": 137, @@ -7666,7 +7639,7 @@ "symbol": "REVV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12373/thumb/REVV_TOKEN_Refined_2021_%281%29.png?1627652390", - "priceUsd": "0.0010976782277687927" + "priceUsd": "0.000920539919799999" }, { "chainId": 137, @@ -7702,7 +7675,7 @@ "symbol": "RNDR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", - "priceUsd": "0.1588243042447544" + "priceUsd": "0.19985873274995922" }, { "chainId": 137, @@ -7720,7 +7693,7 @@ "symbol": "SAND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", - "priceUsd": "0.262065" + "priceUsd": "0.21716" }, { "chainId": 137, @@ -7729,7 +7702,7 @@ "symbol": "SD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", - "priceUsd": "0.5060047497001328" + "priceUsd": "0.48399043069342473" }, { "chainId": 137, @@ -7747,7 +7720,7 @@ "symbol": "SLP", "decimals": 0, "logoUrl": "https://assets.coingecko.com/coins/images/10366/thumb/SLP.png?1578640057", - "priceUsd": "0.0014142877607304268" + "priceUsd": "0.001209724615972755" }, { "chainId": 137, @@ -7756,7 +7729,7 @@ "symbol": "SNX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", - "priceUsd": "1.07340797536133" + "priceUsd": "1.1750604447321746" }, { "chainId": 137, @@ -7765,7 +7738,7 @@ "symbol": "SPELL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", - "priceUsd": null + "priceUsd": "0.004704688854309346" }, { "chainId": 137, @@ -7774,7 +7747,7 @@ "symbol": "STORJ", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", - "priceUsd": "0.2652282806190598" + "priceUsd": "0.2266293190741539" }, { "chainId": 137, @@ -7792,7 +7765,7 @@ "symbol": "SUKU", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11969/thumb/UmfW5S6f_400x400.jpg?1596602238", - "priceUsd": "0.026236927932920286" + "priceUsd": "0.01994342488766759" }, { "chainId": 137, @@ -7801,7 +7774,7 @@ "symbol": "SUPER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", - "priceUsd": "0.5593452120432967" + "priceUsd": "0.4026162188265138" }, { "chainId": 137, @@ -7810,7 +7783,7 @@ "symbol": "sUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", - "priceUsd": "0.21806966598410188" + "priceUsd": "0.7521010003387928" }, { "chainId": 137, @@ -7819,7 +7792,7 @@ "symbol": "SUSHI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", - "priceUsd": "0.684912787715374" + "priceUsd": "0.5390334263531873" }, { "chainId": 137, @@ -7855,7 +7828,7 @@ "symbol": "TRB", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", - "priceUsd": "32.42" + "priceUsd": "27.02" }, { "chainId": 137, @@ -7900,7 +7873,7 @@ "symbol": "UNI", "decimals": 18, "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", - "priceUsd": "7.84" + "priceUsd": "6.62" }, { "chainId": 137, @@ -7909,7 +7882,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.999705" + "priceUsd": "0.999806" }, { "chainId": 137, @@ -7918,7 +7891,7 @@ "symbol": "USDC.e", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.99969" + "priceUsd": "0.999803" }, { "chainId": 137, @@ -7927,7 +7900,7 @@ "symbol": "USDP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", - "priceUsd": "11967590282.942545" + "priceUsd": "3744660.85707" }, { "chainId": 137, @@ -7945,7 +7918,7 @@ "symbol": "VANRY", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/33466/large/apple-touch-icon.png?1701942541", - "priceUsd": "0.02012472763151889" + "priceUsd": null }, { "chainId": 137, @@ -7954,7 +7927,7 @@ "symbol": "VOXEL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/21260/large/voxies.png", - "priceUsd": "0.04894837" + "priceUsd": "0.04055356" }, { "chainId": 137, @@ -7963,7 +7936,7 @@ "symbol": "WBTC", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", - "priceUsd": "122678" + "priceUsd": "115173" }, { "chainId": 137, @@ -7981,7 +7954,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4373.11" + "priceUsd": "4157.97" }, { "chainId": 137, @@ -7990,7 +7963,7 @@ "symbol": "WMATIC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", - "priceUsd": "0.238182" + "priceUsd": "0.195096" }, { "chainId": 137, @@ -7999,7 +7972,7 @@ "symbol": "WOO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", - "priceUsd": "0.07740721550939068" + "priceUsd": "0.04237612173376819" }, { "chainId": 137, @@ -8008,7 +7981,7 @@ "symbol": "XSGD", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", - "priceUsd": "0.7712373977627099" + "priceUsd": "0.7716978895498146" }, { "chainId": 137, @@ -8017,7 +7990,7 @@ "symbol": "XYO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", - "priceUsd": "0.008801852407976166" + "priceUsd": "0.00755212350782007" }, { "chainId": 137, @@ -8026,7 +7999,7 @@ "symbol": "YFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", - "priceUsd": "5302.52" + "priceUsd": "5109.958385239824" }, { "chainId": 137, @@ -8044,7 +8017,7 @@ "symbol": "YGG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17358/thumb/le1nzlO6_400x400.jpg?1632465691", - "priceUsd": "0.13199009150801538" + "priceUsd": "0.12924202350765468" }, { "chainId": 137, @@ -8053,7 +8026,7 @@ "symbol": "ZRO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", - "priceUsd": "2.341099101056258" + "priceUsd": "1.73" }, { "chainId": 137, @@ -8071,7 +8044,7 @@ "symbol": "ZK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/38043/large/ZKTokenBlack.png?17186145029", - "priceUsd": "0.0542" + "priceUsd": "0.03336501" }, { "chainId": 480, @@ -8080,7 +8053,7 @@ "symbol": "USDC.e", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/35218/large/USDC_Icon.png?1707908537", - "priceUsd": "0.999705" + "priceUsd": "0.999806" }, { "chainId": 480, @@ -8089,7 +8062,7 @@ "symbol": "WBTC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857", - "priceUsd": "122478.91939409873" + "priceUsd": "115393" }, { "chainId": 480, @@ -8098,7 +8071,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.813939" + "priceUsd": "4123.44" }, { "chainId": 1868, @@ -8107,7 +8080,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "1" + "priceUsd": "1.001" }, { "chainId": 1868, @@ -8116,7 +8089,7 @@ "symbol": "USDT", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", - "priceUsd": "0.998894" + "priceUsd": "0.999673" }, { "chainId": 1868, @@ -8125,7 +8098,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4362.48" + "priceUsd": "4123.59" }, { "chainId": 8453, @@ -8134,7 +8107,7 @@ "symbol": "1INCH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", - "priceUsd": "0.24584676791931187" + "priceUsd": null }, { "chainId": 8453, @@ -8143,7 +8116,7 @@ "symbol": "AAVE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", - "priceUsd": "276.06" + "priceUsd": "236.62" }, { "chainId": 8453, @@ -8152,7 +8125,7 @@ "symbol": "ABT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2341/thumb/arcblock.png?1547036543", - "priceUsd": "0.6174385112333802" + "priceUsd": "0.6961723783595525" }, { "chainId": 8453, @@ -8170,7 +8143,7 @@ "symbol": "AERO", "decimals": 18, "logoUrl": "https://basescan.org/token/images/aerodrome_32.png", - "priceUsd": "1.072" + "priceUsd": "0.940409" }, { "chainId": 8453, @@ -8179,7 +8152,7 @@ "symbol": "AIXBT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/51784/large/3.png?1731981138", - "priceUsd": "0.091058" + "priceUsd": "0.085459" }, { "chainId": 8453, @@ -8188,7 +8161,7 @@ "symbol": "ALI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", - "priceUsd": "0.00568759" + "priceUsd": null }, { "chainId": 8453, @@ -8197,7 +8170,7 @@ "symbol": "AWE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/8713/large/awe-network.jpg?1747816016", - "priceUsd": "0.096117" + "priceUsd": null }, { "chainId": 8453, @@ -8206,7 +8179,16 @@ "symbol": "B3", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54287/large/B3.png?1739001374", - "priceUsd": "0.00258218" + "priceUsd": "0.00236488" + }, + { + "chainId": 8453, + "address": "0xf5Dbaa3DFC5e81405c7306039fB037a3DCD57Ce2", + "name": "Biconomy", + "symbol": "BICO", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", + "priceUsd": "0.0651" }, { "chainId": 8453, @@ -8215,7 +8197,7 @@ "symbol": "BITCOIN", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", - "priceUsd": "0.096997" + "priceUsd": "0.064113" }, { "chainId": 8453, @@ -8224,7 +8206,16 @@ "symbol": "BNKR", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52626/large/bankr-static.png?1736405365", - "priceUsd": "0.00055007" + "priceUsd": "0.00052452" + }, + { + "chainId": 8453, + "address": "0x1F9bD96DDB4Bd07d6061f8933e9bA9EDE9967550", + "name": "Boba Network", + "symbol": "BOBA", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/20285/thumb/BOBA.png?1636811576", + "priceUsd": null }, { "chainId": 8453, @@ -8233,7 +8224,7 @@ "symbol": "BTRST", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18100/thumb/braintrust.PNG?1630475394", - "priceUsd": "0.16859347976140426" + "priceUsd": null }, { "chainId": 8453, @@ -8242,7 +8233,7 @@ "symbol": "cbADA", "decimals": 6, "logoUrl": "https://coin-images.coingecko.com/coins/images/66647/large/Coinbase_Wrapped_Ada_%28cbADA%29.png?1750129533", - "priceUsd": "0.8149971266903104" + "priceUsd": "0.6712" }, { "chainId": 8453, @@ -8251,7 +8242,7 @@ "symbol": "cbBTC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/40143/standard/cbbtc.webp", - "priceUsd": "122687" + "priceUsd": "115346" }, { "chainId": 8453, @@ -8260,7 +8251,7 @@ "symbol": "CBDOGE", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/66268/large/Coinbase_Wrapped_Doge_%28cbDOGE%29.png?1749023465", - "priceUsd": "0.246198" + "priceUsd": "0.201852" }, { "chainId": 8453, @@ -8269,7 +8260,7 @@ "symbol": "cbETH", "decimals": 18, "logoUrl": "https://ethereum-optimism.github.io/data/cbETH/logo.svg", - "priceUsd": "4816.72" + "priceUsd": "4577.84" }, { "chainId": 8453, @@ -8278,7 +8269,7 @@ "symbol": "cbLTC", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/66646/large/Coinbase_Wrapped_Litecoin_%28cbLTC%29.png?1750129508", - "priceUsd": "116.81383558776494" + "priceUsd": null }, { "chainId": 8453, @@ -8287,7 +8278,7 @@ "symbol": "cbXRP", "decimals": 6, "logoUrl": "https://coin-images.coingecko.com/coins/images/66267/large/Coinbase_Wrapped_XPR_%28cbXRP%29.png?1749023398", - "priceUsd": "2.83" + "priceUsd": "2.67" }, { "chainId": 8453, @@ -8296,7 +8287,7 @@ "symbol": "CLANKER", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/51440/large/CLANKER.png?1731232869", - "priceUsd": "29.77" + "priceUsd": "123.59" }, { "chainId": 8453, @@ -8305,7 +8296,7 @@ "symbol": "COMP", "decimals": 18, "logoUrl": "https://ethereum-optimism.github.io/data/COMP/logo.svg", - "priceUsd": "41.87896029568512" + "priceUsd": "37.67343893857246" }, { "chainId": 8453, @@ -8314,25 +8305,7 @@ "symbol": "COOKIE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/38450/large/cookie_token_logo_200x200.png?1733194528", - "priceUsd": "0.118352" - }, - { - "chainId": 8453, - "address": "0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415", - "name": "Curve DAO Token", - "symbol": "CRV", - "decimals": 18, - "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", - "priceUsd": "0.719583" - }, - { - "chainId": 8453, - "address": "0x259Fac10c5CbFEFE3E710e1D9467f70a76138d45", - "name": "Cartesi", - "symbol": "CTSI", - "decimals": 18, - "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", - "priceUsd": "0.07261" + "priceUsd": "0.0966" }, { "chainId": 8453, @@ -8341,7 +8314,7 @@ "symbol": "DAI", "decimals": 18, "logoUrl": "https://ethereum-optimism.github.io/data/DAI/logo.svg", - "priceUsd": "0.998708" + "priceUsd": "0.999548" }, { "chainId": 8453, @@ -8350,7 +8323,7 @@ "symbol": "DEGEN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/34515/large/android-chrome-512x512.png?1706198225", - "priceUsd": "0.00274528" + "priceUsd": "0.00218005" }, { "chainId": 8453, @@ -8359,7 +8332,7 @@ "symbol": "doginme", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/35123/large/doginme-logo1-transparent200.png?1710856784", - "priceUsd": "0.00037197" + "priceUsd": "0.00044193" }, { "chainId": 8453, @@ -8368,7 +8341,7 @@ "symbol": "DRV", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", - "priceUsd": "0.03600272" + "priceUsd": "0.04366103" }, { "chainId": 8453, @@ -8377,7 +8350,7 @@ "symbol": "EDGE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/55072/large/EDGE-120x120.png?1743598652", - "priceUsd": "0.270258" + "priceUsd": null }, { "chainId": 8453, @@ -8395,7 +8368,25 @@ "symbol": "FAI", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52315/large/FAI.png?1733076295", - "priceUsd": "0.00974502" + "priceUsd": "0.00738795" + }, + { + "chainId": 8453, + "address": "0xD08a2917653d4E460893203471f0000826fb4034", + "name": "Harvest Finance", + "symbol": "FARM", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0x74F804B4140ee70830B3Eef4e690325841575F89", + "name": "Fetch ai", + "symbol": "FET", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", + "priceUsd": "0.2657554640941423" }, { "chainId": 8453, @@ -8404,34 +8395,34 @@ "symbol": "FLOCK", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/53178/large/FLock_Token_Logo.png?1735561398", - "priceUsd": "0.270907" + "priceUsd": "0.264498" }, { "chainId": 8453, - "address": "0xB78e7D4C5d47Af92942321eD40419dab0E573810", - "name": "Goldfinch", - "symbol": "GFI", + "address": "0xb008BDCF9CdFf9da684a190941dC3dCa8C2Cdd44", + "name": "Flux", + "symbol": "FLUX", "decimals": 18, - "logoUrl": "https://assets.coingecko.com/coins/images/19081/thumb/GOLDFINCH.png?1634369662", - "priceUsd": "0.4801060307424254" + "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png", + "priceUsd": null }, { "chainId": 8453, - "address": "0xcD2F22236DD9Dfe2356D7C543161D4d260FD9BcB", - "name": "Aavegotchi", - "symbol": "GHST", + "address": "0x7588310a7aBF34DC608ac98a1c4432F85e194Df5", + "name": "Forta", + "symbol": "FORT", "decimals": 18, - "logoUrl": "https://assets.coingecko.com/coins/images/12467/thumb/ghst_200.png?1600750321", - "priceUsd": "0.37751" + "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", + "priceUsd": null }, { "chainId": 8453, - "address": "0x0F4d237B09Cb37d207BA60353Dc254d4530D4dF1", - "name": "The Graph", - "symbol": "GRT", + "address": "0x968B2323d4b005C7D39c67D31774FE83c9943A60", + "name": "Ampleforth Governance Token", + "symbol": "FORTH", "decimals": 18, - "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", - "priceUsd": "0.08089360514368812" + "logoUrl": "https://assets.coingecko.com/coins/images/14917/thumb/photo_2021-04-22_00.00.03.jpeg?1619020835", + "priceUsd": null }, { "chainId": 8453, @@ -8440,7 +8431,7 @@ "symbol": "HOME", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54873/large/defi-app.png?1742235743", - "priceUsd": "0.02909586" + "priceUsd": "0.02381585" }, { "chainId": 8453, @@ -8449,7 +8440,7 @@ "symbol": "IOTX", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/2777.png", - "priceUsd": "0.02332452" + "priceUsd": null }, { "chainId": 8453, @@ -8467,7 +8458,7 @@ "symbol": "KAITO", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54411/large/Qm4DW488_400x400.jpg?1739552780", - "priceUsd": "1.31" + "priceUsd": "1.14" }, { "chainId": 8453, @@ -8476,25 +8467,43 @@ "symbol": "KEYCAT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/36608/large/keyboard_cat.jpeg?1711965348", - "priceUsd": "0.00273519" + "priceUsd": "0.00275387" }, { "chainId": 8453, - "address": "0x7300B37DfdfAb110d83290A29DfB31B1740219fE", - "name": "Mamo", - "symbol": "MAMO", + "address": "0xDAE49C25fAd3a62a8e8bFB6dA12c46bE611f9f7a", + "name": "KRYLL", + "symbol": "KRL", "decimals": 18, - "logoUrl": "https://coin-images.coingecko.com/coins/images/55958/large/Mamo_Circle_200x200_TransBG.png?1748974093", - "priceUsd": "0.0695" + "logoUrl": "https://assets.coingecko.com/coins/images/2807/thumb/krl.png?1547036979", + "priceUsd": null }, { "chainId": 8453, - "address": "0xBAa5CC21fd487B8Fcc2F632f3F4E8D37262a0842", - "name": "Morpho Token", - "symbol": "MORPHO", + "address": "0xd7468c14ae76C3Fc308aEAdC223D5D1F71d3c171", + "name": "LCX", + "symbol": "LCX", "decimals": 18, - "logoUrl": "https://coin-images.coingecko.com/coins/images/29837/large/Morpho-token-icon.png?1726771230", - "priceUsd": "1.72" + "logoUrl": "https://assets.coingecko.com/coins/images/9985/thumb/zRPSu_0o_400x400.jpg?1574327008", + "priceUsd": "57740799.86236591" + }, + { + "chainId": 8453, + "address": "0x5259384690aCF240e9b0A8811bD0FFbFBDdc125C", + "name": "Liquity", + "symbol": "LQTY", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", + "priceUsd": "0.6430499164620415" + }, + { + "chainId": 8453, + "address": "0x7300B37DfdfAb110d83290A29DfB31B1740219fE", + "name": "Mamo", + "symbol": "MAMO", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/55958/large/Mamo_Circle_200x200_TransBG.png?1748974093", + "priceUsd": null }, { "chainId": 8453, @@ -8503,7 +8512,7 @@ "symbol": "NCT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", - "priceUsd": "0.020033226668842236" + "priceUsd": "0.02642256986218982" }, { "chainId": 8453, @@ -8512,7 +8521,7 @@ "symbol": "ODOS", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52914/large/odos.jpg?1734678948", - "priceUsd": "0.0045635" + "priceUsd": "0.00371267" }, { "chainId": 8453, @@ -8521,7 +8530,7 @@ "symbol": "OGN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", - "priceUsd": "0.057866" + "priceUsd": null }, { "chainId": 8453, @@ -8530,7 +8539,7 @@ "symbol": "PENDLE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", - "priceUsd": "4.48" + "priceUsd": null }, { "chainId": 8453, @@ -8541,6 +8550,15 @@ "logoUrl": "https://assets.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1682922725", "priceUsd": null }, + { + "chainId": 8453, + "address": "0xCD6dDDa305955AcD6b94b934f057E8b0daaD58dE", + "name": "Perpetual Protocol", + "symbol": "PERP", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", + "priceUsd": "0.6819630327634513" + }, { "chainId": 8453, "address": "0x30c7235866872213F68cb1F08c37Cb9eCCB93452", @@ -8548,7 +8566,16 @@ "symbol": "PROMPT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/55169/large/wayfinder.jpg?1744336900", - "priceUsd": "0.137157" + "priceUsd": "0.089085" + }, + { + "chainId": 8453, + "address": "0x38815A4455921667d673B4cb3d48F0383eE93400", + "name": "pSTAKE Finance", + "symbol": "PSTAKE", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/23931/thumb/PSTAKE_Dark.png?1645709930", + "priceUsd": "0.01499234" }, { "chainId": 8453, @@ -8557,7 +8584,7 @@ "symbol": "RPL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/2090/large/rocket_pool_%28RPL%29.png?1696503058", - "priceUsd": "37.709102377815825" + "priceUsd": null }, { "chainId": 8453, @@ -8566,7 +8593,7 @@ "symbol": "RSC", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/28146/large/RH_BOTTLE_CLEAN_Aug_2024_1.png?1732742001", - "priceUsd": "0.447549" + "priceUsd": null }, { "chainId": 8453, @@ -8575,7 +8602,7 @@ "symbol": "RSR", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", - "priceUsd": "0.005826723370570488" + "priceUsd": null }, { "chainId": 8453, @@ -8584,7 +8611,7 @@ "symbol": "SAPIEN", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/68423/large/logo.png?1755710030", - "priceUsd": "0.152168" + "priceUsd": "0.134742" }, { "chainId": 8453, @@ -8593,7 +8620,7 @@ "symbol": "SEAM", "decimals": 18, "logoUrl": "https://basescan.org/token/images/seamless_32.png", - "priceUsd": "0.340919" + "priceUsd": "0.237979" }, { "chainId": 8453, @@ -8602,7 +8629,7 @@ "symbol": "SNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", - "priceUsd": "0.029180593575659698" + "priceUsd": "0.027575417970993776" }, { "chainId": 8453, @@ -8611,7 +8638,7 @@ "symbol": "SNX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", - "priceUsd": "1.087" + "priceUsd": "1.17" }, { "chainId": 8453, @@ -8620,7 +8647,25 @@ "symbol": "SPX", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", - "priceUsd": "1.45" + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0x7D49a065D17d6d4a55dc13649901fdBB98B2AFBA", + "name": "Sushi", + "symbol": "SUSHI", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b", + "name": "tBTC", + "symbol": "tBTC", + "decimals": 18, + "logoUrl": "https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/0x18084fbA666a33d37592fA2633fD49a74DD93a88/logo.png", + "priceUsd": "115172" }, { "chainId": 8453, @@ -8629,7 +8674,7 @@ "symbol": "TOSHI", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/31126/large/Toshi_Logo_-_Circular.png?1721677476", - "priceUsd": "0.00098898" + "priceUsd": "0.00079489" }, { "chainId": 8453, @@ -8638,7 +8683,34 @@ "symbol": "TOWNS", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/55230/large/yImUvwK__400x400.png?1744857671", - "priceUsd": "0.01811908" + "priceUsd": "0.01143012" + }, + { + "chainId": 8453, + "address": "0xA81a52B4dda010896cDd386C7fBdc5CDc835ba23", + "name": "OriginTrail", + "symbol": "TRAC", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/1877/thumb/TRAC.jpg?1635134367", + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0xF8e9E61FFB2b491f7DF29823a76009743671CD96", + "name": "Tellor", + "symbol": "TRB", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", + "priceUsd": "27.343609045944202" + }, + { + "chainId": 8453, + "address": "0xc3De830EA07524a0761646a6a4e4be0e114a3C83", + "name": "Uniswap", + "symbol": "UNI", + "decimals": 18, + "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", + "priceUsd": null }, { "chainId": 8453, @@ -8647,7 +8719,7 @@ "symbol": "USDbC", "decimals": 6, "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", - "priceUsd": "0.999397" + "priceUsd": "1.001" }, { "chainId": 8453, @@ -8656,16 +8728,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://ethereum-optimism.github.io/data/USDC/logo.png", - "priceUsd": "0.999705" - }, - { - "chainId": 8453, - "address": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", - "name": "Tether USD", - "symbol": "USDT", - "decimals": 6, - "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", - "priceUsd": "0.999194" + "priceUsd": "0.999806" }, { "chainId": 8453, @@ -8674,7 +8737,7 @@ "symbol": "VVV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/54023/standard/Venice_Token_(1).png?1738017546", - "priceUsd": "2.41" + "priceUsd": "1.63" }, { "chainId": 8453, @@ -8683,7 +8746,16 @@ "symbol": "WAMPL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", - "priceUsd": "2.1096999193233255" + "priceUsd": null + }, + { + "chainId": 8453, + "address": "0xeF4461891DfB3AC8572cCf7C794664A8DD927945", + "name": "WalletConnect Token", + "symbol": "WCT", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/50390/large/wc-token1.png?1727569464", + "priceUsd": null }, { "chainId": 8453, @@ -8692,7 +8764,7 @@ "symbol": "WELL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26133/large/WELL.png?1696525221", - "priceUsd": "0.02312538" + "priceUsd": null }, { "chainId": 8453, @@ -8701,7 +8773,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://ethereum-optimism.github.io/data/WETH/logo.png", - "priceUsd": "4372.77" + "priceUsd": "4162.46" }, { "chainId": 8453, @@ -8710,7 +8782,7 @@ "symbol": "XYO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4519/thumb/XYO_Network-logo.png?1547039819", - "priceUsd": "0.009040" + "priceUsd": null }, { "chainId": 8453, @@ -8719,7 +8791,16 @@ "symbol": "YFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", - "priceUsd": "5340.755750023404" + "priceUsd": "4815.97076625824" + }, + { + "chainId": 8453, + "address": "0xaAC78d1219c08AecC8e37e03858FE885f5EF1799", + "name": "Yield Guild Games", + "symbol": "YGG", + "decimals": 18, + "logoUrl": "https://assets.coingecko.com/coins/images/17358/thumb/le1nzlO6_400x400.jpg?1632465691", + "priceUsd": null }, { "chainId": 8453, @@ -8728,7 +8809,7 @@ "symbol": "ZEN", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/691/large/Horizen2.0-logo_icon-on-yellow_%281%29.png?1751696763", - "priceUsd": "11.62" + "priceUsd": "13.27" }, { "chainId": 8453, @@ -8737,7 +8818,7 @@ "symbol": "ZORA", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/54693/large/zora.jpg?1741094751", - "priceUsd": "0.053145" + "priceUsd": "0.090137" }, { "chainId": 8453, @@ -8746,7 +8827,7 @@ "symbol": "ZRO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", - "priceUsd": "2.34" + "priceUsd": "1.74" }, { "chainId": 8453, @@ -8755,7 +8836,7 @@ "symbol": "ZRX", "decimals": 18, "logoUrl": "https://ethereum-optimism.github.io/data/ZRX/logo.png", - "priceUsd": "0.09119117231707215" + "priceUsd": null }, { "chainId": 42161, @@ -8764,7 +8845,7 @@ "symbol": "1INCH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028", - "priceUsd": "0.2489588228471673" + "priceUsd": "0.6078882594186588" }, { "chainId": 42161, @@ -8773,7 +8854,7 @@ "symbol": "AAVE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110", - "priceUsd": "276.06" + "priceUsd": "236.62" }, { "chainId": 42161, @@ -8782,7 +8863,7 @@ "symbol": "ACX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28161/large/across-200x200.png?1696527165", - "priceUsd": "0.11094" + "priceUsd": "0.071776" }, { "chainId": 42161, @@ -8791,7 +8872,7 @@ "symbol": "AEVO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35893/standard/aevo.png", - "priceUsd": "0.09675610052146794" + "priceUsd": "0.06517755323815316" }, { "chainId": 42161, @@ -8800,7 +8881,7 @@ "symbol": "agEUR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19479/standard/agEUR.png?1696518915", - "priceUsd": "1.16" + "priceUsd": "1.17" }, { "chainId": 42161, @@ -8809,7 +8890,7 @@ "symbol": "AGLD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18125/thumb/lpgblc4h_400x400.jpg?1630570955", - "priceUsd": "0.5464" + "priceUsd": "0.4096" }, { "chainId": 42161, @@ -8818,7 +8899,7 @@ "symbol": "AIOZ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126", - "priceUsd": "0.2679156726233584" + "priceUsd": "0.1974185608628099" }, { "chainId": 42161, @@ -8827,7 +8908,7 @@ "symbol": "ALEPH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11676/thumb/Monochram-aleph.png?1608483725", - "priceUsd": "0.0792471029790218" + "priceUsd": "0.05210554927691248" }, { "chainId": 42161, @@ -8836,7 +8917,7 @@ "symbol": "ALI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22062/thumb/alethea-logo-transparent-colored.png?1642748848", - "priceUsd": "0.005675368522712019" + "priceUsd": "0.004073556988957618" }, { "chainId": 42161, @@ -8845,7 +8926,7 @@ "symbol": "ALPHA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12738/thumb/AlphaToken_256x256.png?1617160876", - "priceUsd": "0.014732268070836543" + "priceUsd": "0.00986153951879375" }, { "chainId": 42161, @@ -8854,7 +8935,7 @@ "symbol": "ANKR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4324/thumb/U85xTl2.png?1608111978", - "priceUsd": "0.013680940635142481" + "priceUsd": "0.010397149107603334" }, { "chainId": 42161, @@ -8863,7 +8944,7 @@ "symbol": "APE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24383/small/apecoin.jpg?1647476455", - "priceUsd": "0.5603204598658061" + "priceUsd": "0.4834909742143483" }, { "chainId": 42161, @@ -8872,7 +8953,7 @@ "symbol": "API3", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13256/thumb/api3.jpg?1606751424", - "priceUsd": "0.8185828718931633" + "priceUsd": "0.6884809478975566" }, { "chainId": 42161, @@ -8881,7 +8962,7 @@ "symbol": "ARB", "decimals": 18, "logoUrl": "https://arbitrum.foundation/logo.png", - "priceUsd": "0.418755" + "priceUsd": "0.332923" }, { "chainId": 42161, @@ -8890,7 +8971,7 @@ "symbol": "ARKM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/30929/standard/Arkham_Logo_CG.png?1696529771", - "priceUsd": "0.5087351708583264" + "priceUsd": "0.36954103086737095" }, { "chainId": 42161, @@ -8899,7 +8980,7 @@ "symbol": "ATA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15985/thumb/ATA.jpg?1622535745", - "priceUsd": "0.0397" + "priceUsd": "0.0294" }, { "chainId": 42161, @@ -8908,7 +8989,7 @@ "symbol": "ATH", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/36179/large/logogram_circle_dark_green_vb_green_(1).png?1718232706", - "priceUsd": "0.052432130189680765" + "priceUsd": "0.03019314221906034" }, { "chainId": 42161, @@ -8917,7 +8998,7 @@ "symbol": "AXL", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/27277/large/V-65_xQ1_400x400.jpeg", - "priceUsd": "0.291207" + "priceUsd": "0.4303759277790356" }, { "chainId": 42161, @@ -8926,7 +9007,7 @@ "symbol": "AXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13029/thumb/axie_infinity_logo.png?1604471082", - "priceUsd": "2.1266584308552745" + "priceUsd": "1.6167978182788942" }, { "chainId": 42161, @@ -8935,7 +9016,7 @@ "symbol": "BADGER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13287/thumb/badger_dao_logo.jpg?1607054976", - "priceUsd": "0.972151" + "priceUsd": "0.8033663927628695" }, { "chainId": 42161, @@ -8944,7 +9025,7 @@ "symbol": "BAL", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xba100000625a3754423978a60c9317c58a424e3D/logo.png", - "priceUsd": "1.14" + "priceUsd": "1.0153792271458928" }, { "chainId": 42161, @@ -8953,7 +9034,7 @@ "symbol": "BAT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427", - "priceUsd": "0.1571154704702344" + "priceUsd": "0.3271442222027912" }, { "chainId": 42161, @@ -8962,7 +9043,7 @@ "symbol": "BICO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/21061/thumb/biconomy_logo.jpg?1638269749", - "priceUsd": "0.090553" + "priceUsd": "0.065453" }, { "chainId": 42161, @@ -8971,7 +9052,7 @@ "symbol": "BIT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17627/thumb/rI_YptK8.png?1653983088", - "priceUsd": "2.5364775777228057" + "priceUsd": "1.6384162899207104" }, { "chainId": 42161, @@ -8980,7 +9061,7 @@ "symbol": "BITCOIN", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1696529224", - "priceUsd": "0.0975789992870829" + "priceUsd": "839641.9512434003" }, { "chainId": 42161, @@ -8989,7 +9070,7 @@ "symbol": "BLUR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28453/large/blur.png?1670745921", - "priceUsd": "0.07161749917119595" + "priceUsd": "0.17543230356290765" }, { "chainId": 42161, @@ -8998,7 +9079,7 @@ "symbol": "BNT", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C/logo.png", - "priceUsd": "0.6856" + "priceUsd": "0.6202" }, { "chainId": 42161, @@ -9007,7 +9088,7 @@ "symbol": "BOND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12811/thumb/barnbridge.jpg?1602728853", - "priceUsd": "0.154313" + "priceUsd": "0.138686" }, { "chainId": 42161, @@ -9016,7 +9097,7 @@ "symbol": "BUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9576/thumb/BUSD.png?1568947766", - "priceUsd": "1.025531135070954" + "priceUsd": "1.000204250340956" }, { "chainId": 42161, @@ -9025,7 +9106,7 @@ "symbol": "CAKE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/12632/large/pancakeswap-cake-logo_%281%29.png?1696512440", - "priceUsd": "3.829" + "priceUsd": "2.71" }, { "chainId": 42161, @@ -9034,7 +9115,7 @@ "symbol": "cbBTC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/40143/large/cbbtc.webp", - "priceUsd": "122687" + "priceUsd": null }, { "chainId": 42161, @@ -9043,7 +9124,7 @@ "symbol": "cbETH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/27008/large/cbeth.png", - "priceUsd": "4816.72" + "priceUsd": "4577.84" }, { "chainId": 42161, @@ -9052,7 +9133,7 @@ "symbol": "CELO", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/wormhole-foundation/wormhole-token-list/main/assets/celo_wh.png", - "priceUsd": "0.3765" + "priceUsd": null }, { "chainId": 42161, @@ -9061,7 +9142,7 @@ "symbol": "CELR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4379/thumb/Celr.png?1554705437", - "priceUsd": "0.00767154" + "priceUsd": "0.013117794486952043" }, { "chainId": 42161, @@ -9070,7 +9151,7 @@ "symbol": "COMP", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xc00e94Cb662C3520282E6f5717214004A7f26888/logo.png", - "priceUsd": "41.63" + "priceUsd": "37.378103497611136" }, { "chainId": 42161, @@ -9079,7 +9160,7 @@ "symbol": "COTI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2962/thumb/Coti.png?1559653863", - "priceUsd": "0.05108528424791468" + "priceUsd": "0.03531866686871258" }, { "chainId": 42161, @@ -9088,7 +9169,7 @@ "symbol": "COW", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24384/large/CoW-token_logo.png?1719524382", - "priceUsd": "0.274709" + "priceUsd": "0.23909109409417464" }, { "chainId": 42161, @@ -9106,7 +9187,7 @@ "symbol": "CRO", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/7310/thumb/oCw2s3GI_400x400.jpeg?1645172042", - "priceUsd": "0.1933605667352337" + "priceUsd": "0.16025928083338412" }, { "chainId": 42161, @@ -9115,7 +9196,7 @@ "symbol": "CRV", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xD533a949740bb3306d119CC777fa900bA034cd52/logo.png", - "priceUsd": "0.719583" + "priceUsd": "0.567487" }, { "chainId": 42161, @@ -9124,7 +9205,7 @@ "symbol": "CTSI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11038/thumb/cartesi.png?1592288021", - "priceUsd": "0.07261" + "priceUsd": "0.08234877972367989" }, { "chainId": 42161, @@ -9133,7 +9214,7 @@ "symbol": "CTX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14932/thumb/glossy_icon_-_C200px.png?1619073171", - "priceUsd": "1.3272342177375096" + "priceUsd": "1.0847248910250304" }, { "chainId": 42161, @@ -9142,7 +9223,7 @@ "symbol": "CVC", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/788/thumb/civic.png?1547034556", - "priceUsd": "0.08119748484973946" + "priceUsd": "0.06149837750000125" }, { "chainId": 42161, @@ -9151,7 +9232,7 @@ "symbol": "CVX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15585/thumb/convex.png?1621256328", - "priceUsd": "3.298529250840107" + "priceUsd": "2.318644178156374" }, { "chainId": 42161, @@ -9160,7 +9241,7 @@ "symbol": "DAI", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", - "priceUsd": "0.999426" + "priceUsd": "0.999469" }, { "chainId": 42161, @@ -9169,7 +9250,7 @@ "symbol": "DEXT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11603/thumb/dext.png?1605790188", - "priceUsd": "0.465878941863393" + "priceUsd": "0.34858030064201556" }, { "chainId": 42161, @@ -9178,7 +9259,7 @@ "symbol": "DIA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11955/thumb/image.png?1646041751", - "priceUsd": "0.531682261965245" + "priceUsd": "0.5852110693462642" }, { "chainId": 42161, @@ -9187,7 +9268,7 @@ "symbol": "DNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/849/thumb/district0x.png?1547223762", - "priceUsd": "0.024045636188350344" + "priceUsd": "0.019002351343420498" }, { "chainId": 42161, @@ -9196,7 +9277,7 @@ "symbol": "DPI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12465/thumb/defi_pulse_index_set.png?1600051053", - "priceUsd": "101.94157061883172" + "priceUsd": "87.09284639935913" }, { "chainId": 42161, @@ -9205,7 +9286,7 @@ "symbol": "DRV", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/52889/large/Token_Logo.png?1734601695", - "priceUsd": "0.03596978" + "priceUsd": "0.04360608" }, { "chainId": 42161, @@ -9214,7 +9295,7 @@ "symbol": "DYDX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17500/thumb/hjnIm9bV.jpg?1628009360", - "priceUsd": "0.07400739968866298" + "priceUsd": "0.3486" }, { "chainId": 42161, @@ -9223,7 +9304,7 @@ "symbol": "EIGEN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/37441/large/eigen.jpg?1728023974", - "priceUsd": "1.7696757385491755" + "priceUsd": "1.0549039539986145" }, { "chainId": 42161, @@ -9241,7 +9322,7 @@ "symbol": "ENA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/36530/standard/ethena.png", - "priceUsd": "0.5565875400020969" + "priceUsd": "0.4957878634820934" }, { "chainId": 42161, @@ -9250,7 +9331,7 @@ "symbol": "ENJ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1102/thumb/enjin-coin-logo.png?1547035078", - "priceUsd": "0.0746857623618561" + "priceUsd": "0.060145477757476314" }, { "chainId": 42161, @@ -9259,7 +9340,7 @@ "symbol": "ENS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/19785/thumb/acatxTm8_400x400.jpg?1635850140", - "priceUsd": "20.675993532741565" + "priceUsd": "6.393031663854756" }, { "chainId": 42161, @@ -9268,7 +9349,7 @@ "symbol": "ERN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14238/thumb/LOGO_HIGH_QUALITY.png?1647831402", - "priceUsd": "0.08470155516475816" + "priceUsd": "0.07250706399602017" }, { "chainId": 42161, @@ -9277,7 +9358,7 @@ "symbol": "ETHFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35958/standard/etherfi.jpeg", - "priceUsd": "1.6250180581990923" + "priceUsd": "1.1097177138919025" }, { "chainId": 42161, @@ -9286,7 +9367,7 @@ "symbol": "EURC", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/26045/thumb/euro-coin.png?1655394420", - "priceUsd": "1.1619401852007891" + "priceUsd": "1.1683588870459176" }, { "chainId": 42161, @@ -9295,7 +9376,7 @@ "symbol": "FARM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12304/thumb/Harvest.png?1613016180", - "priceUsd": "26.909293668650218" + "priceUsd": "59.19321175830783" }, { "chainId": 42161, @@ -9304,7 +9385,7 @@ "symbol": "FET", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5681/thumb/Fetch.jpg?1572098136", - "priceUsd": "0.5283772641865987" + "priceUsd": "0.2852119946977017" }, { "chainId": 42161, @@ -9313,7 +9394,7 @@ "symbol": "FIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12423/thumb/stafi_logo.jpg?1599730991", - "priceUsd": "0.08052380138181776" + "priceUsd": "0.03138716787553049" }, { "chainId": 42161, @@ -9331,7 +9412,7 @@ "symbol": "FLUX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x720CD16b011b987Da3518fbf38c3071d4F0D1495/logo.png", - "priceUsd": "0.17860945179289117" + "priceUsd": "0.12101332846085132" }, { "chainId": 42161, @@ -9340,7 +9421,7 @@ "symbol": "FORT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/25060/thumb/Forta_lgo_%281%29.png?1655353696", - "priceUsd": "0.04361858" + "priceUsd": "0.036893346462597745" }, { "chainId": 42161, @@ -9349,7 +9430,7 @@ "symbol": "FOX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9988/thumb/FOX.png?1574330622", - "priceUsd": "0.0237047" + "priceUsd": "0.018875749892011493" }, { "chainId": 42161, @@ -9358,7 +9439,7 @@ "symbol": "FRAX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13422/thumb/frax_logo.png?1608476506", - "priceUsd": "0.9971658958023574" + "priceUsd": "0.9961357218675377" }, { "chainId": 42161, @@ -9367,7 +9448,7 @@ "symbol": "FTM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4001/thumb/Fantom.png?1558015016", - "priceUsd": "0.27065749143232554" + "priceUsd": "0.17530954083587758" }, { "chainId": 42161, @@ -9376,7 +9457,7 @@ "symbol": "FXS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13423/thumb/frax_share.png?1608478989", - "priceUsd": "2.124" + "priceUsd": "1.476" }, { "chainId": 42161, @@ -9385,7 +9466,7 @@ "symbol": "GAL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24530/thumb/GAL-Token-Icon.png?1651483533", - "priceUsd": "0.5902255814769584" + "priceUsd": "0.41729735446376437" }, { "chainId": 42161, @@ -9394,7 +9475,7 @@ "symbol": "GALA", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/12493/standard/GALA-COINGECKO.png?1696512310", - "priceUsd": "0.015281502605895954" + "priceUsd": "0.01126892306519357" }, { "chainId": 42161, @@ -9403,7 +9484,7 @@ "symbol": "GMX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18323/large/arbit.png?1631532468", - "priceUsd": "13.78" + "priceUsd": "10.49" }, { "chainId": 42161, @@ -9412,7 +9493,7 @@ "symbol": "GNO", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6810e776880C02933D47DB1b9fc05908e5386b96/logo.png", - "priceUsd": "147.22" + "priceUsd": "86.63430013752458" }, { "chainId": 42161, @@ -9421,7 +9502,7 @@ "symbol": "GRT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13397/thumb/Graph_Token.png?1608145566", - "priceUsd": "0.080888" + "priceUsd": "0.065303" }, { "chainId": 42161, @@ -9430,7 +9511,7 @@ "symbol": "GTC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15810/thumb/gitcoin.png?1621992929", - "priceUsd": "0.28200434355576265" + "priceUsd": "0.21549612205078322" }, { "chainId": 42161, @@ -9439,7 +9520,7 @@ "symbol": "GYEN", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/14191/thumb/icon_gyen_200_200.png?1614843343", - "priceUsd": "0.00636203" + "priceUsd": "0.00641739" }, { "chainId": 42161, @@ -9448,7 +9529,7 @@ "symbol": "HIGH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18973/thumb/logosq200200Coingecko.png?1634090470", - "priceUsd": "0.4591132461391608" + "priceUsd": "0.3083555736531283" }, { "chainId": 42161, @@ -9457,7 +9538,7 @@ "symbol": "HOPR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14061/thumb/Shared_HOPR_logo_512px.png?1614073468", - "priceUsd": "0.04617580665610038" + "priceUsd": "0.041597683373347086" }, { "chainId": 42161, @@ -9466,7 +9547,7 @@ "symbol": "ILV", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14468/large/ILV.JPG", - "priceUsd": "14.081873098293768" + "priceUsd": "11.734569300101931" }, { "chainId": 42161, @@ -9475,7 +9556,7 @@ "symbol": "IMX", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/17233/thumb/imx.png?1636691817", - "priceUsd": "0.6888" + "priceUsd": "4.21326955269227" }, { "chainId": 42161, @@ -9484,7 +9565,7 @@ "symbol": "INJ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12882/thumb/Secondary_Symbol.png?1628233237", - "priceUsd": "12.204482065028753" + "priceUsd": "8.696635946861091" }, { "chainId": 42161, @@ -9493,7 +9574,7 @@ "symbol": "JASMY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13876/thumb/JASMY200x200.jpg?1612473259", - "priceUsd": "0.012479264806524773" + "priceUsd": "0.010391467566622388" }, { "chainId": 42161, @@ -9502,7 +9583,7 @@ "symbol": "K", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/54964/standard/k200.png?1742894885", - "priceUsd": "0.00625894700215694" + "priceUsd": "0.005832953132102416" }, { "chainId": 42161, @@ -9511,7 +9592,7 @@ "symbol": "KRL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2807/thumb/krl.png?1547036979", - "priceUsd": "0.294653" + "priceUsd": "0.259478" }, { "chainId": 42161, @@ -9520,7 +9601,7 @@ "symbol": "KUJI", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/20685/standard/kuji-200x200.png", - "priceUsd": "0.178702" + "priceUsd": null }, { "chainId": 42161, @@ -9529,7 +9610,7 @@ "symbol": "LDO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13573/thumb/Lido_DAO.png?1609873644", - "priceUsd": "1.15" + "priceUsd": "0.952765" }, { "chainId": 42161, @@ -9538,7 +9619,7 @@ "symbol": "LINK", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png", - "priceUsd": "21.77" + "priceUsd": "18.74" }, { "chainId": 42161, @@ -9547,7 +9628,7 @@ "symbol": "LIT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13825/large/logo_200x200.png", - "priceUsd": "0.3310357222962715" + "priceUsd": "0.3309132856049646" }, { "chainId": 42161, @@ -9556,7 +9637,7 @@ "symbol": "LPT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/7137/thumb/logo-circle-green.png?1619593365", - "priceUsd": "6.24" + "priceUsd": "5.15" }, { "chainId": 42161, @@ -9565,7 +9646,7 @@ "symbol": "LQTY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14665/thumb/200-lqty-icon.png?1617631180", - "priceUsd": "0.718233" + "priceUsd": "0.522589" }, { "chainId": 42161, @@ -9574,7 +9655,7 @@ "symbol": "LRC", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD/logo.png", - "priceUsd": "0.084941" + "priceUsd": "0.070043" }, { "chainId": 42161, @@ -9583,7 +9664,7 @@ "symbol": "LUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14666/thumb/Group_3.png?1617631327", - "priceUsd": "1" + "priceUsd": "1.0079233223876802" }, { "chainId": 42161, @@ -9592,7 +9673,7 @@ "symbol": "MAGIC", "decimals": 18, "logoUrl": "https://dynamic-assets.coinbase.com/30320a63f6038b944c9c0202fcb2392e6a1bd333814f74b4674774dd87f2d06d64fdd74c2f1ab4639917c75b749c323450408bec7a2737af8ae0c17871aa90de/asset_icons/98d278cda11639ed7449a0a3086cd2c83937ce71baf4ee43bb5b777423c00a75.png", - "priceUsd": "0.167251" + "priceUsd": "0.144639" }, { "chainId": 42161, @@ -9601,7 +9682,7 @@ "symbol": "MANA", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/878/thumb/decentraland-mana.png?1550108745", - "priceUsd": "0.3135602745851509" + "priceUsd": "0.24391000934235202" }, { "chainId": 42161, @@ -9610,7 +9691,7 @@ "symbol": "MASK", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14051/thumb/Mask_Network.jpg?1614050316", - "priceUsd": "1.2488076059941424" + "priceUsd": "0.8827201495263597" }, { "chainId": 42161, @@ -9619,7 +9700,7 @@ "symbol": "MATH", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11335/thumb/2020-05-19-token-200.png?1589940590", - "priceUsd": "0.082979" + "priceUsd": "0.06871723825997056" }, { "chainId": 42161, @@ -9628,7 +9709,7 @@ "symbol": "MATIC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4713/thumb/matic-token-icon.png?1624446912", - "priceUsd": "0.23802351749186065" + "priceUsd": "0.18226310898945614" }, { "chainId": 42161, @@ -9637,7 +9718,7 @@ "symbol": "METIS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15595/thumb/metis.jpeg?1660285312", - "priceUsd": "12.79" + "priceUsd": "10.378" }, { "chainId": 42161, @@ -9646,7 +9727,7 @@ "symbol": "MIM", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/16786/thumb/mimlogopng.png?1624979612", - "priceUsd": "1.0003382585190026" + "priceUsd": "141.19608307277724" }, { "chainId": 42161, @@ -9655,7 +9736,7 @@ "symbol": "MKR", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png", - "priceUsd": "1575.3018493729548" + "priceUsd": "1330.2886606678849" }, { "chainId": 42161, @@ -9664,7 +9745,7 @@ "symbol": "MLN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/605/thumb/melon.png?1547034295", - "priceUsd": "7.78" + "priceUsd": "7.62" }, { "chainId": 42161, @@ -9673,7 +9754,7 @@ "symbol": "MNT", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/30980/large/Mantle-Logo-mark.png?1739213200", - "priceUsd": "2.582618758162502" + "priceUsd": "1.669972026047881" }, { "chainId": 42161, @@ -9691,7 +9772,7 @@ "symbol": "MORPHO", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/29837/large/Morpho-token-icon.png?1726771230", - "priceUsd": "1.721842542954761" + "priceUsd": "2.0027275073031503" }, { "chainId": 42161, @@ -9700,7 +9781,7 @@ "symbol": "MPL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14097/thumb/photo_2021-05-03_14.20.41.jpeg?1620022863", - "priceUsd": "0.7843683789726165" + "priceUsd": "0.709480239498" }, { "chainId": 42161, @@ -9709,7 +9790,7 @@ "symbol": "MULTI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22087/thumb/1_Wyot-SDGZuxbjdkaOeT2-A.png?1640764238", - "priceUsd": "0.49799624438204476" + "priceUsd": "0.443489357213158" }, { "chainId": 42161, @@ -9718,7 +9799,7 @@ "symbol": "MV", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/17704.png", - "priceUsd": "0.00755254" + "priceUsd": "0.00591319" }, { "chainId": 42161, @@ -9727,7 +9808,7 @@ "symbol": "MXC", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/4604/thumb/mxc.png?1655534336", - "priceUsd": "0.0013593289568151513" + "priceUsd": "0.001453056192" }, { "chainId": 42161, @@ -9736,7 +9817,7 @@ "symbol": "NCT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2843/thumb/ImcYCVfX_400x400.jpg?1628519767", - "priceUsd": "0.020033226668842236" + "priceUsd": "0.0264653662345631" }, { "chainId": 42161, @@ -9745,7 +9826,7 @@ "symbol": "NKN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3375/thumb/nkn.png?1548329212", - "priceUsd": "0.025037864866809825" + "priceUsd": "0.02024018700653332" }, { "chainId": 42161, @@ -9754,7 +9835,7 @@ "symbol": "NMR", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671/logo.png", - "priceUsd": "15.781763374221558" + "priceUsd": "13.00373198044701" }, { "chainId": 42161, @@ -9763,7 +9844,7 @@ "symbol": "OCEAN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3687/thumb/ocean-protocol-logo.jpg?1547038686", - "priceUsd": "0.26036604666238605" + "priceUsd": "0.3792951477263784" }, { "chainId": 42161, @@ -9772,7 +9853,7 @@ "symbol": "OGN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3296/thumb/op.jpg?1547037878", - "priceUsd": "0.058036799074690455" + "priceUsd": "0.04884169058929694" }, { "chainId": 42161, @@ -9781,7 +9862,7 @@ "symbol": "OMG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/776/thumb/OMG_Network.jpg?1591167168", - "priceUsd": "0.1491374127630569" + "priceUsd": "0.12400563236039656" }, { "chainId": 42161, @@ -9790,7 +9871,7 @@ "symbol": "ONDO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656", - "priceUsd": "0.8942291824087522" + "priceUsd": "0.7569254616691672" }, { "chainId": 42161, @@ -9799,7 +9880,7 @@ "symbol": "ORN", "decimals": 8, "logoUrl": "https://assets.coingecko.com/coins/images/11841/thumb/orion_logo.png?1594943318", - "priceUsd": "0.2636902598367738" + "priceUsd": "0.13425888648398807" }, { "chainId": 42161, @@ -9808,7 +9889,7 @@ "symbol": "PAXG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9519/thumb/paxg.PNG?1568542565", - "priceUsd": "4062.2114812691334" + "priceUsd": "22389.918566712087" }, { "chainId": 42161, @@ -9817,7 +9898,7 @@ "symbol": "PENDLE", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/15069/large/Pendle_Logo_Normal-03.png?1696514728", - "priceUsd": "4.48" + "priceUsd": "3.35" }, { "chainId": 42161, @@ -9835,7 +9916,7 @@ "symbol": "PERP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12381/thumb/60d18e06844a844ad75901a9_mark_only_03.png?1628674771", - "priceUsd": "0.279455" + "priceUsd": "0.227234" }, { "chainId": 42161, @@ -9844,7 +9925,7 @@ "symbol": "PIRATE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/38524/standard/_Pirate_Transparent_200x200.png", - "priceUsd": "0.019627200157602193" + "priceUsd": "0.012505094759766331" }, { "chainId": 42161, @@ -9853,7 +9934,7 @@ "symbol": "POL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/32440/large/polygon.png?1698233684", - "priceUsd": "0.23792315034854336" + "priceUsd": "0.20187995410986445" }, { "chainId": 42161, @@ -9862,7 +9943,7 @@ "symbol": "POLS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12648/thumb/polkastarter.png?1609813702", - "priceUsd": "0.1667781665226272" + "priceUsd": "0.1443798951954918" }, { "chainId": 42161, @@ -9871,7 +9952,7 @@ "symbol": "POLY", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/2784/thumb/inKkF01.png?1605007034", - "priceUsd": "0.035190147866426116" + "priceUsd": "0.10321321850179971" }, { "chainId": 42161, @@ -9880,7 +9961,7 @@ "symbol": "POND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/8903/thumb/POND_200x200.png?1622515451", - "priceUsd": "0.0076717" + "priceUsd": "0.00596051" }, { "chainId": 42161, @@ -9889,7 +9970,7 @@ "symbol": "PORTAL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35436/standard/portal.jpeg", - "priceUsd": "0.03734272443005163" + "priceUsd": "0.02657795652524243" }, { "chainId": 42161, @@ -9898,7 +9979,7 @@ "symbol": "POWR", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/1104/thumb/power-ledger.png?1547035082", - "priceUsd": "0.14050192688292037" + "priceUsd": "0.6354199955666424" }, { "chainId": 42161, @@ -9907,7 +9988,7 @@ "symbol": "PRIME", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/29053/large/PRIMELOGOOO.png?1676976222", - "priceUsd": "1.3200802798770386" + "priceUsd": "1.124982696997637" }, { "chainId": 42161, @@ -9916,7 +9997,16 @@ "symbol": "PRQ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11973/thumb/DsNgK0O.png?1596590280", - "priceUsd": "0.007101478494831981" + "priceUsd": "0.0054392296802379435" + }, + { + "chainId": 42161, + "address": "0x327006c8712FE0AbdbbD55B7999DB39b0967342E", + "name": "PayPal USD", + "symbol": "PYUSD", + "decimals": 6, + "logoUrl": "https://assets.coingecko.com/coins/images/31212/large/PYUSD_Logo_%282%29.png?1691458314", + "priceUsd": "1.0002347522980308" }, { "chainId": 42161, @@ -9925,7 +10015,7 @@ "symbol": "QNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/3370/thumb/5ZOu7brX_400x400.jpg?1612437252", - "priceUsd": "101.5877815649179" + "priceUsd": "10349029.457458373" }, { "chainId": 42161, @@ -9934,7 +10024,7 @@ "symbol": "RAD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14013/thumb/radicle.png?1614402918", - "priceUsd": "0.6202724135303846" + "priceUsd": "0.4940538464" }, { "chainId": 42161, @@ -9943,7 +10033,7 @@ "symbol": "RAI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14004/thumb/RAI-logo-coin.png?1613592334", - "priceUsd": "4.93225958103794" + "priceUsd": "4.424408554076399" }, { "chainId": 42161, @@ -9952,7 +10042,7 @@ "symbol": "RARI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11845/thumb/Rari.png?1594946953", - "priceUsd": "0.8255872478802976" + "priceUsd": "0.5422740720025657" }, { "chainId": 42161, @@ -9970,7 +10060,7 @@ "symbol": "REN", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x408e41876cCCDC0F92210600ef50372656052a38/logo.png", - "priceUsd": "0.007193" + "priceUsd": "0.005913" }, { "chainId": 42161, @@ -9979,7 +10069,7 @@ "symbol": "REQ", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/1031/thumb/Request_icon_green.png?1643250951", - "priceUsd": "0.12841247415379142" + "priceUsd": "0.12236564900070716" }, { "chainId": 42161, @@ -9988,7 +10078,7 @@ "symbol": "RGT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12900/thumb/Rari_Logo_Transparent.png?1613978014", - "priceUsd": "0.050538" + "priceUsd": "0.050987" }, { "chainId": 42161, @@ -9997,7 +10087,7 @@ "symbol": "RLC", "decimals": 9, "logoUrl": "https://assets.coingecko.com/coins/images/646/thumb/pL1VuXm.png?1604543202", - "priceUsd": "1.051653423013894" + "priceUsd": null }, { "chainId": 42161, @@ -10006,7 +10096,7 @@ "symbol": "RNDR", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11636/thumb/rndr.png?1638840934", - "priceUsd": "3.254940779380112" + "priceUsd": "2.4758050783244316" }, { "chainId": 42161, @@ -10015,7 +10105,7 @@ "symbol": "RPL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/2090/large/rocket_pool_%28RPL%29.png?1696503058", - "priceUsd": "4.84" + "priceUsd": "3.48" }, { "chainId": 42161, @@ -10024,7 +10114,7 @@ "symbol": "RSR", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/8365/large/RSR_Blue_Circle_1000.png?1721777856", - "priceUsd": "0.00580656" + "priceUsd": "0.005442062899504465" }, { "chainId": 42161, @@ -10033,7 +10123,7 @@ "symbol": "SAND", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12129/thumb/sandbox_logo.jpg?1597397942", - "priceUsd": "0.2627917517454947" + "priceUsd": "0.21683988366919346" }, { "chainId": 42161, @@ -10042,7 +10132,7 @@ "symbol": "SD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20658/standard/SD_Token_Logo.png", - "priceUsd": "0.4953647100681596" + "priceUsd": "0.4886099561402035" }, { "chainId": 42161, @@ -10060,7 +10150,7 @@ "symbol": "SKL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/13245/thumb/SKALE_token_300x300.png?1606789574", - "priceUsd": "0.02366" + "priceUsd": "0.01939" }, { "chainId": 42161, @@ -10069,7 +10159,7 @@ "symbol": "SNT", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/779/thumb/status.png?1548610778", - "priceUsd": "0.021944592080815117" + "priceUsd": "0.03139156650936265" }, { "chainId": 42161, @@ -10078,7 +10168,7 @@ "symbol": "SNX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F/logo.png", - "priceUsd": "1.085785653738852" + "priceUsd": "1.6712698370863466" }, { "chainId": 42161, @@ -10087,7 +10177,7 @@ "symbol": "SOCKS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/10717/thumb/qFrcoiM.png?1582525244", - "priceUsd": "12102.353607339652" + "priceUsd": "121033.87442813884" }, { "chainId": 42161, @@ -10096,7 +10186,7 @@ "symbol": "SOL", "decimals": 9, "logoUrl": "https://assets.coingecko.com/coins/images/22876/thumb/SOL_wh_small.png?1644224316", - "priceUsd": "223.13400262442897" + "priceUsd": "201.5156874573167" }, { "chainId": 42161, @@ -10105,7 +10195,7 @@ "symbol": "SPELL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/15861/thumb/abracadabra-3.png?1622544862", - "priceUsd": "0.00043446" + "priceUsd": "0.0003592533187349904" }, { "chainId": 42161, @@ -10114,7 +10204,7 @@ "symbol": "SPX", "decimals": 8, "logoUrl": "https://coin-images.coingecko.com/coins/images/31401/large/sticker_(1).jpg?1702371083", - "priceUsd": "1.4568236797059237" + "priceUsd": "1.1006264634389789" }, { "chainId": 42161, @@ -10123,7 +10213,7 @@ "symbol": "SQD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/37869/standard/New_Logo_SQD_Icon.png?1720048443", - "priceUsd": "0.167691" + "priceUsd": null }, { "chainId": 42161, @@ -10132,7 +10222,7 @@ "symbol": "STG", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24413/thumb/STG_LOGO.png?1647654518", - "priceUsd": "0.2041" + "priceUsd": "0.1529" }, { "chainId": 42161, @@ -10141,7 +10231,7 @@ "symbol": "STORJ", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC/logo.png", - "priceUsd": "0.22605763175238022" + "priceUsd": "1.5257523293014252" }, { "chainId": 42161, @@ -10150,7 +10240,7 @@ "symbol": "SUPER", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14040/thumb/6YPdWn6.png?1613975899", - "priceUsd": "0.5636453010113891" + "priceUsd": "0.40412683934084404" }, { "chainId": 42161, @@ -10159,7 +10249,7 @@ "symbol": "sUSD", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/5013/thumb/sUSD.png?1616150765", - "priceUsd": "0.996573" + "priceUsd": "0.987485" }, { "chainId": 42161, @@ -10168,7 +10258,7 @@ "symbol": "SUSHI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", - "priceUsd": "0.683853" + "priceUsd": "0.5404360057607395" }, { "chainId": 42161, @@ -10177,7 +10267,7 @@ "symbol": "SWELL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/28777/large/swell1.png?1727899715", - "priceUsd": "0.008353" + "priceUsd": "0.04736192558775863" }, { "chainId": 42161, @@ -10186,7 +10276,7 @@ "symbol": "SYN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/18024/thumb/syn.png?1635002049", - "priceUsd": "0.10881120711539255" + "priceUsd": "0.07912279035556927" }, { "chainId": 42161, @@ -10195,7 +10285,7 @@ "symbol": "T", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/22228/thumb/nFPNiSbL_400x400.jpg?1641220340", - "priceUsd": "0.015217732390053801" + "priceUsd": "0.012161639072906051" }, { "chainId": 42161, @@ -10204,7 +10294,7 @@ "symbol": "tBTC", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/0x18084fbA666a33d37592fA2633fD49a74DD93a88/logo.png", - "priceUsd": "122314.50959016493" + "priceUsd": "62224.35943442483" }, { "chainId": 42161, @@ -10213,7 +10303,7 @@ "symbol": "TRB", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/9644/thumb/Blk_icon_current.png?1584980686", - "priceUsd": "32.42" + "priceUsd": "27.02" }, { "chainId": 42161, @@ -10222,7 +10312,7 @@ "symbol": "TRIBE", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/14575/thumb/tribe.PNG?1617487954", - "priceUsd": "0.6320000108740831" + "priceUsd": "0.5908015192249156" }, { "chainId": 42161, @@ -10231,7 +10321,7 @@ "symbol": "TURBO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/30117/large/TurboMark-QL_200.png?1708079597", - "priceUsd": "0.0035555052020918446" + "priceUsd": "0.011356194729383162" }, { "chainId": 42161, @@ -10240,7 +10330,7 @@ "symbol": "UMA", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828/logo.png", - "priceUsd": "1.2438919348954278" + "priceUsd": "5.092299464102237" }, { "chainId": 42161, @@ -10249,7 +10339,16 @@ "symbol": "UNI", "decimals": 18, "logoUrl": "ipfs://QmXttGpZrECX5qCyXbBQiqgQNytVGeZW5Anewvh2jc4psg", - "priceUsd": "7.84" + "priceUsd": "6.62" + }, + { + "chainId": 42161, + "address": "0x7550dE0A4b9Fb8CAbA8c32E72Ee356AFdd217A33", + "name": "World Liberty Financial USD", + "symbol": "USD1", + "decimals": 18, + "logoUrl": "https://coin-images.coingecko.com/coins/images/54977/large/USD1_1000x1000_transparent.png?1749297002", + "priceUsd": "1.003426570350297" }, { "chainId": 42161, @@ -10258,7 +10357,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.999705" + "priceUsd": "0.999806" }, { "chainId": 42161, @@ -10267,7 +10366,7 @@ "symbol": "USDC.e", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", - "priceUsd": "0.999033" + "priceUsd": "0.99943" }, { "chainId": 42161, @@ -10276,7 +10375,7 @@ "symbol": "USDP", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/6013/standard/Pax_Dollar.png?1696506427", - "priceUsd": "1.0055655527133194" + "priceUsd": "1.0051745205339466" }, { "chainId": 42161, @@ -10285,15 +10384,6 @@ "symbol": "USDS", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/39926/large/usds.webp?1726666683", - "priceUsd": "0.999782" - }, - { - "chainId": 42161, - "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", - "name": "Tether USD", - "symbol": "USDT", - "decimals": 6, - "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png", "priceUsd": "1" }, { @@ -10303,7 +10393,7 @@ "symbol": "USUAL", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/51091/large/USUAL.jpg?1730035787", - "priceUsd": "0.0486659426791032" + "priceUsd": "0.0323735564340137" }, { "chainId": 42161, @@ -10312,7 +10402,7 @@ "symbol": "WAMPL", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/20825/thumb/photo_2021-11-25_02-05-11.jpg?1637811951", - "priceUsd": "2.1012409754645227" + "priceUsd": "7.585555939062373" }, { "chainId": 42161, @@ -10321,7 +10411,7 @@ "symbol": "WBTC", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png", - "priceUsd": "122508" + "priceUsd": "115309" }, { "chainId": 42161, @@ -10330,7 +10420,7 @@ "symbol": "WETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 42161, @@ -10339,7 +10429,7 @@ "symbol": "WLFI", "decimals": 18, "logoUrl": "https://coin-images.coingecko.com/coins/images/50767/large/wlfi.png?1756438915", - "priceUsd": "0.1782" + "priceUsd": "0.1812521809685181" }, { "chainId": 42161, @@ -10348,7 +10438,7 @@ "symbol": "WOO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/12921/thumb/w2UiemF__400x400.jpg?1603670367", - "priceUsd": "0.066112" + "priceUsd": "0.0422311" }, { "chainId": 42161, @@ -10357,7 +10447,7 @@ "symbol": "XCN", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/24210/thumb/Chain_icon_200x200.png?1646895054", - "priceUsd": "0.010634058426173104" + "priceUsd": "0.008925249392378458" }, { "chainId": 42161, @@ -10366,7 +10456,7 @@ "symbol": "XSGD", "decimals": 6, "logoUrl": "https://assets.coingecko.com/coins/images/12832/standard/StraitsX_Singapore_Dollar_%28XSGD%29_Token_Logo.png?1696512623", - "priceUsd": "0.7659771906624153" + "priceUsd": "0.7474896497333917" }, { "chainId": 42161, @@ -10375,7 +10465,7 @@ "symbol": "YFI", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/11849/thumb/yfi-192x192.png?1598325330", - "priceUsd": "5302.52" + "priceUsd": "2498.185246890785" }, { "chainId": 42161, @@ -10384,7 +10474,7 @@ "symbol": "Zeta", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/26718/standard/Twitter_icon.png?1696525788", - "priceUsd": "0.1705" + "priceUsd": "0.1254" }, { "chainId": 42161, @@ -10393,7 +10483,7 @@ "symbol": "ZRO", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/28206/standard/ftxG9_TJ_400x400.jpeg?1696527208", - "priceUsd": "2.34" + "priceUsd": "1.74" }, { "chainId": 42161, @@ -10402,7 +10492,7 @@ "symbol": "ZRX", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xE41d2489571d322189246DaFA5ebDe1F4699F498/logo.png", - "priceUsd": "0.24747200628458066" + "priceUsd": "0.19684215641801323" }, { "chainId": 81457, @@ -10411,7 +10501,7 @@ "symbol": "BLAST", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/35494/standard/Blast.jpg?1719385662", - "priceUsd": "0.00197489" + "priceUsd": "0.0015171262330918084" }, { "chainId": 7777777, @@ -10429,7 +10519,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 10, @@ -10438,7 +10528,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 137, @@ -10447,7 +10537,7 @@ "symbol": "POL", "decimals": 18, "logoUrl": "https://static.debank.com/image/matic_token/logo_url/matic/6f5a6b6f0732a7a235131bd7804d357c.png", - "priceUsd": "0.239712" + "priceUsd": "0.195096" }, { "chainId": 42161, @@ -10456,7 +10546,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4345.25" + "priceUsd": "4156.01" }, { "chainId": 324, @@ -10465,7 +10555,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4118.88" }, { "chainId": 8453, @@ -10474,7 +10564,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 59144, @@ -10483,7 +10573,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4145.45" }, { "chainId": 34443, @@ -10492,7 +10582,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4135.83" }, { "chainId": 81457, @@ -10501,7 +10591,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4149.89" }, { "chainId": 1135, @@ -10510,7 +10600,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4125.71" }, { "chainId": 534352, @@ -10519,7 +10609,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4149.89" }, { "chainId": 480, @@ -10528,7 +10618,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4360.59" + "priceUsd": "4123.44" }, { "chainId": 57073, @@ -10537,7 +10627,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4380.16" + "priceUsd": "4145.26977286" }, { "chainId": 1868, @@ -10546,7 +10636,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4382.82" + "priceUsd": "4121.3" }, { "chainId": 130, @@ -10555,7 +10645,7 @@ "symbol": "ETH", "decimals": 18, "logoUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "priceUsd": "4384.42" + "priceUsd": "4156.01" }, { "chainId": 232, @@ -10564,7 +10654,7 @@ "symbol": "GHO", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/23508.png", - "priceUsd": "0.02897504" + "priceUsd": "0.999707" }, { "chainId": 56, @@ -10573,7 +10663,7 @@ "symbol": "BNB", "decimals": 18, "logoUrl": "https://assets.coingecko.com/coins/images/825/small/binance-coin-logo.png?1547034615", - "priceUsd": "1273.94" + "priceUsd": "1143.97" }, { "chainId": 999, @@ -10582,7 +10672,7 @@ "symbol": "HYPE", "decimals": 18, "logoUrl": "https://static.debank.com/image/hyper_token/logo_url/hyper/0b3e288cfe418e9ce69eef4c96374583.png", - "priceUsd": "44.32" + "priceUsd": "49.05" }, { "chainId": 9745, @@ -10591,7 +10681,7 @@ "symbol": "XPL", "decimals": 18, "logoUrl": "https://s2.coinmarketcap.com/static/img/coins/64x64/36645.png", - "priceUsd": "0.758051" + "priceUsd": "0.373219" }, { "chainId": 34268394551451, @@ -10600,7 +10690,7 @@ "symbol": "SOL", "decimals": 9, "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png", - "priceUsd": "224.60070584181943" + "priceUsd": "202.54229782560608" }, { "chainId": 34268394551451, @@ -10609,7 +10699,7 @@ "symbol": "USDC", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png", - "priceUsd": "0.9998037949521894" + "priceUsd": "0.9998055189615114" }, { "chainId": 34268394551451, @@ -10618,25 +10708,34 @@ "symbol": "USDT", "decimals": 6, "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB/logo.svg", - "priceUsd": "1.0002317501844211" + "priceUsd": "0.9998846967608106" }, { "chainId": 34268394551451, - "address": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", - "name": "Jupiter", - "symbol": "JUP", + "address": "G849nDx4r1vwjibbmpjkZ6pbDWwaMouhkWLq1o8Z5FUN", + "name": "FUN", + "symbol": "FUN", "decimals": 6, - "logoUrl": "https://static.jup.ag/jup/icon.png", - "priceUsd": "0.4331084912112197" + "logoUrl": "https://ipfs.io/ipfs/QmY9Dohw5iZMTxp2YzVNbNKHshRg6TXtojBngDZBuXoNeF", + "priceUsd": "0.015416263662208581" }, { "chainId": 34268394551451, - "address": "6FrrzDk5mQARGc1TDYoyVnSyRdds1t4PbtohCD6p3tgG", - "name": "USX", - "symbol": "USX", + "address": "E7NgL19JbN8BhUDgWjkH8MtnbhJoaGaWJqosxZZepump", + "name": "PayAI Network", + "symbol": "PAYAI", "decimals": 6, - "logoUrl": "https://raw.githubusercontent.com/Thomas-Solstice/usx-metadata/refs/heads/main/usx.png", - "priceUsd": "1.0003810144559233" + "logoUrl": "https://ipfs.io/ipfs/QmSd4swW6fkA3N92cUckYj1ar29mqpAaMzfwhrhgiFSonD", + "priceUsd": "0.048774871808259825" + }, + { + "chainId": 34268394551451, + "address": "METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL", + "name": "Meteora", + "symbol": "MET", + "decimals": 6, + "logoUrl": "https://assets.meteora.ag/met-token.svg", + "priceUsd": "0.4737154840043151" }, { "chainId": 34268394551451, @@ -10645,7 +10744,34 @@ "symbol": "PUMP", "decimals": 6, "logoUrl": "https://ipfs.io/ipfs/bafkreibyb3hcn7gglvdqpmklfev3fut3eqv3kje54l3to3xzxxbgpt5wjm", - "priceUsd": "0.005613151300670042" + "priceUsd": "0.005048458390921077" + }, + { + "chainId": 34268394551451, + "address": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + "name": "Jupiter", + "symbol": "JUP", + "decimals": 6, + "logoUrl": "https://static.jup.ag/jup/icon.png", + "priceUsd": "0.44137938891734746" + }, + { + "chainId": 34268394551451, + "address": "GkyPYa7NnCFbduLknCfBfP7p8564X1VZhwZYJ6CZpump", + "name": "Chill House", + "symbol": "CHILLHOUSE", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmU5D2LBjkriYD4wqL5cEVwv1x1xqj2LCQAPKC79cVGd34", + "priceUsd": "0.021754518592874677" + }, + { + "chainId": 34268394551451, + "address": "63bpnCja1pGB2HSazkS8FAPAUkYgcXoDwYHfvZZveBot", + "name": "MasterBOT", + "symbol": "BOT", + "decimals": 6, + "logoUrl": "https://gateway.pinata.cloud/ipfs/QmWKBJJkPGyaELNDneg8ZNomuJyai3QG8c5DmoKVzkDxSR", + "priceUsd": "0.03364688658432548" }, { "chainId": 34268394551451, @@ -10654,142 +10780,187 @@ "symbol": "ETH", "decimals": 8, "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs/logo.png", - "priceUsd": "4396.634831886667" + "priceUsd": "4159.3116879635445" }, { "chainId": 34268394551451, - "address": "EWsfRP9yrxyt8xTSv28MV1Ldn7UPpXBLgWtZ4YWMpump", - "name": "SOLHolder", - "symbol": "SOLHolder", - "decimals": 6, - "logoUrl": "https://ipfs.io/ipfs/bafkreicssdcgqp4c24pqmbjbfqr766brx6ahn6xb4ei24ejzacj5k4xoqy", - "priceUsd": "0.0006378540496796011" + "address": "BSJXgXNTyfxCGXgJ95MAvRepPg57NLpqfLfQyRYxFpKn", + "name": "TRUMPKIN", + "symbol": "TRUMPKIN", + "decimals": 9, + "logoUrl": "https://imagedelivery.net/yXctMXCu9umHb3zYjgGxfQ/9c0704bd-466a-4db7-24db-ab9ee9b5ec00/public", + "priceUsd": "0.00003854904402414369" + }, + { + "chainId": 34268394551451, + "address": "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "name": "Jito Staked SOL", + "symbol": "JitoSOL", + "decimals": 9, + "logoUrl": "https://storage.googleapis.com/token-metadata/JitoSOL-256.png", + "priceUsd": "250.93061591346034" }, { "chainId": 34268394551451, - "address": "5LwseQRo8fsz4S3y7jbqqe5C7tZTz5PwhXNCHj13jLBi", - "name": "PESHI", - "symbol": "PESHI", + "address": "BANKJmvhT8tiJRsBSS1n2HryMBPvT5Ze4HU95DUAmeta", + "name": "Avici", + "symbol": "AVICI", "decimals": 6, - "logoUrl": "https://bafkreidobd4eiplmvff42dnutldmwmjihkgbti6rpzuxz6p3c425e6qx6q.ipfs.nftstorage.link", - "priceUsd": "0.0000016193854176535999" + "logoUrl": "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/AVICI/AVICI.png", + "priceUsd": "1.9836868021540164" }, { "chainId": 34268394551451, - "address": "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", - "name": "USELESS COIN", - "symbol": "USELESS", + "address": "5bZvQWq86Ug54VAaNpxxebzR9cAHCLgo9AMVHnuANdev", + "name": "Pandorax402", + "symbol": "PANDORA", "decimals": 6, - "logoUrl": "https://i.ibb.co/fdGzcmbt/bafkreihsdoqkmpr5ryebaduoutyhj3nxco6wdp4s4743l2qrae4sz4hqrm.png", - "priceUsd": "0.41485619644127775" + "logoUrl": "https://ipfs.io/ipfs/QmeBKRCub7y4bzsVcJAfWNHmyxZENs8bRD3dn6L4A1SQPv", + "priceUsd": "0.0008014819979926855" }, { "chainId": 34268394551451, - "address": "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", - "name": "Wrapped BTC (Portal)", - "symbol": "WBTC", - "decimals": 8, - "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh/logo.png", - "priceUsd": "123098.76014735346" + "address": "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", + "name": "Jupiter Perps", + "symbol": "JLP", + "decimals": 6, + "logoUrl": "https://static.jup.ag/jlp/icon.png", + "priceUsd": "5.655852856934639" }, { "chainId": 34268394551451, - "address": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", - "name": "Bonk", - "symbol": "Bonk", - "decimals": 5, - "logoUrl": "https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I", - "priceUsd": "0.000019400918377283733" + "address": "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB", + "name": "World Liberty Financial USD", + "symbol": "USD1", + "decimals": 6, + "logoUrl": "https://raw.githubusercontent.com/worldliberty/usd1-metadata/refs/heads/main/logo.png", + "priceUsd": "1.000694613914635" }, { "chainId": 34268394551451, - "address": "5TfqNKZbn9AnNtzq8bbkyhKgcPGTfNDc9wNzFrTBpump", - "name": "Pumpfun Pepe", - "symbol": "PFP", + "address": "7Y2TPeq3hqw21LRTCi4wBWoivDngCpNNJsN1hzhZpump", + "name": "Sachicoin", + "symbol": "SACHI", "decimals": 6, - "logoUrl": "https://ipfs.io/ipfs/bafkreigv6oeomhm7fpx4cbwe2dforkwlknhdyqs7y53searvm3v4er6w4a", - "priceUsd": "0.004479918768904373" + "logoUrl": "https://ipfs.io/ipfs/bafybeid7vmfzlijieyk2yjc5pq4e7dkbn2w7yadoojuf6qsqwably5w7nm", + "priceUsd": "0.003717497297502521" }, { "chainId": 34268394551451, - "address": "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", - "name": "Jito Staked SOL", - "symbol": "JitoSOL", + "address": "BBKPiLM9KjdJW7oQSKt99RVWcZdhF6sEHRKnwqeBGHST", + "name": "GhostwareOS", + "symbol": "GHOST", "decimals": 9, - "logoUrl": "https://storage.googleapis.com/token-metadata/JitoSOL-256.png", - "priceUsd": "277.0770076566333" + "logoUrl": "https://gateway.irys.xyz/Ku6pbA57kzk-tyxlZP9NEOaA2SYAZtYl8bCSYTRlxjU", + "priceUsd": "0.011660242288488071" }, { "chainId": 34268394551451, - "address": "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", - "name": "Jupiter Perps", - "symbol": "JLP", + "address": "GEuuznWpn6iuQAJxLKQDVGXPtrqXHNWTk3gZqqvJpump", + "name": "Ace Data Cloud", + "symbol": "ACE", "decimals": 6, - "logoUrl": "https://static.jup.ag/jlp/icon.png", - "priceUsd": "5.82759745279217" + "logoUrl": "https://ipfs.io/ipfs/QmSSu7Mgyo4Ua8Gb6jAMwyMFE97aM8gZhAUH5wZ2ZS7nfS", + "priceUsd": "0.0037234764915945896" }, { "chainId": 34268394551451, - "address": "4NGbC4RRrUjS78ooSN53Up7gSg4dGrj6F6dxpMWHbonk", - "name": "Pandu Pandas", - "symbol": "PANDU", + "address": "PAYZP1W3UmdEsNLJwmH61TNqACYJTvhXy8SCN4Tmeta", + "name": "Paystream", + "symbol": "PAYS", "decimals": 6, - "logoUrl": "https://ipfs.io/ipfs/bafkreicplblomr55js3zlgztgg63w4jk6vdaxbchdsp5vrn7buv5gdkd2y", - "priceUsd": "0.0001572163356396279" + "logoUrl": "https://raw.githubusercontent.com/metaDAOproject/futarchy/refs/heads/develop/scripts/assets/PAYS/PAYS.jpg", + "priceUsd": "0.11738192035999345" }, { "chainId": 34268394551451, - "address": "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", - "name": "Pudgy Penguins", - "symbol": "PENGU", + "address": "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", + "name": "OFFICIAL TRUMP", + "symbol": "TRUMP", "decimals": 6, - "logoUrl": "https://arweave.net/BW67hICaKGd2_wamSB0IQq-x7Xwtmr2oJj1WnWGJRHU", - "priceUsd": "0.030977051157841096" + "logoUrl": "https://arweave.net/VQrPjACwnQRmxdKBTqNwPiyo65x7LAT773t8Kd7YBzw", + "priceUsd": "7.358333739158169" }, { "chainId": 34268394551451, - "address": "5UUH9RTDiSpq6HKS6bp4NdU9PNJpXRXuiw6ShBTBhgH2", - "name": "TROLL", - "symbol": "TROLL", + "address": "6FrrzDk5mQARGc1TDYoyVnSyRdds1t4PbtohCD6p3tgG", + "name": "USX", + "symbol": "USX", "decimals": 6, - "logoUrl": "https://ipfs.io/ipfs/QmWV8QgmH1gSw41yapzsFwkC8yPbzoSreAcJbzqfvo2sVq", - "priceUsd": "0.12627775321638382" + "logoUrl": "https://raw.githubusercontent.com/Thomas-Solstice/usx-metadata/refs/heads/main/usx.png", + "priceUsd": "1.0003650164631863" }, { "chainId": 34268394551451, - "address": "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", - "name": "Fartcoin", - "symbol": "Fartcoin", + "address": "9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D", + "name": "jupiter lend USDC", + "symbol": "jlUSDC", "decimals": 6, - "logoUrl": "https://ipfs.io/ipfs/QmQr3Fz4h1etNsF7oLGMRHiCzhB5y9a7GjyodnF7zLHK1g", - "priceUsd": "0.6458651487875023" + "logoUrl": "https://cdn.instadapp.io/solana/tokens/icons/usdc.png", + "priceUsd": "1.0165279255355497" }, { "chainId": 34268394551451, - "address": "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB", - "name": "World Liberty Financial USD", - "symbol": "USD1", + "address": "3HfLqhtF5hR5dyBXh6BMtRaTm9qzStvEGuMa8Gx6pump", + "name": "buidl", + "symbol": "buidl", "decimals": 6, - "logoUrl": "https://raw.githubusercontent.com/worldliberty/usd1-metadata/refs/heads/main/logo.png", - "priceUsd": "0.9994202887759748" + "logoUrl": "https://ipfs.io/ipfs/QmPR3CamxoBQW892uHDT2EGRxFvksiJyrEg1ajZYVqJhnf", + "priceUsd": "0.009104047766704934" }, { "chainId": 34268394551451, - "address": "J3NKxxXZcnNiMjKw9hYb2K4LUxgwB6t1FtPtQVsv3KFr", - "name": "SPX6900 (Wormhole)", - "symbol": "SPX", - "decimals": 8, - "logoUrl": "https://i.imgur.com/fLpAyY4.png", - "priceUsd": "1.4590083296363434" + "address": "3wPQhXYqy861Nhoc4bahtpf7G3e89XCLfZ67ptEfZUSA", + "name": "VALOR", + "symbol": "VALOR", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmX51VrS8HdyFYgwi33RrDCB894Zn4iWsikaBwu7x3WWyu", + "priceUsd": "0.008688323817280214" }, { "chainId": 34268394551451, - "address": "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", - "name": "Jupiter Staked SOL", - "symbol": "JupSOL", - "decimals": 9, - "logoUrl": "https://static.jup.ag/jupSOL/icon.png", - "priceUsd": "256.1902508307915" + "address": "6d5zHW5B8RkGKd51Lpb9RqFQSqDudr9GJgZ1SgQZpump", + "name": "Autonomous Virtual Beings", + "symbol": "AVB", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmQxhFrdSo1rZj9bP7DQXPKi42bF2HLyG2Ds5LTmVZ8JJi", + "priceUsd": "0.01010712353364299" + }, + { + "chainId": 34268394551451, + "address": "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", + "name": "USELESS COIN", + "symbol": "USELESS", + "decimals": 6, + "logoUrl": "https://i.imgur.com/S17YRd3.png", + "priceUsd": "0.29930003762001" + }, + { + "chainId": 34268394551451, + "address": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", + "name": "Bonk", + "symbol": "Bonk", + "decimals": 5, + "logoUrl": "https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I", + "priceUsd": "0.00001501992194777061" + }, + { + "chainId": 34268394551451, + "address": "8BtoThi2ZoXnF7QQK1Wjmh2JuBw9FjVvhnGMVZ2vpump", + "name": "Dark Eclipse", + "symbol": "DARK", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmVhcHM7DU62qZpAYfN8NKz8GaNBXzcGahhWBbn4nWGbtm", + "priceUsd": "0.008759234160663348" + }, + { + "chainId": 34268394551451, + "address": "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", + "name": "Wrapped BTC (Portal)", + "symbol": "WBTC", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh/logo.png", + "priceUsd": "115481.7736236503" }, { "chainId": 34268394551451, @@ -10798,72 +10969,72 @@ "symbol": "cbBTC", "decimals": 8, "logoUrl": "https://ipfs.io/ipfs/QmZ7L8yd5j36oXXydUiYFiFsRHbi3EdgC4RuFwvM7dcqge", - "priceUsd": "123114.68686288694" + "priceUsd": "115448.6991480236" }, { "chainId": 34268394551451, - "address": "Ey59PH7Z4BFU4HjyKnyMdWt5GGN76KazTAwQihoUXRnk", - "name": "Launch Coin on Believe", - "symbol": "LAUNCHCOIN", - "decimals": 9, - "logoUrl": "https://ipfs.io/ipfs/bafkreibeqt7fvgn2ubl4tha6sljnici2eus42dauxgtrdvfjf6m3vkdkoi", - "priceUsd": "0.11348470814836654" + "address": "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", + "name": "Pudgy Penguins", + "symbol": "PENGU", + "decimals": 6, + "logoUrl": "https://arweave.net/BW67hICaKGd2_wamSB0IQq-x7Xwtmr2oJj1WnWGJRHU", + "priceUsd": "0.02197043703487708" }, { "chainId": 34268394551451, - "address": "6nR8wBnfsmXfcdDr1hovJKjvFQxNSidN6XFyfAFZpump", - "name": "GeorgePlaysClashRoyale", - "symbol": "Clash", + "address": "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", + "name": "Fartcoin", + "symbol": "Fartcoin", "decimals": 6, - "logoUrl": "https://ipfs.io/ipfs/bafybeidcnencwhveq7w56f6az47edmnuqfzk5qeecnjc77owwvnu3mlhne", - "priceUsd": "0.05129532870950466" + "logoUrl": "https://ipfs.io/ipfs/QmQr3Fz4h1etNsF7oLGMRHiCzhB5y9a7GjyodnF7zLHK1g", + "priceUsd": "0.41372630776703223" }, { "chainId": 34268394551451, - "address": "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", - "name": "PayPal USD", - "symbol": "PYUSD", + "address": "73UdJevxaNKXARgkvPHQGKuv8HCZARszuKW2LTL3pump", + "name": "ZARA AI", + "symbol": "ZARA", "decimals": 6, - "logoUrl": "https://424565.fs1.hubspotusercontent-na1.net/hubfs/424565/PYUSDLOGO.png", - "priceUsd": "0.9999237478615768" + "logoUrl": "https://ipfs.io/ipfs/QmehY3LTG3dZ7p1mCYoEHxkiHYC8hc9UTpcDWMWZMSsKPG", + "priceUsd": "0.005382688865830223" }, { "chainId": 34268394551451, - "address": "DUSDt4AeLZHWYmcXnVGYdgAzjtzU5mXUVnTMdnSzAttM", - "name": "DUSD", - "symbol": "DUSD", + "address": "H8xQ6poBjB9DTPMDTKWzWPrnxu4bDEhybxiouF8Ppump", + "name": "The Spirit of Gambling", + "symbol": "Tokabu", "decimals": 6, - "logoUrl": "https://raw.githubusercontent.com/standcoin/stdc_assets/refs/heads/main/images/dusd.png", - "priceUsd": "0.9999649342545918" + "logoUrl": "https://ipfs.io/ipfs/bafkreicrdi3icewusvj5ff53uwnlzng53dmkk3llrmdbn35eb2ozamnhpq", + "priceUsd": "0.013119829347229026" }, { "chainId": 34268394551451, - "address": "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", - "name": "OFFICIAL TRUMP", - "symbol": "TRUMP", + "address": "4NGbC4RRrUjS78ooSN53Up7gSg4dGrj6F6dxpMWHbonk", + "name": "Pandu Pandas", + "symbol": "PANDU", "decimals": 6, - "logoUrl": "https://arweave.net/VQrPjACwnQRmxdKBTqNwPiyo65x7LAT773t8Kd7YBzw", - "priceUsd": "7.551597277820382" + "logoUrl": "https://ipfs.io/ipfs/bafkreicplblomr55js3zlgztgg63w4jk6vdaxbchdsp5vrn7buv5gdkd2y", + "priceUsd": "0.00007204444071140128" }, { "chainId": 34268394551451, - "address": "DvjbEsdca43oQcw2h3HW1CT7N3x5vRcr3QrvTUHnXvgV", - "name": "Doodles", - "symbol": "DOOD", - "decimals": 9, - "logoUrl": "https://arweave.net/OqHnpGf36DiprL5YIC4xJHG8zRsWFV2FsZcNTfVgAPg", - "priceUsd": "0.011947374001126955" + "address": "F9TgEJLLRUKDRF16HgjUCdJfJ5BK6ucyiW8uJxVPpump", + "name": "AGiXT", + "symbol": "AGiXT", + "decimals": 6, + "logoUrl": "https://ipfs.io/ipfs/QmWMk3J3A4uTRnMW71V6aXCMMGKVHNHqyED1SEW3XRUSxr", + "priceUsd": "0.0017417614745897077" }, { "chainId": 34268394551451, - "address": "CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp", - "name": "Collector Crypt", - "symbol": "CARDS", + "address": "5UUH9RTDiSpq6HKS6bp4NdU9PNJpXRXuiw6ShBTBhgH2", + "name": "TROLL", + "symbol": "TROLL", "decimals": 6, - "logoUrl": "https://gateway.irys.xyz/2wrjAk4pYAACF3siDHAxt7yJY7kHgt57TZAkbgecKHMR", - "priceUsd": "0.17551228609119013" + "logoUrl": "https://ipfs.io/ipfs/QmWV8QgmH1gSw41yapzsFwkC8yPbzoSreAcJbzqfvo2sVq", + "priceUsd": "0.10035863168272262" } ], - "timestamp": 1760012681065, + "timestamp": 1761659941071, "version": "1.0.0" } \ No newline at end of file From e47857dcdd25725dd6b30bf5292717cca3bb5d06 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 16:16:35 +0200 Subject: [PATCH 089/122] more efficient filtering Signed-off-by: Gerhard Steenkamp --- api/_constants.ts | 8 ++++++++ api/user-token-balances/_service.ts | 11 +++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/api/_constants.ts b/api/_constants.ts index 2ced9d9f3..a93fa9aa5 100644 --- a/api/_constants.ts +++ b/api/_constants.ts @@ -374,3 +374,11 @@ export const CUSTOM_GAS_TOKENS = { [CHAIN_IDs.PLASMA]: "XPL", [CHAIN_IDs.HYPERCORE]: "HYPE", }; + +export const EVM_CHAIN_IDs = Object.entries(constants.PUBLIC_NETWORKS) + .filter(([_, chain]) => chain.family !== constants.ChainFamily.SVM) + .map(([chainId]) => Number(chainId)); + +export const SVM_CHAIN_IDs = Object.entries(constants.PUBLIC_NETWORKS) + .filter(([_, chain]) => chain.family === constants.ChainFamily.SVM) + .map(([chainId]) => Number(chainId)); diff --git a/api/user-token-balances/_service.ts b/api/user-token-balances/_service.ts index 3c89705ba..2cac269d8 100644 --- a/api/user-token-balances/_service.ts +++ b/api/user-token-balances/_service.ts @@ -6,12 +6,15 @@ import { getAlchemyRpcFromConfigJson } from "../_providers"; import { isSvmAddress } from "../_address"; import { getSvmBalance } from "../_balance"; import { fetchSwapTokensData, SwapToken } from "../swap/tokens/_service"; +import { CHAIN_IDs, EVM_CHAIN_IDs } from "../_constants"; const logger = getLogger(); -async function getSwapTokens(): Promise { +async function getSwapTokens( + filteredChainIds?: number[] +): Promise { try { - return await fetchSwapTokensData(); + return await fetchSwapTokensData(filteredChainIds); } catch (error) { logger.warn({ at: "getSwapTokens", @@ -346,7 +349,7 @@ export const handleUserTokenBalances = async (account: string) => { const svmChainIds = getSvmChainIds(); // Fetch swap tokens to get the list of token addresses for each chain - const swapTokens = await getSwapTokens(); + const swapTokens = await getSwapTokens([CHAIN_IDs.SOLANA]); logger.debug({ at: "handleUserTokenBalances", @@ -388,7 +391,7 @@ export const handleUserTokenBalances = async (account: string) => { const chainIdsAvailable = getEvmChainIds(); // Fetch swap tokens to get the list of token addresses for each chain - const swapTokens = await getSwapTokens(); + const swapTokens = await getSwapTokens(EVM_CHAIN_IDs); logger.debug({ at: "handleUserTokenBalances", From 2ec620ffee46503c9dce6ccfbf23f095f10ddd7e Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 16:26:11 +0200 Subject: [PATCH 090/122] wire up frontend Signed-off-by: Gerhard Steenkamp --- src/hooks/useUserTokenBalances.ts | 41 ++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/hooks/useUserTokenBalances.ts b/src/hooks/useUserTokenBalances.ts index e2fa9ae84..666bb1797 100644 --- a/src/hooks/useUserTokenBalances.ts +++ b/src/hooks/useUserTokenBalances.ts @@ -1,20 +1,49 @@ import { useQuery } from "@tanstack/react-query"; -import { useConnection } from "./useConnection"; import { UserTokenBalancesResponse } from "utils/serverless-api/types"; import getApiEndpoint from "utils/serverless-api"; +import { useConnectionEVM } from "./useConnectionEVM"; +import { useConnectionSVM } from "./useConnectionSVM"; export function useUserTokenBalances() { - const { account } = useConnection(); + const { account: evmAccount } = useConnectionEVM(); + const { account: svmAccount } = useConnectionSVM(); + + // Convert SVM PublicKey to string if it exists + const svmAccountString = svmAccount?.toString(); return useQuery({ - queryKey: ["userTokenBalances", account], + queryKey: ["userTokenBalances", evmAccount, svmAccountString], queryFn: async (): Promise => { - if (!account) { + // Fetch balances for both accounts if they exist + const promises: Promise[] = []; + + if (evmAccount) { + promises.push(getApiEndpoint().userTokenBalances(evmAccount)); + } + + if (svmAccountString) { + promises.push(getApiEndpoint().userTokenBalances(svmAccountString)); + } + + if (promises.length === 0) { throw new Error("No account connected"); } - return await getApiEndpoint().userTokenBalances(account); + + // Fetch all balances in parallel + const results = await Promise.all(promises); + + // Merge the results + if (results.length === 1) { + return results[0]; + } + + // Merge balances from both EVM and SVM accounts + return { + account: evmAccount || svmAccountString || "", + balances: results.flatMap((result) => result.balances), + }; }, - enabled: !!account, + enabled: !!evmAccount || !!svmAccountString, refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes staleTime: 3 * 60 * 1000, // Consider data stale after 3 minutes }); From 84df56dcfdfd412e6e5b840b17bb3cd6e7722376 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 18:31:26 +0200 Subject: [PATCH 091/122] refactor. handle edge cases Signed-off-by: Gerhard Steenkamp --- api/user-token-balances/_evm.ts | 258 ++++++++++++++++++++++ api/user-token-balances/_service.ts | 322 ++-------------------------- api/user-token-balances/_svm.ts | 157 ++++++++++++++ 3 files changed, 435 insertions(+), 302 deletions(-) create mode 100644 api/user-token-balances/_evm.ts create mode 100644 api/user-token-balances/_svm.ts diff --git a/api/user-token-balances/_evm.ts b/api/user-token-balances/_evm.ts new file mode 100644 index 000000000..ef72b0d5a --- /dev/null +++ b/api/user-token-balances/_evm.ts @@ -0,0 +1,258 @@ +import { BigNumber, ethers } from "ethers"; +import { getLogger } from "../_utils"; +import { getAlchemyRpcFromConfigJson } from "../_providers"; +import { SwapToken } from "../swap/tokens/_service"; + +const logger = getLogger(); + +export function getTokenAddressesForChain( + swapTokens: SwapToken[], + chainId: number +): string[] { + const tokens = swapTokens + .filter((token) => token.chainId === chainId) + .map((token) => token.address); + + return Array.from(new Set(tokens)); +} + +export async function fetchNativeBalance( + chainId: number, + account: string, + rpcUrl: string +): Promise { + try { + logger.debug({ + at: "fetchNativeBalance", + message: "Fetching native balance", + chainId, + account, + }); + + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "eth_getBalance", + params: [account, "latest"], + }), + }); + + if (!response.ok) { + logger.warn({ + at: "fetchNativeBalance", + message: "HTTP error fetching native balance", + chainId, + status: response.status, + statusText: response.statusText, + }); + return null; + } + + const data = await response.json(); + + if (!data || !data.result) { + logger.warn({ + at: "fetchNativeBalance", + message: "Invalid response for native balance", + chainId, + responseData: data, + }); + return null; + } + + return BigNumber.from(data.result).toString(); + } catch (error) { + logger.warn({ + at: "fetchNativeBalance", + message: "Error fetching native balance", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } +} + +export async function fetchErc20Balances( + chainId: number, + account: string, + tokenAddresses: string[], + rpcUrl: string +): Promise> { + // Early return if no token addresses + if (tokenAddresses.length === 0) { + logger.debug({ + at: "fetchErc20Balances", + message: "No ERC20 token addresses to fetch, returning empty array", + chainId, + }); + return []; + } + + try { + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "alchemy_getTokenBalances", + params: [account, tokenAddresses], + }), + }); + + logger.debug({ + at: "fetchErc20Balances", + message: "Making request to Alchemy API for ERC20 tokens", + chainId, + account, + rpcUrl, + tokenAddressCount: tokenAddresses.length, + }); + + if (!response.ok) { + logger.warn({ + at: "fetchErc20Balances", + message: "HTTP error from Alchemy API, returning empty balances", + chainId, + status: response.status, + statusText: response.statusText, + }); + return []; + } + + const data = await response.json(); + + logger.debug({ + at: "fetchErc20Balances", + message: "Received response from Alchemy API", + chainId, + responseData: data, + }); + + // Validate the response structure + if (!data || !data.result || !data.result.tokenBalances) { + logger.warn({ + at: "fetchErc20Balances", + message: "Invalid response from Alchemy API, returning empty balances", + chainId, + responseData: data, + }); + return []; + } + + const erc20Balances = ( + data.result.tokenBalances as { + contractAddress: string; + tokenBalance: string; + }[] + ) + .filter((t) => { + if (!t.tokenBalance) return false; + try { + const balance = BigNumber.from(t.tokenBalance); + // Filter out zero balances and MaxUint256 (Alchemy sometimes borks and returns MaxUint256) + return balance.gt(0) && balance.lt(ethers.constants.MaxUint256); + } catch (error) { + logger.warn({ + at: "fetchErc20Balances", + message: "Invalid token balance value", + chainId, + tokenAddress: t.contractAddress, + tokenBalance: t.tokenBalance, + }); + return false; + } + }) + .map((t) => ({ + address: t.contractAddress, + balance: BigNumber.from(t.tokenBalance).toString(), + })); + + return erc20Balances; + } catch (error) { + logger.warn({ + at: "fetchErc20Balances", + message: "Error fetching ERC20 balances from Alchemy API", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} + +export async function fetchTokenBalancesForChain( + chainId: number, + account: string, + tokenAddresses: string[] +): Promise<{ + chainId: number; + balances: Array<{ address: string; balance: string }>; +}> { + const rpcUrl = getAlchemyRpcFromConfigJson(chainId); + + if (!rpcUrl) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "No Alchemy RPC URL found for chain, returning empty balances", + chainId, + }); + return { + chainId, + balances: [], + }; + } + + try { + // Separate ERC20 tokens from native token + const erc20TokenAddresses = tokenAddresses.filter( + (addr) => addr !== ethers.constants.AddressZero + ); + + logger.debug({ + at: "fetchTokenBalancesForChain", + message: "Fetching token balances for chain", + chainId, + totalTokens: tokenAddresses.length, + erc20Tokens: erc20TokenAddresses.length, + }); + + // Fetch both ERC20 and native balances in parallel + const [erc20Balances, nativeBalance] = await Promise.all([ + fetchErc20Balances(chainId, account, erc20TokenAddresses, rpcUrl), + fetchNativeBalance(chainId, account, rpcUrl), + ]); + + // Add native balance if it exists and is greater than 0 + const balances = [...erc20Balances]; + if (nativeBalance && BigNumber.from(nativeBalance).gt(0)) { + balances.unshift({ + address: ethers.constants.AddressZero, + balance: nativeBalance, + }); + } + + return { + chainId, + balances, + }; + } catch (error) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: + "Error fetching token balances from Alchemy API, returning empty balances", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return { + chainId, + balances: [], + }; + } +} diff --git a/api/user-token-balances/_service.ts b/api/user-token-balances/_service.ts index 2cac269d8..5b0aef021 100644 --- a/api/user-token-balances/_service.ts +++ b/api/user-token-balances/_service.ts @@ -1,12 +1,12 @@ -import { BigNumber, ethers } from "ethers"; import { MAINNET_CHAIN_IDs } from "@across-protocol/constants"; import * as sdk from "@across-protocol/sdk"; import { getLogger } from "../_utils"; import { getAlchemyRpcFromConfigJson } from "../_providers"; import { isSvmAddress } from "../_address"; -import { getSvmBalance } from "../_balance"; import { fetchSwapTokensData, SwapToken } from "../swap/tokens/_service"; import { CHAIN_IDs, EVM_CHAIN_IDs } from "../_constants"; +import * as evmService from "./_evm"; +import * as svmService from "./_svm"; const logger = getLogger(); @@ -38,302 +38,6 @@ function getSvmChainIds(): number[] { .sort((a, b) => a - b); } -function getTokenAddressesForChain( - swapTokens: SwapToken[], - chainId: number -): string[] { - const tokens = swapTokens - .filter((token) => token.chainId === chainId) - .map((token) => token.address); - - // Remove duplicates and filter out native token (AddressZero) since we fetch it separately - return Array.from( - new Set(tokens.filter((addr) => addr !== ethers.constants.AddressZero)) - ); -} - -function getSvmTokenAddressesForChain( - swapTokens: SwapToken[], - chainId: number -): string[] { - const tokens = swapTokens - .filter((token) => token.chainId === chainId) - .map((token) => token.address); - - // Remove duplicates and filter out native token (zero address) since we fetch it separately - return Array.from( - new Set(tokens.filter((addr) => addr !== sdk.constants.ZERO_ADDRESS)) - ); -} - -const fetchNativeBalance = async ( - chainId: number, - account: string, - rpcUrl: string -): Promise => { - try { - logger.debug({ - at: "fetchNativeBalance", - message: "Fetching native balance", - chainId, - account, - }); - - const response = await fetch(rpcUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: 1, - method: "eth_getBalance", - params: [account, "latest"], - }), - }); - - if (!response.ok) { - logger.warn({ - at: "fetchNativeBalance", - message: "HTTP error fetching native balance", - chainId, - status: response.status, - statusText: response.statusText, - }); - return null; - } - - const data = await response.json(); - - if (!data || !data.result) { - logger.warn({ - at: "fetchNativeBalance", - message: "Invalid response for native balance", - chainId, - responseData: data, - }); - return null; - } - - return BigNumber.from(data.result).toString(); - } catch (error) { - logger.warn({ - at: "fetchNativeBalance", - message: "Error fetching native balance", - chainId, - error: error instanceof Error ? error.message : String(error), - }); - return null; - } -}; - -const fetchSolanaTokenBalances = async ( - chainId: number, - account: string, - tokenAddresses: string[] -): Promise<{ - chainId: number; - balances: Array<{ address: string; balance: string }>; -}> => { - try { - logger.debug({ - at: "fetchSolanaTokenBalances", - message: "Fetching Solana token balances", - chainId, - account, - tokenAddressCount: tokenAddresses.length, - }); - - // Include native SOL balance (zero address) - const allTokenAddresses = [sdk.constants.ZERO_ADDRESS, ...tokenAddresses]; - - // Fetch balances for all tokens in parallel - const balancePromises = allTokenAddresses.map(async (tokenAddress) => { - try { - const balance = await getSvmBalance(chainId, account, tokenAddress); - return { - address: tokenAddress, - balance: balance.toString(), - }; - } catch (error) { - logger.warn({ - at: "fetchSolanaTokenBalances", - message: "Error fetching balance for token", - chainId, - tokenAddress, - error: error instanceof Error ? error.message : String(error), - }); - return null; - } - }); - - const results = await Promise.all(balancePromises); - - // Filter out null results and zero balances - const balances = results.filter( - (result): result is { address: string; balance: string } => - result !== null && BigNumber.from(result.balance).gt(0) - ); - - return { - chainId, - balances, - }; - } catch (error) { - logger.warn({ - at: "fetchSolanaTokenBalances", - message: "Error fetching Solana token balances, returning empty balances", - chainId, - error: error instanceof Error ? error.message : String(error), - }); - return { - chainId, - balances: [], - }; - } -}; - -export const fetchTokenBalancesForChain = async ( - chainId: number, - account: string, - tokenAddresses: string[] -): Promise<{ - chainId: number; - balances: Array<{ address: string; balance: string }>; -}> => { - const rpcUrl = getAlchemyRpcFromConfigJson(chainId); - - if (!rpcUrl) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "No Alchemy RPC URL found for chain, returning empty balances", - chainId, - }); - return { - chainId, - balances: [], - }; - } - - try { - // Fetch both ERC20 and native balances in parallel - const [erc20Response, nativeBalance] = await Promise.all([ - fetch(rpcUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: 1, - method: "alchemy_getTokenBalances", - params: [account, tokenAddresses], - }), - }), - fetchNativeBalance(chainId, account, rpcUrl), - ]); - - logger.debug({ - at: "fetchTokenBalancesForChain", - message: "Making request to Alchemy API for ERC20 tokens", - chainId, - account, - rpcUrl, - }); - - if (!erc20Response.ok) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "HTTP error from Alchemy API, returning empty balances", - chainId, - status: erc20Response.status, - statusText: erc20Response.statusText, - }); - return { - chainId, - balances: [], - }; - } - - const data = await erc20Response.json(); - - logger.debug({ - at: "fetchTokenBalancesForChain", - message: "Received response from Alchemy API", - chainId, - responseData: data, - }); - - // Validate the response structure - if (!data || !data.result || !data.result.tokenBalances) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "Invalid response from Alchemy API, returning empty balances", - chainId, - responseData: data, - }); - return { - chainId, - balances: [], - }; - } - - const erc20Balances = ( - data.result.tokenBalances as { - contractAddress: string; - tokenBalance: string; - }[] - ) - .filter((t) => { - if (!t.tokenBalance) return false; - try { - const balance = BigNumber.from(t.tokenBalance); - // Filter out zero balances and MaxUint256 (Alchemy sometimes borks and returns MaxUint256) - return balance.gt(0) && balance.lt(ethers.constants.MaxUint256); - } catch (error) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: "Invalid token balance value", - chainId, - tokenAddress: t.contractAddress, - tokenBalance: t.tokenBalance, - }); - return false; - } - }) - .map((t) => ({ - address: t.contractAddress, - balance: BigNumber.from(t.tokenBalance).toString(), - })); - - // Add native balance if it exists and is greater than 0 - const balances = [...erc20Balances]; - if (nativeBalance && BigNumber.from(nativeBalance).gt(0)) { - balances.unshift({ - address: ethers.constants.AddressZero, - balance: nativeBalance, - }); - } - - return { - chainId, - balances, - }; - } catch (error) { - logger.warn({ - at: "fetchTokenBalancesForChain", - message: - "Error fetching token balances from Alchemy API, returning empty balances", - chainId, - error: error instanceof Error ? error.message : String(error), - }); - return { - chainId, - balances: [], - }; - } -}; - export const handleUserTokenBalances = async (account: string) => { // Check if the account is a Solana address const isSolanaAddress = isSvmAddress(account); @@ -359,14 +63,21 @@ export const handleUserTokenBalances = async (account: string) => { // Fetch balances for all SVM chains in parallel const balancePromises = svmChainIds.map((chainId) => { - const tokenAddresses = getSvmTokenAddressesForChain(swapTokens, chainId); + const tokenAddresses = svmService.getSvmTokenAddressesForChain( + swapTokens, + chainId + ); logger.debug({ at: "handleUserTokenBalances", message: "Token addresses for SVM chain", chainId, tokenAddressCount: tokenAddresses.length, }); - return fetchSolanaTokenBalances(chainId, account, tokenAddresses); + return svmService.fetchTokenBalancesForChain( + chainId, + account, + tokenAddresses + ); }); const chainBalances = await Promise.all(balancePromises); @@ -401,14 +112,21 @@ export const handleUserTokenBalances = async (account: string) => { // Fetch balances for all chains in parallel const balancePromises = chainIdsAvailable.map((chainId) => { - const tokenAddresses = getTokenAddressesForChain(swapTokens, chainId); + const tokenAddresses = evmService.getTokenAddressesForChain( + swapTokens, + chainId + ); logger.debug({ at: "handleUserTokenBalances", message: "Token addresses for chain", chainId, tokenAddressCount: tokenAddresses.length, }); - return fetchTokenBalancesForChain(chainId, account, tokenAddresses); + return evmService.fetchTokenBalancesForChain( + chainId, + account, + tokenAddresses + ); }); const chainBalances = await Promise.all(balancePromises); diff --git a/api/user-token-balances/_svm.ts b/api/user-token-balances/_svm.ts new file mode 100644 index 000000000..8fe5bebaa --- /dev/null +++ b/api/user-token-balances/_svm.ts @@ -0,0 +1,157 @@ +import { BigNumber } from "ethers"; +import * as sdk from "@across-protocol/sdk"; +import { getLogger } from "../_utils"; +import { getSvmBalance } from "../_balance"; +import { SwapToken } from "../swap/tokens/_service"; + +const logger = getLogger(); + +export function getSvmTokenAddressesForChain( + swapTokens: SwapToken[], + chainId: number +): string[] { + const tokens = swapTokens + .filter((token) => token.chainId === chainId) + .map((token) => token.address); + + return Array.from(new Set(tokens)); +} + +export async function fetchNativeBalance( + chainId: number, + account: string +): Promise { + try { + logger.debug({ + at: "fetchNativeBalance", + message: "Fetching native SVM balance", + chainId, + account, + }); + + const balance = await getSvmBalance( + chainId, + account, + sdk.constants.ZERO_ADDRESS + ); + + return balance.toString(); + } catch (error) { + logger.warn({ + at: "fetchNativeBalance", + message: "Error fetching native SVM balance", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } +} + +export async function fetchSplTokenBalances( + chainId: number, + account: string, + tokenAddresses: string[] +): Promise> { + // Early return if no token addresses + if (tokenAddresses.length === 0) { + logger.debug({ + at: "fetchSplTokenBalances", + message: "No SPL token addresses to fetch, returning empty array", + chainId, + }); + return []; + } + + logger.debug({ + at: "fetchSplTokenBalances", + message: "Fetching SPL token balances", + chainId, + account, + tokenAddressCount: tokenAddresses.length, + }); + + // Fetch balances for all tokens in parallel + const balancePromises = tokenAddresses.map(async (tokenAddress) => { + try { + const balance = await getSvmBalance(chainId, account, tokenAddress); + return { + address: tokenAddress, + balance: balance.toString(), + }; + } catch (error) { + logger.warn({ + at: "fetchSplTokenBalances", + message: "Error fetching balance for SPL token", + chainId, + tokenAddress, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } + }); + + const results = await Promise.all(balancePromises); + + // Filter out null results and zero balances + const balances = results.filter( + (result): result is { address: string; balance: string } => + result !== null && BigNumber.from(result.balance).gt(0) + ); + + return balances; +} + +export async function fetchTokenBalancesForChain( + chainId: number, + account: string, + tokenAddresses: string[] +): Promise<{ + chainId: number; + balances: Array<{ address: string; balance: string }>; +}> { + try { + // Separate SPL tokens from native token + const splTokenAddresses = tokenAddresses.filter( + (addr) => addr !== sdk.constants.ZERO_ADDRESS + ); + + logger.debug({ + at: "fetchTokenBalancesForChain", + message: "Fetching token balances for chain", + chainId, + totalTokens: tokenAddresses.length, + splTokens: splTokenAddresses.length, + }); + + // Fetch both SPL tokens and native balance in parallel + const [splBalances, nativeBalance] = await Promise.all([ + fetchSplTokenBalances(chainId, account, splTokenAddresses), + fetchNativeBalance(chainId, account), + ]); + + // Add native balance if it exists and is greater than 0 + const balances = [...splBalances]; + if (nativeBalance && BigNumber.from(nativeBalance).gt(0)) { + balances.unshift({ + address: sdk.constants.ZERO_ADDRESS, + balance: nativeBalance, + }); + } + + return { + chainId, + balances, + }; + } catch (error) { + logger.warn({ + at: "fetchTokenBalancesForChain", + message: "Error fetching SVM token balances, returning empty balances", + chainId, + error: error instanceof Error ? error.message : String(error), + }); + return { + chainId, + balances: [], + }; + } +} From b8d26a2361c3f316281cf115a328f4cb19647888 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 18:48:57 +0200 Subject: [PATCH 092/122] add tracing Signed-off-by: Gerhard Steenkamp --- api/user-token-balances/index.ts | 70 ++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/api/user-token-balances/index.ts b/api/user-token-balances/index.ts index 4f3dc7338..a7ccbadd0 100644 --- a/api/user-token-balances/index.ts +++ b/api/user-token-balances/index.ts @@ -3,6 +3,9 @@ import { assert, Infer, type } from "superstruct"; import { TypedVercelRequest } from "../_types"; import { getLogger, handleErrorCondition, validAddress } from "../_utils"; import { handleUserTokenBalances } from "./_service"; +import { getRequestId, setRequestSpanAttributes } from "../_request_utils"; +import { sendResponse } from "../_response_utils"; +import { tracer, processor } from "../../instrumentation"; const UserTokenBalancesQueryParamsSchema = type({ account: validAddress(), @@ -17,29 +20,52 @@ const handler = async ( response: VercelResponse ) => { const logger = getLogger(); + const requestId = getRequestId(request); + logger.debug({ + at: "user-token-balances", + message: "Request data", + requestId, + }); - try { - const { query } = request; - assert(query, UserTokenBalancesQueryParamsSchema); - const { account } = query; - - const responseData = await handleUserTokenBalances(account); - - logger.debug({ - at: "UserTokenBalances", - message: "Response data", - responseJson: responseData, - }); - - // Cache for 3 minutes - response.setHeader( - "Cache-Control", - "s-maxage=180, stale-while-revalidate=60" - ); - response.status(200).json(responseData); - } catch (error: unknown) { - return handleErrorCondition("user-token-balances", response, logger, error); - } + return tracer.startActiveSpan("user-token-balances", async (span) => { + setRequestSpanAttributes(request, span, requestId); + + try { + const { query } = request; + assert(query, UserTokenBalancesQueryParamsSchema); + const { account } = query; + + const responseData = await handleUserTokenBalances(account); + + logger.debug({ + at: "user-token-balances", + message: "Response data", + responseJson: responseData, + requestId, + }); + + sendResponse({ + response, + body: responseData, + statusCode: 200, + requestId, + cacheSeconds: 60 * 3, + staleWhileRevalidateSeconds: 60, + }); + } catch (error: unknown) { + return handleErrorCondition( + "user-token-balances", + response, + logger, + error, + span, + requestId + ); + } finally { + span.end(); + processor.forceFlush(); + } + }); }; export default handler; From a8b516acd8b2e29326d4f839bfcae174635f35c8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 19:14:54 +0200 Subject: [PATCH 093/122] prompt user to connect svm wallet if svm route Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index d7fdc2e38..eaea51b02 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -45,6 +45,7 @@ export type UseSwapAndBridgeReturn = { buttonDisabled: boolean; buttonLoading: boolean; buttonLabel: string; + walletTypeToConnect?: "evm" | "svm"; // Which wallet type needs to be connected // Account management toAccountManagement: ReturnType; @@ -69,8 +70,16 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const history = useHistory(); - const { account: accountEVM, connect: connectEVM } = useConnectionEVM(); - const { account: accountSVM, connect: connectSVM } = useConnectionSVM(); + const { + account: accountEVM, + connect: connectEVM, + isConnected: isConnectedEVM, + } = useConnectionEVM(); + const { + account: accountSVM, + connect: connectSVM, + isConnected: isConnectedSVM, + } = useConnectionSVM(); const toAccountManagement = useToAccount(outputToken?.chainId); @@ -84,6 +93,27 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const depositor = originChainEcosystem === "evm" ? accountEVM : accountSVM?.toBase58(); + // Check if origin wallet is connected + const isOriginConnected = + originChainEcosystem === "evm" ? isConnectedEVM : isConnectedSVM; + + // Check if destination recipient is set (appropriate wallet connected for destination ecosystem) + const isRecipientSet = + destinationChainEcosystem === "evm" + ? !!toAccountManagement.toAccountEVM + : !!toAccountManagement.toAccountSVM; + + // Determine which wallet type needs to be connected (if any) + const walletTypeToConnect: "evm" | "svm" | undefined = (() => { + if (!isOriginConnected) { + return originChainEcosystem; + } + if (!isRecipientSet) { + return destinationChainEcosystem; + } + return undefined; + })(); + useEffect(() => { if (defaultRoute.inputToken && defaultRoute.outputToken) { setInputToken((prev) => { @@ -162,13 +192,25 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { }, [swapQuote]); const onConfirm = useCallback(async () => { - // If not connected, open the wallet connection modal - if (!approvalAction.isConnected) { + // If origin wallet is not connected, connect it first + if (!isOriginConnected) { if (originChainEcosystem === "evm") { connectEVM({ trackSection: "bridgeForm" }); return; } else { connectSVM({ trackSection: "bridgeForm" }); + return; + } + } + + // If destination recipient is not set, connect the destination wallet + if (!isRecipientSet) { + if (destinationChainEcosystem === "evm") { + connectEVM({ trackSection: "bridgeForm" }); + return; + } else { + connectSVM({ trackSection: "bridgeForm" }); + return; } } @@ -181,13 +223,16 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { ); } }, [ + isOriginConnected, + isRecipientSet, + originChainEcosystem, + destinationChainEcosystem, approvalAction, connectEVM, connectSVM, history, inputToken?.chainId, inputToken?.symbol, - originChainEcosystem, outputToken?.chainId, outputToken?.symbol, ]); @@ -196,7 +241,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const buttonState: BridgeButtonState = useMemo(() => { if (isQuoteLoading) return "loadingQuote"; if (quoteError) return "quoteError"; - if (!approvalAction.isConnected) return "notConnected"; + if (!isOriginConnected || !isRecipientSet) return "notConnected"; if (approvalAction.isButtonActionLoading) return "submitting"; if (!inputToken || !outputToken) return "awaitingTokenSelection"; if (!amount || amount.lte(0)) return "awaitingAmountInput"; @@ -205,7 +250,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { }, [ isQuoteLoading, quoteError, - approvalAction.isConnected, + isOriginConnected, + isRecipientSet, approvalAction.isButtonActionLoading, inputToken, outputToken, @@ -217,7 +263,14 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { return buttonState === "loadingQuote" || buttonState === "submitting"; }, [buttonState]); - const buttonLabel = useMemo(() => buttonLabels[buttonState], [buttonState]); + const buttonLabel = useMemo(() => { + if (buttonState === "notConnected" && walletTypeToConnect) { + return walletTypeToConnect === "evm" + ? "Connect EVM Wallet" + : "Connect SVM Wallet"; + } + return buttonLabels[buttonState]; + }, [buttonState, walletTypeToConnect]); const buttonDisabled = useMemo( () => @@ -265,12 +318,13 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { buttonDisabled, buttonLoading, buttonLabel, + walletTypeToConnect, // Account management toAccountManagement, destinationChainEcosystem, // Legacy properties - isConnected: approvalAction.isConnected, + isConnected: isOriginConnected && isRecipientSet, isWrongNetwork: approvalAction.isWrongNetwork, isSubmitting: approvalAction.isButtonActionLoading, onConfirm, From 51bfa14eacf6371398070faeb03c337387a602b6 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 19:37:53 +0200 Subject: [PATCH 094/122] allow user to manually set recipient Signed-off-by: Gerhard Steenkamp --- .../Bridge/components/ChangeAccountModal.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/views/Bridge/components/ChangeAccountModal.tsx b/src/views/Bridge/components/ChangeAccountModal.tsx index 6fe80f7ff..9fcf7c626 100644 --- a/src/views/Bridge/components/ChangeAccountModal.tsx +++ b/src/views/Bridge/components/ChangeAccountModal.tsx @@ -92,15 +92,15 @@ export const ChangeAccountModal = ({ useHotkeys("esc", () => onCloseModal(), { enableOnFormTags: true }); - if (!currentRecipientAccount) { - return; - } - return ( <> setDisplayModal(true)}> - {shortenAddress(currentRecipientAccount, "..", 4)} - + <> + {currentRecipientAccount + ? shortenAddress(currentRecipientAccount, "..", 4) + : "Set Recipient"} + + Wallet Address - {userInput !== defaultRecipientAccount && ( - setUserInput(defaultRecipientAccount ?? "")} - > - Reset to Default - - )} + {defaultRecipientAccount && + userInput !== defaultRecipientAccount && ( + setUserInput(defaultRecipientAccount ?? "")} + > + Reset to Default + + )} From 67fcb206c89a0e01e75c51aefd6d9f6bb3ea1225 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 28 Oct 2025 19:56:52 +0200 Subject: [PATCH 095/122] fixup Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/hooks/useSwapAndBridge.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index eaea51b02..fd352e40c 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -265,12 +265,17 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const buttonLabel = useMemo(() => { if (buttonState === "notConnected" && walletTypeToConnect) { + // If neither wallet is connected, show generic "Connect Wallet" + if (!isConnectedEVM && !isConnectedSVM) { + return "Connect Wallet"; + } + // Otherwise, show the specific wallet type that needs to be connected return walletTypeToConnect === "evm" ? "Connect EVM Wallet" : "Connect SVM Wallet"; } return buttonLabels[buttonState]; - }, [buttonState, walletTypeToConnect]); + }, [buttonState, walletTypeToConnect, isConnectedEVM, isConnectedSVM]); const buttonDisabled = useMemo( () => From 4c4c8da302bf9fffa05a28bf9274388ed4a142f9 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 29 Oct 2025 12:27:35 +0200 Subject: [PATCH 096/122] fixup balance selector update Signed-off-by: Gerhard Steenkamp --- src/hooks/useEnrichedCrosschainBalances.ts | 3 -- .../components/BalanceSelector.tsx | 46 ++++++++++++++++--- .../SwapAndBridge/components/InputForm.tsx | 3 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/hooks/useEnrichedCrosschainBalances.ts b/src/hooks/useEnrichedCrosschainBalances.ts index 0fe06e843..6d4a9924b 100644 --- a/src/hooks/useEnrichedCrosschainBalances.ts +++ b/src/hooks/useEnrichedCrosschainBalances.ts @@ -43,9 +43,6 @@ export function useEnrichedCrosschainBalances() { : 0, }; }); - // // TODO: consider removing - // // Filter out tokens that don't have a logoURI - // .filter((t) => t.logoURI !== undefined); // Sort high to low balanceUsd const sortedByBalance = enrichedTokens.sort( diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index b5a96eb4f..452e03a99 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -1,25 +1,57 @@ import { motion, AnimatePresence } from "framer-motion"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { BigNumber } from "ethers"; import styled from "@emotion/styled"; -import { COLORS, formatUnitsWithMaxFractions } from "utils"; +import { + COLORS, + formatUnitsWithMaxFractions, + compareAddressesSimple, +} from "utils"; +import { useUserTokenBalances } from "hooks/useUserTokenBalances"; type BalanceSelectorProps = { - balance: BigNumber; - decimals: number; + token: { + chainId: number; + address: string; + decimals: number; + }; setAmount: (amount: BigNumber | null) => void; disableHover?: boolean; error?: boolean; }; export function BalanceSelector({ - balance, - decimals, + token, setAmount, disableHover, error = false, }: BalanceSelectorProps) { const [isHovered, setIsHovered] = useState(false); + const tokenBalances = useUserTokenBalances(); + + // Derive the balance from the latest token balances + const balance = useMemo(() => { + if (!tokenBalances.data?.balances) { + return BigNumber.from(0); + } + + const chainBalances = tokenBalances.data.balances.find( + (cb) => cb.chainId === String(token.chainId) + ); + + if (!chainBalances) { + return BigNumber.from(0); + } + + const tokenBalance = chainBalances.balances.find((b) => + compareAddressesSimple(b.address, token.address) + ); + + return tokenBalance?.balance + ? BigNumber.from(tokenBalance.balance) + : BigNumber.from(0); + }, [tokenBalances.data, token.chainId, token.address]); + if (!balance || balance.lte(0)) return null; const percentages = ["25%", "50%", "75%", "MAX"]; @@ -33,7 +65,7 @@ export function BalanceSelector({ } }; - const formattedBalance = formatUnitsWithMaxFractions(balance, decimals); + const formattedBalance = formatUnitsWithMaxFractions(balance, token.decimals); return ( { if (amount) { From 30e844811ccbfb6619cb8d73dea15d25b9aed1c1 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 30 Oct 2025 15:30:10 +0200 Subject: [PATCH 097/122] resolve longtail token info Signed-off-by: Gerhard Steenkamp --- src/hooks/useAvailableCrosschainRoutes.ts | 42 ++++++---- src/hooks/useSwapTokens.ts | 13 ++- src/hooks/useToken.ts | 80 +++++++++++++++++++ src/hooks/useTokenConversion.ts | 41 +--------- src/utils/constants.ts | 18 +++-- src/utils/token.ts | 64 +++++++++++++++ .../components/DepositTimesCard.tsx | 70 +++++++++++----- .../components/SharedSocialsCard.tsx | 16 ++-- .../DepositStatus/hooks/useDepositTracking.ts | 36 ++++++--- .../useDepositTracking/strategies/evm.ts | 8 +- .../useDepositTracking/strategies/svm.ts | 8 +- .../hooks/useResolveFromBridgePagePayload.ts | 39 +++++---- 12 files changed, 307 insertions(+), 128 deletions(-) create mode 100644 src/hooks/useToken.ts diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 434ff72fe..7b9a16c20 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -35,21 +35,33 @@ export default function useAvailableCrosschainRoutes( // Build token map by chain from API tokens const tokensByChain = (swapTokensQuery.data || []).reduce( (acc, token) => { - const mapped: LifiToken = { - chainId: token.chainId, - address: token.address, - name: token.name, - symbol: token.symbol, - decimals: token.decimals, - logoURI: token.logoUrl || "", - priceUSD: token.priceUsd || "0", - coinKey: token.symbol, - routeSource: "swap", - }; - if (!acc[token.chainId]) { - acc[token.chainId] = []; - } - acc[token.chainId].push(mapped); + // Get the chainId from the addresses record (TokenInfo has addresses object) + const chainIds = token.addresses + ? Object.keys(token.addresses).map(Number) + : []; + + chainIds.forEach((chainId) => { + const address = token.addresses?.[chainId]; + if (!address) return; + + const mapped: LifiToken = { + chainId: chainId, + address: address, + name: token.name, + symbol: token.symbol, + decimals: token.decimals, + logoURI: token.logoURI || "", + priceUSD: "0", // TokenInfo doesn't have price, would need to be enriched separately + coinKey: token.symbol, + routeSource: "swap", + }; + + if (!acc[chainId]) { + acc[chainId] = []; + } + acc[chainId].push(mapped); + }); + return acc; }, {} as Record> diff --git a/src/hooks/useSwapTokens.ts b/src/hooks/useSwapTokens.ts index 7f4d7cf56..d79a894a0 100644 --- a/src/hooks/useSwapTokens.ts +++ b/src/hooks/useSwapTokens.ts @@ -1,5 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import getApiEndpoint from "utils/serverless-api"; +import { swapTokenToTokenInfo } from "utils/token"; +import { TokenInfo } from "constants/tokens"; // Import cached tokens data import cachedTokensData from "../data/swap-tokens.json"; @@ -24,12 +26,17 @@ function filterTokensByChainId( return tokens.filter((token) => chainIds.includes(token.chainId)); } +function convertSwapTokensToTokenInfo(tokens: SwapToken[]): TokenInfo[] { + return tokens.map((token) => swapTokenToTokenInfo(token)); +} + export function useSwapTokens(query?: SwapTokensQuery) { - return useQuery({ + return useQuery({ queryKey: ["swapTokens", query], queryFn: async () => { const api = getApiEndpoint(); - return await api.swapTokens(query); + const tokens = await api.swapTokens(query); + return convertSwapTokensToTokenInfo(tokens); }, // Use cached data as initial data for immediate loading initialData: () => { @@ -39,7 +46,7 @@ export function useSwapTokens(query?: SwapTokensQuery) { cache.tokens, query?.chainId ); - return filteredTokens; + return convertSwapTokensToTokenInfo(filteredTokens); } catch (error) { console.warn("Failed to load cached swap tokens:", error); return undefined; diff --git a/src/hooks/useToken.ts b/src/hooks/useToken.ts new file mode 100644 index 000000000..c1ba79522 --- /dev/null +++ b/src/hooks/useToken.ts @@ -0,0 +1,80 @@ +import { useMemo } from "react"; +import { + getToken, + TOKEN_SYMBOLS_MAP, + TokenInfo, + applyChainSpecificTokenDisplay, +} from "utils"; +import { orderedTokenLogos } from "../constants/tokens"; +import { useSwapTokens } from "./useSwapTokens"; +import unknownLogo from "assets/icons/question-circle.svg"; + +/** + * Hook to resolve token info for a given symbol + * Resolution order: + * 1. Try getToken from constants + * 2. Try TOKEN_SYMBOLS_MAP directly (local definitions) + * 3. Fallback to swap tokens from API + * + * If chainId is provided, applies chain-specific display modifications (e.g., USDT -> USDT0) + */ +export function useToken( + symbol: string, + chainId?: number +): TokenInfo | undefined { + const { data: swapTokens } = useSwapTokens(); + + const token = useMemo(() => { + let resolvedToken: TokenInfo | undefined; + + // Try to get token from constants first + try { + resolvedToken = getToken(symbol); + } catch (error) { + // If getToken fails, try TOKEN_SYMBOLS_MAP directly + const tokenFromMap = + TOKEN_SYMBOLS_MAP[ + symbol.toUpperCase() as keyof typeof TOKEN_SYMBOLS_MAP + ]; + + if (tokenFromMap) { + // Get logoURI from orderedTokenLogos or use unknown logo + const logoURI = + orderedTokenLogos[ + symbol.toUpperCase() as keyof typeof orderedTokenLogos + ] || unknownLogo; + + resolvedToken = { + ...tokenFromMap, + logoURI, + } as TokenInfo; + } else if (swapTokens) { + // If still not found, try to find it in swap API data + // Search across all chains for a token with matching symbol + // Note: swapTokens is now already converted to TokenInfo[] + const foundToken = swapTokens.find( + (t) => t.symbol.toUpperCase() === symbol.toUpperCase() + ); + + if (foundToken) { + resolvedToken = foundToken; + } + } + + // If still not found, log warning + if (!resolvedToken) { + console.warn(`Unable to resolve token info for symbol ${symbol}`); + return undefined; + } + } + + // Apply chain-specific display modifications if chainId is provided + if (chainId !== undefined) { + return applyChainSpecificTokenDisplay(resolvedToken, chainId); + } + + return resolvedToken; + }, [symbol, chainId, swapTokens]); + + return token; +} diff --git a/src/hooks/useTokenConversion.ts b/src/hooks/useTokenConversion.ts index cff38f6e1..5c93773a1 100644 --- a/src/hooks/useTokenConversion.ts +++ b/src/hooks/useTokenConversion.ts @@ -3,15 +3,13 @@ import { BigNumber, BigNumberish, ethers } from "ethers"; import { useCallback } from "react"; import { fixedPointAdjustment, - getToken, TOKEN_SYMBOLS_MAP, isDefined, getConfig, hubPoolChainId, - TokenInfo, } from "utils"; import { ConvertDecimals } from "utils/convertdecimals"; -import { useSwapTokens } from "./useSwapTokens"; +import { useToken } from "./useToken"; const config = getConfig(); @@ -20,41 +18,8 @@ export function useTokenConversion( baseCurrency: string, historicalDateISO?: string ) { - const { data: swapTokens } = useSwapTokens(); - - // Try to get token from constants first, fallback to swap API data - let token: TokenInfo | undefined; - try { - token = getToken(symbol); - } catch (error) { - // If token not found in constants, try to find it in swap API data - if (swapTokens) { - // Search across all chains for a token with matching symbol - const foundToken = swapTokens.find( - (t) => t.symbol.toUpperCase() === symbol.toUpperCase() - ); - if (foundToken) { - // Convert SwapToken to TokenInfo format - token = { - symbol: foundToken.symbol, - name: foundToken.name, - decimals: foundToken.decimals, - addresses: { [foundToken.chainId]: foundToken.address }, - mainnetAddress: foundToken.address, // Use the found address as mainnet address - logoURI: foundToken.logoUrl || "", // Use logoUrl from SwapToken - }; - } - - // If still not found, re-throw the original error - if (!token) { - throw error; - } - } else { - // If swapTokens is not available, re-throw the original error - console.error(`Unable to resolve token info for symbol ${symbol}`); - throw error; - } - } + // Use the useToken hook to resolve token info + const token = useToken(symbol); // If the token is OP, we need to use the address of the token on Optimism const l1Token = diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b93c7c3d8..4a0ac2bb4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -314,15 +314,17 @@ export const getToken = (symbol: string): TokenInfo => { }; /** - * Gets token info with chain-specific display modifications (temporary for USDT0) - * This is a temporary function that will be removed once all chains migrate to USDT0 + * Apply chain-specific display modifications to a token + * Currently handles: USDT -> USDT0 for supported chains + * + * @param token Token info to transform + * @param chainId Chain ID to apply transformations for + * @returns Transformed token info, or original if no transformations needed */ -export const getTokenForChain = ( - symbol: string, +export function applyChainSpecificTokenDisplay( + token: TokenInfo, chainId: number -): TokenInfo => { - const token = getToken(symbol); - +): TokenInfo { // Handle USDT -> USDT0 display for specific chains if (token.symbol === "USDT" && chainsWithUsdt0Enabled.includes(chainId)) { return { @@ -333,7 +335,7 @@ export const getTokenForChain = ( } return token; -}; +} export const getRewardToken = (deposit: Deposit): TokenInfo | undefined => { if (!deposit.rewards) { diff --git a/src/utils/token.ts b/src/utils/token.ts index 675b156ff..c0e24d7cd 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -9,6 +9,15 @@ import { parseUnits, } from "utils"; import { ERC20__factory } from "utils/typechain"; +import { SwapToken } from "utils/serverless-api/types"; +import { TokenInfo } from "constants/tokens"; +import { + CHAIN_IDs, + chainsWithUsdt0Enabled, + getToken, + tokenTable, +} from "utils/constants"; +import usdt0Logo from "assets/token-logos/usdt0.svg"; export async function getNativeBalance( chainId: ChainId, @@ -147,3 +156,58 @@ export function convertUSDToToken( } return result18Dec; } + +/** + * Gets token info with chain-specific display modifications (temporary for USDT0) + * This is a temporary function that will be removed once all chains migrate to USDT0 + */ +export const getTokenForChain = ( + symbol: string, + chainId: number +): TokenInfo => { + const token = getToken(symbol); + + // Handle USDT -> USDT0 display for specific chains + if (token.symbol === "USDT" && chainsWithUsdt0Enabled.includes(chainId)) { + return { + ...token, + displaySymbol: "USDT0", + logoURI: usdt0Logo, + }; + } + + return token; +}; + +/** + * Attempts to coerce a SwapToken into a TokenInfo type + * Checks local token definitions to enrich with mainnetAddress and displaySymbol + * @param swapToken - The SwapToken to convert + * @returns A TokenInfo object with available properties mapped + */ +export function swapTokenToTokenInfo(swapToken: SwapToken): TokenInfo { + // Try to find the token in our local token definitions + const localToken = tokenTable?.[swapToken.symbol.toUpperCase()]; + + const baseTokenInfo: TokenInfo = { + name: swapToken.name, + symbol: swapToken.symbol, + decimals: swapToken.decimals, + logoURI: swapToken.logoUrl || "", + addresses: { + [swapToken.chainId]: swapToken.address, + }, + }; + + // If we found a local token definition, merge in mainnetAddress and displaySymbol + if (localToken) { + return { + ...baseTokenInfo, + mainnetAddress: localToken.mainnetAddress, + displaySymbol: localToken.displaySymbol, + logoURI: localToken.logoURI || baseTokenInfo.logoURI, // Prefer local logo if available + }; + } + + return baseTokenInfo; +} diff --git a/src/views/DepositStatus/components/DepositTimesCard.tsx b/src/views/DepositStatus/components/DepositTimesCard.tsx index ad0719aa7..45a706c9b 100644 --- a/src/views/DepositStatus/components/DepositTimesCard.tsx +++ b/src/views/DepositStatus/components/DepositTimesCard.tsx @@ -15,8 +15,6 @@ import { getBridgeUrlWithQueryParams, isDefined, formatUSD, - getToken, - getTokenForChain, } from "utils"; import { useAmplitude } from "hooks"; import { ampli } from "ampli"; @@ -32,6 +30,7 @@ import { getTokensForFeesCalc, } from "views/Bridge/utils"; import { useTokenConversion } from "hooks/useTokenConversion"; +import { useToken } from "hooks/useToken"; import EstimatedTable from "views/Bridge/components/EstimatedTable"; type Props = { @@ -73,19 +72,32 @@ export function DepositTimesCard({ const netFee = estimatedRewards?.netFeeAsBaseCurrency?.toString(); const amountSentBaseCurrency = amountAsBaseCurrency?.toString(); - const { inputToken, bridgeToken } = getTokensForFeesCalc({ - inputToken: getToken(inputTokenSymbol), - outputToken: getToken(outputTokenSymbol || inputTokenSymbol), - isUniversalSwap: isUniversalSwap, - universalSwapQuote: fromBridgePagePayload?.universalSwapQuote, - fromChainId: fromChainId, - toChainId: toChainId, - }); + const inputTokenFromHook = useToken(inputTokenSymbol); + const outputTokenFromHook = useToken(outputTokenSymbol || inputTokenSymbol); + const outputTokenForChain = useToken( + outputTokenSymbol || inputTokenSymbol, + toChainId + ); + + const { inputToken, bridgeToken } = + inputTokenFromHook && outputTokenFromHook + ? getTokensForFeesCalc({ + inputToken: inputTokenFromHook, + outputToken: outputTokenFromHook, + isUniversalSwap: isUniversalSwap, + universalSwapQuote: fromBridgePagePayload?.universalSwapQuote, + fromChainId: fromChainId, + toChainId: toChainId, + }) + : { + inputToken: inputTokenFromHook!, + bridgeToken: inputTokenFromHook!, + }; const { convertTokenToBaseCurrency: convertInputTokenToUsd } = - useTokenConversion(inputToken.symbol, "usd"); + useTokenConversion(inputToken?.symbol || inputTokenSymbol, "usd"); const { convertTokenToBaseCurrency: convertBridgeTokenToUsd } = - useTokenConversion(bridgeToken.symbol, "usd"); + useTokenConversion(bridgeToken?.symbol || inputTokenSymbol, "usd"); const { convertTokenToBaseCurrency: convertOutputTokenToUsd, convertBaseCurrencyToToken: convertUsdToOutputToken, @@ -184,16 +196,17 @@ export function DepositTimesCard({ )} {(netFee || amountSentBaseCurrency) && } - {isDefined(outputAmount) && - isDefined(outputTokenSymbol) && + + {isDefined(fromBridgePagePayload?.depositArgs?.initialAmount) && + inputToken && isDefined(amountSentBaseCurrency) && ( Amount sent )} - {isDefined(outputTokenSymbol) && ( + {isDefined(outputAmount) && + isDefined(outputTokenSymbol) && + outputTokenForChain && + isDefined(outputAmountUsd) && ( + + Amount received + + + (${formatUSD(outputAmountUsd)}) + + + )} + {isDefined(outputTokenSymbol) && outputTokenForChain && ( { + const token = useToken(inputTokenSymbol); const fromChain = getChainInfo(fromChainId).name; const toChain = getChainInfo(toChainId).name; - const amountSentText = amountSent - ? `~${formatUnitsWithMaxFractions(amountSent, getToken(inputTokenSymbol).decimals)}` - : undefined; + const amountSentText = + amountSent && token + ? `~${formatUnitsWithMaxFractions(amountSent, token.decimals)}` + : undefined; const shareText = [ `I just bridged ${amountSentText}${inputTokenSymbol} from ${fromChain} to ${toChain} using @AcrossProtocol.`, diff --git a/src/views/DepositStatus/hooks/useDepositTracking.ts b/src/views/DepositStatus/hooks/useDepositTracking.ts index ad0e3a710..fb208ba9e 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking.ts @@ -5,7 +5,6 @@ import { BigNumber } from "ethers"; import { useAmplitude } from "hooks"; import { generateDepositConfirmed, - getToken, recordTransferUserProperties, wait, getChainInfo, @@ -25,6 +24,7 @@ import { DepositStatus } from "../types"; import { DepositData } from "./useDepositTracking/types"; import { useConnectionSVM } from "hooks/useConnectionSVM"; import { useConnectionEVM } from "hooks/useConnectionEVM"; +import { useToken } from "hooks/useToken"; /** * Hook to track deposit and fill status across EVM and SVM chains @@ -54,6 +54,11 @@ export function useDepositTracking({ const account = getEcosystem(fromChainId) === "evm" ? accountEVM : accountSVM?.toBase58(); + // Resolve token info for analytics + const tokenForAnalytics = useToken( + fromBridgePagePayload?.quoteForAnalytics.tokenSymbol || "" + ); + // Create appropriate strategy for the source chain const { depositStrategy, fillStrategy } = useMemo( () => createChainStrategies(fromChainId, toChainId), @@ -189,16 +194,25 @@ export function useDepositTracking({ const { quoteForAnalytics, depositArgs, tokenPrice } = fromBridgePagePayload; - recordTransferUserProperties( - BigNumber.from(depositArgs.amount), - BigNumber.from(tokenPrice), - getToken(quoteForAnalytics.tokenSymbol).decimals, - quoteForAnalytics.tokenSymbol.toLowerCase(), - Number(quoteForAnalytics.fromChainId), - Number(quoteForAnalytics.toChainId), - quoteForAnalytics.fromChainName - ); - }, [fillQuery.data, depositTxHash, fromBridgePagePayload, fillStrategy]); + // Only record if we have token info + if (tokenForAnalytics) { + recordTransferUserProperties( + BigNumber.from(depositArgs.amount), + BigNumber.from(tokenPrice), + tokenForAnalytics.decimals, + quoteForAnalytics.tokenSymbol.toLowerCase(), + Number(quoteForAnalytics.fromChainId), + Number(quoteForAnalytics.toChainId), + quoteForAnalytics.fromChainName + ); + } + }, [ + fillQuery.data, + depositTxHash, + fromBridgePagePayload, + fillStrategy, + tokenForAnalytics, + ]); const status: DepositStatus = !depositQuery.data?.depositTimestamp ? "depositing" diff --git a/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts b/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts index 673a5b2bc..a0a03f98f 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts @@ -292,11 +292,11 @@ export class EVMStrategy implements IChainStrategy { fromBridgePagePayload; const { depositId, depositor, recipient, message, inputAmount } = depositInfo.depositLog; - const inputToken = config.getTokenInfoBySymbol( + const inputToken = config.getTokenInfoBySymbolSafe( selectedRoute.fromChain, selectedRoute.fromTokenSymbol ); - const outputToken = config.getTokenInfoBySymbol( + const outputToken = config.getTokenInfoBySymbolSafe( selectedRoute.toChain, selectedRoute.toTokenSymbol ); @@ -361,11 +361,11 @@ export class EVMStrategy implements IChainStrategy { const { selectedRoute, depositArgs, quoteForAnalytics } = bridgePayload; const { depositId, depositor, recipient, message, inputAmount } = fillInfo.depositInfo.depositLog; - const inputToken = config.getTokenInfoBySymbol( + const inputToken = config.getTokenInfoBySymbolSafe( selectedRoute.fromChain, selectedRoute.fromTokenSymbol ); - const outputToken = config.getTokenInfoBySymbol( + const outputToken = config.getTokenInfoBySymbolSafe( selectedRoute.toChain, selectedRoute.toTokenSymbol ); diff --git a/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts b/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts index 7633d624a..f516bd9fe 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts @@ -196,11 +196,11 @@ export class SVMStrategy implements IChainStrategy { fromBridgePagePayload; const { depositId, depositor, recipient, message, inputAmount } = depositInfo.depositLog; - const inputToken = config.getTokenInfoBySymbol( + const inputToken = config.getTokenInfoBySymbolSafe( selectedRoute.fromChain, selectedRoute.fromTokenSymbol ); - const outputToken = config.getTokenInfoBySymbol( + const outputToken = config.getTokenInfoBySymbolSafe( selectedRoute.toChain, selectedRoute.toTokenSymbol ); @@ -266,11 +266,11 @@ export class SVMStrategy implements IChainStrategy { const { selectedRoute, depositArgs, quoteForAnalytics } = bridgePayload; const { depositId, depositor, recipient, message, inputAmount } = fillInfo.depositInfo.depositLog; - const inputToken = config.getTokenInfoBySymbol( + const inputToken = config.getTokenInfoBySymbolSafe( selectedRoute.fromChain, selectedRoute.fromTokenSymbol ); - const outputToken = config.getTokenInfoBySymbol( + const outputToken = config.getTokenInfoBySymbolSafe( selectedRoute.toChain, selectedRoute.toTokenSymbol ); diff --git a/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts b/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts index 8e561b3fc..c3e29b56f 100644 --- a/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts +++ b/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts @@ -1,4 +1,3 @@ -import { getToken } from "utils"; import { useEstimatedRewards } from "views/Bridge/hooks/useEstimatedRewards"; import { calcFeesForEstimatedTable, @@ -7,6 +6,7 @@ import { import { FromBridgePagePayload } from "views/Bridge/hooks/useBridgeAction"; import { useTokenConversion } from "hooks/useTokenConversion"; +import { useToken } from "hooks/useToken"; import { bigNumberifyObject } from "utils/bignumber"; import { UniversalSwapQuote } from "hooks/useUniversalSwapQuote"; import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote"; @@ -31,27 +31,32 @@ export function useResolveFromBridgePagePayload( const isSwap = selectedRoute?.type === "swap"; const isUniversalSwap = selectedRoute?.type === "universal-swap"; - const swapToken = isSwap - ? getToken(selectedRoute.swapTokenSymbol) - : undefined; - const outputToken = getToken(outputTokenSymbol); - const { inputToken, bridgeToken } = getTokensForFeesCalc({ - inputToken: getToken(inputTokenSymbol), - outputToken, - isUniversalSwap: !!universalSwapQuote, - universalSwapQuote, - fromChainId: fromChainId, - toChainId: toChainId, - }); + const swapTokenSymbol = isSwap ? selectedRoute.swapTokenSymbol : ""; + + const swapToken = useToken(swapTokenSymbol); + const outputToken = useToken(outputTokenSymbol); + const inputTokenFromHook = useToken(inputTokenSymbol); + + const { inputToken, bridgeToken } = + inputTokenFromHook && outputToken + ? getTokensForFeesCalc({ + inputToken: inputTokenFromHook, + outputToken, + isUniversalSwap: !!universalSwapQuote, + universalSwapQuote, + fromChainId: fromChainId, + toChainId: toChainId, + }) + : { inputToken: inputTokenFromHook!, bridgeToken: inputTokenFromHook! }; const { convertTokenToBaseCurrency: convertInputTokenToUsd } = - useTokenConversion(inputToken.symbol, "usd"); + useTokenConversion(inputToken?.symbol || inputTokenSymbol, "usd"); const { convertTokenToBaseCurrency: convertBridgeTokenToUsd, convertBaseCurrencyToToken: convertUsdToBridgeToken, - } = useTokenConversion(bridgeToken.symbol, "usd"); + } = useTokenConversion(bridgeToken?.symbol || inputTokenSymbol, "usd"); const { convertTokenToBaseCurrency: convertOutputTokenToUsd } = - useTokenConversion(outputToken.symbol, "usd"); + useTokenConversion(outputToken?.symbol || outputTokenSymbol, "usd"); const { bridgeFeeUsd, @@ -82,7 +87,7 @@ export function useResolveFromBridgePagePayload( const capitalFee = convertUsdToBridgeToken(capitalFeeUsd); const estimatedRewards = useEstimatedRewards( - bridgeToken, + bridgeToken!, toChainId, isSwap || isUniversalSwap, parsedAmount, From 3be3d22fd78e5b2fd8527424a640929c995374d1 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 30 Oct 2025 16:19:06 +0200 Subject: [PATCH 098/122] preserve selected chain Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/components/ChainTokenSelector/Modal.tsx | 8 +++++--- .../components/ChainTokenSelector/SelectorButton.tsx | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx index 24400c2c1..af18562d8 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx @@ -63,6 +63,7 @@ type DisplayedTokens = { type Props = { onSelect: (token: EnrichedToken) => void; isOriginToken: boolean; + currentToken?: EnrichedToken | null; // The currently selected token we're changing from otherToken?: EnrichedToken | null; // The currently selected token on the other side displayModal: boolean; setDisplayModal: (displayModal: boolean) => void; @@ -73,13 +74,14 @@ export default function ChainTokenSelectorModal({ displayModal, setDisplayModal, onSelect, + currentToken, otherToken, }: Props) { const crossChainRoutes = useEnrichedCrosschainBalances(); const { isMobile } = useCurrentBreakpoint(); const [selectedChain, setSelectedChain] = useState( - popularChains[0] + currentToken?.chainId ?? popularChains[0] ); const [mobileStep, setMobileStep] = useState<"chain" | "token">("chain"); @@ -91,8 +93,8 @@ export default function ChainTokenSelectorModal({ setMobileStep("chain"); setChainSearch(""); setTokenSearch(""); - setSelectedChain(popularChains[0]); - }, [displayModal]); + setSelectedChain(currentToken?.chainId ?? popularChains[0]); + }, [displayModal, currentToken]); const displayedTokens = useMemo(() => { let tokens = selectedChain ? (crossChainRoutes[selectedChain] ?? []) : []; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index b21abf423..be3e6cdf5 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -55,6 +55,7 @@ export default function SelectorButton({ displayModal={displayModal} setDisplayModal={setDisplayModal} isOriginToken={isOriginToken} + currentToken={selectedToken} otherToken={otherToken} /> @@ -85,6 +86,7 @@ export default function SelectorButton({ displayModal={displayModal} setDisplayModal={setDisplayModal} isOriginToken={isOriginToken} + currentToken={selectedToken} otherToken={otherToken} /> From de20df4d070ae27193017c6bfe2d4cc9941d21fe Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 30 Oct 2025 16:49:16 +0200 Subject: [PATCH 099/122] fix confirmation button states Signed-off-by: Gerhard Steenkamp --- src/components/GlobalStyles/GlobalStyles.tsx | 5 +++ .../components/ConfirmationButton.tsx | 42 ++++++++++++------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/components/GlobalStyles/GlobalStyles.tsx b/src/components/GlobalStyles/GlobalStyles.tsx index 72af5947f..43d5626f4 100644 --- a/src/components/GlobalStyles/GlobalStyles.tsx +++ b/src/components/GlobalStyles/GlobalStyles.tsx @@ -241,8 +241,13 @@ const globalStyles = css` border: none; background: none; color: inherit; + } + button:not(:disabled) { cursor: pointer; } + button:disabled { + cursor: not-allowed; + } html, body { min-height: 100vh; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index fc8479425..f369080d2 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -1,5 +1,5 @@ "use client"; -import { ButtonHTMLAttributes } from "react"; +import { ButtonHTMLAttributes, useEffect } from "react"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; import { ReactComponent as LoadingIcon } from "assets/icons/loading-2.svg"; import { ReactComponent as Info } from "assets/icons/info.svg"; @@ -81,8 +81,22 @@ const ExpandableLabelSection: React.FC< // Render state-specific content let content: React.ReactNode = null; + const defaultState = ( + <> + + + Fast & Secure + + + Across V4. More Chains Faster. + + + ); + // Show validation messages for all non-ready states - if (quoteWarningMessage && state === "quoteError") { + if (state === "notConnected") { + content = defaultState; + } else if (quoteWarningMessage && state === "quoteError") { // Show quote warning message when ready to confirm but there's a warning content = ( <> @@ -92,7 +106,7 @@ const ExpandableLabelSection: React.FC< ); - } else if (state !== "readyToConfirm" && validationErrorFormatted) { + } else if (!hasQuote && !!validationErrorFormatted) { content = ( <> @@ -101,7 +115,7 @@ const ExpandableLabelSection: React.FC< ); - } else if (state === "readyToConfirm" && hasQuote) { + } else if (hasQuote) { // Only show quote details when ready to confirm content = ( <> @@ -127,17 +141,7 @@ const ExpandableLabelSection: React.FC< ); } else { // Default state - show Across V4 branding - content = ( - <> - - - Fast & Secure - - - Across V4. More Chains Faster. - - - ); + content = defaultState; } return ( @@ -146,7 +150,7 @@ const ExpandableLabelSection: React.FC< type="button" onClick={onToggle} aria-expanded={expanded} - disabled={state !== "readyToConfirm"} + disabled={!hasQuote} > {content} @@ -273,6 +277,12 @@ export const ConfirmationButton: React.FC = ({ // When notConnected, make button clickable so it can open wallet modal const isButtonDisabled = state === "notConnected" ? false : buttonDisabled; + useEffect(() => { + if (!swapQuote) { + setExpanded(false); + } + }, [swapQuote]); + // Render unified group driven by state const content = ( <> From 957e4f334dd4e6adad0c922075b16660ba6a85df Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 30 Oct 2025 17:24:35 +0200 Subject: [PATCH 100/122] allow quote with placeholder addresses Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/hooks/useSwapQuote.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index 06b18b786..c82b4ff16 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -6,6 +6,11 @@ import { SwapApprovalApiCallReturnType, SwapApprovalApiQueryParams, } from "utils/serverless-api/prod/swap-approval"; +import { chainIsSvm } from "utils/sdk"; + +// Placeholder addresses for quote simulation when wallet is not connected +const PLACEHOLDER_EVM_ADDRESS = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; +const PLACEHOLDER_SVM_ADDRESS = "FmMK62wrtWVb5SVoTZftSCGw3nEDA79hDbZNTRnC1R6t"; type SwapQuoteParams = { origin: SwapApiToken | null; @@ -30,7 +35,7 @@ const useSwapQuote = ({ refundAddress, depositor, refundOnOrigin = true, - slippageTolerance = 1, + slippageTolerance = 0.05, }: SwapQuoteParams) => { const { data, isLoading, error } = useQuery({ queryKey: [ @@ -46,21 +51,36 @@ const useSwapQuote = ({ if (Number(amount) <= 0) { return undefined; } - if (!origin || !destination || !amount || !depositor) { + if (!origin || !destination || !amount) { throw new Error("Missing required swap quote parameters"); } + // Use appropriate placeholder address based on chain ecosystem when wallet is not connected + const getPlaceholderAddress = (chainId: number) => { + return chainIsSvm(chainId) + ? PLACEHOLDER_SVM_ADDRESS + : PLACEHOLDER_EVM_ADDRESS; + }; + + const isUsingPlaceholderDepositor = !depositor; + const effectiveDepositor = + depositor || getPlaceholderAddress(origin.chainId); + const effectiveRecipient = + recipient || getPlaceholderAddress(destination.chainId); + const params: SwapApprovalApiQueryParams = { tradeType: isInputAmount ? "exactInput" : "minOutput", inputToken: origin.address, outputToken: destination.address, originChainId: origin.chainId, destinationChainId: destination.chainId, - depositor, - recipient: recipient || depositor, + depositor: effectiveDepositor, + recipient: effectiveRecipient, amount: amount.toString(), refundOnOrigin, slippageTolerance, + // Skip transaction estimation when using placeholder address + skipOriginTxEstimation: isUsingPlaceholderDepositor, ...(integratorId ? { integratorId } : {}), ...(refundAddress ? { refundAddress } : {}), }; @@ -68,8 +88,7 @@ const useSwapQuote = ({ const data = await swapApprovalApiCall(params); return data; }, - enabled: - !!origin?.address && !!destination?.address && !!amount && !!depositor, + enabled: !!origin?.address && !!destination?.address && !!amount, retry: 2, refetchInterval(query) { From 92c9b5a3114cee38eca6172733a9f59d1efd75bc Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 30 Oct 2025 18:46:42 +0200 Subject: [PATCH 101/122] add back prices cus I'm an idiot Signed-off-by: Gerhard Steenkamp --- src/constants/tokens.ts | 2 ++ src/hooks/useAvailableCrosschainRoutes.ts | 2 +- src/utils/token.ts | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 255fa0b91..731f5de32 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -41,6 +41,8 @@ export type TokenInfo = { addresses?: Record; // optional, if this is a stable coin isStable?: boolean; + // optional price in USD from swap tokens API + priceUsd?: string | null; }; export type TokenInfoList = TokenInfo[]; diff --git a/src/hooks/useAvailableCrosschainRoutes.ts b/src/hooks/useAvailableCrosschainRoutes.ts index 7b9a16c20..70dbcc6ce 100644 --- a/src/hooks/useAvailableCrosschainRoutes.ts +++ b/src/hooks/useAvailableCrosschainRoutes.ts @@ -51,7 +51,7 @@ export default function useAvailableCrosschainRoutes( symbol: token.symbol, decimals: token.decimals, logoURI: token.logoURI || "", - priceUSD: "0", // TokenInfo doesn't have price, would need to be enriched separately + priceUSD: token.priceUsd || "0", // Use price from SwapToken, fallback to "0" if not available coinKey: token.symbol, routeSource: "swap", }; diff --git a/src/utils/token.ts b/src/utils/token.ts index c0e24d7cd..1f7203690 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -197,6 +197,7 @@ export function swapTokenToTokenInfo(swapToken: SwapToken): TokenInfo { addresses: { [swapToken.chainId]: swapToken.address, }, + priceUsd: swapToken.priceUsd, }; // If we found a local token definition, merge in mainnetAddress and displaySymbol From d0f8cb1fc684a584205f8f1a5ac68aa052acdeb0 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 11:11:10 +0200 Subject: [PATCH 102/122] fix input colors. allow switching wiht empty input Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 6 +-- .../SwapAndBridge/components/InputForm.tsx | 47 +++++++++++-------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index f3a3d0533..74c5c62ba 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -157,10 +157,9 @@ export function useTokenInput({ setAmountString(a); } catch (e) { setAmountString("0"); - } finally { - setUnit("usd"); } } + setUnit("usd"); } else { // Convert USD amount to token string for display if (amountString && token && convertedAmount) { @@ -170,10 +169,9 @@ export function useTokenInput({ setAmountString(a); } catch (e) { setAmountString("0"); - } finally { - setUnit("token"); } } + setUnit("token"); } }, [unit, amountString, token, convertedAmount, setUnit]); diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 7dd9b7f7a..1822ffd03 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -217,14 +217,10 @@ const TokenInput = ({ error={insufficientInputBalance} /> - - - - {" "} - Value: {formattedConvertedAmount} - - - + + {" "} + Value: {formattedConvertedAmount} + + ${({ showPrefix, value, error }) => showPrefix && ` &::before { @@ -336,6 +341,8 @@ const TokenAmountInputWrapper = styled.div<{ font-weight: 300; line-height: 120%; letter-spacing: -1.92px; + color: ${error ? COLORS.error : value ? COLORS.aqua : "var(--base-bright-gray, #e0f3ff)"}; + opacity: ${value ? 1 : 0.5}; } `} `; @@ -351,22 +358,22 @@ const TokenAmountInput = styled.input<{ font: inherit; font-size: inherit; color: ${({ value, error }) => - error ? COLORS.error : value ? COLORS.aqua : COLORS["light-200"]}; + error + ? COLORS.error + : value + ? COLORS.aqua + : "var(--base-bright-gray, #e0f3ff)"}; flex-shrink: 0; &:focus { font-size: 48px; outline: none; } -`; -const TokenAmountInputEstimatedUsd = styled.div` - color: ${COLORS["light-200"]}; - font-family: Barlow; - font-size: 14px; - font-weight: 400; - line-height: 130%; - opacity: 0.5; + &::placeholder { + color: var(--base-bright-gray, #e0f3ff); + opacity: 0.5; + } `; const TokenInputWrapper = styled.div` From 017fbcf5212dfdf515e647abae52050f2d57b29c Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 11:31:08 +0200 Subject: [PATCH 103/122] fix button states and styling errors Signed-off-by: Gerhard Steenkamp --- .../components/BalanceSelector.tsx | 15 ++-- .../components/ConfirmationButton.tsx | 11 +-- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 3 +- .../hooks/useValidateSwapAndBridge.ts | 69 ++++++++++--------- 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index 452e03a99..d7c4649f1 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -99,14 +99,9 @@ export function BalanceSelector({ delay: (percentages.length - 1 - index) * 0.07, }} whileHover={{ - scale: 1.05, + scale: 1.06, backgroundColor: "rgba(224, 243, 255, 0.1)", color: "#E0F3FF", - transition: { - type: "spring", - stiffness: 400, - damping: 28, - }, }} onClick={() => handlePillClick(percentage)} > @@ -133,16 +128,16 @@ const BalanceWrapper = styled.div` `; const BalanceText = styled.div<{ error?: boolean }>` - color: ${({ error }) => (error ? COLORS.error : COLORS.white)}; + color: ${({ error }) => + error ? COLORS.error : "var(--Base-bright-gray, #E0F3FF)"}; opacity: 1; font-size: 14px; - font-weight: 600; + font-weight: 400; line-height: 130%; span { opacity: 0.5; - color: ${COLORS.white}; - font-weight: 400; + color: var(--Base-bright-gray, #e0f3ff); } `; diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index f369080d2..39c86283a 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -94,9 +94,8 @@ const ExpandableLabelSection: React.FC< ); // Show validation messages for all non-ready states - if (state === "notConnected") { - content = defaultState; - } else if (quoteWarningMessage && state === "quoteError") { + // Prioritize showing quote if available, even when wallet is not connected + if (quoteWarningMessage && state === "quoteError") { // Show quote warning message when ready to confirm but there's a warning content = ( <> @@ -116,7 +115,7 @@ const ExpandableLabelSection: React.FC< ); } else if (hasQuote) { - // Only show quote details when ready to confirm + // Show quote details when available, regardless of connection state content = ( <> @@ -125,6 +124,8 @@ const ExpandableLabelSection: React.FC< {!expanded && ( + + {fee} @@ -139,6 +140,8 @@ const ExpandableLabelSection: React.FC< ); + } else if (state === "notConnected") { + content = defaultState; } else { // Default state - show Across V4 branding content = defaultState; diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index fd352e40c..d810b6300 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -180,7 +180,8 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { amount, isAmountOrigin, inputToken, - outputToken + outputToken, + isOriginConnected ); const expectedInputAmount = useMemo(() => { diff --git a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts index f9376482a..6f9341ae9 100644 --- a/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useValidateSwapAndBridge.ts @@ -15,43 +15,48 @@ export function useValidateSwapAndBridge( amount: BigNumber | null, isAmountOrigin: boolean, inputToken: EnrichedToken | null, - outputToken: EnrichedToken | null + outputToken: EnrichedToken | null, + isConnected: boolean ): ValidationResult { const validation = useMemo(() => { let errorType: AmountInputError | undefined = undefined; - // Check if input token is selected - if (!inputToken) { - errorType = AmountInputError.NO_INPUT_TOKEN_SELECTED; - } - // Check if output token is selected - else if (!outputToken) { - errorType = AmountInputError.NO_OUTPUT_TOKEN_SELECTED; - } - // Check if amount is entered - else if (!amount || amount.isZero()) { - errorType = AmountInputError.NO_AMOUNT_ENTERED; - } - // invalid amount - else if (amount.lte(0)) { - errorType = AmountInputError.INVALID; - } - // balance check for origin-side inputs - else if (isAmountOrigin && inputToken?.balance) { - if (amount.gt(inputToken.balance)) { - errorType = AmountInputError.INSUFFICIENT_BALANCE; + // Only validate if wallet is connected + if (isConnected) { + // Check if input token is selected + if (!inputToken) { + errorType = AmountInputError.NO_INPUT_TOKEN_SELECTED; + } + // Check if output token is selected + else if (!outputToken) { + errorType = AmountInputError.NO_OUTPUT_TOKEN_SELECTED; } + // Check if amount is entered + else if (!amount || amount.isZero()) { + errorType = AmountInputError.NO_AMOUNT_ENTERED; + } + // invalid amount + else if (amount.lte(0)) { + errorType = AmountInputError.INVALID; + } + // balance check for origin-side inputs + else if (isAmountOrigin && inputToken?.balance) { + if (amount.gt(inputToken.balance)) { + errorType = AmountInputError.INSUFFICIENT_BALANCE; + } + } + // // backend availability + // if (!errorType && error && axios.isAxiosError(error)) { + // const axiosError = error as AxiosError; + // const code = axiosError.response?.data?.code; + // if (code === "AMOUNT_TOO_LOW") { + // errorType = AmountInputError.AMOUNT_TOO_LOW; + // } else if (code === "SWAP_QUOTE_UNAVAILABLE") { + // errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; + // } + // } } - // // backend availability - // if (!errorType && error && axios.isAxiosError(error)) { - // const axiosError = error as AxiosError; - // const code = axiosError.response?.data?.code; - // if (code === "AMOUNT_TOO_LOW") { - // errorType = AmountInputError.AMOUNT_TOO_LOW; - // } else if (code === "SWAP_QUOTE_UNAVAILABLE") { - // errorType = AmountInputError.SWAP_QUOTE_UNAVAILABLE; - // } - // } + return { error: errorType, warn: undefined as AmountInputError | undefined, @@ -61,7 +66,7 @@ export function useValidateSwapAndBridge( outputToken, }), }; - }, [amount, isAmountOrigin, inputToken, outputToken]); + }, [isConnected, inputToken, outputToken, amount, isAmountOrigin]); return validation; } From 3f2367b316b39225fabfe678dc423a64fde0cb5b Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 15:33:56 +0200 Subject: [PATCH 104/122] fix hover effect Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/components/InputForm.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/views/SwapAndBridge/components/InputForm.tsx b/src/views/SwapAndBridge/components/InputForm.tsx index 1822ffd03..3ddd4add2 100644 --- a/src/views/SwapAndBridge/components/InputForm.tsx +++ b/src/views/SwapAndBridge/components/InputForm.tsx @@ -1,4 +1,4 @@ -import { COLORS, formatUSD } from "utils"; +import { COLORS, formatUSD, withOpacity } from "utils"; import SelectorButton from "./ChainTokenSelector/SelectorButton"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; import { BalanceSelector } from "./BalanceSelector"; @@ -268,15 +268,16 @@ const ValueRow = styled.div` `; const UnitToggleButton = styled.button` - color: var(--base-bright-gray, #e0f3ff); - opacity: 0.5; + color: ${withOpacity("#e0f3ff", 0.5)}; display: inline-flex; align-items: center; gap: 4px; &:hover:not(:disabled) { - opacity: 1; + svg { + color: #e0f3ff; + } } span { From aeebe6c731f909d4a6fa37e9458794387f7d2a77 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 15:59:08 +0200 Subject: [PATCH 105/122] fixup button colors Signed-off-by: Gerhard Steenkamp --- .../SwapAndBridge/components/ConfirmationButton.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 39c86283a..01ac20739 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -362,7 +362,11 @@ export const ConfirmationButton: React.FC = ({ ); - return {content}; + return ( + + {content} + + ); }; const ValidationText = styled.div` @@ -376,9 +380,9 @@ const ValidationText = styled.div` `; // Styled components -const Container = styled(motion.div)<{ disabled: boolean }>` - background: ${({ disabled }) => - disabled ? COLORS["grey-400-5"] : "rgba(108, 249, 216, 0.1)"}; +const Container = styled(motion.div)<{ dark: boolean }>` + background: ${({ dark }) => + dark ? COLORS["grey-400-5"] : "rgba(108, 249, 216, 0.1)"}; border-radius: 24px; display: flex; flex-direction: column; From 19a83dd1efac1e85a52d36868deec7985abc435f Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 17:34:30 +0200 Subject: [PATCH 106/122] consolidate input validation messages Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 42 ++++--------------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 26 +++++++----- src/views/SwapAndBridge/index.tsx | 5 --- 3 files changed, 25 insertions(+), 48 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 01ac20739..b2de572bb 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -17,14 +17,11 @@ import { BigNumber } from "ethers"; import { COLORS, formatUSDString, isDefined } from "utils"; import { EnrichedToken } from "./ChainTokenSelector/Modal"; import styled from "@emotion/styled"; -import { AmountInputError } from "../../Bridge/utils"; import { Tooltip } from "components/Tooltip"; import { SwapApprovalApiCallReturnType } from "utils/serverless-api/prod/swap-approval"; export type BridgeButtonState = | "notConnected" - | "awaitingTokenSelection" - | "awaitingAmountInput" | "readyToConfirm" | "submitting" | "wrongNetwork" @@ -41,9 +38,6 @@ interface ConfirmationButtonProps isQuoteLoading: boolean; onConfirm?: () => Promise; quoteWarningMessage: string | null; - validationError?: AmountInputError; - validationWarning?: AmountInputError; - validationErrorFormatted?: string; // External state props buttonState: BridgeButtonState; buttonDisabled: boolean; @@ -61,9 +55,6 @@ const ExpandableLabelSection: React.FC< visible: boolean; state: BridgeButtonState; hasQuote: boolean; - validationError?: AmountInputError; - validationWarning?: AmountInputError; - validationErrorFormatted?: string; quoteWarningMessage?: string | null; }> > = ({ @@ -74,8 +65,6 @@ const ExpandableLabelSection: React.FC< state, children, hasQuote, - validationError, - validationErrorFormatted, quoteWarningMessage, }) => { // Render state-specific content @@ -93,10 +82,8 @@ const ExpandableLabelSection: React.FC< ); - // Show validation messages for all non-ready states - // Prioritize showing quote if available, even when wallet is not connected + // Show quote warning message for quoteError state if (quoteWarningMessage && state === "quoteError") { - // Show quote warning message when ready to confirm but there's a warning content = ( <> @@ -105,17 +92,8 @@ const ExpandableLabelSection: React.FC< ); - } else if (!hasQuote && !!validationErrorFormatted) { - content = ( - <> - - - {validationErrorFormatted} - - - ); } else if (hasQuote) { - // Show quote details when available, regardless of connection state + // Show quote details when available content = ( <> @@ -140,8 +118,6 @@ const ExpandableLabelSection: React.FC< ); - } else if (state === "notConnected") { - content = defaultState; } else { // Default state - show Across V4 branding content = defaultState; @@ -223,9 +199,6 @@ export const ConfirmationButton: React.FC = ({ swapQuote, onConfirm, quoteWarningMessage, - validationError, - validationWarning, - validationErrorFormatted, buttonState, buttonDisabled, buttonLoading, @@ -296,9 +269,6 @@ export const ConfirmationButton: React.FC = ({ onToggle={() => setExpanded((e) => !e)} visible={true} state={state} - validationError={validationError} - validationWarning={validationWarning} - validationErrorFormatted={validationErrorFormatted} quoteWarningMessage={quoteWarningMessage} hasQuote={!!swapQuote} > @@ -363,7 +333,13 @@ export const ConfirmationButton: React.FC = ({ ); return ( - + {content} ); diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index d810b6300..44c279fa5 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -240,13 +240,11 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { // Button state logic const buttonState: BridgeButtonState = useMemo(() => { + if (approvalAction.isButtonActionLoading) return "submitting"; if (isQuoteLoading) return "loadingQuote"; if (quoteError) return "quoteError"; - if (!isOriginConnected || !isRecipientSet) return "notConnected"; - if (approvalAction.isButtonActionLoading) return "submitting"; - if (!inputToken || !outputToken) return "awaitingTokenSelection"; - if (!amount || amount.lte(0)) return "awaitingAmountInput"; if (validation.error) return "validationError"; + if (!isOriginConnected || !isRecipientSet) return "notConnected"; return "readyToConfirm"; }, [ isQuoteLoading, @@ -254,9 +252,6 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { isOriginConnected, isRecipientSet, approvalAction.isButtonActionLoading, - inputToken, - outputToken, - amount, validation.error, ]); @@ -265,6 +260,11 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { }, [buttonState]); const buttonLabel = useMemo(() => { + // Show validation error in button label if present + if (validation.errorFormatted && buttonState === "validationError") { + return validation.errorFormatted; + } + if (buttonState === "notConnected" && walletTypeToConnect) { // If neither wallet is connected, show generic "Connect Wallet" if (!isConnectedEVM && !isConnectedSVM) { @@ -276,7 +276,15 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { : "Connect SVM Wallet"; } return buttonLabels[buttonState]; - }, [buttonState, walletTypeToConnect, isConnectedEVM, isConnectedSVM]); + }, [ + buttonState, + walletTypeToConnect, + isConnectedEVM, + isConnectedSVM, + validation.errorFormatted, + ]); + + console.log("validation.errorFormatted", validation.errorFormatted); const buttonDisabled = useMemo( () => @@ -341,8 +349,6 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const buttonLabels: Record = { notConnected: "Connect Wallet", - awaitingTokenSelection: "Confirm Swap", - awaitingAmountInput: "Confirm Swap", readyToConfirm: "Confirm Swap", quoteError: "Confirm Swap", submitting: "Confirming...", diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 31f2cb9da..3b5567410 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -19,8 +19,6 @@ export default function SwapAndBridge() { expectedInputAmount, expectedOutputAmount, validationError, - validationWarning, - validationErrorFormatted, buttonState, buttonDisabled, buttonLoading, @@ -57,9 +55,6 @@ export default function SwapAndBridge() { isQuoteLoading={isQuoteLoading} onConfirm={onConfirm} quoteWarningMessage={quoteWarningMessage} - validationError={validationError} - validationWarning={validationWarning} - validationErrorFormatted={validationErrorFormatted} buttonState={buttonState} buttonDisabled={buttonDisabled} buttonLoading={buttonLoading} From 11e6bff296e5bd9406b915b763a520e939fdd391 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 17:47:30 +0200 Subject: [PATCH 107/122] consolidate api errors into the button itself Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 45 ++++--------------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 18 +++++--- src/views/SwapAndBridge/index.tsx | 2 - 3 files changed, 20 insertions(+), 45 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index b2de572bb..76199f383 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -27,7 +27,7 @@ export type BridgeButtonState = | "wrongNetwork" | "loadingQuote" | "validationError" - | "quoteError"; + | "apiError"; interface ConfirmationButtonProps extends ButtonHTMLAttributes { @@ -37,7 +37,6 @@ interface ConfirmationButtonProps swapQuote: SwapApprovalApiCallReturnType | null; isQuoteLoading: boolean; onConfirm?: () => Promise; - quoteWarningMessage: string | null; // External state props buttonState: BridgeButtonState; buttonDisabled: boolean; @@ -55,18 +54,8 @@ const ExpandableLabelSection: React.FC< visible: boolean; state: BridgeButtonState; hasQuote: boolean; - quoteWarningMessage?: string | null; }> -> = ({ - fee, - time, - expanded, - onToggle, - state, - children, - hasQuote, - quoteWarningMessage, -}) => { +> = ({ fee, time, expanded, onToggle, state, children, hasQuote }) => { // Render state-specific content let content: React.ReactNode = null; @@ -82,17 +71,8 @@ const ExpandableLabelSection: React.FC< ); - // Show quote warning message for quoteError state - if (quoteWarningMessage && state === "quoteError") { - content = ( - <> - - - {quoteWarningMessage} - - - ); - } else if (hasQuote) { + // Show quote breakdown when quote is available, otherwise show default state + if (hasQuote) { // Show quote details when available content = ( <> @@ -187,6 +167,9 @@ const ButtonCore: React.FC<{ {state === "notConnected" && ( )} + {(state === "apiError" || state === "validationError") && ( + + )} {label} @@ -198,7 +181,6 @@ export const ConfirmationButton: React.FC = ({ amount, swapQuote, onConfirm, - quoteWarningMessage, buttonState, buttonDisabled, buttonLoading, @@ -269,7 +251,6 @@ export const ConfirmationButton: React.FC = ({ onToggle={() => setExpanded((e) => !e)} visible={true} state={state} - quoteWarningMessage={quoteWarningMessage} hasQuote={!!swapQuote} > @@ -336,7 +317,7 @@ export const ConfirmationButton: React.FC = ({ @@ -345,16 +326,6 @@ export const ConfirmationButton: React.FC = ({ ); }; -const ValidationText = styled.div` - color: ${COLORS.white}; - font-size: 14px; - font-weight: 400; - margin-inline: auto; - display: flex; - align-items: center; - gap: 4px; -`; - // Styled components const Container = styled(motion.div)<{ dark: boolean }>` background: ${({ dark }) => diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 44c279fa5..06c2a3152 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -242,7 +242,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const buttonState: BridgeButtonState = useMemo(() => { if (approvalAction.isButtonActionLoading) return "submitting"; if (isQuoteLoading) return "loadingQuote"; - if (quoteError) return "quoteError"; + if (quoteError) return "apiError"; if (validation.error) return "validationError"; if (!isOriginConnected || !isRecipientSet) return "notConnected"; return "readyToConfirm"; @@ -259,12 +259,21 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { return buttonState === "loadingQuote" || buttonState === "submitting"; }, [buttonState]); + const quoteWarningMessage = useMemo(() => { + return getQuoteWarningMessage(quoteError); + }, [quoteError]); + const buttonLabel = useMemo(() => { // Show validation error in button label if present if (validation.errorFormatted && buttonState === "validationError") { return validation.errorFormatted; } + // Show API error in button label if present + if (quoteWarningMessage && buttonState === "apiError") { + return quoteWarningMessage; + } + if (buttonState === "notConnected" && walletTypeToConnect) { // If neither wallet is connected, show generic "Connect Wallet" if (!isConnectedEVM && !isConnectedSVM) { @@ -282,6 +291,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { isConnectedEVM, isConnectedSVM, validation.errorFormatted, + quoteWarningMessage, ]); console.log("validation.errorFormatted", validation.errorFormatted); @@ -303,10 +313,6 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { ] ); - const quoteWarningMessage = useMemo(() => { - return getQuoteWarningMessage(quoteError); - }, [quoteError]); - return { inputToken, outputToken, @@ -350,7 +356,7 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { const buttonLabels: Record = { notConnected: "Connect Wallet", readyToConfirm: "Confirm Swap", - quoteError: "Confirm Swap", + apiError: "Confirm Swap", submitting: "Confirming...", wrongNetwork: "Confirm Swap", loadingQuote: "Finalizing quote...", diff --git a/src/views/SwapAndBridge/index.tsx b/src/views/SwapAndBridge/index.tsx index 3b5567410..a77e0d9d7 100644 --- a/src/views/SwapAndBridge/index.tsx +++ b/src/views/SwapAndBridge/index.tsx @@ -24,7 +24,6 @@ export default function SwapAndBridge() { buttonLoading, buttonLabel, onConfirm, - quoteWarningMessage, destinationChainEcosystem, toAccountManagement, } = useSwapAndBridge(); @@ -54,7 +53,6 @@ export default function SwapAndBridge() { swapQuote={swapQuote || null} isQuoteLoading={isQuoteLoading} onConfirm={onConfirm} - quoteWarningMessage={quoteWarningMessage} buttonState={buttonState} buttonDisabled={buttonDisabled} buttonLoading={buttonLoading} From 2624b0e1e9344dfae0ad4071642585e948540ef2 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Fri, 31 Oct 2025 17:51:24 +0200 Subject: [PATCH 108/122] fixup Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/components/ConfirmationButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index 76199f383..3a15ab5a1 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -167,7 +167,7 @@ const ButtonCore: React.FC<{ {state === "notConnected" && ( )} - {(state === "apiError" || state === "validationError") && ( + {state === "apiError" && ( )} {label} From 82c948a91041ac15bdad4586fc0ab42310069ff0 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 3 Nov 2025 15:39:25 +0200 Subject: [PATCH 109/122] better balance caching. refetch on fill Signed-off-by: Gerhard Steenkamp --- api/user-token-balances/index.ts | 4 +- src/hooks/useUserTokenBalances.ts | 6 +- .../DepositStatus/hooks/useDepositTracking.ts | 60 ++++++++++++------- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 2 - 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/api/user-token-balances/index.ts b/api/user-token-balances/index.ts index a7ccbadd0..d79cea396 100644 --- a/api/user-token-balances/index.ts +++ b/api/user-token-balances/index.ts @@ -49,8 +49,8 @@ const handler = async ( body: responseData, statusCode: 200, requestId, - cacheSeconds: 60 * 3, - staleWhileRevalidateSeconds: 60, + cacheSeconds: 10, // 30 seconds cache - balances update frequently after transactions + staleWhileRevalidateSeconds: 10, }); } catch (error: unknown) { return handleErrorCondition( diff --git a/src/hooks/useUserTokenBalances.ts b/src/hooks/useUserTokenBalances.ts index 666bb1797..7195a93ad 100644 --- a/src/hooks/useUserTokenBalances.ts +++ b/src/hooks/useUserTokenBalances.ts @@ -12,7 +12,7 @@ export function useUserTokenBalances() { const svmAccountString = svmAccount?.toString(); return useQuery({ - queryKey: ["userTokenBalances", evmAccount, svmAccountString], + queryKey: makeUseUserTokenBalancesQueryKey([evmAccount, svmAccountString]), queryFn: async (): Promise => { // Fetch balances for both accounts if they exist const promises: Promise[] = []; @@ -48,3 +48,7 @@ export function useUserTokenBalances() { staleTime: 3 * 60 * 1000, // Consider data stale after 3 minutes }); } + +export const makeUseUserTokenBalancesQueryKey = ( + keys?: Parameters[0]["queryKey"] +) => ["userTokenBalances", ...(keys ? keys : [])]; diff --git a/src/views/DepositStatus/hooks/useDepositTracking.ts b/src/views/DepositStatus/hooks/useDepositTracking.ts index fb208ba9e..7c49b505a 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking.ts @@ -1,4 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useState, useEffect, useMemo } from "react"; import { BigNumber } from "ethers"; @@ -25,6 +25,7 @@ import { DepositData } from "./useDepositTracking/types"; import { useConnectionSVM } from "hooks/useConnectionSVM"; import { useConnectionEVM } from "hooks/useConnectionEVM"; import { useToken } from "hooks/useToken"; +import { makeUseUserTokenBalancesQueryKey } from "hooks/useUserTokenBalances"; /** * Hook to track deposit and fill status across EVM and SVM chains @@ -48,6 +49,7 @@ export function useDepositTracking({ }) { const [shouldRetryDepositQuery, setShouldRetryDepositQuery] = useState(true); + const queryClient = useQueryClient(); const { addToAmpliQueue } = useAmplitude(); const { account: accountEVM } = useConnectionEVM(); const { account: accountSVM } = useConnectionSVM(); @@ -172,9 +174,14 @@ export function useDepositTracking({ useEffect(() => { const fillInfo = fillQuery.data; - if (!fromBridgePagePayload || !fillInfo || fillInfo.status === "filling") { + if (!fillInfo || fillInfo.status === "filling") { return; } + // Refetch user balances + queryClient.refetchQueries({ + queryKey: makeUseUserTokenBalancesQueryKey(), + type: "all", // Refetch both active and inactive queries + }); // Remove existing deposit and add updated one with fill information const localDepositByTxHash = getLocalDepositByTxHash(depositTxHash); @@ -182,29 +189,31 @@ export function useDepositTracking({ removeLocalDeposits([depositTxHash]); } - // Add to local storage with fill information - // Use the strategy-specific conversion method - const localDeposit = fillStrategy.convertForFillQuery( - fillInfo, - fromBridgePagePayload - ); - addLocalDeposit(localDeposit); + if (fromBridgePagePayload) { + // Add to local storage with fill information + // Use the strategy-specific conversion method + const localDeposit = fillStrategy.convertForFillQuery( + fillInfo, + fromBridgePagePayload + ); + addLocalDeposit(localDeposit); - // Record transfer properties - const { quoteForAnalytics, depositArgs, tokenPrice } = - fromBridgePagePayload; + // Record transfer properties + const { quoteForAnalytics, depositArgs, tokenPrice } = + fromBridgePagePayload; - // Only record if we have token info - if (tokenForAnalytics) { - recordTransferUserProperties( - BigNumber.from(depositArgs.amount), - BigNumber.from(tokenPrice), - tokenForAnalytics.decimals, - quoteForAnalytics.tokenSymbol.toLowerCase(), - Number(quoteForAnalytics.fromChainId), - Number(quoteForAnalytics.toChainId), - quoteForAnalytics.fromChainName - ); + // Only record if we have token info + if (tokenForAnalytics) { + recordTransferUserProperties( + BigNumber.from(depositArgs.amount), + BigNumber.from(tokenPrice), + tokenForAnalytics.decimals, + quoteForAnalytics.tokenSymbol.toLowerCase(), + Number(quoteForAnalytics.fromChainId), + Number(quoteForAnalytics.toChainId), + quoteForAnalytics.fromChainName + ); + } } }, [ fillQuery.data, @@ -212,6 +221,11 @@ export function useDepositTracking({ fromBridgePagePayload, fillStrategy, tokenForAnalytics, + queryClient, + fromChainId, + toChainId, + accountSVM, + accountEVM, ]); const status: DepositStatus = !depositQuery.data?.depositTimestamp diff --git a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts index 06c2a3152..59c47452e 100644 --- a/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts +++ b/src/views/SwapAndBridge/hooks/useSwapAndBridge.ts @@ -294,8 +294,6 @@ export function useSwapAndBridge(): UseSwapAndBridgeReturn { quoteWarningMessage, ]); - console.log("validation.errorFormatted", validation.errorFormatted); - const buttonDisabled = useMemo( () => approvalAction.buttonDisabled || From 8121eea3eb895e164df9b6cf88365bf93aecd47a Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 3 Nov 2025 16:43:24 +0200 Subject: [PATCH 110/122] fix fallback image trigger Signed-off-by: Gerhard Steenkamp --- src/components/TokenImage/TokenImage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/TokenImage/TokenImage.tsx b/src/components/TokenImage/TokenImage.tsx index e4b5f6927..a93ced743 100644 --- a/src/components/TokenImage/TokenImage.tsx +++ b/src/components/TokenImage/TokenImage.tsx @@ -1,4 +1,4 @@ -import React, { useState, ImgHTMLAttributes } from "react"; +import { useState, useEffect, ImgHTMLAttributes } from "react"; import fallbackLogo from "assets/token-logos/fallback.svg"; type TokenImageProps = ImgHTMLAttributes & { @@ -13,6 +13,11 @@ type TokenImageProps = ImgHTMLAttributes & { export function TokenImage({ src, alt, ...props }: TokenImageProps) { const [imageError, setImageError] = useState(false); + // Reset error state when src changes + useEffect(() => { + setImageError(false); + }, [src]); + const imageSrc = !src || imageError ? fallbackLogo : src; return ( From 68987e6cb1da98e53529b7b06f5e168635e4dbde Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 4 Nov 2025 14:26:06 +0200 Subject: [PATCH 111/122] reset if unreachable. add warning Signed-off-by: Gerhard Steenkamp --- src/assets/icons/warning_triangle.svg | 4 +- src/assets/icons/warning_triangle_filled.svg | 3 + src/components/Dialogs/Dialog.tsx | 2 +- src/hooks/useTokenInput.ts | 2 +- ...{Modal.tsx => ChainTokenSelectorModal.tsx} | 136 +++++++++++++++--- .../ChainTokenSelector/SelectorButton.tsx | 9 +- .../components/ConfirmationButton.tsx | 4 +- .../SwapAndBridge/components/InputForm.tsx | 7 +- .../SwapAndBridge/components/QuoteWarning.tsx | 2 +- .../SwapAndBridge/hooks/useDefaultRoute.ts | 2 +- .../SwapAndBridge/hooks/useSwapAndBridge.ts | 2 +- .../hooks/useValidateSwapAndBridge.ts | 2 +- 12 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 src/assets/icons/warning_triangle_filled.svg rename src/views/SwapAndBridge/components/ChainTokenSelector/{Modal.tsx => ChainTokenSelectorModal.tsx} (89%) diff --git a/src/assets/icons/warning_triangle.svg b/src/assets/icons/warning_triangle.svg index 762ee642c..4cd75e33f 100644 --- a/src/assets/icons/warning_triangle.svg +++ b/src/assets/icons/warning_triangle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/warning_triangle_filled.svg b/src/assets/icons/warning_triangle_filled.svg new file mode 100644 index 000000000..762ee642c --- /dev/null +++ b/src/assets/icons/warning_triangle_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Dialogs/Dialog.tsx b/src/components/Dialogs/Dialog.tsx index d1f894736..d5d7a3b30 100644 --- a/src/components/Dialogs/Dialog.tsx +++ b/src/components/Dialogs/Dialog.tsx @@ -2,7 +2,7 @@ import Modal from "components/Modal"; import { ModalProps } from "components/Modal/Modal"; import styled from "@emotion/styled"; import { COLORS, QUERIES, withOpacity } from "utils"; -import { ReactComponent as Warning } from "assets/icons/warning_triangle.svg"; +import { ReactComponent as Warning } from "assets/icons/warning_triangle_filled.svg"; import { ReactComponent as Siren } from "assets/icons/siren.svg"; import { ReactComponent as Info } from "assets/icons/info.svg"; import { PropsWithChildren } from "react"; diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 74c5c62ba..755ac780f 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { BigNumber, utils } from "ethers"; import { convertTokenToUSD, convertUSDToToken } from "utils"; -import { EnrichedToken } from "views/SwapAndBridge/components/ChainTokenSelector/Modal"; +import { EnrichedToken } from "views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal"; import { formatUnitsWithMaxFractions } from "utils"; export type UnitType = "usd" | "token"; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx similarity index 89% rename from src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx rename to src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx index af18562d8..fecfc07db 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/Modal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx @@ -17,6 +17,7 @@ import { useMemo, useState, useEffect, useRef } from "react"; import { ReactComponent as CheckmarkCircleFilled } from "assets/icons/checkmark-circle-filled.svg"; import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; import { ReactComponent as SearchResults } from "assets/icons/search_results.svg"; +import { ReactComponent as WarningIcon } from "assets/icons/warning_triangle.svg"; import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; import { useEnrichedCrosschainBalances } from "hooks/useEnrichedCrosschainBalances"; import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; @@ -55,13 +56,18 @@ export type EnrichedToken = LifiToken & { routeSource: "bridge" | "swap"; }; +type EnrichedTokenWithReachability = EnrichedToken & { + isUnreachable: boolean; +}; + type DisplayedTokens = { - popular: EnrichedToken[]; - all: EnrichedToken[]; + popular: EnrichedTokenWithReachability[]; + all: EnrichedTokenWithReachability[]; }; type Props = { onSelect: (token: EnrichedToken) => void; + onSelectOtherToken?: (token: EnrichedToken | null) => void; // Callback to reset the other selector isOriginToken: boolean; currentToken?: EnrichedToken | null; // The currently selected token we're changing from otherToken?: EnrichedToken | null; // The currently selected token on the other side @@ -69,11 +75,12 @@ type Props = { setDisplayModal: (displayModal: boolean) => void; }; -export default function ChainTokenSelectorModal({ +export function ChainTokenSelectorModal({ isOriginToken, displayModal, setDisplayModal, onSelect, + onSelectOtherToken, currentToken, otherToken, }: Props) { @@ -103,16 +110,22 @@ export default function ChainTokenSelectorModal({ tokens = Object.values(crossChainRoutes).flatMap((t) => t); } - // Enrich tokens with route source information + // Enrich tokens with route source information and unreachable flag const enrichedTokens = tokens.map((token) => { // Find the corresponding token in crossChainRoutes to get route source const routeToken = crossChainRoutes?.[token.chainId]?.find( (rt) => rt.address.toLowerCase() === token.address.toLowerCase() ); + // Token is unreachable if otherToken exists and is from the same chain + const isUnreachable = otherToken + ? token.chainId === otherToken.chainId + : false; + return { ...token, routeSource: routeToken?.routeSource || "bridge", // Default to bridge if not found + isUnreachable, }; }); @@ -136,7 +149,7 @@ export default function ChainTokenSelectorModal({ }); // Sort function that prioritizes tokens with balance, then by balance amount, then alphabetically - const sortTokens = (tokens: EnrichedToken[]) => { + const sortTokens = (tokens: EnrichedTokenWithReachability[]) => { return tokens.sort((a, b) => { // Sort by balance - tokens with balance go to top const aHasBalance = a.balance.gt(0) && a.balanceUsd > 0.01; @@ -206,8 +219,7 @@ export default function ChainTokenSelectorModal({ .map((chainInfo) => { return { ...chainInfo, - isDisabled: - otherToken && Number(chainInfo.chainId) === otherToken.chainId, // same chain can't be both input and output + isDisabled: false, // No longer disable chains at chain level }; }); @@ -253,6 +265,7 @@ export default function ChainTokenSelectorModal({ setMobileStep("token"); }} onTokenSelect={onSelect} + onSelectOtherToken={onSelectOtherToken} /> ) : ( ); } @@ -288,6 +302,7 @@ const MobileModal = ({ displayedTokens, onChainSelect, onTokenSelect, + onSelectOtherToken, }: { isOriginToken: boolean; displayModal: boolean; @@ -303,6 +318,7 @@ const MobileModal = ({ displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedToken) => void; + onSelectOtherToken?: (token: EnrichedToken | null) => void; }) => { return ( setDisplayModal(false)} /> @@ -366,6 +384,7 @@ const DesktopModal = ({ displayedTokens, onChainSelect, onTokenSelect, + onSelectOtherToken, }: { isOriginToken: boolean; displayModal: boolean; @@ -379,6 +398,7 @@ const DesktopModal = ({ displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedToken) => void; + onSelectOtherToken?: (token: EnrichedToken | null) => void; }) => { return ( setDisplayModal(false)} /> @@ -412,6 +434,7 @@ const DesktopModal = ({ // Mobile Layout Component - 2-step process const MobileLayout = ({ + isOriginToken, mobileStep, selectedChain, chainSearch, @@ -422,8 +445,10 @@ const MobileLayout = ({ displayedTokens, onChainSelect, onTokenSelect, + onSelectOtherToken, onModalClose, }: { + isOriginToken: boolean; mobileStep: "chain" | "token"; selectedChain: number | null; chainSearch: string; @@ -434,6 +459,7 @@ const MobileLayout = ({ displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedToken) => void; + onSelectOtherToken?: (token: EnrichedToken | null) => void; onModalClose: () => void; }) => { const chainSearchInputRef = useRef(null); @@ -521,8 +547,12 @@ const MobileLayout = ({ { + if (token.isUnreachable && onSelectOtherToken) { + onSelectOtherToken(null); + } onTokenSelect(token); onModalClose(); }} @@ -540,8 +570,12 @@ const MobileLayout = ({ { + if (token.isUnreachable && onSelectOtherToken) { + onSelectOtherToken(null); + } onTokenSelect(token); onModalClose(); }} @@ -561,6 +595,7 @@ const MobileLayout = ({ // Desktop Layout Component - Side-by-side columns const DesktopLayout = ({ + isOriginToken, selectedChain, chainSearch, setChainSearch, @@ -570,8 +605,10 @@ const DesktopLayout = ({ displayedTokens, onChainSelect, onTokenSelect, + onSelectOtherToken, onModalClose, }: { + isOriginToken: boolean; selectedChain: number | null; chainSearch: string; setChainSearch: (search: string) => void; @@ -581,6 +618,7 @@ const DesktopLayout = ({ displayedTokens: DisplayedTokens; onChainSelect: (chainId: number | null) => void; onTokenSelect: (token: EnrichedToken) => void; + onSelectOtherToken?: (token: EnrichedToken | null) => void; onModalClose: () => void; }) => { const chainSearchInputRef = useRef(null); @@ -688,8 +726,12 @@ const DesktopLayout = ({ { + if (token.isUnreachable && onSelectOtherToken) { + onSelectOtherToken(null); + } onTokenSelect(token); onModalClose(); }} @@ -707,8 +749,12 @@ const DesktopLayout = ({ { + if (token.isUnreachable && onSelectOtherToken) { + onSelectOtherToken(null); + } onTokenSelect(token); onModalClose(); }} @@ -760,7 +806,7 @@ const ChainEntry = ({ name: "All", }; return ( - {chainInfo.name} {isSelected && } - + ); }; const TokenEntry = ({ token, + isOriginToken, isSelected, onClick, tabIndex, }: { - token: LifiToken & { balanceUsd: number; balance: BigNumber }; + token: EnrichedTokenWithReachability; + isOriginToken: boolean; isSelected: boolean; onClick: () => void; tabIndex?: number; }) => { const hasBalance = token.balance.gt(0) && token.balanceUsd > 0.01; + const warningMessage = isOriginToken + ? "Output token will be reset" + : "Input token will be reset"; return ( - - - - {token.name} - {token.symbol} - - {hasBalance && ( + + + + {token.name} + {token.symbol} + + + + {token.isUnreachable ? ( + + {" "} + {warningMessage} + + ) : ( +
+ )} + + {hasBalance ? ( {formatUnitsWithMaxFractions( @@ -810,8 +873,10 @@ const TokenEntry = ({ ${formatUSD(parseUnits(token.balanceUsd.toString(), 18))} + ) : ( +
)} - + ); }; @@ -855,6 +920,15 @@ const SearchBarStyled = styled(Searchbar)` flex-shrink: 0; `; +const TokenInfoWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + height: 100%; + align-items: center; + gap: 8px; +`; + const TokenItemImageWrapper = styled.div` width: 32px; height: 32px; @@ -1052,6 +1126,15 @@ const EntryItem = styled.button<{ isSelected: boolean; isDisabled?: boolean }>` } `; +const ChainEntryItem = EntryItem; + +const TokenEntryItem = styled(EntryItem)` + display: grid; + grid-template-columns: 3fr 2fr 1fr; // [TOKEN_INFO - WARNING - BALANCE] + gap: 8px; + align-items: center; +`; + const ChainItemImage = styled.img` width: 32px; height: 32px; @@ -1122,11 +1205,9 @@ const TokenSymbol = styled.div` const TokenBalanceStack = styled.div` display: flex; flex-direction: column; - align-items: flex-end; - + justify-content: center; gap: 4px; - margin-left: auto; `; const TokenBalance = styled.div` @@ -1150,6 +1231,19 @@ const TokenBalanceUsd = styled.div` opacity: 0.5; `; +const UnreachableWarning = styled.div` + color: var(--functional-red); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 130%; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; +`; + const SectionHeader = styled.div` color: var(--base-bright-gray, #e0f3ff); font-size: 14px; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx index be3e6cdf5..c590fd174 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/SelectorButton.tsx @@ -3,11 +3,15 @@ import { useCallback, useEffect, useState } from "react"; import { COLORS, getChainInfo } from "utils"; import { ReactComponent as ChevronDownIcon } from "assets/icons/chevron-down.svg"; import { TokenImage } from "components/TokenImage"; -import ChainTokenSelectorModal, { EnrichedToken } from "./Modal"; +import { + ChainTokenSelectorModal, + EnrichedToken, +} from "./ChainTokenSelectorModal"; type Props = { selectedToken: EnrichedToken | null; onSelect?: (token: EnrichedToken) => void; + onSelectOtherToken?: (token: EnrichedToken | null) => void; // Callback to reset the other selector isOriginToken: boolean; otherToken?: EnrichedToken | null; // The currently selected token on the other side marginBottom?: string; @@ -16,6 +20,7 @@ type Props = { export default function SelectorButton({ onSelect, + onSelectOtherToken, selectedToken, isOriginToken, otherToken, @@ -52,6 +57,7 @@ export default function SelectorButton({ { setAmount(amount); @@ -88,6 +89,7 @@ export const InputForm = ({ { setAmount(amount); @@ -110,6 +112,7 @@ export const InputForm = ({ const TokenInput = ({ setToken, + setOtherToken, token, setAmount, isOrigin, @@ -125,6 +128,7 @@ const TokenInput = ({ destinationChainEcosystem, }: { setToken: (token: EnrichedToken) => void; + setOtherToken: (token: EnrichedToken | null) => void; token: EnrichedToken | null; setAmount: (amount: BigNumber | null) => void; isOrigin: boolean; @@ -225,6 +229,7 @@ const TokenInput = ({ Date: Tue, 4 Nov 2025 16:51:44 +0200 Subject: [PATCH 112/122] small fixes Signed-off-by: Gerhard Steenkamp --- src/components/Modal/Modal.styles.ts | 13 ++- .../components/BalanceSelector.tsx | 4 +- .../ChainTokenSelectorModal.tsx | 89 +++++++++++++------ 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 073437731..f343061c8 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -1,6 +1,6 @@ import { keyframes } from "@emotion/react"; import styled from "@emotion/styled"; -import { COLORS, QUERIESV2 } from "utils"; +import { COLORS, QUERIES, QUERIESV2 } from "utils"; import { ModalDirection } from "./Modal"; const fadeBackground = keyframes` @@ -93,8 +93,8 @@ type ModalWrapperType = { const minimumMargin = 32; export const ModalContentWrapper = styled.div` - --padding-modal-content: ${({ padding }) => - padding === "normal" ? "24px" : "16px"}; + --padding-base: ${({ padding }) => (padding === "normal" ? "24px" : "16px")}; + --padding-modal-content: calc(var(--padding-base) / 1.5); max-height: ${({ height, topYOffset }) => height ? `min(calc(100svh - ${minimumMargin * 2}px - ${topYOffset ?? 0}px), ${height}px)` @@ -114,11 +114,16 @@ export const ModalContentWrapper = styled.div` background: #202024; border: 1px solid #34353b; box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.32); - border-radius: 24px; + border-radius: 16px; position: relative; overflow: hidden; + + ${QUERIES.tabletAndUp} { + --padding-modal-content: var(--padding-base); + border-radius: 24px; + } `; export const TitleAndExitWrapper = styled.div` diff --git a/src/views/SwapAndBridge/components/BalanceSelector.tsx b/src/views/SwapAndBridge/components/BalanceSelector.tsx index d7c4649f1..8212c87c7 100644 --- a/src/views/SwapAndBridge/components/BalanceSelector.tsx +++ b/src/views/SwapAndBridge/components/BalanceSelector.tsx @@ -52,7 +52,6 @@ export function BalanceSelector({ : BigNumber.from(0); }, [tokenBalances.data, token.chainId, token.address]); - if (!balance || balance.lte(0)) return null; const percentages = ["25%", "50%", "75%", "MAX"]; const handlePillClick = (percentage: string) => { @@ -69,12 +68,13 @@ export function BalanceSelector({ return ( !disableHover && setIsHovered(true)} + onMouseEnter={() => !disableHover && balance.gt(0) && setIsHovered(true)} onMouseLeave={() => !disableHover && setIsHovered(false)} > {isHovered && + balance.gt(0) && percentages.map((percentage, index) => ( { return tokens.sort((a, b) => { - // Sort by balance - tokens with balance go to top - const aHasBalance = a.balance.gt(0) && a.balanceUsd > 0.01; - const bHasBalance = b.balance.gt(0) && b.balanceUsd > 0.01; + // Sort by token balance - tokens with balance go to top + const aHasTokenBalance = a.balance.gt(0); + const bHasTokenBalance = b.balance.gt(0); - if (aHasBalance !== bHasBalance) { - return aHasBalance ? -1 : 1; + if (aHasTokenBalance !== bHasTokenBalance) { + return aHasTokenBalance ? -1 : 1; } - // If both have balance or both don't have balance, sort by balance amount - if (aHasBalance && bHasBalance) { - if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { - return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); + // If both have token balance, prioritize sorting by USD value if available + if (aHasTokenBalance && bHasTokenBalance) { + const aHasUsdBalance = a.balanceUsd > 0.01; + const bHasUsdBalance = b.balanceUsd > 0.01; + + // Both have USD values - sort by USD + if (aHasUsdBalance && bHasUsdBalance) { + if (Math.abs(b.balanceUsd - a.balanceUsd) < 0.0001) { + return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); + } + return b.balanceUsd - a.balanceUsd; } - return b.balanceUsd - a.balanceUsd; + + // Only one has USD value - prioritize the one with USD + if (aHasUsdBalance !== bHasUsdBalance) { + return aHasUsdBalance ? -1 : 1; + } + + // Neither has USD value - sort alphabetically + return a.symbol.toLocaleLowerCase().localeCompare(b.symbol); } // If neither has balance, sort alphabetically @@ -403,7 +418,9 @@ const DesktopModal = ({ return ( Select ${isOriginToken ? "Origin" : "Destination"} Token + } isOpen={displayModal} padding="thin" exitModalHandler={() => setDisplayModal(false)} @@ -474,6 +491,8 @@ const MobileLayout = ({ } }, [mobileStep]); + const warningMessage = "No Route"; + return ( {mobileStep === "chain" ? ( @@ -547,8 +566,8 @@ const MobileLayout = ({ { if (token.isUnreachable && onSelectOtherToken) { onSelectOtherToken(null); @@ -570,8 +589,8 @@ const MobileLayout = ({ { if (token.isUnreachable && onSelectOtherToken) { onSelectOtherToken(null); @@ -635,6 +654,10 @@ const DesktopLayout = ({ tokenSearchInputRef.current?.focus(); } + const warningMessage = isOriginToken + ? "Output token will be reset" + : "Input token will be reset"; + /** * Tab order strategy for keyboard navigation: * - Chain search: tabIndex 1 (always focused first) @@ -726,8 +749,8 @@ const DesktopLayout = ({ { if (token.isUnreachable && onSelectOtherToken) { onSelectOtherToken(null); @@ -749,8 +772,8 @@ const DesktopLayout = ({ { if (token.isUnreachable && onSelectOtherToken) { onSelectOtherToken(null); @@ -821,21 +844,19 @@ const ChainEntry = ({ const TokenEntry = ({ token, - isOriginToken, isSelected, onClick, tabIndex, + warningMessage, }: { token: EnrichedTokenWithReachability; - isOriginToken: boolean; isSelected: boolean; onClick: () => void; + warningMessage: string; tabIndex?: number; }) => { - const hasBalance = token.balance.gt(0) && token.balanceUsd > 0.01; - const warningMessage = isOriginToken - ? "Output token will be reset" - : "Input token will be reset"; + const hasTokenBalance = token.balance.gt(0); + const hasUsdBalance = token.balanceUsd >= 0.01; return ( - + {token.name} @@ -861,16 +882,19 @@ const TokenEntry = ({
)} - {hasBalance ? ( - + {hasTokenBalance ? ( + {formatUnitsWithMaxFractions( token.balance.toBigInt(), token.decimals )} + - ${formatUSD(parseUnits(token.balanceUsd.toString(), 18))} + {hasUsdBalance + ? "$" + formatUSD(parseUnits(token.balanceUsd.toString(), 18)) + : "??"} ) : ( @@ -920,13 +944,15 @@ const SearchBarStyled = styled(Searchbar)` flex-shrink: 0; `; -const TokenInfoWrapper = styled.div` +const TokenInfoWrapper = styled.div<{ dim?: boolean }>` display: flex; flex-direction: row; justify-content: flex-start; height: 100%; align-items: center; gap: 8px; + + opacity: ${({ dim }) => (dim ? 0.5 : 1)}; `; const TokenItemImageWrapper = styled.div` @@ -1055,10 +1081,15 @@ const Title = styled.div` overflow: hidden; color: var(--base-bright-gray, #e0f3ff); font-family: Barlow; - font-size: 20px; + font-size: 16px; font-style: normal; font-weight: 400; line-height: 130%; /* 26px */ + padding-left: 8px; + + ${QUERIES.tabletAndUp} { + font-size: 20px; + } `; const ListWrapper = styled.div` @@ -1101,7 +1132,6 @@ const EntryItem = styled.button<{ isSelected: boolean; isDisabled?: boolean }>` align-items: center; - padding: 8px; height: 48px; gap: 8px; @@ -1202,12 +1232,13 @@ const TokenSymbol = styled.div` text-transform: uppercase; `; -const TokenBalanceStack = styled.div` +const TokenBalanceStack = styled.div<{ dim?: boolean }>` display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 4px; + opacity: ${({ dim }) => (dim ? 0.5 : 1)}; `; const TokenBalance = styled.div` From d85ad71182b15e35b3c199ec1afaca789d516b2b Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Tue, 4 Nov 2025 16:57:34 +0200 Subject: [PATCH 113/122] fixup Signed-off-by: Gerhard Steenkamp --- .../components/ChainTokenSelector/ChainTokenSelectorModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx index 8f3802c53..6dd3d9db9 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx @@ -419,7 +419,7 @@ const DesktopModal = ({ Select ${isOriginToken ? "Origin" : "Destination"} Token + Select {isOriginToken ? "Origin" : "Destination"} Token } isOpen={displayModal} padding="thin" From 73309767c555e7ad15237ccd4cdad283c34ad069 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 5 Nov 2025 12:01:28 +0200 Subject: [PATCH 114/122] add blockexplorer link Signed-off-by: Gerhard Steenkamp --- src/assets/icons/arrow-up-right-boxed.svg | 2 +- src/utils/token.ts | 8 ++ .../ChainTokenSelectorModal.tsx | 93 ++++++++++++++----- 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/src/assets/icons/arrow-up-right-boxed.svg b/src/assets/icons/arrow-up-right-boxed.svg index c0b442de5..be7dfb05a 100644 --- a/src/assets/icons/arrow-up-right-boxed.svg +++ b/src/assets/icons/arrow-up-right-boxed.svg @@ -1,3 +1,3 @@ - + diff --git a/src/utils/token.ts b/src/utils/token.ts index 1f7203690..5dcf6fa0e 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -212,3 +212,11 @@ export function swapTokenToTokenInfo(swapToken: SwapToken): TokenInfo { return baseTokenInfo; } + +export function getTokenExplorerLinkFromAddress( + chainId: number, + address: string +) { + const explorerBaseUrl = getChainInfo(chainId).explorerUrl; + return `${explorerBaseUrl}/address/${address}`; +} diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx index 6dd3d9db9..e6d261d23 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx @@ -10,6 +10,7 @@ import { formatUnitsWithMaxFractions, formatUSD, getChainInfo, + getTokenExplorerLinkFromAddress, parseUnits, QUERIES, TOKEN_SYMBOLS_MAP, @@ -19,6 +20,7 @@ import { ReactComponent as CheckmarkCircleFilled } from "assets/icons/checkmark- import { ReactComponent as ChevronRight } from "assets/icons/chevron-right.svg"; import { ReactComponent as SearchResults } from "assets/icons/search_results.svg"; import { ReactComponent as WarningIcon } from "assets/icons/warning_triangle.svg"; +import { ReactComponent as LinkExternalIcon } from "assets/icons/arrow-up-right-boxed.svg"; import AllChainsIcon from "assets/chain-logos/all-swap-chain.png"; import { useEnrichedCrosschainBalances } from "hooks/useEnrichedCrosschainBalances"; import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; @@ -868,8 +870,21 @@ const TokenEntry = ({ - {token.name} - {token.symbol} + + {token.name} + + {token.symbol} + + + + {getChainInfo(token.chainId).name} @@ -894,7 +909,7 @@ const TokenEntry = ({ {hasUsdBalance ? "$" + formatUSD(parseUnits(token.balanceUsd.toString(), 18)) - : "??"} + : "-"} ) : ( @@ -1191,31 +1206,13 @@ const ChainItemCheckmark = styled(CheckmarkCircleFilled)` const TokenNameSymbolWrapper = styled.div` display: flex; - flex-direction: row; - gap: 4px; - + flex-direction: column; width: 100%; - align-items: center; + align-items: flex-start; justify-content: start; `; -const TokenName = styled.div` - overflow: hidden; - color: var(--base-bright-gray, #e0f3ff); - /* Body/Medium */ - font-family: Barlow; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: 130%; /* 20.8px */ - - max-width: 20ch; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - const TokenSymbol = styled.div` overflow: hidden; color: var(--base-bright-gray, #e0f3ff); @@ -1228,8 +1225,6 @@ const TokenSymbol = styled.div` line-height: 130%; /* 15.6px */ opacity: 0.5; - - text-transform: uppercase; `; const TokenBalanceStack = styled.div<{ dim?: boolean }>` @@ -1325,3 +1320,51 @@ const Key = styled.div` flex-shrink: 0; color: rgba(224, 243, 255, 0.4); `; + +const TokenName = styled.div` + overflow: hidden; + color: var(--base-bright-gray, #e0f3ff); + /* Body/Medium */ + font-family: Barlow; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 130%; /* 20.8px */ + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-flex; + align-items: baseline; + gap: 4px; +`; + +const TokenLink = styled.a` + display: inline-flex; + flex-direction: row; + gap: 4px; + align-items: baseline; + text-decoration: none; + color: var(--base-bright-gray, #e0f3ff); + opacity: 0.5; + font-weight: 400; + + svg, + span { + font-size: 14px; + display: none; + color: inherit; + } + + &:hover { + text-decoration: underline; + color: ${COLORS.aqua}; + opacity: 1; + } + + &:hover { + svg { + display: inline; + } + } +`; From a0b5da384021fae45cddae5d6c268347ad1396a6 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 5 Nov 2025 12:01:49 +0200 Subject: [PATCH 115/122] let API use default slippage Signed-off-by: Gerhard Steenkamp --- src/views/SwapAndBridge/hooks/useSwapQuote.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/views/SwapAndBridge/hooks/useSwapQuote.ts b/src/views/SwapAndBridge/hooks/useSwapQuote.ts index c82b4ff16..8ff561892 100644 --- a/src/views/SwapAndBridge/hooks/useSwapQuote.ts +++ b/src/views/SwapAndBridge/hooks/useSwapQuote.ts @@ -35,7 +35,6 @@ const useSwapQuote = ({ refundAddress, depositor, refundOnOrigin = true, - slippageTolerance = 0.05, }: SwapQuoteParams) => { const { data, isLoading, error } = useQuery({ queryKey: [ @@ -78,7 +77,6 @@ const useSwapQuote = ({ recipient: effectiveRecipient, amount: amount.toString(), refundOnOrigin, - slippageTolerance, // Skip transaction estimation when using placeholder address skipOriginTxEstimation: isUsingPlaceholderDepositor, ...(integratorId ? { integratorId } : {}), From a692fac33e0d55d0eeb2ef0700f54f5815b7b829 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 5 Nov 2025 12:07:38 +0200 Subject: [PATCH 116/122] fixup Signed-off-by: Gerhard Steenkamp --- .../ChainTokenSelector/ChainTokenSelectorModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx index e6d261d23..180c81f6a 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx @@ -1192,9 +1192,9 @@ const ChainItemName = styled.div` text-overflow: ellipsis; /* Body/Medium */ font-family: Barlow; - font-size: 16px; + font-size: 14px; font-style: normal; - font-weight: 400; + font-weight: 600; line-height: 130%; /* 20.8px */ `; @@ -1328,7 +1328,7 @@ const TokenName = styled.div` font-family: Barlow; font-size: 14px; font-style: normal; - font-weight: 500; + font-weight: 600; line-height: 130%; /* 20.8px */ white-space: nowrap; @@ -1343,7 +1343,7 @@ const TokenLink = styled.a` display: inline-flex; flex-direction: row; gap: 4px; - align-items: baseline; + align-items: center; text-decoration: none; color: var(--base-bright-gray, #e0f3ff); opacity: 0.5; From d7028381a2073f465ad5a82c47af3e9a79e10e09 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 5 Nov 2025 17:04:18 +0200 Subject: [PATCH 117/122] fix(balance-selector): respect usd as unit Signed-off-by: Gerhard Steenkamp --- src/hooks/useTokenInput.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/hooks/useTokenInput.ts b/src/hooks/useTokenInput.ts index 755ac780f..5e884417c 100644 --- a/src/hooks/useTokenInput.ts +++ b/src/hooks/useTokenInput.ts @@ -187,9 +187,21 @@ export function useTokenInput({ const handleBalanceClick = useCallback( (amount: BigNumber, decimals: number) => { setAmount(amount); - setAmountString(formatUnitsWithMaxFractions(amount, decimals)); + if (unit === "usd" && token) { + // Convert token amount to USD for display + const tokenAmountFormatted = formatUnitsWithMaxFractions( + amount, + decimals + ); + const usdValue = convertTokenToUSD(tokenAmountFormatted, token); + // convertTokenToUSD returns in 18 decimal precision + setAmountString(utils.formatUnits(usdValue, 18)); + } else { + // Display as token amount + setAmountString(formatUnitsWithMaxFractions(amount, decimals)); + } }, - [setAmount] + [setAmount, unit, token] ); return { From b86662ec782204e9b4c78d828053ea1c2c09b140 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 6 Nov 2025 15:18:21 +0200 Subject: [PATCH 118/122] rework getter for updated api response. use new fee structure Signed-off-by: Gerhard Steenkamp --- src/components/Tooltip/Tooltip.styles.tsx | 3 +- .../serverless-api/mocked/swap-approval.ts | 203 +++++++------ .../serverless-api/prod/swap-approval.ts | 268 ++++++++---------- .../components/ConfirmationButton.tsx | 50 ++-- 4 files changed, 272 insertions(+), 252 deletions(-) diff --git a/src/components/Tooltip/Tooltip.styles.tsx b/src/components/Tooltip/Tooltip.styles.tsx index 0e3476674..7face2c94 100644 --- a/src/components/Tooltip/Tooltip.styles.tsx +++ b/src/components/Tooltip/Tooltip.styles.tsx @@ -3,7 +3,8 @@ import { Tooltip } from "react-tooltip"; import { ReactComponent as RoundedCheckmark16 } from "assets/icons/checkmark-circle.svg"; export const StyledAnchor = styled.a<{ width?: string }>` - line-height: 1; + line-height: 1em; + height: 1em; width: ${({ width }) => width}; `; diff --git a/src/utils/serverless-api/mocked/swap-approval.ts b/src/utils/serverless-api/mocked/swap-approval.ts index fee482870..97bd8e43c 100644 --- a/src/utils/serverless-api/mocked/swap-approval.ts +++ b/src/utils/serverless-api/mocked/swap-approval.ts @@ -20,7 +20,30 @@ export async function swapApprovalApiCall( params.outputToken ); + const inputTokenInfo = { + address: params.inputToken, + chainId: params.originChainId, + decimals: inputToken?.decimals ?? 18, + symbol: inputToken?.symbol ?? "UNKNOWN", + }; + + const outputTokenInfo = { + address: params.outputToken, + chainId: params.destinationChainId, + decimals: outputToken?.decimals ?? 18, + symbol: outputToken?.symbol ?? "UNKNOWN", + }; + + const nativeTokenInfo = { + chainId: params.originChainId, + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + symbol: "ETH", + }; + return { + crossSwapType: "BRIDGEABLE_TO_BRIDGEABLE", + amountType: params.tradeType || "exactInput", checks: { allowance: { token: params.inputToken, @@ -40,21 +63,12 @@ export async function swapApprovalApiCall( bridge: { inputAmount: BigNumber.from("0"), outputAmount: BigNumber.from("0"), - tokenIn: { - address: params.inputToken, - chainId: params.originChainId, - decimals: inputToken?.decimals ?? 18, - symbol: inputToken?.symbol ?? "UNKNOWN", - }, - tokenOut: { - address: params.outputToken, - chainId: params.destinationChainId, - decimals: outputToken?.decimals ?? 18, - symbol: outputToken?.symbol ?? "UNKNOWN", - }, + tokenIn: inputTokenInfo, + tokenOut: outputTokenInfo, fees: { amount: BigNumber.from("0"), pct: BigNumber.from("0"), + token: inputTokenInfo, details: { type: "across", lp: { @@ -71,9 +85,12 @@ export async function swapApprovalApiCall( }, }, }, + provider: "across", }, destinationSwap: undefined, }, + inputToken: inputTokenInfo, + outputToken: outputTokenInfo, refundToken: { address: params.inputToken, chainId: params.originChainId, @@ -81,6 +98,7 @@ export async function swapApprovalApiCall( symbol: params.inputToken, }, inputAmount: BigNumber.from(params.amount), + maxInputAmount: BigNumber.from(params.amount), expectedOutputAmount: BigNumber.from(params.amount), minOutputAmount: BigNumber.from(params.amount), expectedFillTime: 1, @@ -98,85 +116,106 @@ export async function swapApprovalApiCall( total: { amount: BigNumber.from("0"), amountUsd: "0", - pct: "0", - token: { - decimals: outputToken?.decimals ?? 18, - symbol: outputToken?.symbol ?? "UNKNOWN", - address: params.outputToken, - name: outputToken?.symbol ?? "UNKNOWN", - chainId: params.destinationChainId, - }, - }, - originGas: { - amount: BigNumber.from("0"), - amountUsd: "0", - token: { - chainId: params.originChainId, - address: "0x0000000000000000000000000000000000000000", - decimals: 18, - symbol: "ETH", - }, - }, - destinationGas: { - amount: BigNumber.from("0"), - amountUsd: "0", - pct: "0", - token: { - chainId: params.destinationChainId, - address: "0x0000000000000000000000000000000000000000", - decimals: 18, - symbol: "ETH", - }, - }, - relayerCapital: { - amount: BigNumber.from("0"), - amountUsd: "0", - pct: "0", - token: { - decimals: outputToken?.decimals ?? 18, - symbol: outputToken?.symbol ?? "UNKNOWN", - address: params.outputToken, - name: outputToken?.symbol ?? "UNKNOWN", - chainId: params.destinationChainId, - }, - }, - lpFee: { - amount: BigNumber.from("0"), - amountUsd: "0", - pct: "0", - token: { - decimals: outputToken?.decimals ?? 18, - symbol: outputToken?.symbol ?? "UNKNOWN", - address: params.outputToken, - name: outputToken?.symbol ?? "UNKNOWN", - chainId: params.destinationChainId, + pct: BigNumber.from("0"), + token: outputTokenInfo, + details: { + type: "TOTAL_BREAKDOWN", + swapImpact: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + }, + app: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: outputTokenInfo, + }, + bridge: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + details: { + type: "across", + lp: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + }, + relayerCapital: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + }, + destinationGas: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: nativeTokenInfo, + }, + }, + }, }, }, - relayerTotal: { + totalMax: { amount: BigNumber.from("0"), amountUsd: "0", - pct: "0", - token: { - decimals: outputToken?.decimals ?? 18, - symbol: outputToken?.symbol ?? "UNKNOWN", - address: params.outputToken, - name: outputToken?.symbol ?? "UNKNOWN", - chainId: params.destinationChainId, + pct: BigNumber.from("0"), + token: outputTokenInfo, + details: { + type: "MAX_TOTAL_BREAKDOWN", + maxSwapImpact: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + }, + app: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: outputTokenInfo, + }, + bridge: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + details: { + type: "across", + lp: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + }, + relayerCapital: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: inputTokenInfo, + }, + destinationGas: { + amount: BigNumber.from("0"), + amountUsd: "0", + pct: BigNumber.from("0"), + token: nativeTokenInfo, + }, + }, + }, }, }, - app: { + originGas: { amount: BigNumber.from("0"), amountUsd: "0", - pct: "0", - token: { - decimals: outputToken?.decimals ?? 18, - symbol: outputToken?.symbol ?? "UNKNOWN", - address: params.outputToken, - name: outputToken?.symbol ?? "UNKNOWN", - chainId: params.destinationChainId, - }, + token: nativeTokenInfo, + pct: BigNumber.from("0"), }, - swap: undefined, }, + eip712: undefined, }; } diff --git a/src/utils/serverless-api/prod/swap-approval.ts b/src/utils/serverless-api/prod/swap-approval.ts index 32e97c4de..f3f2c75a7 100644 --- a/src/utils/serverless-api/prod/swap-approval.ts +++ b/src/utils/serverless-api/prod/swap-approval.ts @@ -15,7 +15,37 @@ export type SwapApprovalApiReturnType = Awaited< ReturnType >; +type FeeComponent = { + amount: string; + amountUsd: string; + pct?: string; + token: SwapApiToken; +}; + +type AcrossBridgeFeeDetails = { + type: "across"; + lp: FeeComponent; + relayerCapital: FeeComponent; + destinationGas: FeeComponent; +}; + +type TotalFeeBreakdownDetails = { + type: "TOTAL_BREAKDOWN"; + swapImpact: FeeComponent; + app: FeeComponent; + bridge: FeeComponent & { details?: AcrossBridgeFeeDetails }; +}; + +type MaxTotalFeeBreakdownDetails = { + type: "MAX_TOTAL_BREAKDOWN"; + maxSwapImpact: FeeComponent; + app: FeeComponent; + bridge: FeeComponent & { details?: AcrossBridgeFeeDetails }; +}; + export type SwapApprovalApiResponse = { + crossSwapType: string; + amountType: string; checks: { allowance: { token: string; @@ -29,7 +59,7 @@ export type SwapApprovalApiResponse = { expected: string; }; }; - approvalTxns: { + approvalTxns?: { chainId: number; to: string; data: string; @@ -46,6 +76,7 @@ export type SwapApprovalApiResponse = { name: string; sources: string[]; }; + slippage: number; }; bridge: { inputAmount: string; @@ -55,6 +86,7 @@ export type SwapApprovalApiResponse = { fees: { amount: string; pct: string; + token: SwapApiToken; details: { type: "across"; lp: { @@ -71,6 +103,7 @@ export type SwapApprovalApiResponse = { }; }; }; + provider: string; }; destinationSwap?: { tokenIn: SwapApiToken; @@ -83,10 +116,14 @@ export type SwapApprovalApiResponse = { name: string; sources: string[]; }; + slippage: number; }; }; + inputToken: SwapApiToken; + outputToken: SwapApiToken; refundToken: SwapApiToken; inputAmount: string; + maxInputAmount: string; expectedOutputAmount: string; minOutputAmount: string; expectedFillTime: number; @@ -100,101 +137,12 @@ export type SwapApprovalApiResponse = { maxFeePerGas?: string; maxPriorityFeePerGas?: string; }; - fees: { - total: { - amount: string; - amountUsd: string; - pct: string; - token: { - decimals: number; - symbol: string; - address: string; - name: string; - chainId: number; - }; - }; - originGas: { - amount: string; - amountUsd: string; - token: { - chainId: number; - address: string; - decimals: number; - symbol: string; - }; - }; - destinationGas: { - amount: string; - amountUsd: string; - pct: string; - token: { - chainId: number; - address: string; - decimals: number; - symbol: string; - }; - }; - relayerCapital: { - amount: string; - amountUsd: string; - pct: string; - token: { - decimals: number; - symbol: string; - address: string; - name: string; - chainId: number; - }; - }; - lpFee: { - amount: string; - amountUsd: string; - pct: string; - token: { - decimals: number; - symbol: string; - address: string; - name: string; - chainId: number; - }; - }; - relayerTotal: { - amount: string; - amountUsd: string; - pct: string; - token: { - decimals: number; - symbol: string; - address: string; - name: string; - chainId: number; - }; - }; - app: { - amount: string; - amountUsd: string; - pct: string; - token: { - decimals: number; - symbol: string; - address: string; - name: string; - chainId: number; - }; - }; - swap?: { - amount: string; - amountUsd: string; - pct: string; - token: { - decimals: number; - symbol: string; - address: string; - name: string; - chainId: number; - }; - }; + fees?: { + total: FeeComponent & { details: TotalFeeBreakdownDetails }; + totalMax: FeeComponent & { details: MaxTotalFeeBreakdownDetails }; + originGas: FeeComponent; }; + eip712?: any; }; export type SwapApprovalApiQueryParams = { @@ -223,7 +171,17 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { const result = response.data; + // Helper function to convert fee component + const convertFeeComponent = (fee: FeeComponent) => ({ + amount: BigNumber.from(fee.amount), + amountUsd: fee.amountUsd, + pct: fee.pct ? BigNumber.from(fee.pct) : undefined, + token: fee.token, + }); + return { + crossSwapType: result.crossSwapType, + amountType: result.amountType, checks: { allowance: { token: result.checks.allowance.token, @@ -252,6 +210,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { result.steps.originSwap.maxInputAmount ), swapProvider: result.steps.originSwap.swapProvider, + slippage: result.steps.originSwap.slippage, } : undefined, bridge: { @@ -262,6 +221,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { fees: { amount: BigNumber.from(result.steps.bridge.fees.amount), pct: BigNumber.from(result.steps.bridge.fees.pct), + token: result.steps.bridge.fees.token, details: { type: "across", lp: { @@ -288,6 +248,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { }, }, }, + provider: result.steps.bridge.provider, }, destinationSwap: result.steps.destinationSwap ? { @@ -306,11 +267,15 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { result.steps.destinationSwap.minOutputAmount ), swapProvider: result.steps.destinationSwap.swapProvider, + slippage: result.steps.destinationSwap.slippage, } : undefined, }, + inputToken: result.inputToken, + outputToken: result.outputToken, refundToken: result.refundToken, inputAmount: BigNumber.from(result.inputAmount), + maxInputAmount: BigNumber.from(result.maxInputAmount), expectedOutputAmount: BigNumber.from(result.expectedOutputAmount), minOutputAmount: BigNumber.from(result.minOutputAmount), expectedFillTime: result.expectedFillTime, @@ -330,57 +295,68 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { ? BigNumber.from(result.swapTx.maxPriorityFeePerGas) : undefined, }, - fees: { - total: { - amount: BigNumber.from(result.fees.total.amount), - amountUsd: result.fees.total.amountUsd, - pct: result.fees.total.pct, - token: result.fees.total.token, - }, - originGas: { - amount: BigNumber.from(result.fees.originGas.amount), - amountUsd: result.fees.originGas.amountUsd, - token: result.fees.originGas.token, - }, - destinationGas: { - amount: BigNumber.from(result.fees.destinationGas.amount), - amountUsd: result.fees.destinationGas.amountUsd, - pct: result.fees.destinationGas.pct, - token: result.fees.destinationGas.token, - }, - relayerCapital: { - amount: BigNumber.from(result.fees.relayerCapital.amount), - amountUsd: result.fees.relayerCapital.amountUsd, - pct: result.fees.relayerCapital.pct, - token: result.fees.relayerCapital.token, - }, - lpFee: { - amount: BigNumber.from(result.fees.lpFee.amount), - amountUsd: result.fees.lpFee.amountUsd, - pct: result.fees.lpFee.pct, - token: result.fees.lpFee.token, - }, - relayerTotal: { - amount: BigNumber.from(result.fees.relayerTotal.amount), - amountUsd: result.fees.relayerTotal.amountUsd, - pct: result.fees.relayerTotal.pct, - token: result.fees.relayerTotal.token, - }, - app: { - amount: BigNumber.from(result.fees.app.amount), - amountUsd: result.fees.app.amountUsd, - pct: result.fees.app.pct, - token: result.fees.app.token, - }, - swap: result.fees.swap - ? { - amount: BigNumber.from(result.fees.swap.amount), - amountUsd: result.fees.swap.amountUsd, - pct: result.fees.swap.pct, - token: result.fees.swap.token, - } - : undefined, - }, + fees: result.fees + ? { + total: { + ...convertFeeComponent(result.fees.total), + details: { + type: result.fees.total.details.type, + swapImpact: convertFeeComponent( + result.fees.total.details.swapImpact + ), + app: convertFeeComponent(result.fees.total.details.app), + bridge: { + ...convertFeeComponent(result.fees.total.details.bridge), + details: result.fees.total.details.bridge.details + ? { + type: result.fees.total.details.bridge.details.type, + lp: convertFeeComponent( + result.fees.total.details.bridge.details.lp + ), + relayerCapital: convertFeeComponent( + result.fees.total.details.bridge.details.relayerCapital + ), + destinationGas: convertFeeComponent( + result.fees.total.details.bridge.details.destinationGas + ), + } + : undefined, + }, + }, + }, + totalMax: { + ...convertFeeComponent(result.fees.totalMax), + details: { + type: result.fees.totalMax.details.type, + maxSwapImpact: convertFeeComponent( + result.fees.totalMax.details.maxSwapImpact + ), + app: convertFeeComponent(result.fees.totalMax.details.app), + bridge: { + ...convertFeeComponent(result.fees.totalMax.details.bridge), + details: result.fees.totalMax.details.bridge.details + ? { + type: result.fees.totalMax.details.bridge.details.type, + lp: convertFeeComponent( + result.fees.totalMax.details.bridge.details.lp + ), + relayerCapital: convertFeeComponent( + result.fees.totalMax.details.bridge.details + .relayerCapital + ), + destinationGas: convertFeeComponent( + result.fees.totalMax.details.bridge.details + .destinationGas + ), + } + : undefined, + }, + }, + }, + originGas: convertFeeComponent(result.fees.originGas), + } + : undefined, + eip712: result.eip712, }; } diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index d01cb8f1a..b610ef80f 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -194,26 +194,26 @@ export const ConfirmationButton: React.FC = ({ // Resolve conversion helpers outside memo to respect hooks rules const displayValues = React.useMemo(() => { - if (!swapQuote || !inputToken || !outputToken) { + if (!swapQuote || !inputToken || !outputToken || !swapQuote.fees) { return { fee: "-", time: "-", bridgeFee: "-", - gasFee: "-", - swapFee: "-", + appFee: undefined, + swapImpact: undefined, route: "Across V4", estimatedTime: "-", - netFee: "-", + totalFee: "-", }; } - const bridgeFeesUsd = swapQuote.fees.relayerTotal.amountUsd; - const gasFeeUsd = ( - Number(swapQuote.fees.originGas.amountUsd) + - Number(swapQuote.fees.destinationGas.amountUsd) - ).toString(); - const swapFeeUsd = swapQuote.fees.swap?.amountUsd; const totalFeeUsd = swapQuote.fees.total.amountUsd; + const bridgeFeesUsd = swapQuote.fees.total.details.bridge.amountUsd; + const appFeesUsd = swapQuote.fees.total.details.app.amountUsd; + const swapImpactUsd = swapQuote.fees.total.details.swapImpact.amountUsd; + + const hasAppFee = Number(appFeesUsd) > 0; + const hasSwapImpact = Number(swapImpactUsd) > 0; const totalSeconds = Math.max(0, Number(swapQuote.expectedFillTime || 0)); const underOneMinute = totalSeconds < 60; @@ -222,11 +222,11 @@ export const ConfirmationButton: React.FC = ({ : `~${Math.ceil(totalSeconds / 60)} min`; return { - fee: formatUSDString(totalFeeUsd), + totalFee: formatUSDString(totalFeeUsd), time, bridgeFee: formatUSDString(bridgeFeesUsd), - gasFee: formatUSDString(gasFeeUsd), - swapFee: swapFeeUsd ? formatUSDString(swapFeeUsd) : undefined, + appFee: hasAppFee ? formatUSDString(appFeesUsd) : undefined, + swapImpact: hasSwapImpact ? formatUSDString(swapImpactUsd) : undefined, route: "Across V4", estimatedTime: time, }; @@ -245,7 +245,7 @@ export const ConfirmationButton: React.FC = ({ const content = ( <> setExpanded((e) => !e)} @@ -274,7 +274,7 @@ export const ConfirmationButton: React.FC = ({ - Net Fee + Total Fee = ({ - {displayValues.netFee} + {displayValues.totalFee} Bridge Fee {displayValues.bridgeFee} - - Gas Fee - {displayValues.gasFee} - - {isDefined(displayValues.swapFee) && ( + {isDefined(displayValues.appFee) && ( + + App Fee + {displayValues.appFee} + + )} + {isDefined(displayValues.swapImpact) && ( - Swap Fee - {displayValues.swapFee} + Swap Impact + + {displayValues.swapImpact} + )} From d88cf335610c36ad7702ede0d6d8faa0f3f04690 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 6 Nov 2025 17:00:20 +0200 Subject: [PATCH 119/122] style fees. add tooltips Signed-off-by: Gerhard Steenkamp --- .../components/ConfirmationButton.tsx | 65 ++++++++++++++----- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/views/SwapAndBridge/components/ConfirmationButton.tsx b/src/views/SwapAndBridge/components/ConfirmationButton.tsx index b610ef80f..271743605 100644 --- a/src/views/SwapAndBridge/components/ConfirmationButton.tsx +++ b/src/views/SwapAndBridge/components/ConfirmationButton.tsx @@ -212,8 +212,9 @@ export const ConfirmationButton: React.FC = ({ const appFeesUsd = swapQuote.fees.total.details.app.amountUsd; const swapImpactUsd = swapQuote.fees.total.details.swapImpact.amountUsd; - const hasAppFee = Number(appFeesUsd) > 0; - const hasSwapImpact = Number(swapImpactUsd) > 0; + // Only show fee items if they're at least 1 cent + const hasAppFee = Number(appFeesUsd) >= 0.01; + const hasSwapImpact = Number(swapImpactUsd) >= 0.01; const totalSeconds = Math.max(0, Number(swapQuote.expectedFillTime || 0)); const underOneMinute = totalSeconds < 60; @@ -269,35 +270,45 @@ export const ConfirmationButton: React.FC = ({