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,
|
targetAddress: parsedPayload.targetAddress,
|
||||||
originChain: parsedPayload.originChain,
|
originChain: parsedPayload.originChain,
|
||||||
originAddress: parsedPayload.originAddress,
|
originAddress: parsedPayload.originAddress,
|
||||||
|
amount:
|
||||||
|
"amount" in parsedPayload
|
||||||
|
? parsedPayload.amount.toString()
|
||||||
|
: "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,18 +5,33 @@ import {
|
||||||
CHAIN_ID_ETHEREUM_ROPSTEN,
|
CHAIN_ID_ETHEREUM_ROPSTEN,
|
||||||
CHAIN_ID_POLYGON,
|
CHAIN_ID_POLYGON,
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
|
isEVMChain,
|
||||||
|
MAX_VAA_DECIMALS,
|
||||||
WSOL_ADDRESS,
|
WSOL_ADDRESS,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { Checkbox, FormControlLabel } from "@material-ui/core";
|
import { formatUnits } from "@ethersproject/units";
|
||||||
import { useCallback, useState } from "react";
|
import {
|
||||||
import { useSelector } from "react-redux";
|
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 { useHandleRedeem } from "../../hooks/useHandleRedeem";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
|
import useMetadata from "../../hooks/useMetadata";
|
||||||
import {
|
import {
|
||||||
|
selectTransferAmount,
|
||||||
|
selectTransferIsRecovery,
|
||||||
selectTransferTargetAsset,
|
selectTransferTargetAsset,
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
|
import { reset } from "../../store/transferSlice";
|
||||||
import {
|
import {
|
||||||
|
getHowToAddTokensToWalletUrl,
|
||||||
ROPSTEN_WETH_ADDRESS,
|
ROPSTEN_WETH_ADDRESS,
|
||||||
WAVAX_ADDRESS,
|
WAVAX_ADDRESS,
|
||||||
WBNB_ADDRESS,
|
WBNB_ADDRESS,
|
||||||
|
@ -25,16 +40,36 @@ import {
|
||||||
} from "../../utils/consts";
|
} from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import SmartAddress from "../SmartAddress";
|
||||||
import { SolanaCreateAssociatedAddressAlternate } from "../SolanaCreateAssociatedAddress";
|
import { SolanaCreateAssociatedAddressAlternate } from "../SolanaCreateAssociatedAddress";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
|
import AddToMetamask from "./AddToMetamask";
|
||||||
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
alert: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function Redeem() {
|
function Redeem() {
|
||||||
const { handleClick, handleNativeClick, disabled, showLoader } =
|
const { handleClick, handleNativeClick, disabled, showLoader } =
|
||||||
useHandleRedeem();
|
useHandleRedeem();
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
const targetAsset = useSelector(selectTransferTargetAsset);
|
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 { isReady, statusMessage } = useIsWalletReady(targetChain);
|
||||||
|
const targetAssetArrayed = useMemo(
|
||||||
|
() => (targetAsset ? [targetAsset] : []),
|
||||||
|
[targetAsset]
|
||||||
|
);
|
||||||
|
const metadata = useMetadata(targetChain, targetAssetArrayed);
|
||||||
//TODO better check, probably involving a hook & the VAA
|
//TODO better check, probably involving a hook & the VAA
|
||||||
const isEthNative =
|
const isEthNative =
|
||||||
targetChain === CHAIN_ID_ETH &&
|
targetChain === CHAIN_ID_ETH &&
|
||||||
|
@ -71,6 +106,18 @@ function Redeem() {
|
||||||
const toggleNativeRedeem = useCallback(() => {
|
const toggleNativeRedeem = useCallback(() => {
|
||||||
setUseNativeRedeem(!useNativeRedeem);
|
setUseNativeRedeem(!useNativeRedeem);
|
||||||
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -94,16 +141,51 @@ function Redeem() {
|
||||||
|
|
||||||
<ButtonWithLoader
|
<ButtonWithLoader
|
||||||
//TODO disable when the associated token account is confirmed to not exist
|
//TODO disable when the associated token account is confirmed to not exist
|
||||||
disabled={!isReady || disabled}
|
disabled={
|
||||||
|
!isReady ||
|
||||||
|
disabled ||
|
||||||
|
(isRecovery && (isTransferCompletedLoading || isTransferCompleted))
|
||||||
|
}
|
||||||
onClick={
|
onClick={
|
||||||
isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick
|
isNativeEligible && useNativeRedeem ? handleNativeClick : handleClick
|
||||||
}
|
}
|
||||||
showLoader={showLoader}
|
showLoader={showLoader || (isRecovery && isTransferCompletedLoading)}
|
||||||
error={statusMessage}
|
error={statusMessage}
|
||||||
>
|
>
|
||||||
Redeem
|
Redeem
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
<WaitingForWalletMessage />
|
<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";
|
} from "../store/helpers";
|
||||||
import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice";
|
import { setTargetAsset as setNFTTargetAsset } from "../store/nftSlice";
|
||||||
import {
|
import {
|
||||||
selectNFTIsRecovery,
|
|
||||||
selectNFTIsSourceAssetWormholeWrapped,
|
selectNFTIsSourceAssetWormholeWrapped,
|
||||||
selectNFTOriginAsset,
|
selectNFTOriginAsset,
|
||||||
selectNFTOriginChain,
|
selectNFTOriginChain,
|
||||||
selectNFTOriginTokenId,
|
selectNFTOriginTokenId,
|
||||||
selectNFTTargetChain,
|
selectNFTTargetChain,
|
||||||
selectTransferIsRecovery,
|
|
||||||
selectTransferIsSourceAssetWormholeWrapped,
|
selectTransferIsSourceAssetWormholeWrapped,
|
||||||
selectTransferOriginAsset,
|
selectTransferOriginAsset,
|
||||||
selectTransferOriginChain,
|
selectTransferOriginChain,
|
||||||
|
@ -74,9 +72,6 @@ function useFetchTargetAsset(nft?: boolean) {
|
||||||
const { provider, chainId: evmChainId } = useEthereumProvider();
|
const { provider, chainId: evmChainId } = useEthereumProvider();
|
||||||
const correctEvmNetwork = getEvmChainId(targetChain);
|
const correctEvmNetwork = getEvmChainId(targetChain);
|
||||||
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
|
const hasCorrectEvmNetwork = evmChainId === correctEvmNetwork;
|
||||||
const isRecovery = useSelector(
|
|
||||||
nft ? selectNFTIsRecovery : selectTransferIsRecovery
|
|
||||||
);
|
|
||||||
const [lastSuccessfulArgs, setLastSuccessfulArgs] = useState<{
|
const [lastSuccessfulArgs, setLastSuccessfulArgs] = useState<{
|
||||||
isSourceAssetWormholeWrapped: boolean | undefined;
|
isSourceAssetWormholeWrapped: boolean | undefined;
|
||||||
originChain: ChainId | undefined;
|
originChain: ChainId | undefined;
|
||||||
|
@ -114,7 +109,7 @@ function useFetchTargetAsset(nft?: boolean) {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRecovery || argsMatchLastSuccess) {
|
if (argsMatchLastSuccess) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLastSuccessfulArgs(null);
|
setLastSuccessfulArgs(null);
|
||||||
|
@ -250,7 +245,6 @@ function useFetchTargetAsset(nft?: boolean) {
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
dispatch,
|
dispatch,
|
||||||
isRecovery,
|
|
||||||
isSourceAssetWormholeWrapped,
|
isSourceAssetWormholeWrapped,
|
||||||
originChain,
|
originChain,
|
||||||
originAsset,
|
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;
|
symbol?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
tokenName?: string;
|
tokenName?: string;
|
||||||
//decimals?: number;
|
decimals?: number;
|
||||||
//TODO more items
|
//TODO more items
|
||||||
raw?: any;
|
raw?: any;
|
||||||
};
|
};
|
||||||
|
|
|
@ -234,6 +234,7 @@ export const transferSlice = createSlice({
|
||||||
targetAddress: string;
|
targetAddress: string;
|
||||||
originChain: ChainId;
|
originChain: ChainId;
|
||||||
originAddress: string;
|
originAddress: string;
|
||||||
|
amount: string;
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
|
@ -252,6 +253,7 @@ export const transferSlice = createSlice({
|
||||||
state.targetAddressHex = action.payload.parsedPayload.targetAddress;
|
state.targetAddressHex = action.payload.parsedPayload.targetAddress;
|
||||||
state.originChain = action.payload.parsedPayload.originChain;
|
state.originChain = action.payload.parsedPayload.originChain;
|
||||||
state.originAsset = action.payload.parsedPayload.originAddress;
|
state.originAsset = action.payload.parsedPayload.originAddress;
|
||||||
|
state.amount = action.payload.parsedPayload.amount;
|
||||||
state.activeStep = 3;
|
state.activeStep = 3;
|
||||||
state.isRecovery = true;
|
state.isRecovery = true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
CHAIN_ID_POLYGON,
|
CHAIN_ID_POLYGON,
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
CHAIN_ID_TERRA,
|
CHAIN_ID_TERRA,
|
||||||
|
isEVMChain,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { clusterApiUrl } from "@solana/web3.js";
|
import { clusterApiUrl } from "@solana/web3.js";
|
||||||
import { getAddress } from "ethers/lib/utils";
|
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",
|
"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