diff --git a/bridge_ui/src/components/Recovery.tsx b/bridge_ui/src/components/Recovery.tsx
index 2947155fc..33d93929c 100644
--- a/bridge_ui/src/components/Recovery.tsx
+++ b/bridge_ui/src/components/Recovery.tsx
@@ -335,6 +335,10 @@ export default function Recovery() {
targetAddress: parsedPayload.targetAddress,
originChain: parsedPayload.originChain,
originAddress: parsedPayload.originAddress,
+ amount:
+ "amount" in parsedPayload
+ ? parsedPayload.amount.toString()
+ : "",
},
})
);
diff --git a/bridge_ui/src/components/Transfer/Redeem.tsx b/bridge_ui/src/components/Transfer/Redeem.tsx
index 353e837d5..3a184d184 100644
--- a/bridge_ui/src/components/Transfer/Redeem.tsx
+++ b/bridge_ui/src/components/Transfer/Redeem.tsx
@@ -5,18 +5,33 @@ import {
CHAIN_ID_ETHEREUM_ROPSTEN,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
+ isEVMChain,
+ MAX_VAA_DECIMALS,
WSOL_ADDRESS,
} from "@certusone/wormhole-sdk";
-import { Checkbox, FormControlLabel } from "@material-ui/core";
-import { useCallback, useState } from "react";
-import { useSelector } from "react-redux";
+import { formatUnits } from "@ethersproject/units";
+import {
+ Checkbox,
+ FormControlLabel,
+ Link,
+ makeStyles,
+} from "@material-ui/core";
+import { Alert } from "@material-ui/lab";
+import { useCallback, useMemo, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import useGetIsTransferCompleted from "../../hooks/useGetIsTransferCompleted";
import { useHandleRedeem } from "../../hooks/useHandleRedeem";
import useIsWalletReady from "../../hooks/useIsWalletReady";
+import useMetadata from "../../hooks/useMetadata";
import {
+ selectTransferAmount,
+ selectTransferIsRecovery,
selectTransferTargetAsset,
selectTransferTargetChain,
} from "../../store/selectors";
+import { reset } from "../../store/transferSlice";
import {
+ getHowToAddTokensToWalletUrl,
ROPSTEN_WETH_ADDRESS,
WAVAX_ADDRESS,
WBNB_ADDRESS,
@@ -25,16 +40,36 @@ import {
} from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
+import SmartAddress from "../SmartAddress";
import { SolanaCreateAssociatedAddressAlternate } from "../SolanaCreateAssociatedAddress";
import StepDescription from "../StepDescription";
+import AddToMetamask from "./AddToMetamask";
import WaitingForWalletMessage from "./WaitingForWalletMessage";
+const useStyles = makeStyles((theme) => ({
+ alert: {
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+ },
+}));
+
function Redeem() {
const { handleClick, handleNativeClick, disabled, showLoader } =
useHandleRedeem();
const targetChain = useSelector(selectTransferTargetChain);
const targetAsset = useSelector(selectTransferTargetAsset);
+ const isRecovery = useSelector(selectTransferIsRecovery);
+ const transferAmount = useSelector(selectTransferAmount);
+ const { isTransferCompletedLoading, isTransferCompleted } =
+ useGetIsTransferCompleted(true);
+ const classes = useStyles();
+ const dispatch = useDispatch();
const { isReady, statusMessage } = useIsWalletReady(targetChain);
+ const targetAssetArrayed = useMemo(
+ () => (targetAsset ? [targetAsset] : []),
+ [targetAsset]
+ );
+ const metadata = useMetadata(targetChain, targetAssetArrayed);
//TODO better check, probably involving a hook & the VAA
const isEthNative =
targetChain === CHAIN_ID_ETH &&
@@ -71,6 +106,18 @@ function Redeem() {
const toggleNativeRedeem = useCallback(() => {
setUseNativeRedeem(!useNativeRedeem);
}, [useNativeRedeem]);
+ const handleResetClick = useCallback(() => {
+ dispatch(reset());
+ }, [dispatch]);
+ const howToAddTokensUrl = getHowToAddTokensToWalletUrl(targetChain);
+
+ const formattedTransferAmount = useMemo(() => {
+ const decimals =
+ (targetAsset && metadata.data?.get(targetAsset)?.decimals) || undefined;
+ return decimals
+ ? formatUnits(transferAmount, Math.min(decimals, MAX_VAA_DECIMALS))
+ : undefined;
+ }, [targetAsset, metadata, transferAmount]);
return (
<>
@@ -94,16 +141,51 @@ function Redeem() {
Redeem
+
+ {isRecovery && isReady && isTransferCompleted ? (
+ <>
+
+ These tokens have already been redeemed.{" "}
+ {!isEVMChain(targetChain) && howToAddTokensUrl ? (
+
+ Click here to see how to add them to your wallet.
+
+ ) : null}
+
+ {targetAsset ? (
+ <>
+ Token Address:
+
+ {formattedTransferAmount ? {`Amount: ${formattedTransferAmount}`} : null}
+ >
+ ) : null}
+ {isEVMChain(targetChain) ? : null}
+
+ Transfer More Tokens!
+
+ >
+ ) : null}
>
);
}
diff --git a/bridge_ui/src/hooks/useFetchTargetAsset.ts b/bridge_ui/src/hooks/useFetchTargetAsset.ts
index bd594e94b..f24c4845e 100644
--- a/bridge_ui/src/hooks/useFetchTargetAsset.ts
+++ b/bridge_ui/src/hooks/useFetchTargetAsset.ts
@@ -28,13 +28,11 @@ import {
} from "../store/helpers";
import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice";
import {
- selectNFTIsRecovery,
selectNFTIsSourceAssetWormholeWrapped,
selectNFTOriginAsset,
selectNFTOriginChain,
selectNFTOriginTokenId,
selectNFTTargetChain,
- selectTransferIsRecovery,
selectTransferIsSourceAssetWormholeWrapped,
selectTransferOriginAsset,
selectTransferOriginChain,
@@ -74,9 +72,6 @@ function useFetchTargetAsset(nft?: boolean) {
const { provider, chainId: evmChainId } = useEthereumProvider();
const correctEvmNetwork = getEvmChainId(targetChain);
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
- const isRecovery = useSelector(
- nft ? selectNFTIsRecovery : selectTransferIsRecovery
- );
const [lastSuccessfulArgs, setLastSuccessfulArgs] = useState<{
isSourceAssetWormholeWrapped: boolean | undefined;
originChain: ChainId | undefined;
@@ -114,7 +109,7 @@ function useFetchTargetAsset(nft?: boolean) {
]
);
useEffect(() => {
- if (isRecovery || argsMatchLastSuccess) {
+ if (argsMatchLastSuccess) {
return;
}
setLastSuccessfulArgs(null);
@@ -250,7 +245,6 @@ function useFetchTargetAsset(nft?: boolean) {
};
}, [
dispatch,
- isRecovery,
isSourceAssetWormholeWrapped,
originChain,
originAsset,
diff --git a/bridge_ui/src/hooks/useGetIsTransferCompleted.ts b/bridge_ui/src/hooks/useGetIsTransferCompleted.ts
new file mode 100644
index 000000000..a1123c9c0
--- /dev/null
+++ b/bridge_ui/src/hooks/useGetIsTransferCompleted.ts
@@ -0,0 +1,130 @@
+import {
+ CHAIN_ID_SOLANA,
+ CHAIN_ID_TERRA,
+ getIsTransferCompletedEth,
+ getIsTransferCompletedSolana,
+ getIsTransferCompletedTerra,
+ isEVMChain,
+} from "@certusone/wormhole-sdk";
+import { Connection } from "@solana/web3.js";
+import { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { useEthereumProvider } from "../contexts/EthereumProviderContext";
+import {
+ selectTransferIsRecovery,
+ selectTransferTargetAddressHex,
+ selectTransferTargetChain,
+} from "../store/selectors";
+import {
+ getEvmChainId,
+ getTokenBridgeAddressForChain,
+ SOLANA_HOST,
+ TERRA_GAS_PRICES_URL,
+ TERRA_HOST,
+} from "../utils/consts";
+import useTransferSignedVAA from "./useTransferSignedVAA";
+import { LCDClient } from "@terra-money/terra.js";
+import useIsWalletReady from "./useIsWalletReady";
+
+/**
+ * @param recoveryOnly Only fire when in recovery mode
+ */
+export default function useGetIsTransferCompleted(recoveryOnly: boolean): {
+ isTransferCompletedLoading: boolean;
+ isTransferCompleted: boolean;
+} {
+ const [isLoading, setIsLoading] = useState(false);
+ const [isTransferCompleted, setIsTransferCompleted] = useState(false);
+
+ const isRecovery = useSelector(selectTransferIsRecovery);
+ const targetAddress = useSelector(selectTransferTargetAddressHex);
+ const targetChain = useSelector(selectTransferTargetChain);
+
+ const { isReady, walletAddress } = useIsWalletReady(targetChain, false);
+ const { provider, chainId: evmChainId } = useEthereumProvider();
+ const signedVAA = useTransferSignedVAA();
+
+ const hasCorrectEvmNetwork = evmChainId === getEvmChainId(targetChain);
+ const shouldFire = !recoveryOnly || isRecovery;
+
+ useEffect(() => {
+ if (!shouldFire) {
+ return;
+ }
+
+ let cancelled = false;
+ let transferCompleted = false;
+ if (targetChain && targetAddress && signedVAA && isReady) {
+ if (isEVMChain(targetChain) && hasCorrectEvmNetwork && provider) {
+ setIsLoading(true);
+ (async () => {
+ try {
+ transferCompleted = await getIsTransferCompletedEth(
+ getTokenBridgeAddressForChain(targetChain),
+ provider,
+ signedVAA
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ if (!cancelled) {
+ setIsTransferCompleted(transferCompleted);
+ setIsLoading(false);
+ }
+ })();
+ } else if (targetChain === CHAIN_ID_SOLANA) {
+ setIsLoading(true);
+ (async () => {
+ try {
+ const connection = new Connection(SOLANA_HOST, "confirmed");
+ transferCompleted = await getIsTransferCompletedSolana(
+ getTokenBridgeAddressForChain(targetChain),
+ signedVAA,
+ connection
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ if (!cancelled) {
+ setIsTransferCompleted(transferCompleted);
+ setIsLoading(false);
+ }
+ })();
+ } else if (targetChain === CHAIN_ID_TERRA && walletAddress) {
+ setIsLoading(true);
+ (async () => {
+ try {
+ const lcdClient = new LCDClient(TERRA_HOST);
+ transferCompleted = await getIsTransferCompletedTerra(
+ getTokenBridgeAddressForChain(targetChain),
+ signedVAA,
+ walletAddress,
+ lcdClient,
+ TERRA_GAS_PRICES_URL
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ if (!cancelled) {
+ setIsTransferCompleted(transferCompleted);
+ setIsLoading(false);
+ }
+ })();
+ }
+ }
+ return () => {
+ cancelled = true;
+ };
+ }, [
+ shouldFire,
+ hasCorrectEvmNetwork,
+ targetChain,
+ targetAddress,
+ signedVAA,
+ isReady,
+ walletAddress,
+ provider,
+ ]);
+
+ return { isTransferCompletedLoading: isLoading, isTransferCompleted };
+}
diff --git a/bridge_ui/src/hooks/useMetadata.ts b/bridge_ui/src/hooks/useMetadata.ts
index b68a7784a..1f2a9274d 100644
--- a/bridge_ui/src/hooks/useMetadata.ts
+++ b/bridge_ui/src/hooks/useMetadata.ts
@@ -19,7 +19,7 @@ export type GenericMetadata = {
symbol?: string;
logo?: string;
tokenName?: string;
- //decimals?: number;
+ decimals?: number;
//TODO more items
raw?: any;
};
diff --git a/bridge_ui/src/store/transferSlice.ts b/bridge_ui/src/store/transferSlice.ts
index a382fb0c1..b48cbfb66 100644
--- a/bridge_ui/src/store/transferSlice.ts
+++ b/bridge_ui/src/store/transferSlice.ts
@@ -234,6 +234,7 @@ export const transferSlice = createSlice({
targetAddress: string;
originChain: ChainId;
originAddress: string;
+ amount: string;
};
}>
) => {
@@ -252,6 +253,7 @@ export const transferSlice = createSlice({
state.targetAddressHex = action.payload.parsedPayload.targetAddress;
state.originChain = action.payload.parsedPayload.originChain;
state.originAsset = action.payload.parsedPayload.originAddress;
+ state.amount = action.payload.parsedPayload.amount;
state.activeStep = 3;
state.isRecovery = true;
},
diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts
index 555e2fa8f..2dc6c3daf 100644
--- a/bridge_ui/src/utils/consts.ts
+++ b/bridge_ui/src/utils/consts.ts
@@ -7,6 +7,7 @@ import {
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
+ isEVMChain,
} from "@certusone/wormhole-sdk";
import { clusterApiUrl } from "@solana/web3.js";
import { getAddress } from "ethers/lib/utils";
@@ -814,3 +815,12 @@ export const logoOverrides = new Map([
"https://orion.money/assets/ORION-LOGO-2.1-GREEN@256x256.png",
],
]);
+
+export const getHowToAddTokensToWalletUrl = (chainId: ChainId) => {
+ if (isEVMChain(chainId)) {
+ return "https://docs.wormholenetwork.com/wormhole/video-tutorial-how-to-manually-add-tokens-to-your-wallet#1.-metamask-ethereum-polygon-and-bsc";
+ } else if (chainId === CHAIN_ID_TERRA) {
+ return "https://docs.wormholenetwork.com/wormhole/video-tutorial-how-to-manually-add-tokens-to-your-wallet#2.-terra-station";
+ }
+ return "";
+};