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 ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({
transferField: {
@ -68,6 +69,7 @@ function Source() {
onChange={handleAssetChange}
disabled={shouldLockFields}
/>
<LowBalanceWarning chainId={sourceChain} />
<ButtonWithLoader
disabled={!isSourceComplete}
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 { useDispatch, useSelector } from "react-redux";
import { incrementStep, setTargetChain } from "../../store/attestSlice";
@ -8,11 +9,20 @@ import {
selectAttestSourceChain,
selectAttestTargetChain,
} from "../../store/selectors";
import { CHAINS } from "../../utils/consts";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
const useStyles = makeStyles((theme) => ({
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function Target() {
const classes = useStyles();
const dispatch = useDispatch();
const sourceChain = useSelector(selectAttestSourceChain);
const chains = useMemo(
@ -47,6 +57,11 @@ function Target() {
))}
</TextField>
<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
disabled={!isTargetComplete}
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,
} from "../../utils/solana";
import ButtonWithLoader from "../ButtonWithLoader";
import LowBalanceWarning from "../LowBalanceWarning";
import ShowTx from "../ShowTx";
import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState,
@ -402,6 +403,7 @@ export default function Workflow({
<Divider className={classes.divider} />
<SolanaWalletKey />
<LowBalanceWarning chainId={CHAIN_ID_SOLANA} />
{fromTokenAccount && toTokenAccount && fromTokenAccountBalance ? (
<>
<Typography variant="body2">

View File

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

View File

@ -18,15 +18,21 @@ import {
selectNFTTargetError,
} from "../../store/selectors";
import { hexToNativeString } from "../../utils/array";
import { CHAINS } from "../../utils/consts";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import StepDescription from "../StepDescription";
import LowBalanceWarning from "../LowBalanceWarning";
import { Alert } from "@material-ui/lab";
const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function Target() {
@ -104,6 +110,11 @@ function Target() {
) : 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
disabled={!isTargetComplete} //|| !associatedAccountExists}
onClick={handleNextClick}

View File

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

View File

@ -1,5 +1,6 @@
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { makeStyles, MenuItem, TextField } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useCallback, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import useIsWalletReady from "../../hooks/useIsWalletReady";
@ -17,9 +18,10 @@ import {
} from "../../store/selectors";
import { incrementStep, setTargetChain } from "../../store/transferSlice";
import { hexToNativeString } from "../../utils/array";
import { CHAINS } from "../../utils/consts";
import { CHAINS, CHAINS_BY_ID } from "../../utils/consts";
import ButtonWithLoader from "../ButtonWithLoader";
import KeyAndBalance from "../KeyAndBalance";
import LowBalanceWarning from "../LowBalanceWarning";
import SolanaCreateAssociatedAddress, {
useAssociatedAccountExistsState,
} from "../SolanaCreateAssociatedAddress";
@ -30,6 +32,10 @@ const useStyles = makeStyles((theme) => ({
transferField: {
marginTop: theme.spacing(5),
},
alert: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));
function Target() {
@ -105,6 +111,11 @@ function Target() {
value={targetAsset || ""}
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
disabled={!isTargetComplete || !associatedAccountExists}
onClick={handleNextClick}

View File

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