bridge_ui: token verifier page
Change-Id: I48c48519caee597ae5455f7777326c6f07361886
This commit is contained in:
parent
afe4fad438
commit
be244f4632
|
@ -41,6 +41,7 @@ import { useBetaContext } from "./contexts/BetaContext";
|
|||
import { COLORS } from "./muiTheme";
|
||||
import { CLUSTER } from "./utils/consts";
|
||||
import Stats from "./components/Stats";
|
||||
import TokenOriginVerifier from "./components/TokenOriginVerifier";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
|
@ -270,6 +271,9 @@ function App() {
|
|||
<Route exact path="/nft-origin-verifier">
|
||||
<NFTOriginVerifier />
|
||||
</Route>
|
||||
<Route exact path="/token-origin-verifier">
|
||||
<TokenOriginVerifier />
|
||||
</Route>
|
||||
<Route exact path="/register">
|
||||
<Attest />
|
||||
</Route>
|
||||
|
|
|
@ -45,7 +45,9 @@ function Attest() {
|
|||
}, [preventNavigation]);
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<HeaderText>Token Registration</HeaderText>
|
||||
<HeaderText white small>
|
||||
Token Registration
|
||||
</HeaderText>
|
||||
<Alert severity="info">
|
||||
This form allows you to register a token on a new foreign chain. Tokens
|
||||
must be registered before they can be transferred.
|
||||
|
|
|
@ -25,13 +25,22 @@ const useStyles = makeStyles((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export default function HeaderText({ children }: { children: ReactChild }) {
|
||||
export default function HeaderText({
|
||||
children,
|
||||
white,
|
||||
small,
|
||||
}: {
|
||||
children: ReactChild;
|
||||
white?: boolean;
|
||||
small?: boolean;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.centeredContainer}>
|
||||
<Typography
|
||||
variant="h1"
|
||||
className={clsx(classes.header, classes.linearGradient)}
|
||||
variant={small ? "h2" : "h1"}
|
||||
component="h1"
|
||||
className={clsx(classes.header, { [classes.linearGradient]: !white })}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
|
|
|
@ -215,7 +215,9 @@ export default function NFTOriginVerifier() {
|
|||
return (
|
||||
<div>
|
||||
<Container maxWidth="md">
|
||||
<HeaderText>NFT Origin Verifier</HeaderText>
|
||||
<HeaderText white small>
|
||||
NFT Origin Verifier
|
||||
</HeaderText>
|
||||
</Container>
|
||||
<Container maxWidth="sm">
|
||||
<Card className={classes.mainCard}>
|
||||
|
|
|
@ -0,0 +1,372 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
nativeToHexString,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
Card,
|
||||
CircularProgress,
|
||||
Container,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useBetaContext } from "../contexts/BetaContext";
|
||||
import useFetchForeignAsset, {
|
||||
ForeignAssetInfo,
|
||||
} from "../hooks/useFetchForeignAsset";
|
||||
import useIsWalletReady from "../hooks/useIsWalletReady";
|
||||
import useMetadata from "../hooks/useMetadata";
|
||||
import useOriginalAsset, { OriginalAssetInfo } from "../hooks/useOriginalAsset";
|
||||
import { COLORS } from "../muiTheme";
|
||||
import { BETA_CHAINS, CHAINS, CHAINS_BY_ID } from "../utils/consts";
|
||||
import { isEVMChain } from "../utils/ethereum";
|
||||
import HeaderText from "./HeaderText";
|
||||
import KeyAndBalance from "./KeyAndBalance";
|
||||
import SmartAddress from "./SmartAddress";
|
||||
import { RegisterNowButtonCore } from "./Transfer/RegisterNowButton";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
flexBox: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
"& > *": {
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
mainCard: {
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||
},
|
||||
spacer: {
|
||||
height: theme.spacing(3),
|
||||
},
|
||||
centered: {
|
||||
textAlign: "center",
|
||||
},
|
||||
arrowIcon: {
|
||||
margin: "0 auto",
|
||||
fontSize: "70px",
|
||||
},
|
||||
resultContainer: {
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
function PrimaryAssetInfomation({
|
||||
lookupChain,
|
||||
lookupAsset,
|
||||
originChain,
|
||||
originAsset,
|
||||
showLoader,
|
||||
}: {
|
||||
lookupChain: ChainId;
|
||||
lookupAsset: string;
|
||||
originChain: ChainId;
|
||||
originAsset: string;
|
||||
showLoader: boolean;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const tokenArray = useMemo(() => [originAsset], [originAsset]);
|
||||
const metadata = useMetadata(originChain, tokenArray);
|
||||
const nativeContent = (
|
||||
<div>
|
||||
<Typography>{`This is not a Wormhole wrapped token.`}</Typography>
|
||||
</div>
|
||||
);
|
||||
const wrapped = (
|
||||
<div>
|
||||
<Typography>{`This is wrapped by Wormhole! Here is the original token: `}</Typography>
|
||||
<div className={classes.flexBox}>
|
||||
<Typography>{`Chain: ${CHAINS_BY_ID[originChain].name}`}</Typography>
|
||||
<div>
|
||||
<Typography component="div">
|
||||
{"Token: "}
|
||||
<SmartAddress
|
||||
address={originAsset}
|
||||
chainId={originChain}
|
||||
symbol={metadata.data?.get(originAsset)?.symbol}
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return lookupChain === originChain ? nativeContent : wrapped;
|
||||
}
|
||||
|
||||
function SecondaryAssetInformation({
|
||||
chainId,
|
||||
foreignAssetInfo,
|
||||
originAssetInfo,
|
||||
}: {
|
||||
chainId: ChainId;
|
||||
foreignAssetInfo?: ForeignAssetInfo;
|
||||
originAssetInfo?: OriginalAssetInfo;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const tokenArray: string[] = useMemo(() => {
|
||||
//Saved to a variable to help typescript cope
|
||||
const originAddress = originAssetInfo?.originAddress;
|
||||
return originAddress && chainId === originAssetInfo?.originChain
|
||||
? [originAddress]
|
||||
: foreignAssetInfo?.address
|
||||
? [foreignAssetInfo?.address]
|
||||
: [];
|
||||
}, [foreignAssetInfo, originAssetInfo, chainId]);
|
||||
const metadata = useMetadata(chainId, tokenArray);
|
||||
//TODO when this is the origin chain
|
||||
return !originAssetInfo ? null : chainId === originAssetInfo.originChain ? (
|
||||
<div>
|
||||
<Typography>{`Transferring to ${CHAINS_BY_ID[chainId].name} will unwrap the token:`}</Typography>
|
||||
<div className={classes.resultContainer}>
|
||||
<SmartAddress
|
||||
chainId={chainId}
|
||||
address={originAssetInfo.originAddress || undefined}
|
||||
symbol={
|
||||
metadata.data?.get(originAssetInfo.originAddress || "")?.symbol ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : !foreignAssetInfo ? null : foreignAssetInfo.doesExist === false ? (
|
||||
<div>
|
||||
<Typography>{`This token has not yet been registered on ${CHAINS_BY_ID[chainId].name}`}</Typography>
|
||||
<RegisterNowButtonCore
|
||||
originChain={originAssetInfo?.originChain || undefined}
|
||||
originAsset={
|
||||
nativeToHexString(
|
||||
originAssetInfo?.originAddress || undefined,
|
||||
originAssetInfo?.originChain || CHAIN_ID_SOLANA // this should exist
|
||||
) || undefined
|
||||
}
|
||||
targetChain={chainId}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Typography>When bridged, this asset becomes: </Typography>
|
||||
<div className={classes.resultContainer}>
|
||||
<SmartAddress
|
||||
chainId={chainId}
|
||||
address={foreignAssetInfo.address || undefined}
|
||||
symbol={
|
||||
metadata.data?.get(foreignAssetInfo.address || "")?.symbol ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TokenOriginVerifier() {
|
||||
const classes = useStyles();
|
||||
const isBeta = useBetaContext();
|
||||
|
||||
const [primaryLookupChain, setPrimaryLookupChain] = useState(CHAIN_ID_SOLANA);
|
||||
const [primaryLookupAsset, setPrimaryLookupAsset] = useState("");
|
||||
|
||||
const [secondaryLookupChain, setSecondaryLookupChain] =
|
||||
useState(CHAIN_ID_TERRA);
|
||||
|
||||
const primaryLookupChainOptions = useMemo(
|
||||
() => (isBeta ? CHAINS.filter((x) => !BETA_CHAINS.includes(x.id)) : CHAINS),
|
||||
[isBeta]
|
||||
);
|
||||
const secondaryLookupChainOptions = useMemo(
|
||||
() =>
|
||||
isBeta
|
||||
? CHAINS.filter(
|
||||
(x) => !BETA_CHAINS.includes(x.id) && x.id !== primaryLookupChain
|
||||
)
|
||||
: CHAINS.filter((x) => x.id !== primaryLookupChain),
|
||||
[isBeta, primaryLookupChain]
|
||||
);
|
||||
|
||||
const handlePrimaryLookupChainChange = useCallback(
|
||||
(e) => {
|
||||
setPrimaryLookupChain(e.target.value);
|
||||
if (secondaryLookupChain === e.target.value) {
|
||||
setSecondaryLookupChain(
|
||||
e.target.value === CHAIN_ID_SOLANA ? CHAIN_ID_TERRA : CHAIN_ID_SOLANA
|
||||
);
|
||||
}
|
||||
setPrimaryLookupAsset("");
|
||||
},
|
||||
[secondaryLookupChain]
|
||||
);
|
||||
const handleSecondaryLookupChainChange = useCallback((e) => {
|
||||
setSecondaryLookupChain(e.target.value);
|
||||
}, []);
|
||||
const handlePrimaryLookupAssetChange = useCallback((event) => {
|
||||
setPrimaryLookupAsset(event.target.value);
|
||||
}, []);
|
||||
|
||||
const originInfo = useOriginalAsset(
|
||||
primaryLookupChain,
|
||||
primaryLookupAsset,
|
||||
false
|
||||
);
|
||||
const foreignAssetInfo = useFetchForeignAsset(
|
||||
originInfo.data?.originChain || 1,
|
||||
originInfo.data?.originAddress || "",
|
||||
secondaryLookupChain
|
||||
);
|
||||
|
||||
const primaryWalletIsActive = !originInfo.data;
|
||||
const secondaryWalletIsActive = !primaryWalletIsActive;
|
||||
|
||||
const primaryWallet = useIsWalletReady(
|
||||
primaryLookupChain,
|
||||
primaryWalletIsActive
|
||||
);
|
||||
const secondaryWallet = useIsWalletReady(
|
||||
secondaryLookupChain,
|
||||
secondaryWalletIsActive
|
||||
);
|
||||
|
||||
const primaryWalletError =
|
||||
isEVMChain(primaryLookupChain) &&
|
||||
primaryLookupAsset &&
|
||||
!originInfo.data &&
|
||||
!originInfo.error &&
|
||||
(!primaryWallet.isReady ? primaryWallet.statusMessage : "");
|
||||
const originError = originInfo.error;
|
||||
const primaryError = primaryWalletError || originError;
|
||||
|
||||
const secondaryWalletError =
|
||||
isEVMChain(secondaryLookupChain) &&
|
||||
originInfo.data?.originAddress &&
|
||||
originInfo.data?.originChain &&
|
||||
!foreignAssetInfo.data &&
|
||||
(!secondaryWallet.isReady ? secondaryWallet.statusMessage : "");
|
||||
const foreignError = foreignAssetInfo.error;
|
||||
const secondaryError = secondaryWalletError || foreignError;
|
||||
|
||||
const primaryContent = (
|
||||
<>
|
||||
<Typography variant="h5">Source Information</Typography>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
Enter a token from any supported chain to get started.
|
||||
</Typography>
|
||||
<div className={classes.spacer} />
|
||||
<TextField
|
||||
select
|
||||
variant="outlined"
|
||||
label="Chain"
|
||||
value={primaryLookupChain}
|
||||
onChange={handlePrimaryLookupChainChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
>
|
||||
{primaryLookupChainOptions.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
label="Paste an address"
|
||||
value={primaryLookupAsset}
|
||||
onChange={handlePrimaryLookupAssetChange}
|
||||
/>
|
||||
<div className={classes.centered}>
|
||||
{isEVMChain(primaryLookupChain) ? (
|
||||
<KeyAndBalance chainId={primaryLookupChain} />
|
||||
) : null}
|
||||
{primaryError ? (
|
||||
<Typography color="error">{primaryError}</Typography>
|
||||
) : null}
|
||||
<div className={classes.spacer} />
|
||||
{originInfo.isFetching ? (
|
||||
<CircularProgress />
|
||||
) : originInfo.data?.originChain && originInfo.data.originAddress ? (
|
||||
<PrimaryAssetInfomation
|
||||
lookupAsset={primaryLookupAsset}
|
||||
lookupChain={primaryLookupChain}
|
||||
originChain={originInfo.data.originChain}
|
||||
originAsset={originInfo.data.originAddress}
|
||||
showLoader={originInfo.isFetching}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const secondaryContent = originInfo.data ? (
|
||||
<>
|
||||
<Typography variant="h5">Bridge Results</Typography>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
Select a chain to see the result of bridging this token.
|
||||
</Typography>
|
||||
<div className={classes.spacer} />
|
||||
<TextField
|
||||
select
|
||||
variant="outlined"
|
||||
label="Other Chain"
|
||||
value={secondaryLookupChain}
|
||||
onChange={handleSecondaryLookupChainChange}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
>
|
||||
{secondaryLookupChainOptions.map(({ id, name }) => (
|
||||
<MenuItem key={id} value={id}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<div className={classes.centered}>
|
||||
{isEVMChain(secondaryLookupChain) ? (
|
||||
<KeyAndBalance chainId={secondaryLookupChain} />
|
||||
) : null}
|
||||
{secondaryError ? (
|
||||
<Typography color="error">{secondaryError}</Typography>
|
||||
) : null}
|
||||
<div className={classes.spacer} />
|
||||
{foreignAssetInfo.isFetching ? (
|
||||
<CircularProgress />
|
||||
) : originInfo.data?.originChain && originInfo.data.originAddress ? (
|
||||
<SecondaryAssetInformation
|
||||
foreignAssetInfo={foreignAssetInfo.data || undefined}
|
||||
originAssetInfo={originInfo.data || undefined}
|
||||
chainId={secondaryLookupChain}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const content = (
|
||||
<div>
|
||||
<Container maxWidth="md" className={classes.centered}>
|
||||
<HeaderText white small>
|
||||
Token Origin Verifier
|
||||
</HeaderText>
|
||||
<div className={classes.spacer} />
|
||||
</Container>
|
||||
<Container maxWidth="sm">
|
||||
<Card className={classes.mainCard}>{primaryContent}</Card>
|
||||
{secondaryContent ? (
|
||||
<>
|
||||
<div className={classes.centered}>
|
||||
<ArrowDropDownIcon className={classes.arrowIcon} />
|
||||
</div>
|
||||
<Card className={classes.mainCard}>{secondaryContent}</Card>
|
||||
</>
|
||||
) : null}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
|
@ -14,14 +14,19 @@ import {
|
|||
selectTransferOriginChain,
|
||||
selectTransferTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { hexToNativeString } from "@certusone/wormhole-sdk";
|
||||
import { ChainId, hexToNativeString } from "@certusone/wormhole-sdk";
|
||||
|
||||
export default function RegisterNowButton() {
|
||||
export function RegisterNowButtonCore({
|
||||
originChain,
|
||||
originAsset,
|
||||
targetChain,
|
||||
}: {
|
||||
originChain: ChainId | undefined;
|
||||
originAsset: string | undefined;
|
||||
targetChain: ChainId;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const originChain = useSelector(selectTransferOriginChain);
|
||||
const originAsset = useSelector(selectTransferOriginAsset);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
// user might be in the middle of a different attest
|
||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
||||
const canSwitch = originChain && originAsset && !signedVAAHex;
|
||||
|
@ -48,3 +53,16 @@ export default function RegisterNowButton() {
|
|||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RegisterNowButton() {
|
||||
const originChain = useSelector(selectTransferOriginChain);
|
||||
const originAsset = useSelector(selectTransferOriginAsset);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
return (
|
||||
<RegisterNowButtonCore
|
||||
originChain={originChain}
|
||||
originAsset={originAsset}
|
||||
targetChain={targetChain}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
} 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 { useDispatch, useSelector } from "react-redux";
|
||||
import { useHistory } from "react-router";
|
||||
|
@ -106,7 +108,21 @@ function Source() {
|
|||
return (
|
||||
<>
|
||||
<StepDescription>
|
||||
Select tokens to send through the Wormhole Token Bridge.
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
Select tokens to send through the Wormhole Bridge.
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<div>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/token-origin-verifier"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
endIcon={<VerifiedUser />}
|
||||
>
|
||||
Token Origin Verifier
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StepDescription>
|
||||
<ChainSelect
|
||||
select
|
||||
|
|
|
@ -57,7 +57,7 @@ function useEvmMetadata(
|
|||
addresses: string[],
|
||||
chainId: ChainId
|
||||
): DataWrapper<Map<string, EvmMetadata>> {
|
||||
const { isReady } = useIsWalletReady(chainId);
|
||||
const { isReady } = useIsWalletReady(chainId, false);
|
||||
const { provider } = useEthereumProvider();
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { ethers } from "ethers";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { DataWrapper } from "../store/helpers";
|
||||
import {
|
||||
|
@ -35,39 +35,70 @@ function useFetchForeignAsset(
|
|||
foreignChain: ChainId
|
||||
): DataWrapper<ForeignAssetInfo> {
|
||||
const { provider, chainId: evmChainId } = useEthereumProvider();
|
||||
const { isReady, statusMessage } = useIsWalletReady(foreignChain);
|
||||
const { isReady } = useIsWalletReady(foreignChain, false);
|
||||
const correctEvmNetwork = getEvmChainId(foreignChain);
|
||||
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
|
||||
|
||||
const [assetAddress, setAssetAddress] = useState<string | null>(null);
|
||||
const [doesExist, setDoesExist] = useState(false);
|
||||
const [doesExist, setDoesExist] = useState<boolean | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const originAssetHex = useMemo(
|
||||
() => nativeToHexString(originAsset, originChain),
|
||||
[originAsset, originChain]
|
||||
);
|
||||
const originAssetHex = useMemo(() => {
|
||||
try {
|
||||
return nativeToHexString(originAsset, originChain);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [originAsset, originChain]);
|
||||
const [previousArgs, setPreviousArgs] = useState<{
|
||||
originChain: ChainId;
|
||||
originAsset: string;
|
||||
foreignChain: ChainId;
|
||||
} | null>(null);
|
||||
const argsEqual =
|
||||
!!previousArgs &&
|
||||
previousArgs.originChain === originChain &&
|
||||
previousArgs.originAsset === originAsset &&
|
||||
previousArgs.foreignChain === foreignChain;
|
||||
const setArgs = useCallback(() => {
|
||||
setPreviousArgs({ foreignChain, originChain, originAsset });
|
||||
}, [foreignChain, originChain, originAsset]);
|
||||
|
||||
const argumentError = useMemo(
|
||||
() =>
|
||||
!originChain ||
|
||||
!originAsset ||
|
||||
!foreignChain ||
|
||||
!originAssetHex ||
|
||||
foreignChain === originChain ||
|
||||
(isEVMChain(foreignChain) && !isReady) ||
|
||||
(isEVMChain(foreignChain) && !hasCorrectEvmNetwork),
|
||||
[isReady, foreignChain, originChain, hasCorrectEvmNetwork, originAssetHex]
|
||||
(isEVMChain(foreignChain) && !hasCorrectEvmNetwork) ||
|
||||
argsEqual,
|
||||
[
|
||||
isReady,
|
||||
foreignChain,
|
||||
originAsset,
|
||||
originChain,
|
||||
hasCorrectEvmNetwork,
|
||||
originAssetHex,
|
||||
argsEqual,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!argsEqual) {
|
||||
setAssetAddress(null);
|
||||
setError("");
|
||||
setDoesExist(null);
|
||||
setPreviousArgs(null);
|
||||
}
|
||||
if (argumentError || !originAssetHex) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setIsLoading(true);
|
||||
setAssetAddress(null);
|
||||
setError("");
|
||||
setDoesExist(false);
|
||||
try {
|
||||
const getterFunc: () => Promise<string | null> = isEVMChain(foreignChain)
|
||||
? () =>
|
||||
getForeignAssetEth(
|
||||
|
@ -96,9 +127,7 @@ function useFetchForeignAsset(
|
|||
);
|
||||
};
|
||||
|
||||
const promise = getterFunc();
|
||||
|
||||
promise
|
||||
getterFunc()
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
if (
|
||||
|
@ -108,10 +137,12 @@ function useFetchForeignAsset(
|
|||
result === ethers.constants.AddressZero
|
||||
)
|
||||
) {
|
||||
setArgs();
|
||||
setDoesExist(true);
|
||||
setIsLoading(false);
|
||||
setAssetAddress(result);
|
||||
} else {
|
||||
setArgs();
|
||||
setDoesExist(false);
|
||||
setIsLoading(false);
|
||||
setAssetAddress(null);
|
||||
|
@ -124,23 +155,36 @@ function useFetchForeignAsset(
|
|||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
}, [argumentError, foreignChain, originAssetHex, originChain, provider]);
|
||||
} catch (e) {
|
||||
//This catch mostly just detects poorly formatted addresses
|
||||
if (!cancelled) {
|
||||
setError("Could not retrieve the foreign asset.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
argumentError,
|
||||
foreignChain,
|
||||
originAssetHex,
|
||||
originChain,
|
||||
provider,
|
||||
setArgs,
|
||||
argsEqual,
|
||||
]);
|
||||
|
||||
const compoundError = useMemo(() => {
|
||||
return error
|
||||
? error
|
||||
: !isReady
|
||||
? statusMessage
|
||||
: argumentError
|
||||
? "Invalid arguments."
|
||||
: "";
|
||||
}, [error, isReady, statusMessage, argumentError]);
|
||||
return error ? error : "";
|
||||
}, [error]); //now swallows wallet errors
|
||||
|
||||
const output: DataWrapper<ForeignAssetInfo> = useMemo(
|
||||
() => ({
|
||||
error: compoundError,
|
||||
isFetching: isLoading,
|
||||
data: { address: assetAddress, doesExist },
|
||||
data:
|
||||
(assetAddress !== null && assetAddress !== undefined) ||
|
||||
(doesExist !== null && doesExist !== undefined)
|
||||
? { address: assetAddress, doesExist: !!doesExist }
|
||||
: null,
|
||||
receivedAt: null,
|
||||
}),
|
||||
[compoundError, isLoading, assetAddress, doesExist]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
getForeignAssetEth,
|
||||
|
@ -16,7 +17,7 @@ import { arrayify } from "@ethersproject/bytes";
|
|||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { ethers } from "ethers";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import {
|
||||
|
@ -76,10 +77,47 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
const isRecovery = useSelector(
|
||||
nft ? selectNFTIsRecovery : selectTransferIsRecovery
|
||||
);
|
||||
const [lastSuccessfulArgs, setLastSuccessfulArgs] = useState<{
|
||||
isSourceAssetWormholeWrapped: boolean | undefined;
|
||||
originChain: ChainId | undefined;
|
||||
originAsset: string | undefined;
|
||||
targetChain: ChainId;
|
||||
nft?: boolean;
|
||||
tokenId?: string;
|
||||
} | null>(null);
|
||||
const argsMatchLastSuccess =
|
||||
!!lastSuccessfulArgs &&
|
||||
lastSuccessfulArgs.isSourceAssetWormholeWrapped ===
|
||||
isSourceAssetWormholeWrapped &&
|
||||
lastSuccessfulArgs.originChain === originChain &&
|
||||
lastSuccessfulArgs.originAsset === originAsset &&
|
||||
lastSuccessfulArgs.targetChain === targetChain &&
|
||||
lastSuccessfulArgs.nft === nft &&
|
||||
lastSuccessfulArgs.tokenId === tokenId;
|
||||
const setArgs = useCallback(
|
||||
() =>
|
||||
setLastSuccessfulArgs({
|
||||
isSourceAssetWormholeWrapped,
|
||||
originChain,
|
||||
originAsset,
|
||||
targetChain,
|
||||
nft,
|
||||
tokenId,
|
||||
}),
|
||||
[
|
||||
isSourceAssetWormholeWrapped,
|
||||
originChain,
|
||||
originAsset,
|
||||
targetChain,
|
||||
nft,
|
||||
tokenId,
|
||||
]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isRecovery) {
|
||||
if (isRecovery || argsMatchLastSuccess) {
|
||||
return;
|
||||
}
|
||||
setLastSuccessfulArgs(null);
|
||||
if (isSourceAssetWormholeWrapped && originChain === targetChain) {
|
||||
dispatch(
|
||||
setTargetAsset(
|
||||
|
@ -89,6 +127,7 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
})
|
||||
)
|
||||
);
|
||||
setArgs();
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
|
@ -124,6 +163,7 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
})
|
||||
)
|
||||
);
|
||||
setArgs();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
|
@ -160,6 +200,7 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
receiveDataWrapper({ doesExist: !!asset, address: asset })
|
||||
)
|
||||
);
|
||||
setArgs();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
|
@ -189,6 +230,7 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
receiveDataWrapper({ doesExist: !!asset, address: asset })
|
||||
)
|
||||
);
|
||||
setArgs();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
|
@ -218,6 +260,8 @@ function useFetchTargetAsset(nft?: boolean) {
|
|||
setTargetAsset,
|
||||
tokenId,
|
||||
hasCorrectEvmNetwork,
|
||||
argsMatchLastSuccess,
|
||||
setArgs,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
} from "@certusone/wormhole-sdk";
|
||||
import { hexlify, hexStripZeros } from "@ethersproject/bytes";
|
||||
import { useConnectedWallet } from "@terra-money/wallet-provider";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
import { CLUSTER, getEvmChainId } from "../utils/consts";
|
||||
|
@ -14,18 +14,25 @@ import { isEVMChain } from "../utils/ethereum";
|
|||
const createWalletStatus = (
|
||||
isReady: boolean,
|
||||
statusMessage: string = "",
|
||||
forceNetworkSwitch: () => void,
|
||||
walletAddress?: string
|
||||
) => ({
|
||||
isReady,
|
||||
statusMessage,
|
||||
forceNetworkSwitch,
|
||||
walletAddress,
|
||||
});
|
||||
|
||||
function useIsWalletReady(chainId: ChainId): {
|
||||
function useIsWalletReady(
|
||||
chainId: ChainId,
|
||||
enableNetworkAutoswitch: boolean = true
|
||||
): {
|
||||
isReady: boolean;
|
||||
statusMessage: string;
|
||||
walletAddress?: string;
|
||||
forceNetworkSwitch: () => void;
|
||||
} {
|
||||
const autoSwitch = enableNetworkAutoswitch;
|
||||
const solanaWallet = useSolanaWallet();
|
||||
const solPK = solanaWallet?.publicKey;
|
||||
const terraWallet = useConnectedWallet();
|
||||
|
@ -39,6 +46,19 @@ function useIsWalletReady(chainId: ChainId): {
|
|||
const correctEvmNetwork = getEvmChainId(chainId);
|
||||
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
|
||||
|
||||
const forceNetworkSwitch = useCallback(() => {
|
||||
if (provider && correctEvmNetwork) {
|
||||
if (!isEVMChain(chainId)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
provider.send("wallet_switchEthereumChain", [
|
||||
{ chainId: hexStripZeros(hexlify(correctEvmNetwork)) },
|
||||
]);
|
||||
} catch (e) {}
|
||||
}
|
||||
}, [provider, correctEvmNetwork, chainId]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
chainId === CHAIN_ID_TERRA &&
|
||||
|
@ -46,33 +66,52 @@ function useIsWalletReady(chainId: ChainId): {
|
|||
terraWallet?.walletAddress
|
||||
) {
|
||||
// TODO: terraWallet does not update on wallet changes
|
||||
return createWalletStatus(true, undefined, terraWallet.walletAddress);
|
||||
return createWalletStatus(
|
||||
true,
|
||||
undefined,
|
||||
forceNetworkSwitch,
|
||||
terraWallet.walletAddress
|
||||
);
|
||||
}
|
||||
if (chainId === CHAIN_ID_SOLANA && solPK) {
|
||||
return createWalletStatus(true, undefined, solPK.toString());
|
||||
return createWalletStatus(
|
||||
true,
|
||||
undefined,
|
||||
forceNetworkSwitch,
|
||||
solPK.toString()
|
||||
);
|
||||
}
|
||||
if (isEVMChain(chainId) && hasEthInfo && signerAddress) {
|
||||
if (hasCorrectEvmNetwork) {
|
||||
return createWalletStatus(true, undefined, signerAddress);
|
||||
return createWalletStatus(
|
||||
true,
|
||||
undefined,
|
||||
forceNetworkSwitch,
|
||||
signerAddress
|
||||
);
|
||||
} else {
|
||||
if (provider && correctEvmNetwork) {
|
||||
try {
|
||||
provider.send("wallet_switchEthereumChain", [
|
||||
{ chainId: hexStripZeros(hexlify(correctEvmNetwork)) },
|
||||
]);
|
||||
} catch (e) {}
|
||||
if (provider && correctEvmNetwork && autoSwitch) {
|
||||
forceNetworkSwitch();
|
||||
}
|
||||
return createWalletStatus(
|
||||
false,
|
||||
`Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${correctEvmNetwork}`,
|
||||
forceNetworkSwitch,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
//TODO bsc
|
||||
return createWalletStatus(false, "Wallet not connected");
|
||||
|
||||
return createWalletStatus(
|
||||
false,
|
||||
"Wallet not connected",
|
||||
forceNetworkSwitch,
|
||||
undefined
|
||||
);
|
||||
}, [
|
||||
chainId,
|
||||
autoSwitch,
|
||||
forceNetworkSwitch,
|
||||
hasTerraWallet,
|
||||
solPK,
|
||||
hasEthInfo,
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_SOLANA,
|
||||
CHAIN_ID_TERRA,
|
||||
getOriginalAssetEth,
|
||||
getOriginalAssetSol,
|
||||
getOriginalAssetTerra,
|
||||
hexToNativeString,
|
||||
uint8ArrayToHex,
|
||||
uint8ArrayToNative,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
getOriginalAssetEth as getOriginalAssetEthNFT,
|
||||
getOriginalAssetSol as getOriginalAssetSolNFT,
|
||||
WormholeWrappedNFTInfo,
|
||||
} from "@certusone/wormhole-sdk/lib/nft_bridge";
|
||||
import { Web3Provider } from "@certusone/wormhole-sdk/node_modules/@ethersproject/providers";
|
||||
import { ethers } from "@certusone/wormhole-sdk/node_modules/ethers";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { LCDClient } from "@terra-money/terra.js";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Provider,
|
||||
useEthereumProvider,
|
||||
} from "../contexts/EthereumProviderContext";
|
||||
import { DataWrapper } from "../store/helpers";
|
||||
import {
|
||||
getNFTBridgeAddressForChain,
|
||||
getTokenBridgeAddressForChain,
|
||||
SOLANA_HOST,
|
||||
SOLANA_SYSTEM_PROGRAM_ADDRESS,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
TERRA_HOST,
|
||||
} from "../utils/consts";
|
||||
import { isEVMChain } from "../utils/ethereum";
|
||||
import useIsWalletReady from "./useIsWalletReady";
|
||||
|
||||
export type OriginalAssetInfo = {
|
||||
originChain: ChainId | null;
|
||||
originAddress: string | null;
|
||||
originTokenId: string | null;
|
||||
};
|
||||
|
||||
export async function getOriginalAssetToken(
|
||||
foreignChain: ChainId,
|
||||
foreignNativeStringAddress: string,
|
||||
provider?: Web3Provider
|
||||
) {
|
||||
let promise = null;
|
||||
try {
|
||||
if (isEVMChain(foreignChain) && provider) {
|
||||
promise = await getOriginalAssetEth(
|
||||
getTokenBridgeAddressForChain(foreignChain),
|
||||
provider,
|
||||
foreignNativeStringAddress,
|
||||
foreignChain
|
||||
);
|
||||
} else if (foreignChain === CHAIN_ID_SOLANA) {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
promise = await getOriginalAssetSol(
|
||||
connection,
|
||||
SOL_TOKEN_BRIDGE_ADDRESS,
|
||||
foreignNativeStringAddress
|
||||
);
|
||||
} else if (foreignChain === CHAIN_ID_TERRA) {
|
||||
const lcd = new LCDClient(TERRA_HOST);
|
||||
promise = await getOriginalAssetTerra(lcd, foreignNativeStringAddress);
|
||||
}
|
||||
} catch (e) {
|
||||
promise = Promise.reject("Invalid foreign arguments.");
|
||||
}
|
||||
if (!promise) {
|
||||
promise = Promise.reject("Invalid foreign arguments.");
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function getOriginalAssetNFT(
|
||||
foreignChain: ChainId,
|
||||
foreignNativeStringAddress: string,
|
||||
tokenId?: string,
|
||||
provider?: Provider
|
||||
) {
|
||||
let promise = null;
|
||||
try {
|
||||
if (isEVMChain(foreignChain) && provider && tokenId) {
|
||||
promise = getOriginalAssetEthNFT(
|
||||
getNFTBridgeAddressForChain(foreignChain),
|
||||
provider,
|
||||
foreignNativeStringAddress,
|
||||
tokenId,
|
||||
foreignChain
|
||||
);
|
||||
} else if (foreignChain === CHAIN_ID_SOLANA) {
|
||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||
promise = getOriginalAssetSolNFT(
|
||||
connection,
|
||||
SOL_NFT_BRIDGE_ADDRESS,
|
||||
foreignNativeStringAddress
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
promise = Promise.reject("Invalid foreign arguments.");
|
||||
}
|
||||
if (!promise) {
|
||||
promise = Promise.reject("Invalid foreign arguments.");
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
//TODO refactor useCheckIfWormholeWrapped to use this function, and probably move to SDK
|
||||
export async function getOriginalAsset(
|
||||
foreignChain: ChainId,
|
||||
foreignNativeStringAddress: string,
|
||||
nft: boolean,
|
||||
tokenId?: string,
|
||||
provider?: Provider
|
||||
): Promise<WormholeWrappedNFTInfo> {
|
||||
const result = nft
|
||||
? await getOriginalAssetNFT(
|
||||
foreignChain,
|
||||
foreignNativeStringAddress,
|
||||
tokenId,
|
||||
provider
|
||||
)
|
||||
: await getOriginalAssetToken(
|
||||
foreignChain,
|
||||
foreignNativeStringAddress,
|
||||
provider
|
||||
);
|
||||
|
||||
if (
|
||||
isEVMChain(result.chainId) &&
|
||||
uint8ArrayToNative(result.assetAddress, result.chainId) ===
|
||||
ethers.constants.AddressZero
|
||||
) {
|
||||
throw new Error("Unable to find address.");
|
||||
}
|
||||
if (
|
||||
result.chainId === CHAIN_ID_SOLANA &&
|
||||
uint8ArrayToNative(result.assetAddress, result.chainId) ===
|
||||
SOLANA_SYSTEM_PROGRAM_ADDRESS
|
||||
) {
|
||||
throw new Error("Unable to find address.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//This potentially returns the same chain as the foreign chain, in the case where the asset is native
|
||||
function useOriginalAsset(
|
||||
foreignChain: ChainId,
|
||||
foreignAddress: string,
|
||||
nft: boolean,
|
||||
tokenId?: string
|
||||
): DataWrapper<OriginalAssetInfo> {
|
||||
const { provider } = useEthereumProvider();
|
||||
const { isReady } = useIsWalletReady(foreignChain, false);
|
||||
const [originAddress, setOriginAddress] = useState<string | null>(null);
|
||||
const [originTokenId, setOriginTokenId] = useState<string | null>(null);
|
||||
const [originChain, setOriginChain] = useState<ChainId | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [previousArgs, setPreviousArgs] = useState<{
|
||||
foreignChain: ChainId;
|
||||
foreignAddress: string;
|
||||
nft: boolean;
|
||||
tokenId?: string;
|
||||
} | null>(null);
|
||||
const argsEqual =
|
||||
!!previousArgs &&
|
||||
previousArgs.foreignChain === foreignChain &&
|
||||
previousArgs.foreignAddress === foreignAddress &&
|
||||
previousArgs.nft === nft &&
|
||||
previousArgs.tokenId === tokenId;
|
||||
const setArgs = useCallback(
|
||||
() => setPreviousArgs({ foreignChain, foreignAddress, nft, tokenId }),
|
||||
[foreignChain, foreignAddress, nft, tokenId]
|
||||
);
|
||||
|
||||
const argumentError = useMemo(
|
||||
() =>
|
||||
!foreignChain ||
|
||||
!foreignAddress ||
|
||||
(isEVMChain(foreignChain) && !isReady) ||
|
||||
(isEVMChain(foreignChain) && nft && !tokenId) ||
|
||||
argsEqual,
|
||||
[isReady, nft, tokenId, argsEqual, foreignChain, foreignAddress]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!argsEqual) {
|
||||
setError("");
|
||||
setOriginAddress(null);
|
||||
setOriginTokenId(null);
|
||||
setOriginChain(null);
|
||||
setPreviousArgs(null);
|
||||
}
|
||||
if (argumentError) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setIsLoading(true);
|
||||
|
||||
getOriginalAsset(foreignChain, foreignAddress, nft, tokenId, provider)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
setArgs();
|
||||
setOriginAddress(
|
||||
hexToNativeString(
|
||||
uint8ArrayToHex(result.assetAddress),
|
||||
result.chainId
|
||||
) || null
|
||||
);
|
||||
setOriginTokenId(result.tokenId || null);
|
||||
setOriginChain(result.chainId);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
setError("Unable to determine original asset.");
|
||||
}
|
||||
});
|
||||
}, [
|
||||
foreignChain,
|
||||
foreignAddress,
|
||||
nft,
|
||||
provider,
|
||||
setArgs,
|
||||
argumentError,
|
||||
tokenId,
|
||||
argsEqual,
|
||||
]);
|
||||
|
||||
const output: DataWrapper<OriginalAssetInfo> = useMemo(
|
||||
() => ({
|
||||
error: error,
|
||||
isFetching: isLoading,
|
||||
data:
|
||||
originChain || originAddress || originTokenId
|
||||
? { originChain, originAddress, originTokenId }
|
||||
: null,
|
||||
receivedAt: null,
|
||||
}),
|
||||
[isLoading, originAddress, originChain, originTokenId, error]
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export default useOriginalAsset;
|
|
@ -44,7 +44,7 @@ export const theme = responsiveFontSizes(
|
|||
fontWeight: "200",
|
||||
},
|
||||
h2: {
|
||||
fontWeight: "300",
|
||||
fontWeight: "200",
|
||||
},
|
||||
h4: {
|
||||
fontWeight: "500",
|
||||
|
|
|
@ -661,3 +661,5 @@ export const MULTI_CHAIN_TOKENS: {
|
|||
|
||||
export const AVAILABLE_MARKETS_URL =
|
||||
"https://docs.wormholenetwork.com/wormhole/overview-liquid-markets";
|
||||
|
||||
export const SOLANA_SYSTEM_PROGRAM_ADDRESS = "11111111111111111111111111111111";
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## 0.0.10
|
||||
|
||||
uint8ArrayToNative utility function for converting to native addresses from the uint8 format
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Added
|
||||
|
|
|
@ -67,3 +67,6 @@ export const nativeToHexString = (
|
|||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const uint8ArrayToNative = (a: Uint8Array, chainId: ChainId) =>
|
||||
hexToNativeString(uint8ArrayToHex(a), chainId);
|
||||
|
|
Loading…
Reference in New Issue