bridge_ui: Transfer already redeemed feature

Notify the user when they're trying to redeem a transfer that has
already been redeemed.

Added useGetIsTransferCompleted hook
This commit is contained in:
Kevin Peters 2021-12-13 16:46:30 +00:00 committed by Evan Gray
parent e50541912b
commit 86c3423b6a
7 changed files with 235 additions and 13 deletions

View File

@ -335,6 +335,10 @@ export default function Recovery() {
targetAddress: parsedPayload.targetAddress,
originChain: parsedPayload.originChain,
originAddress: parsedPayload.originAddress,
amount:
"amount" in parsedPayload
? parsedPayload.amount.toString()
: "",
},
})
);

View File

@ -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() {
<ButtonWithLoader
//TODO disable when the associated token account is confirmed to not exist
disabled={!isReady || disabled}
disabled={
!isReady ||
disabled ||
(isRecovery && (isTransferCompletedLoading || isTransferCompleted))
}
onClick={
isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick
}
showLoader={showLoader}
showLoader={showLoader || (isRecovery && isTransferCompletedLoading)}
error={statusMessage}
>
Redeem
</ButtonWithLoader>
<WaitingForWalletMessage />
{isRecovery && isReady && isTransferCompleted ? (
<>
<Alert severity="info" variant="outlined" className={classes.alert}>
These tokens have already been redeemed.{" "}
{!isEVMChain(targetChain) && howToAddTokensUrl ? (
<Link
href={howToAddTokensUrl}
target="_blank"
rel="noopener noreferrer"
>
Click here to see how to add them to your wallet.
</Link>
) : null}
</Alert>
{targetAsset ? (
<>
<span>Token Address:</span>
<SmartAddress
chainId={targetChain}
address={targetAsset || undefined}
/>
{formattedTransferAmount ? <span>{`Amount: ${formattedTransferAmount}`}</span> : null}
</>
) : null}
{isEVMChain(targetChain) ? <AddToMetamask /> : null}
<ButtonWithLoader onClick={handleResetClick}>
Transfer More Tokens!
</ButtonWithLoader>
</>
) : null}
</>
);
}

View File

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

View File

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

View File

@ -19,7 +19,7 @@ export type GenericMetadata = {
symbol?: string;
logo?: string;
tokenName?: string;
//decimals?: number;
decimals?: number;
//TODO more items
raw?: any;
};

View File

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

View File

@ -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<string, string>([
"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 "";
};