bridge_ui: basic gas fees warning

Change-Id: I7bf78f45d475de9ef0a2a1f372790d2b43322f36
This commit is contained in:
Chase Moran 2021-09-19 01:10:12 -04:00 committed by Evan Gray
parent b39d72e32f
commit 01d84dc7ba
10 changed files with 226 additions and 7 deletions

View File

@ -15,6 +15,7 @@ import {
import { CHAINS } from "../../utils/consts"; import { CHAINS } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
transferField: { transferField: {
@ -68,6 +69,7 @@ function Source() {
onChange={handleAssetChange} onChange={handleAssetChange}
disabled={shouldLockFields} disabled={shouldLockFields}
/> />
<LowBalanceWarning chainId={sourceChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={!isSourceComplete} disabled={!isSourceComplete}
onClick={handleNextClick} onClick={handleNextClick}

View File

@ -1,4 +1,5 @@
import { MenuItem, TextField } from "@material-ui/core"; import { makeStyles, MenuItem, TextField } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { incrementStep, setTargetChain } from "../../store/attestSlice"; import { incrementStep, setTargetChain } from "../../store/attestSlice";
@ -8,11 +9,20 @@ import {
selectAttestSourceChain, selectAttestSourceChain,
selectAttestTargetChain, selectAttestTargetChain,
} from "../../store/selectors"; } from "../../store/selectors";
import { CHAINS } from "../../utils/consts"; import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function Target() { function Target() {
const classes = useStyles();
const dispatch = useDispatch(); const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain); const sourceChain = useSelector(selectAttestSourceChain);
const chains = useMemo( const chains = useMemo(
@ -47,6 +57,11 @@ function Target() {
))} ))}
</TextField> </TextField>
<KeyAndBalance chainId={targetChain} /> <KeyAndBalance chainId={targetChain} />
<Alert severity="info" className={classes.alert}>
You will have to pay transaction fees on{" "}
{CHAINS_BY_ID[targetChain].name} to attest this token.
</Alert>
<LowBalanceWarning chainId={targetChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={!isTargetComplete} disabled={!isTargetComplete}
onClick={handleNextClick} onClick={handleNextClick}

View File

@ -0,0 +1,43 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import { Typography } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { makeStyles } from "@material-ui/core";
import useTransactionFees from "../hooks/useTransactionFees";
import useIsWalletReady from "../hooks/useIsWalletReady";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function LowBalanceWarning({ chainId }: { chainId: ChainId }) {
const classes = useStyles();
const { isReady } = useIsWalletReady(chainId);
const transactionFeeWarning = useTransactionFees(chainId);
const displayWarning =
isReady &&
transactionFeeWarning.balanceString &&
transactionFeeWarning.isSufficientBalance === false;
const warningMessage = `This wallet has a very low ${
chainId === CHAIN_ID_SOLANA ? "SOL" : chainId === CHAIN_ID_ETH ? "ETH" : ""
} balance and may not be able to pay for the upcoming transaction fees.`;
const content = (
<Alert severity="warning" className={classes.alert}>
<Typography variant="body1">{warningMessage}</Typography>
<Typography variant="body1">
{"Current balance: " + transactionFeeWarning.balanceString}
</Typography>
</Alert>
);
return displayWarning ? content : null;
}
export default LowBalanceWarning;

View File

@ -29,6 +29,7 @@ import {
signSendAndConfirm, signSendAndConfirm,
} from "../../utils/solana"; } from "../../utils/solana";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import LowBalanceWarning from "../LowBalanceWarning";
import ShowTx from "../ShowTx"; import ShowTx from "../ShowTx";
import SolanaCreateAssociatedAddress, { import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState, useAssociatedAccountExistsState,
@ -402,6 +403,7 @@ export default function Workflow({
<Divider className={classes.divider} /> <Divider className={classes.divider} />
<SolanaWalletKey /> <SolanaWalletKey />
<LowBalanceWarning chainId={CHAIN_ID_SOLANA} />
{fromTokenAccount && toTokenAccount && fromTokenAccountBalance ? ( {fromTokenAccount && toTokenAccount && fromTokenAccountBalance ? (
<> <>
<Typography variant="body2"> <Typography variant="body2">

View File

@ -18,6 +18,7 @@ import KeyAndBalance from "../KeyAndBalance";
import StepDescription from "../StepDescription"; import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
import { Alert } from "@material-ui/lab"; import { Alert } from "@material-ui/lab";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
transferField: { transferField: {
@ -89,6 +90,7 @@ function Source({
<TokenSelector disabled={shouldLockFields} nft={true} /> <TokenSelector disabled={shouldLockFields} nft={true} />
</div> </div>
) : null} ) : null}
<LowBalanceWarning chainId={sourceChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={!isSourceComplete} disabled={!isSourceComplete}
onClick={handleNextClick} onClick={handleNextClick}

View File

@ -18,15 +18,21 @@ import {
selectNFTTargetError, selectNFTTargetError,
} from "../../store/selectors"; } from "../../store/selectors";
import { hexToNativeString } from "../../utils/array"; import { hexToNativeString } from "../../utils/array";
import { CHAINS } from "../../utils/consts"; import { CHAINS, 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 LowBalanceWarning from "../LowBalanceWarning";
import { Alert } from "@material-ui/lab";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
transferField: { transferField: {
marginTop: theme.spacing(5), marginTop: theme.spacing(5),
}, },
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
})); }));
function Target() { function Target() {
@ -104,6 +110,11 @@ function Target() {
) : null} ) : null}
</> </>
) : null} ) : null}
<Alert severity="info" className={classes.alert}>
You will have to pay transaction fees on{" "}
{CHAINS_BY_ID[targetChain].name} to redeem your NFT.
</Alert>
<LowBalanceWarning chainId={targetChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={!isTargetComplete} //|| !associatedAccountExists} disabled={!isTargetComplete} //|| !associatedAccountExists}
onClick={handleNextClick} onClick={handleNextClick}

View File

@ -22,6 +22,7 @@ import {
import { CHAINS, MIGRATION_ASSET_MAP } from "../../utils/consts"; import { CHAINS, MIGRATION_ASSET_MAP } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
import StepDescription from "../StepDescription"; import StepDescription from "../StepDescription";
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector"; import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
import TokenBlacklistWarning from "./TokenBlacklistWarning"; import TokenBlacklistWarning from "./TokenBlacklistWarning";
@ -125,6 +126,7 @@ function Source({
tokenAddress={parsedTokenAccount?.mintKey} tokenAddress={parsedTokenAccount?.mintKey}
symbol={parsedTokenAccount?.symbol} symbol={parsedTokenAccount?.symbol}
/> />
<LowBalanceWarning chainId={sourceChain} />
{hasParsedTokenAccount ? ( {hasParsedTokenAccount ? (
<TextField <TextField
label="Amount" label="Amount"

View File

@ -1,5 +1,6 @@
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk"; 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 { Alert } from "@material-ui/lab";
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 useIsWalletReady from "../../hooks/useIsWalletReady";
@ -17,9 +18,10 @@ import {
} from "../../store/selectors"; } from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice"; import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { hexToNativeString } from "../../utils/array"; import { hexToNativeString } from "../../utils/array";
import { CHAINS } from "../../utils/consts"; import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader"; import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance"; import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
import SolanaCreateAssociatedAddress, { import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState, useAssociatedAccountExistsState,
} from "../SolanaCreateAssociatedAddress"; } from "../SolanaCreateAssociatedAddress";
@ -30,6 +32,10 @@ const useStyles = makeStyles((theme) => ({
transferField: { transferField: {
marginTop: theme.spacing(5), marginTop: theme.spacing(5),
}, },
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
})); }));
function Target() { function Target() {
@ -105,6 +111,11 @@ function Target() {
value={targetAsset || ""} value={targetAsset || ""}
disabled={true} disabled={true}
/> />
<Alert severity="info" className={classes.alert}>
You will have to pay transaction fees on{" "}
{CHAINS_BY_ID[targetChain].name} to redeem your tokens.
</Alert>
<LowBalanceWarning chainId={targetChain} />
<ButtonWithLoader <ButtonWithLoader
disabled={!isTargetComplete || !associatedAccountExists} disabled={!isTargetComplete || !associatedAccountExists}
onClick={handleNextClick} onClick={handleNextClick}

View File

@ -1,5 +1,4 @@
import { import {
Bridge__factory,
CHAIN_ID_ETH, CHAIN_ID_ETH,
CHAIN_ID_SOLANA, CHAIN_ID_SOLANA,
CHAIN_ID_TERRA, CHAIN_ID_TERRA,
@ -51,7 +50,6 @@ import {
} from "../store/transferSlice"; } from "../store/transferSlice";
import { import {
COVALENT_GET_TOKENS_URL, COVALENT_GET_TOKENS_URL,
ETH_TOKEN_BRIDGE_ADDRESS,
SOLANA_HOST, SOLANA_HOST,
WETH_ADDRESS, WETH_ADDRESS,
WETH_DECIMALS, WETH_DECIMALS,
@ -318,7 +316,7 @@ function useGetAvailableTokens(nft: boolean = false) {
); );
const solanaWallet = useSolanaWallet(); const solanaWallet = useSolanaWallet();
const solPK = solanaWallet?.publicKey; const solPK = solanaWallet?.publicKey;
const { provider, signer, signerAddress } = useEthereumProvider(); const { provider, signerAddress } = useEthereumProvider();
const [covalent, setCovalent] = useState<any>(undefined); const [covalent, setCovalent] = useState<any>(undefined);
const [covalentLoading, setCovalentLoading] = useState(false); const [covalentLoading, setCovalentLoading] = useState(false);

View File

@ -0,0 +1,133 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
} from "@certusone/wormhole-sdk";
import { Provider } from "@certusone/wormhole-sdk/node_modules/@ethersproject/abstract-provider";
import { formatUnits } from "@ethersproject/units";
import { Connection, PublicKey } from "@solana/web3.js";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { SOLANA_HOST } from "../utils/consts";
import { getMultipleAccountsRPC } from "../utils/solana";
import useIsWalletReady from "./useIsWalletReady";
//It's difficult to project how many fees the user will accrue during the
//workflow, as a variable number of transactions can be sent, and different
//execution paths can be hit in the smart contracts, altering gas used.
//As such, for the moment it is best to just check for a reasonable 'low balance' threshold.
//Still it would be good to calculate a reasonable value at runtime based off current gas prices,
//rather than a hardcoded value.
const SOLANA_THRESHOLD_LAMPORTS: bigint = BigInt(300000);
const ETHEREUM_THRESHOLD_WEI: bigint = BigInt(35000000000000000);
const isSufficientBalance = (chainId: ChainId, balance: bigint | undefined) => {
if (balance === undefined || !chainId) {
return true;
}
if (CHAIN_ID_SOLANA === chainId) {
return balance > SOLANA_THRESHOLD_LAMPORTS;
}
if (CHAIN_ID_ETH === chainId) {
return balance > ETHEREUM_THRESHOLD_WEI;
}
if (CHAIN_ID_TERRA === chainId) {
//Terra is complicated because the fees can be paid in multiple currencies.
return true;
}
return true;
};
//TODO move to more generic location
const getBalanceSolana = async (walletAddress: string) => {
const connection = new Connection(SOLANA_HOST);
return getMultipleAccountsRPC(connection, [
new PublicKey(walletAddress),
]).then(
(results) => {
if (results.length && results[0]) {
return BigInt(results[0].lamports);
}
},
(error) => {
return BigInt(0);
}
);
};
const getBalanceEth = async (walletAddress: string, provider: Provider) => {
return provider.getBalance(walletAddress).then((result) => result.toBigInt());
};
const toBalanceString = (balance: bigint | undefined, chainId: ChainId) => {
if (!chainId || balance === undefined) {
return "";
}
if (chainId === CHAIN_ID_ETH) {
return formatUnits(balance, 18); //wei decimals
} else if (chainId === CHAIN_ID_SOLANA) {
return formatUnits(balance, 9); //lamports to sol decmals
} else return "";
};
export default function useTransactionFees(chainId: ChainId) {
const { walletAddress, isReady } = useIsWalletReady(chainId);
const { provider } = useEthereumProvider();
const [balance, setBalance] = useState<bigint | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const loadStart = useCallback(() => {
setBalance(undefined);
setIsLoading(true);
setError("");
}, []);
useEffect(() => {
if (chainId === CHAIN_ID_SOLANA && isReady && walletAddress) {
loadStart();
getBalanceSolana(walletAddress).then(
(result) => {
const adjustedresult =
result === undefined || result === null ? BigInt(0) : result;
setIsLoading(false);
setBalance(adjustedresult);
},
(error) => {
setIsLoading(false);
setError("Cannot load wallet balance");
}
);
} else if (chainId === CHAIN_ID_ETH && isReady && walletAddress) {
if (provider) {
loadStart();
getBalanceEth(walletAddress, provider).then(
(result) => {
const adjustedresult =
result === undefined || result === null ? BigInt(0) : result;
setIsLoading(false);
setBalance(adjustedresult);
},
(error) => {
setIsLoading(false);
setError("Cannot load wallet balance");
}
);
}
}
}, [provider, walletAddress, isReady, chainId, loadStart]);
const results = useMemo(() => {
return {
isSufficientBalance: isSufficientBalance(chainId, balance),
balance,
balanceString: toBalanceString(balance, chainId),
isLoading,
error,
};
}, [balance, chainId, isLoading, error]);
return results;
}