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:
parent
e50541912b
commit
86c3423b6a
|
@ -335,6 +335,10 @@ export default function Recovery() {
|
|||
targetAddress: parsedPayload.targetAddress,
|
||||
originChain: parsedPayload.originChain,
|
||||
originAddress: parsedPayload.originAddress,
|
||||
amount:
|
||||
"amount" in parsedPayload
|
||||
? parsedPayload.amount.toString()
|
||||
: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
}
|
|
@ -19,7 +19,7 @@ export type GenericMetadata = {
|
|||
symbol?: string;
|
||||
logo?: string;
|
||||
tokenName?: string;
|
||||
//decimals?: number;
|
||||
decimals?: number;
|
||||
//TODO more items
|
||||
raw?: any;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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 "";
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue