diff --git a/bridge_ui/src/components/TokenOriginVerifier.tsx b/bridge_ui/src/components/TokenOriginVerifier.tsx
new file mode 100644
index 000000000..1fd2a2e4c
--- /dev/null
+++ b/bridge_ui/src/components/TokenOriginVerifier.tsx
@@ -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 = (
+
+ {`This is not a Wormhole wrapped token.`}
+
+ );
+ const wrapped = (
+
+
{`This is wrapped by Wormhole! Here is the original token: `}
+
+
{`Chain: ${CHAINS_BY_ID[originChain].name}`}
+
+
+ {"Token: "}
+
+
+
+
+
+ );
+ 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 ? (
+
+
{`Transferring to ${CHAINS_BY_ID[chainId].name} will unwrap the token:`}
+
+
+
+
+ ) : !foreignAssetInfo ? null : foreignAssetInfo.doesExist === false ? (
+
+ {`This token has not yet been registered on ${CHAINS_BY_ID[chainId].name}`}
+
+
+ ) : (
+
+
When bridged, this asset becomes:
+
+
+
+
+ );
+}
+
+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 = (
+ <>
+ Source Information
+
+ Enter a token from any supported chain to get started.
+
+
+
+ {primaryLookupChainOptions.map(({ id, name }) => (
+
+ ))}
+
+
+
+ {isEVMChain(primaryLookupChain) ? (
+
+ ) : null}
+ {primaryError ? (
+
{primaryError}
+ ) : null}
+
+ {originInfo.isFetching ? (
+
+ ) : originInfo.data?.originChain && originInfo.data.originAddress ? (
+
+ ) : null}
+
+ >
+ );
+
+ const secondaryContent = originInfo.data ? (
+ <>
+ Bridge Results
+
+ Select a chain to see the result of bridging this token.
+
+
+
+ {secondaryLookupChainOptions.map(({ id, name }) => (
+
+ ))}
+
+
+ {isEVMChain(secondaryLookupChain) ? (
+
+ ) : null}
+ {secondaryError ? (
+
{secondaryError}
+ ) : null}
+
+ {foreignAssetInfo.isFetching ? (
+
+ ) : originInfo.data?.originChain && originInfo.data.originAddress ? (
+
+ ) : null}
+
+ >
+ ) : null;
+
+ const content = (
+
+
+
+ Token Origin Verifier
+
+
+
+
+ {primaryContent}
+ {secondaryContent ? (
+ <>
+
+ {secondaryContent}
+ >
+ ) : null}
+
+
+ );
+
+ return content;
+}
diff --git a/bridge_ui/src/components/Transfer/RegisterNowButton.tsx b/bridge_ui/src/components/Transfer/RegisterNowButton.tsx
index c7cab1844..7f191aa9c 100644
--- a/bridge_ui/src/components/Transfer/RegisterNowButton.tsx
+++ b/bridge_ui/src/components/Transfer/RegisterNowButton.tsx
@@ -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() {
);
}
+
+export default function RegisterNowButton() {
+ const originChain = useSelector(selectTransferOriginChain);
+ const originAsset = useSelector(selectTransferOriginAsset);
+ const targetChain = useSelector(selectTransferTargetChain);
+ return (
+
+ );
+}
diff --git a/bridge_ui/src/components/Transfer/Source.tsx b/bridge_ui/src/components/Transfer/Source.tsx
index 7cccecb77..a04dbb6bc 100644
--- a/bridge_ui/src/components/Transfer/Source.tsx
+++ b/bridge_ui/src/components/Transfer/Source.tsx
@@ -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 (
<>
- Select tokens to send through the Wormhole Token Bridge.
+
+ Select tokens to send through the Wormhole Bridge.
+
+
+ }
+ >
+ Token Origin Verifier
+
+
+
> {
- const { isReady } = useIsWalletReady(chainId);
+ const { isReady } = useIsWalletReady(chainId, false);
const { provider } = useEthereumProvider();
const [isFetching, setIsFetching] = useState(false);
diff --git a/bridge_ui/src/hooks/useFetchForeignAsset.ts b/bridge_ui/src/hooks/useFetchForeignAsset.ts
index 504352e27..a620beac5 100644
--- a/bridge_ui/src/hooks/useFetchForeignAsset.ts
+++ b/bridge_ui/src/hooks/useFetchForeignAsset.ts
@@ -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 {
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(null);
- const [doesExist, setDoesExist] = useState(false);
+ const [doesExist, setDoesExist] = useState(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 = 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 = 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 = 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]
diff --git a/bridge_ui/src/hooks/useFetchTargetAsset.ts b/bridge_ui/src/hooks/useFetchTargetAsset.ts
index 176ee912f..cd2cd5685 100644
--- a/bridge_ui/src/hooks/useFetchTargetAsset.ts
+++ b/bridge_ui/src/hooks/useFetchTargetAsset.ts
@@ -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,
]);
}
diff --git a/bridge_ui/src/hooks/useIsWalletReady.ts b/bridge_ui/src/hooks/useIsWalletReady.ts
index 4ee0e8c34..4b5d4dd5f 100644
--- a/bridge_ui/src/hooks/useIsWalletReady.ts
+++ b/bridge_ui/src/hooks/useIsWalletReady.ts
@@ -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,
diff --git a/bridge_ui/src/hooks/useOriginalAsset.ts b/bridge_ui/src/hooks/useOriginalAsset.ts
new file mode 100644
index 000000000..5e7f23db4
--- /dev/null
+++ b/bridge_ui/src/hooks/useOriginalAsset.ts
@@ -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 {
+ 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 {
+ const { provider } = useEthereumProvider();
+ const { isReady } = useIsWalletReady(foreignChain, false);
+ const [originAddress, setOriginAddress] = useState(null);
+ const [originTokenId, setOriginTokenId] = useState(null);
+ const [originChain, setOriginChain] = useState(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 = 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;
diff --git a/bridge_ui/src/muiTheme.js b/bridge_ui/src/muiTheme.js
index 28882b85d..a6aac41d6 100644
--- a/bridge_ui/src/muiTheme.js
+++ b/bridge_ui/src/muiTheme.js
@@ -44,7 +44,7 @@ export const theme = responsiveFontSizes(
fontWeight: "200",
},
h2: {
- fontWeight: "300",
+ fontWeight: "200",
},
h4: {
fontWeight: "500",
diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts
index 29dd41841..2d993af4c 100644
--- a/bridge_ui/src/utils/consts.ts
+++ b/bridge_ui/src/utils/consts.ts
@@ -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";
diff --git a/sdk/js/CHANGELOG.md b/sdk/js/CHANGELOG.md
index a397d14ef..ea2c98a50 100644
--- a/sdk/js/CHANGELOG.md
+++ b/sdk/js/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## 0.0.10
+
+uint8ArrayToNative utility function for converting to native addresses from the uint8 format
+
## 0.0.9
### Added
diff --git a/sdk/js/src/utils/array.ts b/sdk/js/src/utils/array.ts
index 53bcb1564..3e8e21e13 100644
--- a/sdk/js/src/utils/array.ts
+++ b/sdk/js/src/utils/array.ts
@@ -67,3 +67,6 @@ export const nativeToHexString = (
return null;
}
};
+
+export const uint8ArrayToNative = (a: Uint8Array, chainId: ChainId) =>
+ hexToNativeString(uint8ArrayToHex(a), chainId);