From 14e891ac6e831542f7116e8ca4813d0615b2084c Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Thu, 2 Sep 2021 15:20:45 -0400 Subject: [PATCH] bridge_ui: success message fixes https://github.com/certusone/wormhole/issues/374 Change-Id: Idb255da1e08fccfe3c79092ed552998178d71c02 --- bridge_ui/src/components/ButtonWithLoader.tsx | 4 +- bridge_ui/src/components/ShowTx.tsx | 59 +++++++++++++++++++ .../src/components/Transfer/RedeemPreview.tsx | 35 ++++++++--- .../src/components/Transfer/SendPreview.tsx | 32 +++++++--- bridge_ui/src/components/Transfer/index.tsx | 28 +++++---- .../hooks/useGetSourceParsedTokenAccounts.ts | 6 +- bridge_ui/src/hooks/useHandleRedeem.ts | 23 +++++--- bridge_ui/src/hooks/useHandleTransfer.ts | 1 - bridge_ui/src/hooks/useIsWalletReady.ts | 4 +- bridge_ui/src/store/selectors.ts | 4 ++ bridge_ui/src/store/transferSlice.ts | 6 ++ bridge_ui/src/utils/consts.ts | 33 ++++++----- bridge_ui/src/utils/getIsWrappedAsset.ts | 13 ---- 13 files changed, 180 insertions(+), 68 deletions(-) create mode 100644 bridge_ui/src/components/ShowTx.tsx delete mode 100644 bridge_ui/src/utils/getIsWrappedAsset.ts diff --git a/bridge_ui/src/components/ButtonWithLoader.tsx b/bridge_ui/src/components/ButtonWithLoader.tsx index b3da51a0a..be00a62e8 100644 --- a/bridge_ui/src/components/ButtonWithLoader.tsx +++ b/bridge_ui/src/components/ButtonWithLoader.tsx @@ -35,9 +35,9 @@ export default function ButtonWithLoader({ error, children, }: { - disabled: boolean; + disabled?: boolean; onClick: () => void; - showLoader: boolean; + showLoader?: boolean; error?: string; children: ReactChild; }) { diff --git a/bridge_ui/src/components/ShowTx.tsx b/bridge_ui/src/components/ShowTx.tsx new file mode 100644 index 000000000..e0a919b6d --- /dev/null +++ b/bridge_ui/src/components/ShowTx.tsx @@ -0,0 +1,59 @@ +import { + ChainId, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, +} from "@certusone/wormhole-sdk"; +import { Button, makeStyles, Typography } from "@material-ui/core"; +import { Transaction } from "../store/transferSlice"; +import { CLUSTER } from "../utils/consts"; + +const useStyles = makeStyles((theme) => ({ + tx: { + marginTop: theme.spacing(1), + textAlign: "center", + }, + viewButton: { + marginTop: theme.spacing(1), + }, +})); + +export default function ShowTx({ + chainId, + tx, +}: { + chainId: ChainId; + tx: Transaction; +}) { + const classes = useStyles(); + const showExplorerLink = CLUSTER === "testnet" || CLUSTER === "mainnet"; + const explorerAddress = + chainId === CHAIN_ID_ETH + ? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${ + tx?.id + }` + : chainId === CHAIN_ID_SOLANA + ? `https://explorer.solana.com/tx/${tx?.id}${ + CLUSTER === "testnet" ? "?cluster=testnet" : "" + }` + : undefined; + const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer"; + + return ( +
+ + {tx.id} + + {showExplorerLink && explorerAddress ? ( + + ) : null} +
+ ); +} diff --git a/bridge_ui/src/components/Transfer/RedeemPreview.tsx b/bridge_ui/src/components/Transfer/RedeemPreview.tsx index 622b87771..7885f83c3 100644 --- a/bridge_ui/src/components/Transfer/RedeemPreview.tsx +++ b/bridge_ui/src/components/Transfer/RedeemPreview.tsx @@ -1,4 +1,13 @@ import { makeStyles, Typography } from "@material-ui/core"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectTransferRedeemTx, + selectTransferTargetChain, +} from "../../store/selectors"; +import { reset } from "../../store/transferSlice"; +import ButtonWithLoader from "../ButtonWithLoader"; +import ShowTx from "../ShowTx"; const useStyles = makeStyles((theme) => ({ description: { @@ -8,17 +17,29 @@ const useStyles = makeStyles((theme) => ({ export default function RedeemPreview() { const classes = useStyles(); + const dispatch = useDispatch(); + const targetChain = useSelector(selectTransferTargetChain); + const redeemTx = useSelector(selectTransferRedeemTx); + const handleResetClick = useCallback(() => { + dispatch(reset()); + }, [dispatch]); const explainerString = "Success! The redeem transaction was submitted. The tokens will become available once the transaction confirms."; return ( - - {explainerString} - + <> + + {explainerString} + + {redeemTx ? : null} + + Transfer More Tokens! + + ); } diff --git a/bridge_ui/src/components/Transfer/SendPreview.tsx b/bridge_ui/src/components/Transfer/SendPreview.tsx index d0a5d618f..d0fe5fff0 100644 --- a/bridge_ui/src/components/Transfer/SendPreview.tsx +++ b/bridge_ui/src/components/Transfer/SendPreview.tsx @@ -1,23 +1,41 @@ import { makeStyles, Typography } from "@material-ui/core"; +import { useSelector } from "react-redux"; +import { + selectTransferSourceChain, + selectTransferTransferTx, +} from "../../store/selectors"; +import ShowTx from "../ShowTx"; const useStyles = makeStyles((theme) => ({ description: { textAlign: "center", }, + tx: { + marginTop: theme.spacing(1), + textAlign: "center", + }, + viewButton: { + marginTop: theme.spacing(1), + }, })); export default function SendPreview() { const classes = useStyles(); + const sourceChain = useSelector(selectTransferSourceChain); + const transferTx = useSelector(selectTransferTransferTx); const explainerString = "The tokens have been sent!"; return ( - - {explainerString} - + <> + + {explainerString} + + {transferTx ? : null} + ); } diff --git a/bridge_ui/src/components/Transfer/index.tsx b/bridge_ui/src/components/Transfer/index.tsx index fc0ee2a22..d7e81aec9 100644 --- a/bridge_ui/src/components/Transfer/index.tsx +++ b/bridge_ui/src/components/Transfer/index.tsx @@ -13,6 +13,7 @@ import useFetchTargetAsset from "../../hooks/useFetchTargetAsset"; import useGetBalanceEffect from "../../hooks/useGetBalanceEffect"; import { selectTransferActiveStep, + selectTransferIsRedeemComplete, selectTransferIsRedeeming, selectTransferIsSendComplete, selectTransferIsSending, @@ -20,13 +21,13 @@ import { import { setStep } from "../../store/transferSlice"; import Recovery from "./Recovery"; import Redeem from "./Redeem"; -import Send from "./Send"; -import Source from "./Source"; -import Target from "./Target"; -import SourcePreview from "./SourcePreview"; -import TargetPreview from "./TargetPreview"; -import SendPreview from "./SendPreview"; import RedeemPreview from "./RedeemPreview"; +import Send from "./Send"; +import SendPreview from "./SendPreview"; +import Source from "./Source"; +import SourcePreview from "./SourcePreview"; +import Target from "./Target"; +import TargetPreview from "./TargetPreview"; // TODO: ensure that both wallets are connected to the same known network // TODO: loaders and such, navigation block? // TODO: refresh displayed token amount after transfer somehow, could be resolved by having different components appear @@ -49,6 +50,7 @@ function Transfer() { const isSending = useSelector(selectTransferIsSending); const isSendComplete = useSelector(selectTransferIsSendComplete); const isRedeeming = useSelector(selectTransferIsRedeeming); + const isRedeemComplete = useSelector(selectTransferIsRedeemComplete); const preventNavigation = isSending || isSendComplete || isRedeeming; useEffect(() => { if (preventNavigation) { @@ -65,7 +67,10 @@ function Transfer() { orientation="vertical" className={classes.rootContainer} > - = 0} disabled={preventNavigation}> + = 0} + disabled={preventNavigation || isRedeemComplete} + > dispatch(setStep(0))}>Source {activeStep === 0 ? ( @@ -75,13 +80,16 @@ function Transfer() { )} - = 1} disabled={preventNavigation}> + = 1} + disabled={preventNavigation || isRedeemComplete} + > dispatch(setStep(1))}>Target {activeStep === 1 ? : } - = 2}> + = 2} disabled={isSendComplete}> dispatch(setStep(2))}> Send tokens @@ -97,7 +105,7 @@ function Transfer() { Redeem tokens - {activeStep === 3 ? : } + {isRedeemComplete ? : } diff --git a/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts b/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts index ff5d5ac1d..e68cb783e 100644 --- a/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts +++ b/bridge_ui/src/hooks/useGetSourceParsedTokenAccounts.ts @@ -31,7 +31,7 @@ import { ParsedTokenAccount, receiveSourceParsedTokenAccounts, } from "../store/transferSlice"; -import { COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts"; +import { CLUSTER, COVALENT_GET_TOKENS_URL, SOLANA_HOST } from "../utils/consts"; import { decodeMetadata, getMetadataAddress, @@ -128,8 +128,7 @@ const getEthereumAccountsCovalent = async ( } }; -const environment = - process.env.REACT_APP_CLUSTER === "testnet" ? ENV.Testnet : ENV.MainnetBeta; +const environment = CLUSTER === "testnet" ? ENV.Testnet : ENV.MainnetBeta; const getMetaplexData = async (mintAddresses: string[]) => { const promises = []; @@ -176,6 +175,7 @@ const getSolanaParsedTokenAccounts = ( }) .then( (result) => { + console.log(result); const mappedItems = result.value.map((item) => createParsedTokenAccountFromInfo(item.pubkey, item.account) ); diff --git a/bridge_ui/src/hooks/useHandleRedeem.ts b/bridge_ui/src/hooks/useHandleRedeem.ts index f5f3137d6..3ae88dace 100644 --- a/bridge_ui/src/hooks/useHandleRedeem.ts +++ b/bridge_ui/src/hooks/useHandleRedeem.ts @@ -24,7 +24,7 @@ import { selectTransferIsRedeeming, selectTransferTargetChain, } from "../store/selectors"; -import { reset, setIsRedeeming } from "../store/transferSlice"; +import { setIsRedeeming, setRedeemTx } from "../store/transferSlice"; import { ETH_TOKEN_BRIDGE_ADDRESS, SOLANA_HOST, @@ -43,8 +43,14 @@ async function eth( ) { dispatch(setIsRedeeming(true)); try { - await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA); - dispatch(reset()); + const receipt = await redeemOnEth( + ETH_TOKEN_BRIDGE_ADDRESS, + signer, + signedVAA + ); + dispatch( + setRedeemTx({ id: receipt.transactionHash, block: receipt.blockNumber }) + ); enqueueSnackbar("Transaction confirmed", { variant: "success" }); } catch (e) { enqueueSnackbar(parseError(e), { variant: "error" }); @@ -78,8 +84,9 @@ async function solana( payerAddress, signedVAA ); - await signSendAndConfirm(wallet, connection, transaction); - dispatch(reset()); + const txid = await signSendAndConfirm(wallet, connection, transaction); + // TODO: didn't want to make an info call we didn't need, can we get the block without it by modifying the above call? + dispatch(setRedeemTx({ id: txid, block: 1 })); enqueueSnackbar("Transaction confirmed", { variant: "success" }); } catch (e) { enqueueSnackbar(parseError(e), { variant: "error" }); @@ -100,11 +107,13 @@ async function terra( wallet.terraAddress, signedVAA ); - await wallet.post({ + const result = await wallet.post({ msgs: [msg], memo: "Wormhole - Complete Transfer", }); - dispatch(reset()); + dispatch( + setRedeemTx({ id: result.result.txhash, block: result.result.height }) + ); enqueueSnackbar("Transaction confirmed", { variant: "success" }); } catch (e) { enqueueSnackbar(parseError(e), { variant: "error" }); diff --git a/bridge_ui/src/hooks/useHandleTransfer.ts b/bridge_ui/src/hooks/useHandleTransfer.ts index 4d29dadd4..11b2a6e28 100644 --- a/bridge_ui/src/hooks/useHandleTransfer.ts +++ b/bridge_ui/src/hooks/useHandleTransfer.ts @@ -143,7 +143,6 @@ async function solana( throw new Error("An error occurred while fetching the transaction info"); } dispatch(setTransferTx({ id: txid, block: info.slot })); - enqueueSnackbar("Transaction confirmed", { variant: "success" }); const sequence = parseSequenceFromLogSolana(info); const emitterAddress = await getEmitterAddressSolana( SOL_TOKEN_BRIDGE_ADDRESS diff --git a/bridge_ui/src/hooks/useIsWalletReady.ts b/bridge_ui/src/hooks/useIsWalletReady.ts index e2a28f131..de44f3933 100644 --- a/bridge_ui/src/hooks/useIsWalletReady.ts +++ b/bridge_ui/src/hooks/useIsWalletReady.ts @@ -8,7 +8,7 @@ import { useConnectedWallet } from "@terra-money/wallet-provider"; import { useMemo } from "react"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; -import { ETH_NETWORK_CHAIN_ID } from "../utils/consts"; +import { CLUSTER, ETH_NETWORK_CHAIN_ID } from "../utils/consts"; const createWalletStatus = (isReady: boolean, statusMessage: string = "") => ({ isReady, @@ -45,7 +45,7 @@ function useIsWalletReady(chainId: ChainId): { } else { return createWalletStatus( false, - `Wallet is not connected to ${process.env.REACT_APP_CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}` + `Wallet is not connected to ${CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}` ); } } diff --git a/bridge_ui/src/store/selectors.ts b/bridge_ui/src/store/selectors.ts index 6be682c41..1633fc9b1 100644 --- a/bridge_ui/src/store/selectors.ts +++ b/bridge_ui/src/store/selectors.ts @@ -73,6 +73,8 @@ export const selectTransferIsSending = (state: RootState) => state.transfer.isSending; export const selectTransferIsRedeeming = (state: RootState) => state.transfer.isRedeeming; +export const selectTransferRedeemTx = (state: RootState) => + state.transfer.redeemTx; export const selectTransferSourceError = ( state: RootState ): string | undefined => { @@ -158,6 +160,8 @@ export const selectTransferIsTargetComplete = (state: RootState) => !selectTransferTargetError(state); export const selectTransferIsSendComplete = (state: RootState) => !!selectTransferSignedVAAHex(state); +export const selectTransferIsRedeemComplete = (state: RootState) => + !!selectTransferRedeemTx(state); export const selectTransferShouldLockFields = (state: RootState) => selectTransferIsSending(state) || selectTransferIsSendComplete(state); diff --git a/bridge_ui/src/store/transferSlice.ts b/bridge_ui/src/store/transferSlice.ts index 35285f65e..3efb1023f 100644 --- a/bridge_ui/src/store/transferSlice.ts +++ b/bridge_ui/src/store/transferSlice.ts @@ -48,6 +48,7 @@ export interface TransferState { signedVAAHex: string | undefined; isSending: boolean; isRedeeming: boolean; + redeemTx: Transaction | undefined; } const initialState: TransferState = { @@ -67,6 +68,7 @@ const initialState: TransferState = { signedVAAHex: undefined, isSending: false, isRedeeming: false, + redeemTx: undefined, }; export const transferSlice = createSlice({ @@ -176,6 +178,9 @@ export const transferSlice = createSlice({ setIsRedeeming: (state, action: PayloadAction) => { state.isRedeeming = action.payload; }, + setRedeemTx: (state, action: PayloadAction) => { + state.redeemTx = action.payload; + }, reset: () => initialState, }, }); @@ -199,6 +204,7 @@ export const { setSignedVAAHex, setIsSending, setIsRedeeming, + setRedeemTx, reset, } = transferSlice.actions; diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 4dd9cd3a5..0c9f7bc09 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -8,12 +8,19 @@ import { import { clusterApiUrl } from "@solana/web3.js"; import { getAddress } from "ethers/lib/utils"; +export type Cluster = "devnet" | "testnet" | "mainnet"; +export const CLUSTER: Cluster = + process.env.REACT_APP_CLUSTER === "mainnet" + ? "mainnet" + : process.env.REACT_APP_CLUSTER === "testnet" + ? "testnet" + : "devnet"; export interface ChainInfo { id: ChainId; name: string; } export const CHAINS = - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? [ { id: CHAIN_ID_ETH, @@ -48,49 +55,43 @@ export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => { return obj; }, {} as ChainsById); export const WORMHOLE_RPC_HOST = - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "https://wormhole-v2-testnet-api.certus.one" : "http://localhost:8080"; export const ETH_NETWORK_CHAIN_ID = - process.env.REACT_APP_CLUSTER === "mainnet" - ? 1 - : process.env.REACT_APP_CLUSTER === "testnet" - ? 5 - : 1337; + CLUSTER === "mainnet" ? 1 : CLUSTER === "testnet" ? 5 : 1337; export const SOLANA_HOST = - process.env.REACT_APP_CLUSTER === "testnet" - ? clusterApiUrl("testnet") - : "http://localhost:8899"; + CLUSTER === "testnet" ? clusterApiUrl("testnet") : "http://localhost:8899"; export const TERRA_HOST = { URL: "http://localhost:1317", chainID: "columbus-4", name: "localterra", }; export const ETH_TEST_TOKEN_ADDRESS = getAddress( - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "0xcEE940033DA197F551BBEdED7F4aA55Ee55C582B" : "0x67B5656d60a809915323Bf2C40A8bEF15A152e3e" ); export const ETH_BRIDGE_ADDRESS = getAddress( - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "0x44F3e7c20850B3B5f3031114726A9240911D912a" : "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550" ); export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress( - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "0xa6CDAddA6e4B6704705b065E01E52e2486c0FBf6" : "0x0290FB167208Af455bB137780163b7B7a9a10C16" ); export const SOL_TEST_TOKEN_ADDRESS = - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "6uzMjLkcTwhYo5Fwx9DtVtQ7VRrCQ7bTUd7rHXTiPDXp" : "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"; export const SOL_BRIDGE_ADDRESS = - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "Brdguy7BmNB4qwEbcqqMbyV5CyJd2sxQNUn6NEpMSsUb" : "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; export const SOL_TOKEN_BRIDGE_ADDRESS = - process.env.REACT_APP_CLUSTER === "testnet" + CLUSTER === "testnet" ? "A4Us8EhCC76XdGAN17L4KpRNEK423nMivVHZzZqFqqBg" : "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"; export const TERRA_TEST_TOKEN_ADDRESS = diff --git a/bridge_ui/src/utils/getIsWrappedAsset.ts b/bridge_ui/src/utils/getIsWrappedAsset.ts deleted file mode 100644 index 315bde538..000000000 --- a/bridge_ui/src/utils/getIsWrappedAsset.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getIsWrappedAssetSol as getIsWrappedAssetSolTx } from "@certusone/wormhole-sdk"; -import { Connection } from "@solana/web3.js"; -import { SOLANA_HOST, SOL_TOKEN_BRIDGE_ADDRESS } from "./consts"; - -export async function getIsWrappedAssetSol(mintAddress: string) { - // TODO: share connection in context? - const connection = new Connection(SOLANA_HOST, "confirmed"); - return await getIsWrappedAssetSolTx( - connection, - SOL_TOKEN_BRIDGE_ADDRESS, - mintAddress - ); -}