bridge_ui: improve wallet checks

https: //github.com/certusone/wormhole/issues/360
Change-Id: I7ce3696aa0e038faea0da504aa9d8f4c69d7c6a6
This commit is contained in:
Evan Gray 2021-09-01 00:46:07 -04:00
parent e7a1dd600b
commit e11e59095f
9 changed files with 116 additions and 45 deletions

View File

@ -1,16 +1,26 @@
import { useSelector } from "react-redux";
import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped"; import { useHandleCreateWrapped } from "../../hooks/useHandleCreateWrapped";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { selectAttestTargetChain } from "../../store/selectors";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
function Create() { function Create() {
const { handleClick, disabled, showLoader } = useHandleCreateWrapped(); const { handleClick, disabled, showLoader } = useHandleCreateWrapped();
const targetChain = useSelector(selectAttestTargetChain);
const { isReady, statusMessage } = useIsWalletReady(targetChain);
return ( return (
<ButtonWithLoader <>
disabled={disabled} <KeyAndBalance chainId={targetChain} />
onClick={handleClick} <ButtonWithLoader
showLoader={showLoader} disabled={!isReady || disabled}
> onClick={handleClick}
Create showLoader={showLoader}
</ButtonWithLoader> error={statusMessage}
>
Create
</ButtonWithLoader>
</>
); );
} }

View File

@ -1,16 +1,27 @@
import { useSelector } from "react-redux";
import { useHandleAttest } from "../../hooks/useHandleAttest"; import { useHandleAttest } from "../../hooks/useHandleAttest";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { selectAttestSourceChain } from "../../store/selectors";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
function Send() { function Send() {
const { handleClick, disabled, showLoader } = useHandleAttest(); const { handleClick, disabled, showLoader } = useHandleAttest();
const sourceChain = useSelector(selectAttestSourceChain);
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
return ( return (
<ButtonWithLoader <>
disabled={disabled} <KeyAndBalance chainId={sourceChain} />
onClick={handleClick} <ButtonWithLoader
showLoader={showLoader} disabled={!isReady || disabled}
> onClick={handleClick}
Attest showLoader={showLoader}
</ButtonWithLoader> error={statusMessage}
>
Attest
</ButtonWithLoader>
</>
); );
} }

View File

@ -1,5 +1,6 @@
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useHandleRedeem } from "../../hooks/useHandleRedeem"; import { useHandleRedeem } from "../../hooks/useHandleRedeem";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { selectTransferTargetChain } from "../../store/selectors"; import { selectTransferTargetChain } from "../../store/selectors";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
@ -7,13 +8,15 @@ import KeyAndBalance from "../KeyAndBalance";
function Redeem() { function Redeem() {
const { handleClick, disabled, showLoader } = useHandleRedeem(); const { handleClick, disabled, showLoader } = useHandleRedeem();
const targetChain = useSelector(selectTransferTargetChain); const targetChain = useSelector(selectTransferTargetChain);
const { isReady, statusMessage } = useIsWalletReady(targetChain);
return ( return (
<> <>
<KeyAndBalance chainId={targetChain} /> <KeyAndBalance chainId={targetChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={disabled} disabled={!isReady || disabled}
onClick={handleClick} onClick={handleClick}
showLoader={showLoader} showLoader={showLoader}
error={statusMessage}
> >
Redeem Redeem
</ButtonWithLoader> </ButtonWithLoader>

View File

@ -1,5 +1,6 @@
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useHandleTransfer } from "../../hooks/useHandleTransfer"; import { useHandleTransfer } from "../../hooks/useHandleTransfer";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import { import {
selectTransferSourceChain, selectTransferSourceChain,
selectTransferTargetError, selectTransferTargetError,
@ -11,14 +12,15 @@ function Send() {
const { handleClick, disabled, showLoader } = useHandleTransfer(); const { handleClick, disabled, showLoader } = useHandleTransfer();
const sourceChain = useSelector(selectTransferSourceChain); const sourceChain = useSelector(selectTransferSourceChain);
const error = useSelector(selectTransferTargetError); const error = useSelector(selectTransferTargetError);
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
return ( return (
<> <>
<KeyAndBalance chainId={sourceChain} /> <KeyAndBalance chainId={sourceChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={disabled} disabled={!isReady || disabled}
onClick={handleClick} onClick={handleClick}
showLoader={showLoader} showLoader={showLoader}
error={error} error={statusMessage || error}
> >
Transfer Transfer
</ButtonWithLoader> </ButtonWithLoader>

View File

@ -35,7 +35,7 @@ function Source() {
const error = useSelector(selectTransferSourceError); const error = useSelector(selectTransferSourceError);
const isSourceComplete = useSelector(selectTransferIsSourceComplete); const isSourceComplete = useSelector(selectTransferIsSourceComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields); const shouldLockFields = useSelector(selectTransferShouldLockFields);
const isWalletReady = useIsWalletReady(sourceChain); const { isReady, statusMessage } = useIsWalletReady(sourceChain);
const handleSourceChange = useCallback( const handleSourceChange = useCallback(
(event) => { (event) => {
dispatch(setSourceChain(event.target.value)); dispatch(setSourceChain(event.target.value));
@ -67,7 +67,7 @@ function Source() {
))} ))}
</TextField> </TextField>
<KeyAndBalance chainId={sourceChain} balance={uiAmountString} /> <KeyAndBalance chainId={sourceChain} balance={uiAmountString} />
{isWalletReady || uiAmountString ? ( {isReady || uiAmountString ? (
<div className={classes.transferField}> <div className={classes.transferField}>
<TokenSelector disabled={shouldLockFields} /> <TokenSelector disabled={shouldLockFields} />
</div> </div>
@ -86,7 +86,7 @@ function Source() {
disabled={!isSourceComplete} disabled={!isSourceComplete}
onClick={handleNextClick} onClick={handleNextClick}
showLoader={false} showLoader={false}
error={error} error={statusMessage || error}
> >
Next Next
</ButtonWithLoader> </ButtonWithLoader>

View File

@ -2,6 +2,7 @@ import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { makeStyles, MenuItem, TextField } from "@material-ui/core"; import { makeStyles, MenuItem, TextField } from "@material-ui/core";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import useSyncTargetAddress from "../../hooks/useSyncTargetAddress"; import useSyncTargetAddress from "../../hooks/useSyncTargetAddress";
import { import {
selectTransferIsTargetComplete, selectTransferIsTargetComplete,
@ -43,6 +44,7 @@ function Target() {
const error = useSelector(selectTransferTargetError); const error = useSelector(selectTransferTargetError);
const isTargetComplete = useSelector(selectTransferIsTargetComplete); const isTargetComplete = useSelector(selectTransferIsTargetComplete);
const shouldLockFields = useSelector(selectTransferShouldLockFields); const shouldLockFields = useSelector(selectTransferShouldLockFields);
const { statusMessage } = useIsWalletReady(targetChain);
useSyncTargetAddress(!shouldLockFields); useSyncTargetAddress(!shouldLockFields);
const handleTargetChange = useCallback( const handleTargetChange = useCallback(
(event) => { (event) => {
@ -93,7 +95,7 @@ function Target() {
disabled={!isTargetComplete} disabled={!isTargetComplete}
onClick={handleNextClick} onClick={handleNextClick}
showLoader={false} showLoader={false}
error={error} error={statusMessage || error}
> >
Next Next
</ButtonWithLoader> </ButtonWithLoader>

View File

@ -1,5 +1,5 @@
import detectEthereumProvider from "@metamask/detect-provider"; import detectEthereumProvider from "@metamask/detect-provider";
import { ethers } from "ethers"; import { BigNumber, ethers } from "ethers";
import React, { import React, {
ReactChildren, ReactChildren,
useCallback, useCallback,
@ -10,13 +10,12 @@ import React, {
type Provider = ethers.providers.Web3Provider | undefined; type Provider = ethers.providers.Web3Provider | undefined;
type Signer = ethers.Signer | undefined; type Signer = ethers.Signer | undefined;
type Network = ethers.providers.Network | undefined;
interface IEthereumProviderContext { interface IEthereumProviderContext {
connect(): void; connect(): void;
disconnect(): void; disconnect(): void;
provider: Provider; provider: Provider;
network: Network; chainId: number | undefined;
signer: Signer; signer: Signer;
signerAddress: string | undefined; signerAddress: string | undefined;
providerError: string | null; providerError: string | null;
@ -26,7 +25,7 @@ const EthereumProviderContext = React.createContext<IEthereumProviderContext>({
connect: () => {}, connect: () => {},
disconnect: () => {}, disconnect: () => {},
provider: undefined, provider: undefined,
network: undefined, chainId: undefined,
signer: undefined, signer: undefined,
signerAddress: undefined, signerAddress: undefined,
providerError: null, providerError: null,
@ -38,7 +37,7 @@ export const EthereumProviderProvider = ({
}) => { }) => {
const [providerError, setProviderError] = useState<string | null>(null); const [providerError, setProviderError] = useState<string | null>(null);
const [provider, setProvider] = useState<Provider>(undefined); const [provider, setProvider] = useState<Provider>(undefined);
const [network, setNetwork] = useState<Network>(undefined); const [chainId, setChainId] = useState<number | undefined>(undefined);
const [signer, setSigner] = useState<Signer>(undefined); const [signer, setSigner] = useState<Signer>(undefined);
const [signerAddress, setSignerAddress] = useState<string | undefined>( const [signerAddress, setSignerAddress] = useState<string | undefined>(
undefined undefined
@ -51,7 +50,7 @@ export const EthereumProviderProvider = ({
const provider = new ethers.providers.Web3Provider( const provider = new ethers.providers.Web3Provider(
// @ts-ignore // @ts-ignore
detectedProvider, detectedProvider,
"any" //TODO: should we only allow homestead? env perhaps? "any"
); );
provider provider
.send("eth_requestAccounts", []) .send("eth_requestAccounts", [])
@ -61,7 +60,7 @@ export const EthereumProviderProvider = ({
provider provider
.getNetwork() .getNetwork()
.then((network) => { .then((network) => {
setNetwork(network); setChainId(network.chainId);
}) })
.catch(() => { .catch(() => {
setProviderError( setProviderError(
@ -80,6 +79,16 @@ export const EthereumProviderProvider = ({
"An error occurred while getting the signer address" "An error occurred while getting the signer address"
); );
}); });
// TODO: try using ethers directly
// @ts-ignore
if (detectedProvider && detectedProvider.on) {
// @ts-ignore
detectedProvider.on("chainChanged", (chainId) => {
try {
setChainId(BigNumber.from(chainId).toNumber());
} catch (e) {}
});
}
}) })
.catch(() => { .catch(() => {
setProviderError( setProviderError(
@ -97,7 +106,7 @@ export const EthereumProviderProvider = ({
const disconnect = useCallback(() => { const disconnect = useCallback(() => {
setProviderError(null); setProviderError(null);
setProvider(undefined); setProvider(undefined);
setNetwork(undefined); setChainId(undefined);
setSigner(undefined); setSigner(undefined);
setSignerAddress(undefined); setSignerAddress(undefined);
}, []); }, []);
@ -107,7 +116,7 @@ export const EthereumProviderProvider = ({
connect, connect,
disconnect, disconnect,
provider, provider,
network, chainId,
signer, signer,
signerAddress, signerAddress,
providerError, providerError,
@ -116,7 +125,7 @@ export const EthereumProviderProvider = ({
connect, connect,
disconnect, disconnect,
provider, provider,
network, chainId,
signer, signer,
signerAddress, signerAddress,
providerError, providerError,

View File

@ -5,26 +5,53 @@ import {
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import { useConnectedWallet } from "@terra-money/wallet-provider"; import { useConnectedWallet } from "@terra-money/wallet-provider";
import { useMemo } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import { ETH_NETWORK_CHAIN_ID } from "../utils/consts";
function useIsWalletReady(chainId: ChainId) { const createWalletStatus = (isReady: boolean, statusMessage: string = "") => ({
isReady,
statusMessage,
});
function useIsWalletReady(chainId: ChainId): {
isReady: boolean;
statusMessage: string;
} {
const solanaWallet = useSolanaWallet(); const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey; const solPK = solanaWallet?.publicKey;
const terraWallet = useConnectedWallet(); const terraWallet = useConnectedWallet();
const { provider, signerAddress } = useEthereumProvider(); const hasTerraWallet = !!terraWallet;
const {
provider,
signerAddress,
chainId: ethChainId,
} = useEthereumProvider();
const hasEthInfo = !!provider && !!signerAddress;
const hasCorrectEthNetwork = ethChainId === ETH_NETWORK_CHAIN_ID;
let output = false; return useMemo(() => {
if (chainId === CHAIN_ID_TERRA && terraWallet) { if (chainId === CHAIN_ID_TERRA && hasTerraWallet) {
output = true; // TODO: terraWallet does not update on wallet changes
} else if (chainId === CHAIN_ID_SOLANA && solPK) { return createWalletStatus(true);
output = true; }
} else if (chainId === CHAIN_ID_ETH && provider && signerAddress) { if (chainId === CHAIN_ID_SOLANA && solPK) {
output = true; return createWalletStatus(true);
} }
//TODO bsc if (chainId === CHAIN_ID_ETH && hasEthInfo) {
if (hasCorrectEthNetwork) {
return output; return createWalletStatus(true);
} else {
return createWalletStatus(
false,
`Wallet is not connected to ${process.env.REACT_APP_CLUSTER}. Expected Chain ID: ${ETH_NETWORK_CHAIN_ID}`
);
}
}
//TODO bsc
return createWalletStatus(false, "Wallet not connected");
}, [chainId, hasTerraWallet, solPK, hasEthInfo, hasCorrectEthNetwork]);
} }
export default useIsWalletReady; export default useIsWalletReady;

View File

@ -51,6 +51,12 @@ export const WORMHOLE_RPC_HOST =
process.env.REACT_APP_CLUSTER === "testnet" process.env.REACT_APP_CLUSTER === "testnet"
? "https://wormhole-v2-testnet-api.certus.one" ? "https://wormhole-v2-testnet-api.certus.one"
: "http://localhost:8080"; : "http://localhost:8080";
export const ETH_NETWORK_CHAIN_ID =
process.env.REACT_APP_CLUSTER === "mainnet"
? 1
: process.env.REACT_APP_CLUSTER === "testnet"
? 5
: 1337;
export const SOLANA_HOST = export const SOLANA_HOST =
process.env.REACT_APP_CLUSTER === "testnet" process.env.REACT_APP_CLUSTER === "testnet"
? clusterApiUrl("testnet") ? clusterApiUrl("testnet")
@ -92,7 +98,8 @@ export const TERRA_TEST_TOKEN_ADDRESS =
export const TERRA_BRIDGE_ADDRESS = export const TERRA_BRIDGE_ADDRESS =
"terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"; "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5";
export const TERRA_TOKEN_BRIDGE_ADDRESS = export const TERRA_TOKEN_BRIDGE_ADDRESS =
"terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4"; "terra174kgn5rtw4kf6f938wm7kwh70h2v4vcfd26jlc";
// "terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4";
export const COVALENT_API_KEY = process.env.REACT_APP_COVALENT_API_KEY export const COVALENT_API_KEY = process.env.REACT_APP_COVALENT_API_KEY
? process.env.REACT_APP_COVALENT_API_KEY ? process.env.REACT_APP_COVALENT_API_KEY