bridge_ui: success message
fixes https://github.com/certusone/wormhole/issues/374 Change-Id: Idb255da1e08fccfe3c79092ed552998178d71c02
This commit is contained in:
parent
0b517e9c76
commit
14e891ac6e
|
@ -35,9 +35,9 @@ export default function ButtonWithLoader({
|
|||
error,
|
||||
children,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
showLoader: boolean;
|
||||
showLoader?: boolean;
|
||||
error?: string;
|
||||
children: ReactChild;
|
||||
}) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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" });
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue