bridge_ui: nft and attest niceties
Change-Id: Ib7d9c533bd974751da4c615c9589f3a285f3c9a9
This commit is contained in:
parent
c8aee80b1d
commit
0498454193
|
@ -4,6 +4,7 @@ import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import { selectAttestTargetChain } from "../../store/selectors";
|
import { selectAttestTargetChain } from "../../store/selectors";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
function Create() {
|
function Create() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleCreateWrapped();
|
const { handleClick, disabled, showLoader } = useHandleCreateWrapped();
|
||||||
|
@ -20,6 +21,7 @@ function Create() {
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
<WaitingForWalletMessage />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
selectAttestCreateTx,
|
||||||
|
selectAttestTargetChain,
|
||||||
|
} from "../../store/selectors";
|
||||||
|
import { reset } from "../../store/attestSlice";
|
||||||
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
|
import ShowTx from "../ShowTx";
|
||||||
|
import { useHistory } from "react-router";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
description: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function CreatePreview() {
|
||||||
|
const { push } = useHistory();
|
||||||
|
const classes = useStyles();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const targetChain = useSelector(selectAttestTargetChain);
|
||||||
|
const createTx = useSelector(selectAttestCreateTx);
|
||||||
|
const handleResetClick = useCallback(() => {
|
||||||
|
dispatch(reset());
|
||||||
|
}, [dispatch]);
|
||||||
|
const handleReturnClick = useCallback(() => {
|
||||||
|
dispatch(reset());
|
||||||
|
push("/transfer");
|
||||||
|
}, [dispatch, push]);
|
||||||
|
|
||||||
|
const explainerString =
|
||||||
|
"Success! The redeem transaction was submitted. The tokens will become available once the transaction confirms.";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.description}
|
||||||
|
>
|
||||||
|
{explainerString}
|
||||||
|
</Typography>
|
||||||
|
{createTx ? <ShowTx chainId={targetChain} tx={createTx} /> : null}
|
||||||
|
<ButtonWithLoader onClick={handleResetClick}>
|
||||||
|
Attest Another Token!
|
||||||
|
</ButtonWithLoader>
|
||||||
|
<ButtonWithLoader onClick={handleReturnClick}>
|
||||||
|
Return to Transfer
|
||||||
|
</ButtonWithLoader>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,13 +1,21 @@
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useHandleAttest } from "../../hooks/useHandleAttest";
|
import { useHandleAttest } from "../../hooks/useHandleAttest";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import { selectAttestSourceChain } from "../../store/selectors";
|
import {
|
||||||
|
selectAttestAttestTx,
|
||||||
|
selectAttestIsSendComplete,
|
||||||
|
selectAttestSourceChain,
|
||||||
|
} from "../../store/selectors";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import TransactionProgress from "../TransactionProgress";
|
||||||
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
function Send() {
|
function Send() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleAttest();
|
const { handleClick, disabled, showLoader } = useHandleAttest();
|
||||||
const sourceChain = useSelector(selectAttestSourceChain);
|
const sourceChain = useSelector(selectAttestSourceChain);
|
||||||
|
const attestTx = useSelector(selectAttestAttestTx);
|
||||||
|
const isSendComplete = useSelector(selectAttestIsSendComplete);
|
||||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -21,6 +29,12 @@ function Send() {
|
||||||
>
|
>
|
||||||
Attest
|
Attest
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
<WaitingForWalletMessage />
|
||||||
|
<TransactionProgress
|
||||||
|
chainId={sourceChain}
|
||||||
|
tx={attestTx}
|
||||||
|
isSendComplete={isSendComplete}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
selectAttestSourceChain,
|
||||||
|
selectAttestAttestTx,
|
||||||
|
} 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(selectAttestSourceChain);
|
||||||
|
const attestTx = useSelector(selectAttestAttestTx);
|
||||||
|
|
||||||
|
const explainerString = "The token has been attested!";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.description}
|
||||||
|
>
|
||||||
|
{explainerString}
|
||||||
|
</Typography>
|
||||||
|
{attestTx ? <ShowTx chainId={sourceChain} tx={attestTx} /> : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
selectAttestSourceAsset,
|
||||||
|
selectAttestSourceChain,
|
||||||
|
} from "../../store/selectors";
|
||||||
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
|
import { shortenAddress } from "../../utils/solana";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
description: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function SourcePreview() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const sourceChain = useSelector(selectAttestSourceChain);
|
||||||
|
const sourceAsset = useSelector(selectAttestSourceAsset);
|
||||||
|
|
||||||
|
const explainerString = sourceAsset
|
||||||
|
? `You will attest ${shortenAddress(sourceAsset)} on ${
|
||||||
|
CHAINS_BY_ID[sourceChain].name
|
||||||
|
}`
|
||||||
|
: "Step complete.";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.description}
|
||||||
|
>
|
||||||
|
{explainerString}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { selectAttestTargetChain } from "../../store/selectors";
|
||||||
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
description: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function TargetPreview() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const targetChain = useSelector(selectAttestTargetChain);
|
||||||
|
|
||||||
|
const explainerString = `to ${CHAINS_BY_ID[targetChain].name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
variant="subtitle2"
|
||||||
|
className={classes.description}
|
||||||
|
>
|
||||||
|
{explainerString}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
selectAttestAttestTx,
|
||||||
|
selectAttestCreateTx,
|
||||||
|
selectAttestIsCreating,
|
||||||
|
selectAttestIsSending,
|
||||||
|
selectAttestTargetChain,
|
||||||
|
} from "../../store/selectors";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
message: {
|
||||||
|
color: theme.palette.warning.light,
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const WAITING_FOR_WALLET = "Waiting for wallet approval (likely in a popup)...";
|
||||||
|
|
||||||
|
export default function WaitingForWalletMessage() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const isSending = useSelector(selectAttestIsSending);
|
||||||
|
const attestTx = useSelector(selectAttestAttestTx);
|
||||||
|
const targetChain = useSelector(selectAttestTargetChain);
|
||||||
|
const isCreating = useSelector(selectAttestIsCreating);
|
||||||
|
const createTx = useSelector(selectAttestCreateTx);
|
||||||
|
const showWarning = (isSending && !attestTx) || (isCreating && !createTx);
|
||||||
|
return showWarning ? (
|
||||||
|
<Typography className={classes.message} variant="body2">
|
||||||
|
{WAITING_FOR_WALLET}{" "}
|
||||||
|
{targetChain === CHAIN_ID_SOLANA && isCreating
|
||||||
|
? "Note: there will be several transactions"
|
||||||
|
: null}
|
||||||
|
</Typography>
|
||||||
|
) : null;
|
||||||
|
}
|
|
@ -6,17 +6,25 @@ import {
|
||||||
StepContent,
|
StepContent,
|
||||||
Stepper,
|
Stepper,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { setStep } from "../../store/attestSlice";
|
||||||
import {
|
import {
|
||||||
selectAttestActiveStep,
|
selectAttestActiveStep,
|
||||||
selectAttestSignedVAAHex,
|
selectAttestIsCreateComplete,
|
||||||
|
selectAttestIsCreating,
|
||||||
|
selectAttestIsSendComplete,
|
||||||
|
selectAttestIsSending,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setStep } from "../../store/attestSlice";
|
|
||||||
import Create from "./Create";
|
import Create from "./Create";
|
||||||
|
import CreatePreview from "./CreatePreview";
|
||||||
import Send from "./Send";
|
import Send from "./Send";
|
||||||
|
import SendPreview from "./SendPreview";
|
||||||
import Source from "./Source";
|
import Source from "./Source";
|
||||||
|
import SourcePreview from "./SourcePreview";
|
||||||
import Target from "./Target";
|
import Target from "./Target";
|
||||||
import { Alert } from "@material-ui/lab";
|
import TargetPreview from "./TargetPreview";
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
rootContainer: {
|
rootContainer: {
|
||||||
|
@ -28,7 +36,20 @@ function Attest() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const activeStep = useSelector(selectAttestActiveStep);
|
const activeStep = useSelector(selectAttestActiveStep);
|
||||||
const signedVAAHex = useSelector(selectAttestSignedVAAHex);
|
const isSending = useSelector(selectAttestIsSending);
|
||||||
|
const isSendComplete = useSelector(selectAttestIsSendComplete);
|
||||||
|
const isCreating = useSelector(selectAttestIsCreating);
|
||||||
|
const isCreateComplete = useSelector(selectAttestIsCreateComplete);
|
||||||
|
const preventNavigation =
|
||||||
|
(isSending || isSendComplete || isCreating) && !isCreateComplete;
|
||||||
|
useEffect(() => {
|
||||||
|
if (preventNavigation) {
|
||||||
|
window.onbeforeunload = () => true;
|
||||||
|
return () => {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [preventNavigation]);
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
|
@ -40,39 +61,41 @@ function Attest() {
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
className={classes.rootContainer}
|
className={classes.rootContainer}
|
||||||
>
|
>
|
||||||
<Step>
|
<Step
|
||||||
<StepButton onClick={() => dispatch(setStep(0))}>
|
expanded={activeStep >= 0}
|
||||||
Select a source
|
disabled={preventNavigation || isCreateComplete}
|
||||||
</StepButton>
|
>
|
||||||
|
<StepButton onClick={() => dispatch(setStep(0))}>Source</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Source />
|
{activeStep === 0 ? <Source /> : <SourcePreview />}
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step
|
||||||
<StepButton onClick={() => dispatch(setStep(1))}>
|
expanded={activeStep >= 1}
|
||||||
Select a target
|
disabled={preventNavigation || isCreateComplete}
|
||||||
</StepButton>
|
>
|
||||||
|
<StepButton onClick={() => dispatch(setStep(1))}>Target</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Target />
|
{activeStep === 1 ? <Target /> : <TargetPreview />}
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step expanded={activeStep >= 2} disabled={isSendComplete}>
|
||||||
<StepButton onClick={() => dispatch(setStep(2))}>
|
<StepButton onClick={() => dispatch(setStep(2))}>
|
||||||
Send attestation
|
Send attestation
|
||||||
</StepButton>
|
</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Send />
|
{activeStep === 2 ? <Send /> : <SendPreview />}
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step expanded={activeStep >= 3}>
|
||||||
<StepButton
|
<StepButton
|
||||||
onClick={() => dispatch(setStep(3))}
|
onClick={() => dispatch(setStep(3))}
|
||||||
disabled={!signedVAAHex}
|
disabled={!isSendComplete}
|
||||||
>
|
>
|
||||||
Create wrapped token
|
Create wrapped token
|
||||||
</StepButton>
|
</StepButton>
|
||||||
<StepContent>
|
<StepContent>
|
||||||
<Create />
|
{isCreateComplete ? <CreatePreview /> : <Create />}
|
||||||
</StepContent>
|
</StepContent>
|
||||||
</Step>
|
</Step>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { selectNFTTargetChain } from "../../store/selectors";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
function Redeem() {
|
function Redeem() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleNFTRedeem();
|
const { handleClick, disabled, showLoader } = useHandleNFTRedeem();
|
||||||
|
@ -22,6 +23,7 @@ function Redeem() {
|
||||||
>
|
>
|
||||||
Redeem
|
Redeem
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
<WaitingForWalletMessage />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,15 @@ import {
|
||||||
selectNFTSourceWalletAddress,
|
selectNFTSourceWalletAddress,
|
||||||
selectNFTSourceChain,
|
selectNFTSourceChain,
|
||||||
selectNFTTargetError,
|
selectNFTTargetError,
|
||||||
|
selectNFTTransferTx,
|
||||||
|
selectNFTIsSendComplete,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
import TransferProgress from "../TransferProgress";
|
import TransactionProgress from "../TransactionProgress";
|
||||||
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
function Send() {
|
function Send() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleNFTTransfer();
|
const { handleClick, disabled, showLoader } = useHandleNFTTransfer();
|
||||||
|
@ -20,6 +23,8 @@ function Send() {
|
||||||
const { isReady, statusMessage, walletAddress } =
|
const { isReady, statusMessage, walletAddress } =
|
||||||
useIsWalletReady(sourceChain);
|
useIsWalletReady(sourceChain);
|
||||||
const sourceWalletAddress = useSelector(selectNFTSourceWalletAddress);
|
const sourceWalletAddress = useSelector(selectNFTSourceWalletAddress);
|
||||||
|
const transferTx = useSelector(selectNFTTransferTx);
|
||||||
|
const isSendComplete = useSelector(selectNFTIsSendComplete);
|
||||||
//The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain.
|
//The chain ID compare is handled implicitly, as the isWalletReady hook should report !isReady if the wallet is on the wrong chain.
|
||||||
const isWrongWallet =
|
const isWrongWallet =
|
||||||
sourceWalletAddress &&
|
sourceWalletAddress &&
|
||||||
|
@ -49,7 +54,12 @@ function Send() {
|
||||||
>
|
>
|
||||||
Transfer
|
Transfer
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
<TransferProgress nft />
|
<WaitingForWalletMessage />
|
||||||
|
<TransactionProgress
|
||||||
|
chainId={sourceChain}
|
||||||
|
tx={transferTx}
|
||||||
|
isSendComplete={isSendComplete}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
selectNFTIsRedeeming,
|
||||||
|
selectNFTIsSending,
|
||||||
|
selectNFTRedeemTx,
|
||||||
|
selectNFTSourceChain,
|
||||||
|
selectNFTTargetChain,
|
||||||
|
selectNFTTransferTx,
|
||||||
|
} from "../../store/selectors";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
message: {
|
||||||
|
color: theme.palette.warning.light,
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const WAITING_FOR_WALLET = "Waiting for wallet approval (likely in a popup)...";
|
||||||
|
|
||||||
|
export default function WaitingForWalletMessage() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const sourceChain = useSelector(selectNFTSourceChain);
|
||||||
|
const isSending = useSelector(selectNFTIsSending);
|
||||||
|
const transferTx = useSelector(selectNFTTransferTx);
|
||||||
|
const targetChain = useSelector(selectNFTTargetChain);
|
||||||
|
const isRedeeming = useSelector(selectNFTIsRedeeming);
|
||||||
|
const redeemTx = useSelector(selectNFTRedeemTx);
|
||||||
|
const showWarning = (isSending && !transferTx) || (isRedeeming && !redeemTx);
|
||||||
|
return showWarning ? (
|
||||||
|
<Typography className={classes.message} variant="body2">
|
||||||
|
{WAITING_FOR_WALLET}{" "}
|
||||||
|
{targetChain === CHAIN_ID_SOLANA && isRedeeming
|
||||||
|
? "Note: there will be several transactions"
|
||||||
|
: sourceChain === CHAIN_ID_ETH && isSending
|
||||||
|
? "Note: there will be two transactions"
|
||||||
|
: null}
|
||||||
|
</Typography>
|
||||||
|
) : null;
|
||||||
|
}
|
|
@ -1,17 +1,13 @@
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
import {
|
||||||
|
ChainId,
|
||||||
|
CHAIN_ID_ETH,
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
} from "@certusone/wormhole-sdk";
|
||||||
import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
|
import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
|
||||||
import { Connection } from "@solana/web3.js";
|
import { Connection } from "@solana/web3.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import {
|
import { Transaction } from "../store/transferSlice";
|
||||||
selectNFTIsSendComplete,
|
|
||||||
selectNFTSourceChain,
|
|
||||||
selectNFTTransferTx,
|
|
||||||
selectTransferIsSendComplete,
|
|
||||||
selectTransferSourceChain,
|
|
||||||
selectTransferTransferTx,
|
|
||||||
} from "../store/selectors";
|
|
||||||
import { CHAINS_BY_ID, SOLANA_HOST } from "../utils/consts";
|
import { CHAINS_BY_ID, SOLANA_HOST } from "../utils/consts";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
@ -24,22 +20,21 @@ const useStyles = makeStyles((theme) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function TransferProgress({ nft }: { nft?: boolean }) {
|
export default function TransactionProgress({
|
||||||
|
chainId,
|
||||||
|
tx,
|
||||||
|
isSendComplete,
|
||||||
|
}: {
|
||||||
|
chainId: ChainId;
|
||||||
|
tx: Transaction | undefined;
|
||||||
|
isSendComplete: boolean;
|
||||||
|
}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const sourceChain = useSelector(
|
|
||||||
nft ? selectNFTSourceChain : selectTransferSourceChain
|
|
||||||
);
|
|
||||||
const transferTx = useSelector(
|
|
||||||
nft ? selectNFTTransferTx : selectTransferTransferTx
|
|
||||||
);
|
|
||||||
const isSendComplete = useSelector(
|
|
||||||
nft ? selectNFTIsSendComplete : selectTransferIsSendComplete
|
|
||||||
);
|
|
||||||
const { provider } = useEthereumProvider();
|
const { provider } = useEthereumProvider();
|
||||||
const [currentBlock, setCurrentBlock] = useState(0);
|
const [currentBlock, setCurrentBlock] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSendComplete || !transferTx) return;
|
if (isSendComplete || !tx) return;
|
||||||
if (sourceChain === CHAIN_ID_ETH && provider) {
|
if (chainId === CHAIN_ID_ETH && provider) {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
while (!cancelled) {
|
while (!cancelled) {
|
||||||
|
@ -58,7 +53,7 @@ export default function TransferProgress({ nft }: { nft?: boolean }) {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (sourceChain === CHAIN_ID_SOLANA) {
|
if (chainId === CHAIN_ID_SOLANA) {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
const sub = connection.onSlotChange((slotInfo) => {
|
const sub = connection.onSlotChange((slotInfo) => {
|
||||||
|
@ -71,20 +66,14 @@ export default function TransferProgress({ nft }: { nft?: boolean }) {
|
||||||
connection.removeSlotChangeListener(sub);
|
connection.removeSlotChangeListener(sub);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [isSendComplete, sourceChain, provider, transferTx]);
|
}, [isSendComplete, chainId, provider, tx]);
|
||||||
const blockDiff =
|
const blockDiff =
|
||||||
transferTx && transferTx.block && currentBlock
|
tx && tx.block && currentBlock ? currentBlock - tx.block : undefined;
|
||||||
? currentBlock - transferTx.block
|
|
||||||
: undefined;
|
|
||||||
const expectedBlocks =
|
const expectedBlocks =
|
||||||
sourceChain === CHAIN_ID_SOLANA
|
chainId === CHAIN_ID_SOLANA ? 32 : chainId === CHAIN_ID_ETH ? 15 : 1;
|
||||||
? 32
|
|
||||||
: sourceChain === CHAIN_ID_ETH
|
|
||||||
? 15
|
|
||||||
: 1;
|
|
||||||
if (
|
if (
|
||||||
!isSendComplete &&
|
!isSendComplete &&
|
||||||
(sourceChain === CHAIN_ID_SOLANA || sourceChain === CHAIN_ID_ETH) &&
|
(chainId === CHAIN_ID_SOLANA || chainId === CHAIN_ID_ETH) &&
|
||||||
blockDiff !== undefined
|
blockDiff !== undefined
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
|
@ -97,7 +86,7 @@ export default function TransferProgress({ nft }: { nft?: boolean }) {
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" className={classes.message}>
|
<Typography variant="body2" className={classes.message}>
|
||||||
{blockDiff < expectedBlocks
|
{blockDiff < expectedBlocks
|
||||||
? `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${CHAINS_BY_ID[sourceChain].name}...`
|
? `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${CHAINS_BY_ID[chainId].name}...`
|
||||||
: `Waiting for Wormhole Network consensus...`}
|
: `Waiting for Wormhole Network consensus...`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
|
@ -11,16 +11,18 @@ import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import {
|
import {
|
||||||
selectSourceWalletAddress,
|
selectSourceWalletAddress,
|
||||||
selectTransferAmount,
|
selectTransferAmount,
|
||||||
|
selectTransferIsSendComplete,
|
||||||
selectTransferSourceAsset,
|
selectTransferSourceAsset,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
selectTransferSourceParsedTokenAccount,
|
selectTransferSourceParsedTokenAccount,
|
||||||
selectTransferTargetError,
|
selectTransferTargetError,
|
||||||
|
selectTransferTransferTx,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
import TransferProgress from "../TransferProgress";
|
import TransactionProgress from "../TransactionProgress";
|
||||||
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
function Send() {
|
function Send() {
|
||||||
|
@ -41,6 +43,8 @@ function Send() {
|
||||||
sourceDecimals !== undefined &&
|
sourceDecimals !== undefined &&
|
||||||
sourceDecimals !== null &&
|
sourceDecimals !== null &&
|
||||||
parseUnits("1", sourceDecimals).toBigInt();
|
parseUnits("1", sourceDecimals).toBigInt();
|
||||||
|
const transferTx = useSelector(selectTransferTransferTx);
|
||||||
|
const isSendComplete = useSelector(selectTransferIsSendComplete);
|
||||||
|
|
||||||
const error = useSelector(selectTransferTargetError);
|
const error = useSelector(selectTransferTargetError);
|
||||||
const [allowanceError, setAllowanceError] = useState("");
|
const [allowanceError, setAllowanceError] = useState("");
|
||||||
|
@ -149,7 +153,11 @@ function Send() {
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
)}
|
)}
|
||||||
<WaitingForWalletMessage />
|
<WaitingForWalletMessage />
|
||||||
<TransferProgress />
|
<TransactionProgress
|
||||||
|
chainId={sourceChain}
|
||||||
|
tx={transferTx}
|
||||||
|
isSendComplete={isSendComplete}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,10 +51,8 @@ function Transfer() {
|
||||||
(isSending || isSendComplete || isRedeeming) && !isRedeemComplete;
|
(isSending || isSendComplete || isRedeeming) && !isRedeemComplete;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preventNavigation) {
|
if (preventNavigation) {
|
||||||
console.log("add onbeforeunload");
|
|
||||||
window.onbeforeunload = () => true;
|
window.onbeforeunload = () => true;
|
||||||
return () => {
|
return () => {
|
||||||
console.log("remove onbeforeunload");
|
|
||||||
window.onbeforeunload = null;
|
window.onbeforeunload = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,11 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Signer } from "../../../sdk/js/node_modules/ethers/lib";
|
import { Signer } from "../../../sdk/js/node_modules/ethers/lib";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import { setIsSending, setSignedVAAHex } from "../store/attestSlice";
|
import {
|
||||||
|
setAttestTx,
|
||||||
|
setIsSending,
|
||||||
|
setSignedVAAHex,
|
||||||
|
} from "../store/attestSlice";
|
||||||
import {
|
import {
|
||||||
selectAttestIsSendComplete,
|
selectAttestIsSendComplete,
|
||||||
selectAttestIsSending,
|
selectAttestIsSending,
|
||||||
|
@ -59,6 +63,9 @@ async function eth(
|
||||||
signer,
|
signer,
|
||||||
sourceAsset
|
sourceAsset
|
||||||
);
|
);
|
||||||
|
dispatch(
|
||||||
|
setAttestTx({ id: receipt.transactionHash, block: receipt.blockNumber })
|
||||||
|
);
|
||||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||||
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
||||||
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||||
|
@ -101,6 +108,7 @@ async function solana(
|
||||||
// TODO: error state
|
// TODO: error state
|
||||||
throw new Error("An error occurred while fetching the transaction info");
|
throw new Error("An error occurred while fetching the transaction info");
|
||||||
}
|
}
|
||||||
|
dispatch(setAttestTx({ id: txid, block: info.slot }));
|
||||||
const sequence = parseSequenceFromLogSolana(info);
|
const sequence = parseSequenceFromLogSolana(info);
|
||||||
const emitterAddress = await getEmitterAddressSolana(
|
const emitterAddress = await getEmitterAddressSolana(
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS
|
SOL_TOKEN_BRIDGE_ADDRESS
|
||||||
|
@ -134,6 +142,7 @@ async function terra(
|
||||||
asset
|
asset
|
||||||
);
|
);
|
||||||
const info = await waitForTerraExecution(result);
|
const info = await waitForTerraExecution(result);
|
||||||
|
dispatch(setAttestTx({ id: info.txhash, block: info.height }));
|
||||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||||
const sequence = parseSequenceFromLogTerra(info);
|
const sequence = parseSequenceFromLogTerra(info);
|
||||||
if (!sequence) {
|
if (!sequence) {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import useAttestSignedVAA from "../hooks/useAttestSignedVAA";
|
import useAttestSignedVAA from "../hooks/useAttestSignedVAA";
|
||||||
import { reset, setIsCreating } from "../store/attestSlice";
|
import { setCreateTx, setIsCreating } from "../store/attestSlice";
|
||||||
import {
|
import {
|
||||||
selectAttestIsCreating,
|
selectAttestIsCreating,
|
||||||
selectAttestTargetChain,
|
selectAttestTargetChain,
|
||||||
|
@ -43,8 +43,14 @@ async function eth(
|
||||||
) {
|
) {
|
||||||
dispatch(setIsCreating(true));
|
dispatch(setIsCreating(true));
|
||||||
try {
|
try {
|
||||||
await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
|
const receipt = await createWrappedOnEth(
|
||||||
dispatch(reset());
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
signer,
|
||||||
|
signedVAA
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
setCreateTx({ id: receipt.transactionHash, block: receipt.blockNumber })
|
||||||
|
);
|
||||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
@ -76,8 +82,9 @@ async function solana(
|
||||||
payerAddress,
|
payerAddress,
|
||||||
signedVAA
|
signedVAA
|
||||||
);
|
);
|
||||||
await signSendAndConfirm(wallet, connection, transaction);
|
const txid = await signSendAndConfirm(wallet, connection, transaction);
|
||||||
dispatch(reset());
|
// 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(setCreateTx({ id: txid, block: 1 }));
|
||||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
@ -98,11 +105,13 @@ async function terra(
|
||||||
wallet.terraAddress,
|
wallet.terraAddress,
|
||||||
signedVAA
|
signedVAA
|
||||||
);
|
);
|
||||||
await wallet.post({
|
const result = await wallet.post({
|
||||||
msgs: [msg],
|
msgs: [msg],
|
||||||
memo: "Wormhole - Create Wrapped",
|
memo: "Wormhole - Create Wrapped",
|
||||||
});
|
});
|
||||||
dispatch(reset());
|
dispatch(
|
||||||
|
setCreateTx({ id: result.result.txhash, block: result.result.height })
|
||||||
|
);
|
||||||
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
enqueueSnackbar("Transaction confirmed", { variant: "success" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
enqueueSnackbar(parseError(e), { variant: "error" });
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { Transaction } from "./transferSlice";
|
||||||
|
|
||||||
const LAST_STEP = 3;
|
const LAST_STEP = 3;
|
||||||
|
|
||||||
|
@ -14,9 +15,11 @@ export interface AttestState {
|
||||||
sourceChain: ChainId;
|
sourceChain: ChainId;
|
||||||
sourceAsset: string;
|
sourceAsset: string;
|
||||||
targetChain: ChainId;
|
targetChain: ChainId;
|
||||||
|
attestTx: Transaction | undefined;
|
||||||
signedVAAHex: string | undefined;
|
signedVAAHex: string | undefined;
|
||||||
isSending: boolean;
|
isSending: boolean;
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
|
createTx: Transaction | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AttestState = {
|
const initialState: AttestState = {
|
||||||
|
@ -24,9 +27,11 @@ const initialState: AttestState = {
|
||||||
sourceChain: CHAIN_ID_SOLANA,
|
sourceChain: CHAIN_ID_SOLANA,
|
||||||
sourceAsset: "",
|
sourceAsset: "",
|
||||||
targetChain: CHAIN_ID_ETH,
|
targetChain: CHAIN_ID_ETH,
|
||||||
|
attestTx: undefined,
|
||||||
signedVAAHex: undefined,
|
signedVAAHex: undefined,
|
||||||
isSending: false,
|
isSending: false,
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
|
createTx: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const attestSlice = createSlice({
|
export const attestSlice = createSlice({
|
||||||
|
@ -62,6 +67,9 @@ export const attestSlice = createSlice({
|
||||||
state.sourceAsset = "";
|
state.sourceAsset = "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setAttestTx: (state, action: PayloadAction<Transaction>) => {
|
||||||
|
state.attestTx = action.payload;
|
||||||
|
},
|
||||||
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
setSignedVAAHex: (state, action: PayloadAction<string>) => {
|
||||||
state.signedVAAHex = action.payload;
|
state.signedVAAHex = action.payload;
|
||||||
state.isSending = false;
|
state.isSending = false;
|
||||||
|
@ -73,6 +81,10 @@ export const attestSlice = createSlice({
|
||||||
setIsCreating: (state, action: PayloadAction<boolean>) => {
|
setIsCreating: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isCreating = action.payload;
|
state.isCreating = action.payload;
|
||||||
},
|
},
|
||||||
|
setCreateTx: (state, action: PayloadAction<Transaction>) => {
|
||||||
|
state.createTx = action.payload;
|
||||||
|
state.isCreating = false;
|
||||||
|
},
|
||||||
reset: (state) => ({
|
reset: (state) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
sourceChain: state.sourceChain,
|
sourceChain: state.sourceChain,
|
||||||
|
@ -88,9 +100,11 @@ export const {
|
||||||
setSourceChain,
|
setSourceChain,
|
||||||
setSourceAsset,
|
setSourceAsset,
|
||||||
setTargetChain,
|
setTargetChain,
|
||||||
|
setAttestTx,
|
||||||
setSignedVAAHex,
|
setSignedVAAHex,
|
||||||
setIsSending,
|
setIsSending,
|
||||||
setIsCreating,
|
setIsCreating,
|
||||||
|
setCreateTx,
|
||||||
reset,
|
reset,
|
||||||
} = attestSlice.actions;
|
} = attestSlice.actions;
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,14 @@ export const selectAttestSourceAsset = (state: RootState) =>
|
||||||
state.attest.sourceAsset;
|
state.attest.sourceAsset;
|
||||||
export const selectAttestTargetChain = (state: RootState) =>
|
export const selectAttestTargetChain = (state: RootState) =>
|
||||||
state.attest.targetChain;
|
state.attest.targetChain;
|
||||||
|
export const selectAttestAttestTx = (state: RootState) => state.attest.attestTx;
|
||||||
export const selectAttestSignedVAAHex = (state: RootState) =>
|
export const selectAttestSignedVAAHex = (state: RootState) =>
|
||||||
state.attest.signedVAAHex;
|
state.attest.signedVAAHex;
|
||||||
export const selectAttestIsSending = (state: RootState) =>
|
export const selectAttestIsSending = (state: RootState) =>
|
||||||
state.attest.isSending;
|
state.attest.isSending;
|
||||||
export const selectAttestIsCreating = (state: RootState) =>
|
export const selectAttestIsCreating = (state: RootState) =>
|
||||||
state.attest.isCreating;
|
state.attest.isCreating;
|
||||||
|
export const selectAttestCreateTx = (state: RootState) => state.attest.createTx;
|
||||||
export const selectAttestIsSourceComplete = (state: RootState) =>
|
export const selectAttestIsSourceComplete = (state: RootState) =>
|
||||||
!!state.attest.sourceChain && !!state.attest.sourceAsset;
|
!!state.attest.sourceChain && !!state.attest.sourceAsset;
|
||||||
// TODO: check wrapped asset exists or is native attest
|
// TODO: check wrapped asset exists or is native attest
|
||||||
|
@ -28,6 +30,8 @@ export const selectAttestIsTargetComplete = (state: RootState) =>
|
||||||
selectAttestIsSourceComplete(state) && !!state.attest.targetChain;
|
selectAttestIsSourceComplete(state) && !!state.attest.targetChain;
|
||||||
export const selectAttestIsSendComplete = (state: RootState) =>
|
export const selectAttestIsSendComplete = (state: RootState) =>
|
||||||
!!selectAttestSignedVAAHex(state);
|
!!selectAttestSignedVAAHex(state);
|
||||||
|
export const selectAttestIsCreateComplete = (state: RootState) =>
|
||||||
|
!!selectAttestCreateTx(state);
|
||||||
export const selectAttestShouldLockFields = (state: RootState) =>
|
export const selectAttestShouldLockFields = (state: RootState) =>
|
||||||
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
|
selectAttestIsSending(state) || selectAttestIsSendComplete(state);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue