bridge_ui: featured markets

Change-Id: I72a48fd3f1ff674d64aad58c9a0020585d234c82
This commit is contained in:
Evan Gray 2021-11-20 00:43:47 -05:00
parent 54ebc6481f
commit 793a4b4f5f
8 changed files with 372 additions and 74 deletions

View File

@ -146,36 +146,16 @@ export default function EvmTokenPicker(
} catch (e) { } catch (e) {
//For now, just swallow this one. //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); const migration = isMigrationEligible(account.publicKey);
if (v1 === true && !migration) { if (v1 === true && !migration) {
throw new Error( throw new Error(
"Wormhole v1 assets cannot be transferred with this bridge." "Wormhole v1 assets cannot be transferred with this bridge."
); );
} }
onChange(newAccount); onChange(account);
return Promise.resolve(); return Promise.resolve();
}, },
[chainId, onChange, provider, isMigrationEligible, getAddress] [chainId, onChange, provider, isMigrationEligible]
); );
const RenderComp = useCallback( const RenderComp = useCallback(

View File

@ -7,7 +7,9 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Divider,
IconButton, IconButton,
Link,
List, List,
ListItem, ListItem,
makeStyles, makeStyles,
@ -15,11 +17,16 @@ import {
Tooltip, Tooltip,
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import { InfoOutlined, Launch } from "@material-ui/icons";
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import RefreshIcon from "@material-ui/icons/Refresh"; import RefreshIcon from "@material-ui/icons/Refresh";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import useMarketsMap from "../../hooks/useMarketsMap";
import { NFTParsedTokenAccount } from "../../store/nftSlice"; 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 { shortenAddress } from "../../utils/solana";
import NFTViewer from "./NFTViewer"; import NFTViewer from "./NFTViewer";
@ -62,6 +69,11 @@ const useStyles = makeStyles((theme) =>
"&$tokenImageContainer": { "&$tokenImageContainer": {
maxWidth: 40, maxWidth: 40,
}, },
"&$tokenMarketsList": {
marginTop: theme.spacing(-0.5),
marginLeft: 0,
flexBasis: "100%",
},
"&:last-child": { "&:last-child": {
textAlign: "right", textAlign: "right",
}, },
@ -78,6 +90,15 @@ const useStyles = makeStyles((theme) =>
tokenImage: { tokenImage: {
maxHeight: "2.5rem", //Eyeballing this based off the text size 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: { migrationAlert: {
width: "100%", width: "100%",
"& .MuiAlert-message": { "& .MuiAlert-message": {
@ -107,12 +128,17 @@ export const balancePretty = (uiString: string) => {
} }
}; };
const noClickThrough = (e: any) => {
e.stopPropagation();
};
export const BasicAccountRender = ( export const BasicAccountRender = (
account: NFTParsedTokenAccount, account: MarketParsedTokenAccount,
isMigrationEligible: (address: string) => boolean, isMigrationEligible: (address: string) => boolean,
nft: boolean, nft: boolean,
displayBalance?: (account: NFTParsedTokenAccount) => boolean displayBalance?: (account: NFTParsedTokenAccount) => boolean
) => { ) => {
const { data: marketsData } = useMarketsMap(false);
const classes = useStyles(); const classes = useStyles();
const mintPrettyString = shortenAddress(account.mintKey); const mintPrettyString = shortenAddress(account.mintKey);
const uri = nft ? account.image_256 : account.logo || account.uri; const uri = nft ? account.image_256 : account.logo || account.uri;
@ -139,6 +165,27 @@ export const BasicAccountRender = (
const tokenContent = ( const tokenContent = (
<div className={classes.tokenOverviewContainer}> <div className={classes.tokenOverviewContainer}>
{account.markets ? (
<div className={classes.tokenMarketsList}>
{account.markets.map((market) =>
marketsData?.markets?.[market] ? (
<Button
key={market}
size="small"
variant="outlined"
color="secondary"
endIcon={<Launch />}
href={marketsData.markets[market].link}
target="_blank"
rel="noopener noreferrer"
onClick={noClickThrough}
>
{marketsData.markets[market].name}
</Button>
) : null
)}
</div>
) : null}
<div className={classes.tokenImageContainer}> <div className={classes.tokenImageContainer}>
{uri && <img alt="" className={classes.tokenImage} src={uri} />} {uri && <img alt="" className={classes.tokenImage} src={uri} />}
</div> </div>
@ -185,6 +232,10 @@ export const BasicAccountRender = (
: tokenContent; : tokenContent;
}; };
interface MarketParsedTokenAccount extends NFTParsedTokenAccount {
markets?: string[];
}
export default function TokenPicker({ export default function TokenPicker({
value, value,
options, options,
@ -229,6 +280,9 @@ export default function TokenPicker({
const [dialogIsOpen, setDialogIsOpen] = useState(false); const [dialogIsOpen, setDialogIsOpen] = useState(false);
const [selectionError, setSelectionError] = useState(""); const [selectionError, setSelectionError] = useState("");
const targetChain = useSelector(selectTransferTargetChain);
const { data: marketsData } = useMarketsMap(true);
const openDialog = useCallback(() => { const openDialog = useCallback(() => {
setHolderString(""); setHolderString("");
setSelectionError(""); setSelectionError("");
@ -242,16 +296,31 @@ export default function TokenPicker({
const handleSelectOption = useCallback( const handleSelectOption = useCallback(
async (option: NFTParsedTokenAccount) => { async (option: NFTParsedTokenAccount) => {
setSelectionError(""); setSelectionError("");
onChange(option).then( let newOption = null;
() => { try {
closeDialog(); //Covalent balances tend to be stale, so we make an attempt to correct it at selection time.
}, if (getAddress && !option.isNativeAsset) {
(error) => { newOption = await getAddress(option.mintKey, option.tokenId);
setSelectionError(error?.message || "Error verifying the token."); 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(() => { const resetAccountsWrapper = useCallback(() => {
@ -261,8 +330,8 @@ export default function TokenPicker({
resetAccounts && resetAccounts(); resetAccounts && resetAccounts();
}, [resetAccounts]); }, [resetAccounts]);
const filteredOptions = useMemo(() => { const searchFilter = useCallback(
return options.filter((option: NFTParsedTokenAccount) => { (option: NFTParsedTokenAccount) => {
if (!holderString) { if (!holderString) {
return true; return true;
} }
@ -277,8 +346,61 @@ export default function TokenPicker({
).toLowerCase(); ).toLowerCase();
const searchString = holderString.toLowerCase(); const searchString = holderString.toLowerCase();
return optionString.includes(searchString); 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( const localFind = useCallback(
(address: string, tokenIdHolderString: string) => { (address: string, tokenIdHolderString: string) => {
@ -383,6 +505,17 @@ export default function TokenPicker({
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent className={classes.dialogContent}> <DialogContent className={classes.dialogContent}>
<Alert severity="info">
You should always check for markets and liquidity before sending
tokens.{" "}
<Link
href={AVAILABLE_MARKETS_URL}
target="_blank"
rel="noopener noreferrer"
>
Click here to see available markets for wrapped tokens.
</Link>
</Alert>
<TextField <TextField
variant="outlined" variant="outlined"
label="Search name or paste address" label="Search name or paste address"
@ -405,11 +538,52 @@ export default function TokenPicker({
localLoader localLoader
) : loadingError || selectionError ? ( ) : loadingError || selectionError ? (
displayLocalError displayLocalError
) : filteredOptions.length ? ( ) : (
<List className={classes.tokenList}> <List component="div" className={classes.tokenList}>
{filteredOptions.map((option) => { {featuredOptions.length ? (
<>
<Typography variant="subtitle2" gutterBottom>
Featured {CHAINS_BY_ID[chainId].name} &gt;{" "}
{CHAINS_BY_ID[targetChain].name} markets{" "}
<Tooltip
title={`Markets for these ${CHAINS_BY_ID[chainId].name} tokens exist for the corresponding tokens on ${CHAINS_BY_ID[targetChain].name}`}
>
<InfoOutlined
fontSize="small"
style={{ verticalAlign: "text-bottom" }}
/>
</Tooltip>
</Typography>
{featuredOptions.map((option) => {
return ( return (
<ListItem <ListItem
component="div"
button
onClick={() => handleSelectOption(option)}
key={
option.publicKey +
option.mintKey +
(option.tokenId || "")
}
>
<RenderOption account={option} />
</ListItem>
);
})}
{nonFeaturedOptions.length ? (
<>
<Divider style={{ marginTop: 8, marginBottom: 16 }} />
<Typography variant="subtitle2" gutterBottom>
Other Assets
</Typography>
</>
) : null}
</>
) : null}
{nonFeaturedOptions.map((option) => {
return (
<ListItem
component="div"
button button
onClick={() => handleSelectOption(option)} onClick={() => handleSelectOption(option)}
key={ key={
@ -420,12 +594,13 @@ export default function TokenPicker({
</ListItem> </ListItem>
); );
})} })}
</List> {featuredOptions.length || nonFeaturedOptions.length ? null : (
) : (
<div className={classes.alignCenter}> <div className={classes.alignCenter}>
<Typography>No results found</Typography> <Typography>No results found</Typography>
</div> </div>
)} )}
</List>
)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -4,12 +4,12 @@ import {
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { getAddress } from "@ethersproject/address"; import { getAddress } from "@ethersproject/address";
import { Button, makeStyles } from "@material-ui/core"; import { Button, makeStyles, Typography } from "@material-ui/core";
import { Link } from "react-router-dom"; import { ArrowForward, VerifiedUser } from "@material-ui/icons";
import { VerifiedUser } from "@material-ui/icons"; import { useCallback, useMemo } from "react";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { Link } from "react-router-dom";
import useIsWalletReady from "../../hooks/useIsWalletReady"; import useIsWalletReady from "../../hooks/useIsWalletReady";
import { import {
selectTransferAmount, selectTransferAmount,
@ -19,11 +19,13 @@ import {
selectTransferSourceChain, selectTransferSourceChain,
selectTransferSourceError, selectTransferSourceError,
selectTransferSourceParsedTokenAccount, selectTransferSourceParsedTokenAccount,
selectTransferTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { import {
incrementStep, incrementStep,
setAmount, setAmount,
setSourceChain, setSourceChain,
setTargetChain,
} from "../../store/transferSlice"; } from "../../store/transferSlice";
import { import {
BSC_MIGRATION_ASSET_MAP, BSC_MIGRATION_ASSET_MAP,
@ -40,6 +42,24 @@ import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
const useStyles = makeStyles((theme) => ({ 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: { transferField: {
marginTop: theme.spacing(5), marginTop: theme.spacing(5),
}, },
@ -50,6 +70,11 @@ function Source() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const sourceChain = useSelector(selectTransferSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const targetChain = useSelector(selectTransferTargetChain);
const targetChainOptions = useMemo(
() => CHAINS.filter((c) => c.id !== sourceChain),
[sourceChain]
);
const parsedTokenAccount = useSelector( const parsedTokenAccount = useSelector(
selectTransferSourceParsedTokenAccount selectTransferSourceParsedTokenAccount
); );
@ -91,6 +116,12 @@ function Source() {
}, },
[dispatch] [dispatch]
); );
const handleTargetChange = useCallback(
(event) => {
dispatch(setTargetChain(event.target.value));
},
[dispatch]
);
const handleAmountChange = useCallback( const handleAmountChange = useCallback(
(event) => { (event) => {
dispatch(setAmount(event.target.value)); dispatch(setAmount(event.target.value));
@ -124,6 +155,9 @@ function Source() {
</div> </div>
</div> </div>
</StepDescription> </StepDescription>
<div className={classes.chainSelectWrapper}>
<div className={classes.chainSelectContainer}>
<Typography variant="caption">Source</Typography>
<ChainSelect <ChainSelect
select select
variant="outlined" variant="outlined"
@ -133,6 +167,23 @@ function Source() {
disabled={shouldLockFields} disabled={shouldLockFields}
chains={CHAINS} chains={CHAINS}
/> />
</div>
<div className={classes.chainSelectArrow}>
<ArrowForward style={{ margin: "0px 8px" }} />
</div>
<div className={classes.chainSelectContainer}>
<Typography variant="caption">Target</Typography>
<ChainSelect
variant="outlined"
select
fullWidth
value={targetChain}
onChange={handleTargetChange}
disabled={shouldLockFields}
chains={targetChainOptions}
/>
</div>
</div>
<KeyAndBalance chainId={sourceChain} /> <KeyAndBalance chainId={sourceChain} />
{isReady || uiAmountString ? ( {isReady || uiAmountString ? (
<div className={classes.transferField}> <div className={classes.transferField}>

View File

@ -125,7 +125,7 @@ function Target() {
fullWidth fullWidth
value={targetChain} value={targetChain}
onChange={handleTargetChange} onChange={handleTargetChange}
disabled={shouldLockFields} disabled={true}
chains={chains} chains={chains}
/> />
<KeyAndBalance chainId={targetChain} /> <KeyAndBalance chainId={targetChain} />

View File

@ -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<MarketsMap> => {
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;

View File

@ -292,3 +292,7 @@ export const selectSolanaTokenMap = (state: RootState) => {
export const selectTerraTokenMap = (state: RootState) => { export const selectTerraTokenMap = (state: RootState) => {
return state.tokens.terraTokenMap; return state.tokens.terraTokenMap;
}; };
export const selectMarketsMap = (state: RootState) => {
return state.tokens.marketsMap;
};

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { TokenInfo } from "@solana/spl-token-registry"; import { TokenInfo } from "@solana/spl-token-registry";
import { TerraTokenMap } from "../hooks/useTerraTokenMap"; import { TerraTokenMap } from "../hooks/useTerraTokenMap";
import { MarketsMap } from "../hooks/useMarketsMap";
import { import {
DataWrapper, DataWrapper,
errorDataWrapper, errorDataWrapper,
@ -12,11 +13,13 @@ import {
export interface TokenMetadataState { export interface TokenMetadataState {
solanaTokenMap: DataWrapper<TokenInfo[]>; solanaTokenMap: DataWrapper<TokenInfo[]>;
terraTokenMap: DataWrapper<TerraTokenMap>; //TODO make a decent type for this. terraTokenMap: DataWrapper<TerraTokenMap>; //TODO make a decent type for this.
marketsMap: DataWrapper<MarketsMap>;
} }
const initialState: TokenMetadataState = { const initialState: TokenMetadataState = {
solanaTokenMap: getEmptyDataWrapper(), solanaTokenMap: getEmptyDataWrapper(),
terraTokenMap: getEmptyDataWrapper(), terraTokenMap: getEmptyDataWrapper(),
marketsMap: getEmptyDataWrapper(),
}; };
export const tokenSlice = createSlice({ export const tokenSlice = createSlice({
@ -43,6 +46,16 @@ export const tokenSlice = createSlice({
state.terraTokenMap = errorDataWrapper(action.payload); state.terraTokenMap = errorDataWrapper(action.payload);
}, },
receiveMarketsMap: (state, action: PayloadAction<MarketsMap>) => {
state.marketsMap = receiveDataWrapper(action.payload);
},
fetchMarketsMap: (state) => {
state.marketsMap = fetchDataWrapper();
},
errorMarketsMap: (state, action: PayloadAction<string>) => {
state.marketsMap = errorDataWrapper(action.payload);
},
reset: () => initialState, reset: () => initialState,
}, },
}); });
@ -54,6 +67,9 @@ export const {
receiveTerraTokenMap, receiveTerraTokenMap,
fetchTerraTokenMap, fetchTerraTokenMap,
errorTerraTokenMap, errorTerraTokenMap,
receiveMarketsMap,
fetchMarketsMap,
errorMarketsMap,
reset, reset,
} = tokenSlice.actions; } = tokenSlice.actions;

View File

@ -136,9 +136,7 @@ export const WORMHOLE_RPC_HOSTS =
"https://wormhole-v2-mainnet-api.chainlayer.network", "https://wormhole-v2-mainnet-api.chainlayer.network",
] ]
: CLUSTER === "testnet" : CLUSTER === "testnet"
? [ ? ["https://wormhole-v2-testnet-api.certus.one"]
"https://wormhole-v2-testnet-api.certus.one",
]
: ["http://localhost:7071"]; : ["http://localhost:7071"];
export const ETH_NETWORK_CHAIN_ID = export const ETH_NETWORK_CHAIN_ID =
CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337; CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337;
@ -159,7 +157,7 @@ export const SOLANA_HOST = process.env.REACT_APP_SOLANA_API_URL
: CLUSTER === "mainnet" : CLUSTER === "mainnet"
? clusterApiUrl("mainnet-beta") ? clusterApiUrl("mainnet-beta")
: CLUSTER === "testnet" : CLUSTER === "testnet"
? clusterApiUrl("testnet") ? clusterApiUrl("devnet")
: "http://localhost:8899"; : "http://localhost:8899";
export const TERRA_HOST = export const TERRA_HOST =
@ -184,82 +182,82 @@ export const ETH_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" ? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0x44F3e7c20850B3B5f3031114726A9240911D912a" ? "0xC0231E0957596A90004119f4254aff364f6f1002"
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
); );
export const ETH_NFT_BRIDGE_ADDRESS = getAddress( export const ETH_NFT_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE" ? "0x6FFd7EdE62328b3Af38FCD61461Bbfc52F5651fE"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address ? "0x5B78d166Fc3C2c99783B60b959dC35E316EBB5e7"
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" : "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
); );
export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress( export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x3ee18B2214AFF97000D974cf647E7C347E8fa585" ? "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6" ? "0xc59072C84ECD13DbF30856021C0a33868121Cb9d"
: "0x0290FB167208Af455bB137780163b7B7a9a10C16" : "0x0290FB167208Af455bB137780163b7B7a9a10C16"
); );
export const BSC_BRIDGE_ADDRESS = getAddress( export const BSC_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" ? "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" // TODO: test address ? "0x61D9309dC73CcAC3c639aeC497A11320C5A72074"
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
); );
export const BSC_NFT_BRIDGE_ADDRESS = getAddress( export const BSC_NFT_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE" ? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address ? "0x55A525D72f4b08762991e4ECDB1aDb5Ab55dFf37"
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" : "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
); );
export const BSC_TOKEN_BRIDGE_ADDRESS = getAddress( export const BSC_TOKEN_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7" ? "0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0x0290FB167208Af455bB137780163b7B7a9a10C16" // TODO: test address ? "0xC708B76f0C28040A0f852DbacB26375eDB071c1D"
: "0x0290FB167208Af455bB137780163b7B7a9a10C16" : "0x0290FB167208Af455bB137780163b7B7a9a10C16"
); );
export const POLYGON_BRIDGE_ADDRESS = getAddress( export const POLYGON_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7" ? "0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" // TODO: test address ? "0x61D9309dC73CcAC3c639aeC497A11320C5A72074"
: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
); );
export const POLYGON_NFT_BRIDGE_ADDRESS = getAddress( export const POLYGON_NFT_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x90BBd86a6Fe93D3bc3ed6335935447E75fAb7fCf" ? "0x90BBd86a6Fe93D3bc3ed6335935447E75fAb7fCf"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" // TODO: test address ? "0x55A525D72f4b08762991e4ECDB1aDb5Ab55dFf37"
: "0x26b4afb60d6c903165150c6f0aa14f8016be4aec" : "0x26b4afb60d6c903165150c6f0aa14f8016be4aec"
); );
export const POLYGON_TOKEN_BRIDGE_ADDRESS = getAddress( export const POLYGON_TOKEN_BRIDGE_ADDRESS = getAddress(
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE" ? "0x5a58505a96D1dbf8dF91cB21B54419FC36e93fdE"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "0x0290FB167208Af455bB137780163b7B7a9a10C16" // TODO: test address ? "0xC708B76f0C28040A0f852DbacB26375eDB071c1D"
: "0x0290FB167208Af455bB137780163b7B7a9a10C16" : "0x0290FB167208Af455bB137780163b7B7a9a10C16"
); );
export const SOL_BRIDGE_ADDRESS = export const SOL_BRIDGE_ADDRESS =
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" ? "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb" ? "FvXhjZdGJT4JdaTJHcPtvogBsc1kbgiFo3utK6mZZzdP"
: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; : "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
export const SOL_NFT_BRIDGE_ADDRESS = export const SOL_NFT_BRIDGE_ADDRESS =
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD" ? "WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA" // TODO: test address ? "pnfZ3u1LPAaupt8YoZkxJkWUDoCZxs4XJkGibDQz7fW0"
: "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA"; : "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA";
export const SOL_TOKEN_BRIDGE_ADDRESS = export const SOL_TOKEN_BRIDGE_ADDRESS =
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb" ? "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg" ? "GQemgcTaC6jojXS4pH4YPDD72b6RPsDhNPSjmxMfYcet"
: "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"; : "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE";
export const SOL_CUSTODY_ADDRESS = export const SOL_CUSTODY_ADDRESS =
@ -272,13 +270,13 @@ export const TERRA_BRIDGE_ADDRESS =
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5" ? "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5" ? "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v"
: "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"; : "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
export const TERRA_TOKEN_BRIDGE_ADDRESS = export const TERRA_TOKEN_BRIDGE_ADDRESS =
CLUSTER === "mainnet" CLUSTER === "mainnet"
? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf" ? "terra10nmmwe8r3g99a9newtqa7a75xfgs2e8z87r2sf"
: CLUSTER === "testnet" : CLUSTER === "testnet"
? "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4" ? "terra1pseddrv0yfsn76u4zxrjmtf45kdlmalswdv39a"
: "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"; : "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
export const getBridgeAddressForChain = (chainId: ChainId) => export const getBridgeAddressForChain = (chainId: ChainId) =>
@ -666,3 +664,5 @@ export const AVAILABLE_MARKETS_URL =
"https://docs.wormholenetwork.com/wormhole/overview-liquid-markets"; "https://docs.wormholenetwork.com/wormhole/overview-liquid-markets";
export const SOLANA_SYSTEM_PROGRAM_ADDRESS = "11111111111111111111111111111111"; 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";