bridge_ui: show warnings
fixes https://github.com/certusone/wormhole/issues/361 Change-Id: I69b357a56eaaf25d46c83ab5fd84bc05d3eaee2a
This commit is contained in:
parent
e70db48ef7
commit
49d41733a7
|
@ -1,18 +1,19 @@
|
|||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
incrementStep,
|
||||
setSourceAsset,
|
||||
setSourceChain,
|
||||
} from "../../store/attestSlice";
|
||||
import {
|
||||
selectAttestIsSourceComplete,
|
||||
selectAttestShouldLockFields,
|
||||
selectAttestSourceAsset,
|
||||
selectAttestSourceChain,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
incrementStep,
|
||||
setSourceAsset,
|
||||
setSourceChain,
|
||||
} from "../../store/attestSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
|
@ -40,12 +41,9 @@ function Source() {
|
|||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(() => {
|
||||
dispatch(incrementStep());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -70,14 +68,13 @@ function Source() {
|
|||
onChange={handleAssetChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<Button
|
||||
<ButtonWithLoader
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
showLoader={false}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { Button, MenuItem, TextField } from "@material-ui/core";
|
||||
import { MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { incrementStep, setTargetChain } from "../../store/attestSlice";
|
||||
import {
|
||||
selectAttestIsTargetComplete,
|
||||
selectAttestShouldLockFields,
|
||||
selectAttestSourceChain,
|
||||
selectAttestTargetChain,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/attestSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Target() {
|
||||
|
@ -27,12 +28,9 @@ function Target() {
|
|||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(() => {
|
||||
dispatch(incrementStep());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -50,14 +48,13 @@ function Target() {
|
|||
</TextField>
|
||||
{/* TODO: determine "to" token address */}
|
||||
<KeyAndBalance chainId={targetChain} />
|
||||
<Button
|
||||
<ButtonWithLoader
|
||||
disabled={!isTargetComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
showLoader={false}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Button, CircularProgress, makeStyles } from "@material-ui/core";
|
||||
import {
|
||||
Button,
|
||||
CircularProgress,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { ReactChild } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
|
@ -17,38 +22,51 @@ const useStyles = makeStyles((theme) => ({
|
|||
marginLeft: -12,
|
||||
marginBottom: 6,
|
||||
},
|
||||
error: {
|
||||
marginTop: theme.spacing(1),
|
||||
textAlign: "center",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function ButtonWithLoader({
|
||||
disabled,
|
||||
onClick,
|
||||
showLoader,
|
||||
error,
|
||||
children,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
showLoader: boolean;
|
||||
error?: string;
|
||||
children: ReactChild;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
className={classes.loader}
|
||||
/>
|
||||
<>
|
||||
<div className={classes.root}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
{showLoader ? (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
color="inherit"
|
||||
className={classes.loader}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<Typography color="error" className={classes.error}>
|
||||
{error}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
bottom: 0,
|
||||
left: 0,
|
||||
background: `radial-gradient(100% 100% at 100% 125%,${theme.palette.secondary.dark} 0,rgba(255,255,255,0) 100%)`,
|
||||
zIndex: -1,
|
||||
},
|
||||
hole: {
|
||||
position: "fixed",
|
||||
|
@ -16,6 +17,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
right: 0,
|
||||
opacity: 0.3,
|
||||
filter: "blur(1px)",
|
||||
zIndex: -1,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Toolbar } from "@material-ui/core";
|
||||
import { makeStyles } from "@material-ui/core";
|
||||
import DisconnectIcon from "@material-ui/icons/LinkOff";
|
||||
import {
|
||||
WalletDisconnectButton,
|
||||
|
@ -6,18 +6,31 @@ import {
|
|||
} from "@solana/wallet-adapter-material-ui";
|
||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
textAlign: "center",
|
||||
margin: `${theme.spacing(1)}px auto`,
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
},
|
||||
disconnectButton: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const SolanaWalletKey = () => {
|
||||
const classes = useStyles();
|
||||
const wallet = useSolanaWallet();
|
||||
return (
|
||||
<Toolbar style={{ display: "flex" }}>
|
||||
<div className={classes.root}>
|
||||
<WalletMultiButton />
|
||||
{wallet && (
|
||||
<WalletDisconnectButton
|
||||
startIcon={<DisconnectIcon />}
|
||||
style={{ marginLeft: 8 }}
|
||||
className={classes.disconnectButton}
|
||||
/>
|
||||
)}
|
||||
</Toolbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
||||
import { selectTransferSourceChain } from "../../store/selectors";
|
||||
import {
|
||||
selectTransferSourceChain,
|
||||
selectTransferTargetError,
|
||||
} from "../../store/selectors";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
|
||||
function Send() {
|
||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const error = useSelector(selectTransferTargetError);
|
||||
return (
|
||||
<>
|
||||
<KeyAndBalance chainId={sourceChain} />
|
||||
|
@ -14,6 +18,7 @@ function Send() {
|
|||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
showLoader={showLoader}
|
||||
error={error}
|
||||
>
|
||||
Transfer
|
||||
</ButtonWithLoader>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
|
@ -8,6 +8,7 @@ import {
|
|||
selectTransferShouldLockFields,
|
||||
selectTransferSourceBalanceString,
|
||||
selectTransferSourceChain,
|
||||
selectTransferSourceError,
|
||||
} from "../../store/selectors";
|
||||
import {
|
||||
incrementStep,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
setSourceChain,
|
||||
} from "../../store/transferSlice";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
|
||||
|
||||
|
@ -30,6 +32,7 @@ function Source() {
|
|||
const sourceChain = useSelector(selectTransferSourceChain);
|
||||
const uiAmountString = useSelector(selectTransferSourceBalanceString);
|
||||
const amount = useSelector(selectTransferAmount);
|
||||
const error = useSelector(selectTransferSourceError);
|
||||
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
|
||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
const isWalletReady = useIsWalletReady(sourceChain);
|
||||
|
@ -45,12 +48,9 @@ function Source() {
|
|||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(() => {
|
||||
dispatch(incrementStep());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -82,14 +82,14 @@ function Source() {
|
|||
onChange={handleAmountChange}
|
||||
disabled={shouldLockFields}
|
||||
/>
|
||||
<Button
|
||||
<ButtonWithLoader
|
||||
disabled={!isSourceComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
showLoader={false}
|
||||
error={error}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
|
||||
import {
|
||||
selectTransferIsTargetComplete,
|
||||
|
@ -11,10 +11,12 @@ import {
|
|||
selectTransferTargetAsset,
|
||||
selectTransferTargetBalanceString,
|
||||
selectTransferTargetChain,
|
||||
selectTransferTargetError,
|
||||
} from "../../store/selectors";
|
||||
import { incrementStep, setTargetChain } from "../../store/transferSlice";
|
||||
import { hexToNativeString } from "../../utils/array";
|
||||
import { CHAINS } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import SolanaCreateAssociatedAddress from "../SolanaCreateAssociatedAddress";
|
||||
|
||||
|
@ -33,11 +35,12 @@ function Target() {
|
|||
[sourceChain]
|
||||
);
|
||||
const targetChain = useSelector(selectTransferTargetChain);
|
||||
const targetAddressHex = useSelector(selectTransferTargetAddressHex); // TODO: make readable
|
||||
const targetAddressHex = useSelector(selectTransferTargetAddressHex);
|
||||
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||
const readableTargetAddress =
|
||||
hexToNativeString(targetAddressHex, targetChain) || "";
|
||||
const uiAmountString = useSelector(selectTransferTargetBalanceString);
|
||||
const error = useSelector(selectTransferTargetError);
|
||||
const isTargetComplete = useSelector(selectTransferIsTargetComplete);
|
||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
useSyncTargetAddress(!shouldLockFields);
|
||||
|
@ -47,12 +50,9 @@ function Target() {
|
|||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(
|
||||
(event) => {
|
||||
dispatch(incrementStep());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleNextClick = useCallback(() => {
|
||||
dispatch(incrementStep());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -89,14 +89,14 @@ function Target() {
|
|||
value={targetAsset || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
<Button
|
||||
<ButtonWithLoader
|
||||
disabled={!isTargetComplete}
|
||||
onClick={handleNextClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
showLoader={false}
|
||||
error={error}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</ButtonWithLoader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,7 +69,6 @@ function useFetchTargetAsset() {
|
|||
try {
|
||||
const asset = await getForeignAssetTerra(sourceChain, sourceAsset);
|
||||
if (!cancelled) {
|
||||
console.log("terra target asset", asset);
|
||||
dispatch(setTargetAsset(asset));
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -59,19 +59,22 @@ function useSyncTargetAddress(shouldFire: boolean) {
|
|||
} else if (targetChain === CHAIN_ID_SOLANA && solPK && targetAsset) {
|
||||
// otherwise, use the associated token account (which we create in the case it doesn't exist)
|
||||
(async () => {
|
||||
const associatedTokenAccount = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(targetAsset),
|
||||
solPK
|
||||
);
|
||||
if (!cancelled) {
|
||||
dispatch(
|
||||
setTargetAddressHex(
|
||||
uint8ArrayToHex(zeroPad(associatedTokenAccount.toBytes(), 32))
|
||||
)
|
||||
);
|
||||
}
|
||||
try {
|
||||
const associatedTokenAccount =
|
||||
await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(targetAsset), // this might error
|
||||
solPK
|
||||
);
|
||||
if (!cancelled) {
|
||||
dispatch(
|
||||
setTargetAddressHex(
|
||||
uint8ArrayToHex(zeroPad(associatedTokenAccount.toBytes(), 32))
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
} else if (
|
||||
targetChain === CHAIN_ID_TERRA &&
|
||||
|
|
|
@ -21,9 +21,6 @@ export const selectAttestIsSending = (state: RootState) =>
|
|||
state.attest.isSending;
|
||||
export const selectAttestIsCreating = (state: RootState) =>
|
||||
state.attest.isCreating;
|
||||
|
||||
// safety checks
|
||||
// TODO: could make this return a string with a user informative message
|
||||
export const selectAttestIsSourceComplete = (state: RootState) =>
|
||||
!!state.attest.sourceChain && !!state.attest.sourceAsset;
|
||||
// TODO: check wrapped asset exists or is native attest
|
||||
|
@ -74,49 +71,85 @@ export const selectTransferIsSending = (state: RootState) =>
|
|||
state.transfer.isSending;
|
||||
export const selectTransferIsRedeeming = (state: RootState) =>
|
||||
state.transfer.isRedeeming;
|
||||
|
||||
// safety checks
|
||||
// TODO: could make this return a string with a user informative message
|
||||
export const selectTransferIsSourceComplete = (state: RootState) => {
|
||||
export const selectTransferSourceError = (state: RootState) => {
|
||||
if (!state.transfer.sourceChain) {
|
||||
return "Select a source chain";
|
||||
}
|
||||
if (!state.transfer.sourceParsedTokenAccount) {
|
||||
return "Select a token";
|
||||
}
|
||||
if (!state.transfer.amount) {
|
||||
return "Enter an amount";
|
||||
}
|
||||
if (
|
||||
state.transfer.sourceChain === CHAIN_ID_SOLANA &&
|
||||
!state.transfer.sourceParsedTokenAccount.publicKey
|
||||
) {
|
||||
return "Token account unavailable";
|
||||
}
|
||||
if (!state.transfer.sourceParsedTokenAccount.uiAmountString) {
|
||||
return "Token amount unavailable";
|
||||
}
|
||||
if (state.transfer.sourceParsedTokenAccount.decimals === 0) {
|
||||
// TODO: more advanced NFT check
|
||||
return "NFTs are not currently supported";
|
||||
}
|
||||
try {
|
||||
return (
|
||||
!!state.transfer.sourceChain &&
|
||||
!!state.transfer.sourceParsedTokenAccount &&
|
||||
!!state.transfer.amount &&
|
||||
(state.transfer.sourceChain !== CHAIN_ID_SOLANA ||
|
||||
!!state.transfer.sourceParsedTokenAccount.publicKey) &&
|
||||
!!state.transfer.sourceParsedTokenAccount.uiAmountString &&
|
||||
!!state.transfer.sourceParsedTokenAccount.decimals &&
|
||||
state.transfer.sourceParsedTokenAccount.decimals > 0 && // TODO: more advanced NFT check
|
||||
// may trigger error: fractional component exceeds decimals
|
||||
// these may trigger error: fractional component exceeds decimals
|
||||
if (
|
||||
parseUnits(
|
||||
state.transfer.amount,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
).gt(0) &&
|
||||
// may trigger error: fractional component exceeds decimals
|
||||
).lte(0)
|
||||
) {
|
||||
return "Amount must be greater than zero";
|
||||
}
|
||||
if (
|
||||
parseUnits(
|
||||
state.transfer.amount,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
).lte(
|
||||
).gt(
|
||||
parseUnits(
|
||||
state.transfer.sourceParsedTokenAccount.uiAmountString,
|
||||
state.transfer.sourceParsedTokenAccount.decimals
|
||||
)
|
||||
)
|
||||
);
|
||||
) {
|
||||
return "Amount may not be greater than balance";
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
if (e?.message) {
|
||||
return e.message.substring(0, e.message.indexOf("("));
|
||||
}
|
||||
return "Invalid amount";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
export const selectTransferIsSourceComplete = (state: RootState) =>
|
||||
!selectTransferSourceError(state);
|
||||
export const selectTransferTargetError = (state: RootState) => {
|
||||
const sourceError = selectTransferSourceError(state);
|
||||
if (sourceError) {
|
||||
return `Error in source: ${sourceError}`;
|
||||
}
|
||||
if (!state.transfer.targetChain) {
|
||||
return "Select a target chain";
|
||||
}
|
||||
if (!state.transfer.targetAsset) {
|
||||
return "Target asset unavailable. Is the token attested?";
|
||||
}
|
||||
if (
|
||||
state.transfer.targetChain === CHAIN_ID_ETH &&
|
||||
state.transfer.targetAsset === ethers.constants.AddressZero
|
||||
) {
|
||||
return "Target asset unavailable. Is the token attested?";
|
||||
}
|
||||
if (!state.transfer.targetAddressHex) {
|
||||
return "Target account unavailable";
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: check wrapped asset exists or is native transfer
|
||||
export const selectTransferIsTargetComplete = (state: RootState) =>
|
||||
selectTransferIsSourceComplete(state) &&
|
||||
!!state.transfer.targetChain &&
|
||||
!!state.transfer.targetAsset &&
|
||||
(state.transfer.targetChain !== CHAIN_ID_ETH ||
|
||||
state.transfer.targetAsset !== ethers.constants.AddressZero) &&
|
||||
!!state.transfer.targetAddressHex;
|
||||
!selectTransferTargetError(state);
|
||||
export const selectTransferIsSendComplete = (state: RootState) =>
|
||||
!!selectTransferSignedVAAHex(state);
|
||||
export const selectTransferShouldLockFields = (state: RootState) =>
|
||||
|
|
Loading…
Reference in New Issue