diff --git a/bridge_ui/src/components/Attest/Source.tsx b/bridge_ui/src/components/Attest/Source.tsx
index 1105d5c98..f9a63cdaf 100644
--- a/bridge_ui/src/components/Attest/Source.tsx
+++ b/bridge_ui/src/components/Attest/Source.tsx
@@ -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}
/>
+
({
+ 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() {
))}
+
+ You will have to pay transaction fees on{" "}
+ {CHAINS_BY_ID[targetChain].name} to attest this token.
+
+
({
+ 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 = (
+
+ {warningMessage}
+
+ {"Current balance: " + transactionFeeWarning.balanceString}
+
+
+ );
+
+ return displayWarning ? content : null;
+}
+
+export default LowBalanceWarning;
diff --git a/bridge_ui/src/components/Migration/Workflow.tsx b/bridge_ui/src/components/Migration/Workflow.tsx
index 875e06346..663fb4997 100644
--- a/bridge_ui/src/components/Migration/Workflow.tsx
+++ b/bridge_ui/src/components/Migration/Workflow.tsx
@@ -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({
+
{fromTokenAccount && toTokenAccount && fromTokenAccountBalance ? (
<>
diff --git a/bridge_ui/src/components/NFT/Source.tsx b/bridge_ui/src/components/NFT/Source.tsx
index d83624745..3c336dc0c 100644
--- a/bridge_ui/src/components/NFT/Source.tsx
+++ b/bridge_ui/src/components/NFT/Source.tsx
@@ -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({
) : null}
+
({
transferField: {
marginTop: theme.spacing(5),
},
+ alert: {
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+ },
}));
function Target() {
@@ -104,6 +110,11 @@ function Target() {
) : null}
>
) : null}
+
+ You will have to pay transaction fees on{" "}
+ {CHAINS_BY_ID[targetChain].name} to redeem your NFT.
+
+
+
{hasParsedTokenAccount ? (
({
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}
/>
+
+ You will have to pay transaction fees on{" "}
+ {CHAINS_BY_ID[targetChain].name} to redeem your tokens.
+
+
(undefined);
const [covalentLoading, setCovalentLoading] = useState(false);
diff --git a/bridge_ui/src/hooks/useTransactionFees.ts b/bridge_ui/src/hooks/useTransactionFees.ts
new file mode 100644
index 000000000..e36e69332
--- /dev/null
+++ b/bridge_ui/src/hooks/useTransactionFees.ts
@@ -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(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;
+}