bridge_ui: token verifier page

Change-Id: I48c48519caee597ae5455f7777326c6f07361886
This commit is contained in:
Chase Moran 2021-11-08 17:23:59 -05:00 committed by Evan Gray
parent afe4fad438
commit be244f4632
16 changed files with 917 additions and 104 deletions

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -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}>

View File

@ -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;
}

View File

@ -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}
/>
);
}

View File

@ -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

View File

@ -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);

View File

@ -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,112 +35,156 @@ 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);
const getterFunc: () => Promise<string | null> = isEVMChain(foreignChain)
? () =>
getForeignAssetEth(
getTokenBridgeAddressForChain(foreignChain),
provider as any, //why does this typecheck work elsewhere?
originChain,
hexToUint8Array(originAssetHex)
)
: foreignChain === CHAIN_ID_TERRA
? () => {
const lcd = new LCDClient(TERRA_HOST);
return getForeignAssetTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
lcd,
originChain,
hexToUint8Array(originAssetHex)
);
}
: () => {
const connection = new Connection(SOLANA_HOST, "confirmed");
return getForeignAssetSolana(
connection,
SOL_TOKEN_BRIDGE_ADDRESS,
originChain,
hexToUint8Array(originAssetHex)
);
};
const promise = getterFunc();
promise
.then((result) => {
if (!cancelled) {
if (
result &&
!(
isEVMChain(foreignChain) &&
result === ethers.constants.AddressZero
try {
const getterFunc: () => Promise<string | null> = isEVMChain(foreignChain)
? () =>
getForeignAssetEth(
getTokenBridgeAddressForChain(foreignChain),
provider as any, //why does this typecheck work elsewhere?
originChain,
hexToUint8Array(originAssetHex)
)
) {
setDoesExist(true);
setIsLoading(false);
setAssetAddress(result);
} else {
setDoesExist(false);
setIsLoading(false);
setAssetAddress(null);
: foreignChain === CHAIN_ID_TERRA
? () => {
const lcd = new LCDClient(TERRA_HOST);
return getForeignAssetTerra(
TERRA_TOKEN_BRIDGE_ADDRESS,
lcd,
originChain,
hexToUint8Array(originAssetHex)
);
}
}
})
.catch((e) => {
if (!cancelled) {
setError("Could not retrieve the foreign asset.");
setIsLoading(false);
}
});
}, [argumentError, foreignChain, originAssetHex, originChain, provider]);
: () => {
const connection = new Connection(SOLANA_HOST, "confirmed");
return getForeignAssetSolana(
connection,
SOL_TOKEN_BRIDGE_ADDRESS,
originChain,
hexToUint8Array(originAssetHex)
);
};
getterFunc()
.then((result) => {
if (!cancelled) {
if (
result &&
!(
isEVMChain(foreignChain) &&
result === ethers.constants.AddressZero
)
) {
setArgs();
setDoesExist(true);
setIsLoading(false);
setAssetAddress(result);
} else {
setArgs();
setDoesExist(false);
setIsLoading(false);
setAssetAddress(null);
}
}
})
.catch((e) => {
if (!cancelled) {
setError("Could not retrieve the foreign asset.");
setIsLoading(false);
}
});
} 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]

View File

@ -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,
]);
}

View File

@ -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,

View File

@ -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;

View File

@ -44,7 +44,7 @@ export const theme = responsiveFontSizes(
fontWeight: "200",
},
h2: {
fontWeight: "300",
fontWeight: "200",
},
h4: {
fontWeight: "500",

View File

@ -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";

View File

@ -1,5 +1,9 @@
# Changelog
## 0.0.10
uint8ArrayToNative utility function for converting to native addresses from the uint8 format
## 0.0.9
### Added

View File

@ -67,3 +67,6 @@ export const nativeToHexString = (
return null;
}
};
export const uint8ArrayToNative = (a: Uint8Array, chainId: ChainId) =>
hexToNativeString(uint8ArrayToHex(a), chainId);