diff --git a/bridge_ui/src/components/TokenSelectors/EvmTokenPicker.tsx b/bridge_ui/src/components/TokenSelectors/EvmTokenPicker.tsx index a5e04cd4..b3b42fd3 100644 --- a/bridge_ui/src/components/TokenSelectors/EvmTokenPicker.tsx +++ b/bridge_ui/src/components/TokenSelectors/EvmTokenPicker.tsx @@ -146,36 +146,16 @@ export default function EvmTokenPicker( } catch (e) { //For now, just swallow this one. } - let newAccount = null; - try { - //Covalent balances tend to be stale, so we make an attempt to correct it at selection time. - if (!account.isNativeAsset) { - newAccount = await getAddress(account.mintKey, account.tokenId); - newAccount = { ...account, ...newAccount }; //We spread over the old account so we don't lose the logo, uri, or other useful info we got from covalent. - } else { - newAccount = account; - } - } catch (e) { - //swallow - console.log(e); - } - if (!newAccount) { - //Must reject otherwise downstream checks relying on the balance may fail. - //An error is thrown so that the code above us will display the message. - throw new Error( - "Unable to retrieve required information about this token. Ensure your wallet is connected, then refresh the list." - ); - } const migration = isMigrationEligible(account.publicKey); if (v1 === true && !migration) { throw new Error( "Wormhole v1 assets cannot be transferred with this bridge." ); } - onChange(newAccount); + onChange(account); return Promise.resolve(); }, - [chainId, onChange, provider, isMigrationEligible, getAddress] + [chainId, onChange, provider, isMigrationEligible] ); const RenderComp = useCallback( diff --git a/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx b/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx index ea5706f4..97d525f8 100644 --- a/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx +++ b/bridge_ui/src/components/TokenSelectors/TokenPicker.tsx @@ -7,7 +7,9 @@ import { Dialog, DialogContent, DialogTitle, + Divider, IconButton, + Link, List, ListItem, makeStyles, @@ -15,11 +17,16 @@ import { Tooltip, Typography, } from "@material-ui/core"; +import { InfoOutlined, Launch } from "@material-ui/icons"; import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; import RefreshIcon from "@material-ui/icons/Refresh"; import { Alert } from "@material-ui/lab"; import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import useMarketsMap from "../../hooks/useMarketsMap"; import { NFTParsedTokenAccount } from "../../store/nftSlice"; +import { selectTransferTargetChain } from "../../store/selectors"; +import { AVAILABLE_MARKETS_URL, CHAINS_BY_ID } from "../../utils/consts"; import { shortenAddress } from "../../utils/solana"; import NFTViewer from "./NFTViewer"; @@ -62,6 +69,11 @@ const useStyles = makeStyles((theme) => "&$tokenImageContainer": { maxWidth: 40, }, + "&$tokenMarketsList": { + marginTop: theme.spacing(-0.5), + marginLeft: 0, + flexBasis: "100%", + }, "&:last-child": { textAlign: "right", }, @@ -78,6 +90,15 @@ const useStyles = makeStyles((theme) => tokenImage: { maxHeight: "2.5rem", //Eyeballing this based off the text size }, + tokenMarketsList: { + order: 1, + textAlign: "left", + width: "100%", + "& > .MuiButton-root": { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + }, + }, migrationAlert: { width: "100%", "& .MuiAlert-message": { @@ -107,12 +128,17 @@ export const balancePretty = (uiString: string) => { } }; +const noClickThrough = (e: any) => { + e.stopPropagation(); +}; + export const BasicAccountRender = ( - account: NFTParsedTokenAccount, + account: MarketParsedTokenAccount, isMigrationEligible: (address: string) => boolean, nft: boolean, displayBalance?: (account: NFTParsedTokenAccount) => boolean ) => { + const { data: marketsData } = useMarketsMap(false); const classes = useStyles(); const mintPrettyString = shortenAddress(account.mintKey); const uri = nft ? account.image_256 : account.logo || account.uri; @@ -139,6 +165,27 @@ export const BasicAccountRender = ( const tokenContent = (
+ {account.markets ? ( +
+ {account.markets.map((market) => + marketsData?.markets?.[market] ? ( + + ) : null + )} +
+ ) : null}
{uri && }
@@ -185,6 +232,10 @@ export const BasicAccountRender = ( : tokenContent; }; +interface MarketParsedTokenAccount extends NFTParsedTokenAccount { + markets?: string[]; +} + export default function TokenPicker({ value, options, @@ -229,6 +280,9 @@ export default function TokenPicker({ const [dialogIsOpen, setDialogIsOpen] = useState(false); const [selectionError, setSelectionError] = useState(""); + const targetChain = useSelector(selectTransferTargetChain); + const { data: marketsData } = useMarketsMap(true); + const openDialog = useCallback(() => { setHolderString(""); setSelectionError(""); @@ -242,16 +296,31 @@ export default function TokenPicker({ const handleSelectOption = useCallback( async (option: NFTParsedTokenAccount) => { setSelectionError(""); - onChange(option).then( - () => { - closeDialog(); - }, - (error) => { - setSelectionError(error?.message || "Error verifying the token."); + let newOption = null; + try { + //Covalent balances tend to be stale, so we make an attempt to correct it at selection time. + if (getAddress && !option.isNativeAsset) { + newOption = await getAddress(option.mintKey, option.tokenId); + newOption = { + ...option, + ...newOption, + // keep logo and uri from covalent / market list / etc (otherwise would be overwritten by undefined) + logo: option.logo || newOption.logo, + uri: option.uri || newOption.uri, + } as NFTParsedTokenAccount; + } else { + newOption = option; } - ); + await onChange(newOption); + closeDialog(); + } catch (e) { + console.error(e); + setSelectionError( + "Unable to retrieve required information about this token. Ensure your wallet is connected, then refresh the list." + ); + } }, - [onChange, closeDialog] + [getAddress, onChange, closeDialog] ); const resetAccountsWrapper = useCallback(() => { @@ -261,8 +330,8 @@ export default function TokenPicker({ resetAccounts && resetAccounts(); }, [resetAccounts]); - const filteredOptions = useMemo(() => { - return options.filter((option: NFTParsedTokenAccount) => { + const searchFilter = useCallback( + (option: NFTParsedTokenAccount) => { if (!holderString) { return true; } @@ -277,8 +346,61 @@ export default function TokenPicker({ ).toLowerCase(); const searchString = holderString.toLowerCase(); return optionString.includes(searchString); - }); - }, [holderString, options]); + }, + [holderString] + ); + + const marketChainTokens = marketsData?.tokens?.[chainId]; + const featuredMarkets = marketsData?.tokenMarkets?.[chainId]?.[targetChain]; + + const featuredOptions = useMemo(() => { + // only tokens have featured markets + if (!nft && featuredMarkets) { + const ownedMarketTokens = options + .filter( + (option: NFTParsedTokenAccount) => featuredMarkets?.[option.mintKey] + ) + .map( + (option) => + ({ + ...option, + markets: featuredMarkets[option.mintKey].markets, + } as MarketParsedTokenAccount) + ); + return [ + ...ownedMarketTokens, + ...Object.keys(featuredMarkets) + .filter( + (mintKey) => + !ownedMarketTokens.find((option) => option.mintKey === mintKey) + ) + .map( + (mintKey) => + ({ + amount: "0", + decimals: 0, + markets: featuredMarkets[mintKey].markets, + mintKey, + publicKey: "", + uiAmount: 0, + uiAmountString: "0", // if we can't look up by address, we can select the market that isn't in the list of holdings, but can't proceed since the balance will be 0 + symbol: marketChainTokens?.[mintKey]?.symbol, + logo: marketChainTokens?.[mintKey]?.logo, + } as MarketParsedTokenAccount) + ), + ].filter(searchFilter); + } + return []; + }, [nft, marketChainTokens, featuredMarkets, options, searchFilter]); + + const nonFeaturedOptions = useMemo(() => { + return options.filter( + (option: NFTParsedTokenAccount) => + searchFilter(option) && + // only tokens have featured markets + (nft || !featuredMarkets?.[option.mintKey]) + ); + }, [nft, options, featuredMarkets, searchFilter]); const localFind = useCallback( (address: string, tokenIdHolderString: string) => { @@ -383,6 +505,17 @@ export default function TokenPicker({
+ + You should always check for markets and liquidity before sending + tokens.{" "} + + Click here to see available markets for wrapped tokens. + + - {filteredOptions.map((option) => { + ) : ( + + {featuredOptions.length ? ( + <> + + Featured {CHAINS_BY_ID[chainId].name} >{" "} + {CHAINS_BY_ID[targetChain].name} markets{" "} + + + + + {featuredOptions.map((option) => { + return ( + handleSelectOption(option)} + key={ + option.publicKey + + option.mintKey + + (option.tokenId || "") + } + > + + + ); + })} + {nonFeaturedOptions.length ? ( + <> + + + Other Assets + + + ) : null} + + ) : null} + {nonFeaturedOptions.map((option) => { return ( handleSelectOption(option)} key={ @@ -420,11 +594,12 @@ export default function TokenPicker({ ); })} + {featuredOptions.length || nonFeaturedOptions.length ? null : ( +
+ No results found +
+ )}
- ) : ( -
- No results found -
)}
diff --git a/bridge_ui/src/components/Transfer/Source.tsx b/bridge_ui/src/components/Transfer/Source.tsx index a04dbb6b..21baa1bf 100644 --- a/bridge_ui/src/components/Transfer/Source.tsx +++ b/bridge_ui/src/components/Transfer/Source.tsx @@ -4,12 +4,12 @@ import { CHAIN_ID_SOLANA, } from "@certusone/wormhole-sdk"; import { getAddress } from "@ethersproject/address"; -import { Button, makeStyles } from "@material-ui/core"; -import { Link } from "react-router-dom"; -import { VerifiedUser } from "@material-ui/icons"; -import { useCallback } from "react"; +import { Button, makeStyles, Typography } from "@material-ui/core"; +import { ArrowForward, VerifiedUser } from "@material-ui/icons"; +import { useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router"; +import { Link } from "react-router-dom"; import useIsWalletReady from "../../hooks/useIsWalletReady"; import { selectTransferAmount, @@ -19,11 +19,13 @@ import { selectTransferSourceChain, selectTransferSourceError, selectTransferSourceParsedTokenAccount, + selectTransferTargetChain, } from "../../store/selectors"; import { incrementStep, setAmount, setSourceChain, + setTargetChain, } from "../../store/transferSlice"; import { BSC_MIGRATION_ASSET_MAP, @@ -40,6 +42,24 @@ import StepDescription from "../StepDescription"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; const useStyles = makeStyles((theme) => ({ + chainSelectWrapper: { + display: "flex", + alignItems: "center", + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + }, + }, + chainSelectContainer: { + flexBasis: "100%", + [theme.breakpoints.down("sm")]: { + width: "100%", + }, + }, + chainSelectArrow: { + position: "relative", + top: "12px", + [theme.breakpoints.down("sm")]: { transform: "rotate(90deg)" }, + }, transferField: { marginTop: theme.spacing(5), }, @@ -50,6 +70,11 @@ function Source() { const dispatch = useDispatch(); const history = useHistory(); const sourceChain = useSelector(selectTransferSourceChain); + const targetChain = useSelector(selectTransferTargetChain); + const targetChainOptions = useMemo( + () => CHAINS.filter((c) => c.id !== sourceChain), + [sourceChain] + ); const parsedTokenAccount = useSelector( selectTransferSourceParsedTokenAccount ); @@ -91,6 +116,12 @@ function Source() { }, [dispatch] ); + const handleTargetChange = useCallback( + (event) => { + dispatch(setTargetChain(event.target.value)); + }, + [dispatch] + ); const handleAmountChange = useCallback( (event) => { dispatch(setAmount(event.target.value)); @@ -124,15 +155,35 @@ function Source() { - +
+
+ Source + +
+
+ +
+
+ Target + +
+
{isReady || uiAmountString ? (
diff --git a/bridge_ui/src/components/Transfer/Target.tsx b/bridge_ui/src/components/Transfer/Target.tsx index 506a0691..be39ac55 100644 --- a/bridge_ui/src/components/Transfer/Target.tsx +++ b/bridge_ui/src/components/Transfer/Target.tsx @@ -125,7 +125,7 @@ function Target() { fullWidth value={targetChain} onChange={handleTargetChange} - disabled={shouldLockFields} + disabled={true} chains={chains} /> diff --git a/bridge_ui/src/hooks/useMarketsMap.ts b/bridge_ui/src/hooks/useMarketsMap.ts new file mode 100644 index 00000000..f7598b3c --- /dev/null +++ b/bridge_ui/src/hooks/useMarketsMap.ts @@ -0,0 +1,72 @@ +import { ChainId } from "@certusone/wormhole-sdk"; +import { Dispatch } from "@reduxjs/toolkit"; +import axios from "axios"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { DataWrapper } from "../store/helpers"; +import { selectMarketsMap } from "../store/selectors"; +import { + errorMarketsMap, + fetchMarketsMap, + receiveMarketsMap, +} from "../store/tokenSlice"; +import { FEATURED_MARKETS_JSON_URL } from "../utils/consts"; + +export type MarketsMap = { + markets?: { + [index: string]: { + name: string; + link: string; + }; + }; + tokens?: { + [key in ChainId]?: { + [index: string]: { + symbol: string; + logo: string; + }; + }; + }; + tokenMarkets?: { + [key in ChainId]?: { + [key in ChainId]?: { + [index: string]: { + symbol: string; + logo: string; + markets: string[]; + }; + }; + }; + }; +}; + +const useMarketsMap = (shouldFire: boolean): DataWrapper => { + const marketsMap = useSelector(selectMarketsMap); + const dispatch = useDispatch(); + const internalShouldFire = + shouldFire && + (marketsMap.data === undefined || + (marketsMap.data === null && !marketsMap.isFetching)); + + useEffect(() => { + if (internalShouldFire) { + getMarketsMap(dispatch); + } + }, [internalShouldFire, dispatch]); + + return marketsMap; +}; + +const getMarketsMap = (dispatch: Dispatch) => { + dispatch(fetchMarketsMap()); + axios.get(FEATURED_MARKETS_JSON_URL).then( + (response) => { + dispatch(receiveMarketsMap(response.data as MarketsMap)); + }, + (error) => { + dispatch(errorMarketsMap("Failed to retrieve the Terra Token List.")); + } + ); +}; + +export default useMarketsMap; diff --git a/bridge_ui/src/store/selectors.ts b/bridge_ui/src/store/selectors.ts index cd544745..f4f0fa52 100644 --- a/bridge_ui/src/store/selectors.ts +++ b/bridge_ui/src/store/selectors.ts @@ -292,3 +292,7 @@ export const selectSolanaTokenMap = (state: RootState) => { export const selectTerraTokenMap = (state: RootState) => { return state.tokens.terraTokenMap; }; + +export const selectMarketsMap = (state: RootState) => { + return state.tokens.marketsMap; +}; diff --git a/bridge_ui/src/store/tokenSlice.ts b/bridge_ui/src/store/tokenSlice.ts index ee14ea9f..120b2a66 100644 --- a/bridge_ui/src/store/tokenSlice.ts +++ b/bridge_ui/src/store/tokenSlice.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { TokenInfo } from "@solana/spl-token-registry"; import { TerraTokenMap } from "../hooks/useTerraTokenMap"; +import { MarketsMap } from "../hooks/useMarketsMap"; import { DataWrapper, errorDataWrapper, @@ -12,11 +13,13 @@ import { export interface TokenMetadataState { solanaTokenMap: DataWrapper; terraTokenMap: DataWrapper; //TODO make a decent type for this. + marketsMap: DataWrapper; } const initialState: TokenMetadataState = { solanaTokenMap: getEmptyDataWrapper(), terraTokenMap: getEmptyDataWrapper(), + marketsMap: getEmptyDataWrapper(), }; export const tokenSlice = createSlice({ @@ -43,6 +46,16 @@ export const tokenSlice = createSlice({ state.terraTokenMap = errorDataWrapper(action.payload); }, + receiveMarketsMap: (state, action: PayloadAction) => { + state.marketsMap = receiveDataWrapper(action.payload); + }, + fetchMarketsMap: (state) => { + state.marketsMap = fetchDataWrapper(); + }, + errorMarketsMap: (state, action: PayloadAction) => { + state.marketsMap = errorDataWrapper(action.payload); + }, + reset: () => initialState, }, }); @@ -54,6 +67,9 @@ export const { receiveTerraTokenMap, fetchTerraTokenMap, errorTerraTokenMap, + receiveMarketsMap, + fetchMarketsMap, + errorMarketsMap, reset, } = tokenSlice.actions; diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 363b00db..eac14e3f 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -136,9 +136,7 @@ export const WORMHOLE_RPC_HOSTS = "https://wormhole-v2-mainnet-api.chainlayer.network", ] : CLUSTER === "testnet" - ? [ - "https://wormhole-v2-testnet-api.certus.one", - ] + ? ["https://wormhole-v2-testnet-api.certus.one"] : ["http://localhost:7071"]; export const ETH_NETWORK_CHAIN_ID = CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337; @@ -159,7 +157,7 @@ export const SOLANA_HOST = process.env.REACT_APP_SOLANA_API_URL : CLUSTER === "mainnet" ? clusterApiUrl("mainnet-beta") : CLUSTER === "testnet" - ? clusterApiUrl("testnet") + ? clusterApiUrl("devnet") : "http://localhost:8899"; export const TERRA_HOST = @@ -184,82 +182,82 @@ export const ETH_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" : CLUSTER === "testnet" - ? "0x44F3e7c20850B3B5f3031114726A9240911D912a" + ? "0xC0231E0957596A90004119f4254aff364f6f1002" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" ); export const ETH_NFT_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE" : CLUSTER === "testnet" - ? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address + ? "0x5B78d166Fc3C2c99783B60b959dC35E316EBB5e7" : "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" ); export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x3ee18B2214AFF97000D974cf647E7C347E8fa585" : CLUSTER === "testnet" - ? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6" + ? "0xc59072C84ECD13DbF30856021C0a33868121Cb9d" : "0x0290FB167208Af455bB137780163b7B7a9a10C16" ); export const BSC_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" : CLUSTER === "testnet" - ? "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" // TODO: test address + ? "0x61D9309dC73CcAC3c639aeC497A11320C5A72074" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" ); export const BSC_NFT_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE" : CLUSTER === "testnet" - ? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address + ? "0x55A525D72f4b08762991e4ECDB1aDb5Ab55dFf37" : "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" ); export const BSC_TOKEN_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7" : CLUSTER === "testnet" - ? "0x0290FB167208Af455bB137780163b7B7a9a10C16" // TODO: test address + ? "0xC708B76f0C28040A0f852DbacB26375eDB071c1D" : "0x0290FB167208Af455bB137780163b7B7a9a10C16" ); export const POLYGON_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7" : CLUSTER === "testnet" - ? "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" // TODO: test address + ? "0x61D9309dC73CcAC3c639aeC497A11320C5A72074" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" ); export const POLYGON_NFT_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x90BBd86a6Fe93D3bc3ed6335935447E75fAb7fCf" : CLUSTER === "testnet" - ? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address + ? "0x55A525D72f4b08762991e4ECDB1aDb5Ab55dFf37" : "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" ); export const POLYGON_TOKEN_BRIDGE_ADDRESS = getAddress( CLUSTER === "mainnet" ? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE" : CLUSTER === "testnet" - ? "0x0290FB167208Af455bB137780163b7B7a9a10C16" // TODO: test address + ? "0xC708B76f0C28040A0f852DbacB26375eDB071c1D" : "0x0290FB167208Af455bB137780163b7B7a9a10C16" ); export const SOL_BRIDGE_ADDRESS = CLUSTER === "mainnet" ? "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" : CLUSTER === "testnet" - ? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb" + ? "FvXhjZdGJT4JdaTJHcPtvogBsc1kbgiFo3utK6mZZzdP" : "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; export const SOL_NFT_BRIDGE_ADDRESS = CLUSTER === "mainnet" ? "WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD" : CLUSTER === "testnet" - ? "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA" // TODO: test address + ? "pnfZ3u1LPAaupt8YoZkxJkWUDoCZxs4XJkGibDQz7fW0" : "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA"; export const SOL_TOKEN_BRIDGE_ADDRESS = CLUSTER === "mainnet" ? "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb" : CLUSTER === "testnet" - ? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg" + ? "GQemgcTaC6jojXS4pH4YPDD72b6RPsDhNPSjmxMfYcet" : "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"; export const SOL_CUSTODY_ADDRESS = @@ -272,13 +270,13 @@ export const TERRA_BRIDGE_ADDRESS = CLUSTER === "mainnet" ? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5" : CLUSTER === "testnet" - ? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5" + ? "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v" : "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"; export const TERRA_TOKEN_BRIDGE_ADDRESS = CLUSTER === "mainnet" ? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf" : CLUSTER === "testnet" - ? "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4" + ? "terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a" : "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"; export const getBridgeAddressForChain = (chainId: ChainId) => @@ -666,3 +664,5 @@ export const AVAILABLE_MARKETS_URL = "https://docs.wormholenetwork.com/wormhole/overview-liquid-markets"; export const SOLANA_SYSTEM_PROGRAM_ADDRESS = "11111111111111111111111111111111"; +export const FEATURED_MARKETS_JSON_URL = + "https://raw.githubusercontent.com/certusone/wormhole-token-list/main/src/markets.json";