bridge_ui: success message

fixes https://github.com/certusone/wormhole/issues/374

Change-Id: Idb255da1e08fccfe3c79092ed552998178d71c02
This commit is contained in:
Evan Gray 2021-09-02 15:20:45 -04:00
parent 0b517e9c76
commit 14e891ac6e
13 changed files with 180 additions and 68 deletions

View File

@ -35,9 +35,9 @@ export default function ButtonWithLoader({
error,
children,
}: {
disabled: boolean;
disabled?: boolean;
onClick: () => void;
showLoader: boolean;
showLoader?: boolean;
error?: string;
children: ReactChild;
}) {

View File

@ -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 (
<div className={classes.tx}>
<Typography component="div" variant="body2">
{tx.id}
</Typography>
{showExplorerLink && explorerAddress ? (
<Button
href={explorerAddress}
target="_blank"
size="small"
variant="outlined"
className={classes.viewButton}
>
View on {explorerName}
</Button>
) : null}
</div>
);
}

View File

@ -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,11 +17,18 @@ 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 (
<>
<Typography
component="div"
variant="subtitle2"
@ -20,5 +36,10 @@ export default function RedeemPreview() {
>
{explainerString}
</Typography>
{redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null}
<ButtonWithLoader onClick={handleResetClick}>
Transfer More Tokens!
</ButtonWithLoader>
</>
);
}

View File

@ -1,17 +1,33 @@
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 (
<>
<Typography
component="div"
variant="subtitle2"
@ -19,5 +35,7 @@ export default function SendPreview() {
>
{explainerString}
</Typography>
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
</>
);
}

View File

@ -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}
>
<Step expanded={activeStep >= 0} disabled={preventNavigation}>
<Step
expanded={activeStep >= 0}
disabled={preventNavigation || isRedeemComplete}
>
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
<StepContent>
{activeStep === 0 ? (
@ -75,13 +80,16 @@ function Transfer() {
)}
</StepContent>
</Step>
<Step expanded={activeStep >= 1} disabled={preventNavigation}>
<Step
expanded={activeStep >= 1}
disabled={preventNavigation || isRedeemComplete}
>
<StepButton onClick={() => dispatch(setStep(1))}>Target</StepButton>
<StepContent>
{activeStep === 1 ? <Target /> : <TargetPreview />}
</StepContent>
</Step>
<Step expanded={activeStep >= 2}>
<Step expanded={activeStep >= 2} disabled={isSendComplete}>
<StepButton onClick={() => dispatch(setStep(2))}>
Send tokens
</StepButton>
@ -97,7 +105,7 @@ function Transfer() {
Redeem tokens
</StepButton>
<StepContent>
{activeStep === 3 ? <Redeem /> : <RedeemPreview />}
{isRedeemComplete ? <RedeemPreview /> : <Redeem />}
</StepContent>
</Step>
</Stepper>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<boolean>) => {
state.isRedeeming = action.payload;
},
setRedeemTx: (state, action: PayloadAction<Transaction>) => {
state.redeemTx = action.payload;
},
reset: () => initialState,
},
});
@ -199,6 +204,7 @@ export const {
setSignedVAAHex,
setIsSending,
setIsRedeeming,
setRedeemTx,
reset,
} = transferSlice.actions;

View File

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

View File

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