Browse Source

bridge_ui: Terra fee denomination support

Kevin Peters 3 năm trước cách đây
mục cha
commit
7882eccac4

+ 5 - 1
bridge_ui/src/components/Attest/Create.tsx

@@ -1,3 +1,4 @@
+import { CHAIN_ID_TERRA } from "@certusone/wormhole-sdk";
 import { CircularProgress, makeStyles } from "@material-ui/core";
 import { useSelector } from "react-redux";
 import useFetchForeignAsset from "../../hooks/useFetchForeignAsset";
@@ -10,6 +11,7 @@ import {
 } from "../../store/selectors";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
+import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
 
 const useStyles = makeStyles((theme) => ({
@@ -45,7 +47,9 @@ function Create() {
   return (
     <>
       <KeyAndBalance chainId={targetChain} />
-
+      {targetChain === CHAIN_ID_TERRA && (
+        <TerraFeeDenomPicker disabled={disabled} />
+      )}
       {foreignAssetInfo.isFetching ? (
         <>
           <div className={classes.spacer} />

+ 6 - 2
bridge_ui/src/components/Attest/Send.tsx

@@ -1,4 +1,4 @@
-import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
+import { CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "@certusone/wormhole-sdk";
 import { Alert } from "@material-ui/lab";
 import { Link, makeStyles } from "@material-ui/core";
 import { useMemo } from "react";
@@ -17,6 +17,7 @@ import KeyAndBalance from "../KeyAndBalance";
 import TransactionProgress from "../TransactionProgress";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
 import { SOLANA_TOKEN_METADATA_PROGRAM_URL } from "../../utils/consts";
+import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
 
 const useStyles = makeStyles((theme) => ({
   alert: {
@@ -62,6 +63,9 @@ function Send() {
   return (
     <>
       <KeyAndBalance chainId={sourceChain} />
+      {sourceChain === CHAIN_ID_TERRA && (
+        <TerraFeeDenomPicker disabled={disabled} />
+      )}
       <ButtonWithLoader
         disabled={!isReady || disabled}
         onClick={handleClick}
@@ -70,7 +74,7 @@ function Send() {
       >
         Attest
       </ButtonWithLoader>
-      {sourceChain === CHAIN_ID_SOLANA ? <SolanaTokenMetadataWarning /> : null}
+      {sourceChain === CHAIN_ID_SOLANA && <SolanaTokenMetadataWarning />}
       <WaitingForWalletMessage />
       <TransactionProgress
         chainId={sourceChain}

+ 14 - 8
bridge_ui/src/components/LowBalanceWarning.tsx

@@ -1,4 +1,4 @@
-import { ChainId } from "@certusone/wormhole-sdk";
+import { ChainId, CHAIN_ID_TERRA } from "@certusone/wormhole-sdk";
 import { makeStyles, Typography } from "@material-ui/core";
 import { Alert } from "@material-ui/lab";
 import useIsWalletReady from "../hooks/useIsWalletReady";
@@ -18,18 +18,24 @@ function LowBalanceWarning({ chainId }: { chainId: ChainId }) {
   const transactionFeeWarning = useTransactionFees(chainId);
   const displayWarning =
     isReady &&
-    transactionFeeWarning.balanceString &&
+    (chainId === CHAIN_ID_TERRA || transactionFeeWarning.balanceString) &&
     transactionFeeWarning.isSufficientBalance === false;
-  const warningMessage = `This wallet has a very low ${getDefaultNativeCurrencySymbol(
-    chainId
-  )} balance and may not be able to pay for the upcoming transaction fees.`;
+
+  const warningMessage =
+    chainId === CHAIN_ID_TERRA
+      ? "This wallet may not have sufficient funds to pay for the upcoming transaction fees."
+      : `This wallet has a very low ${getDefaultNativeCurrencySymbol(
+          chainId
+        )} balance and may not be able to pay for the upcoming transaction fees.`;
 
   const content = (
     <Alert severity="warning" variant="outlined" className={classes.alert}>
       <Typography variant="body1">{warningMessage}</Typography>
-      <Typography variant="body1">
-        {"Current balance: " + transactionFeeWarning.balanceString}
-      </Typography>
+      {chainId !== CHAIN_ID_TERRA ? (
+        <Typography variant="body1">
+          {"Current balance: " + transactionFeeWarning.balanceString}
+        </Typography>
+      ) : null}
     </Alert>
   );
 

+ 5 - 0
bridge_ui/src/components/NFT/Redeem.tsx

@@ -1,3 +1,4 @@
+import { CHAIN_ID_TERRA } from "@certusone/wormhole-sdk";
 import { useSelector } from "react-redux";
 import { useHandleNFTRedeem } from "../../hooks/useHandleNFTRedeem";
 import useIsWalletReady from "../../hooks/useIsWalletReady";
@@ -5,6 +6,7 @@ import { selectNFTTargetChain } from "../../store/selectors";
 import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import StepDescription from "../StepDescription";
+import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
 
 function Redeem() {
@@ -15,6 +17,9 @@ function Redeem() {
     <>
       <StepDescription>Receive the NFT on the target chain</StepDescription>
       <KeyAndBalance chainId={targetChain} />
+      {targetChain === CHAIN_ID_TERRA && (
+        <TerraFeeDenomPicker disabled={disabled} />
+      )}
       <ButtonWithLoader
         disabled={!isReady || disabled}
         onClick={handleClick}

+ 5 - 0
bridge_ui/src/components/NFT/Send.tsx

@@ -1,3 +1,4 @@
+import { CHAIN_ID_TERRA } from "@certusone/wormhole-sdk";
 import { Alert } from "@material-ui/lab";
 import { useSelector } from "react-redux";
 import { useHandleNFTTransfer } from "../../hooks/useHandleNFTTransfer";
@@ -14,6 +15,7 @@ import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import ShowTx from "../ShowTx";
 import StepDescription from "../StepDescription";
+import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
 import TransactionProgress from "../TransactionProgress";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
 
@@ -41,6 +43,9 @@ function Send() {
         Transfer the NFT to the Wormhole Token Bridge.
       </StepDescription>
       <KeyAndBalance chainId={sourceChain} />
+      {sourceChain === CHAIN_ID_TERRA && (
+        <TerraFeeDenomPicker disabled={disabled} />
+      )}
       <Alert severity="info" variant="outlined">
         This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
         wait for finalization. If you navigate away from this page before

+ 107 - 0
bridge_ui/src/components/TerraFeeDenomPicker.tsx

@@ -0,0 +1,107 @@
+import {
+  MenuItem,
+  makeStyles,
+  TextField,
+  Typography,
+  ListItemIcon,
+} from "@material-ui/core";
+import { useConnectedWallet } from "@terra-money/wallet-provider";
+import { useMemo } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { setTerraFeeDenom } from "../store/feeSlice";
+import { selectTerraFeeDenom } from "../store/selectors";
+import useTerraNativeBalances from "../hooks/useTerraNativeBalances";
+import { formatNativeDenom, getNativeTerraIcon } from "../utils/terra";
+
+const useStyles = makeStyles((theme) => ({
+  feePickerContainer: {
+    display: "flex",
+    flexDirection: "column",
+    margin: `${theme.spacing(1)}px auto`,
+    maxWidth: 200,
+    width: "100%",
+  },
+  select: {
+    "& .MuiSelect-root": {
+      display: "flex",
+      alignItems: "center",
+    },
+  },
+  listItemIcon: {
+    minWidth: 40,
+  },
+  icon: {
+    height: 24,
+    maxWidth: 24,
+  },
+}));
+
+type TerraFeeDenomPickerProps = {
+  disabled: boolean;
+};
+
+export default function TerraFeeDenomPicker(props: TerraFeeDenomPickerProps) {
+  const terraFeeDenom = useSelector(selectTerraFeeDenom);
+  const wallet = useConnectedWallet();
+  const { balances } = useTerraNativeBalances(wallet?.walletAddress);
+  const dispatch = useDispatch();
+  const classes = useStyles();
+
+  const feeDenomItems = useMemo(() => {
+    const items = [];
+    if (balances) {
+      for (const [denom, amount] of Object.entries(balances)) {
+        if (amount === "0") continue;
+        const symbol = formatNativeDenom(denom);
+        if (symbol) {
+          items.push({
+            denom,
+            symbol,
+            icon: getNativeTerraIcon(symbol),
+          });
+        }
+      }
+    }
+    // prevent an out-of-range value from being selected
+    if (!items.find((item) => item.denom === terraFeeDenom)) {
+      const symbol = formatNativeDenom(terraFeeDenom);
+      items.push({
+        denom: terraFeeDenom,
+        symbol,
+        icon: getNativeTerraIcon(symbol),
+      });
+    }
+    return items;
+  }, [balances, terraFeeDenom]);
+
+  return (
+    <div className={classes.feePickerContainer}>
+      <Typography variant="caption">Fee Denomination</Typography>
+      <TextField
+        variant="outlined"
+        size="small"
+        select
+        fullWidth
+        value={terraFeeDenom}
+        onChange={(event) => dispatch(setTerraFeeDenom(event.target.value))}
+        disabled={props.disabled}
+        className={classes.select}
+      >
+        {feeDenomItems.map((item) => {
+          return (
+            <MenuItem key={item.denom} value={item.denom}>
+              <ListItemIcon>
+                <img
+                  src={item.icon}
+                  alt={item.symbol}
+                  className={classes.icon}
+                />
+              </ListItemIcon>
+              {item.symbol}
+            </MenuItem>
+          );
+        })}
+      </TextField>
+    </div>
+  );
+}

+ 5 - 0
bridge_ui/src/components/Transfer/Redeem.tsx

@@ -6,6 +6,7 @@ import {
   CHAIN_ID_OASIS,
   CHAIN_ID_POLYGON,
   CHAIN_ID_SOLANA,
+  CHAIN_ID_TERRA,
   isEVMChain,
   WSOL_ADDRESS,
 } from "@certusone/wormhole-sdk";
@@ -41,6 +42,7 @@ import KeyAndBalance from "../KeyAndBalance";
 import SmartAddress from "../SmartAddress";
 import { SolanaCreateAssociatedAddressAlternate } from "../SolanaCreateAssociatedAddress";
 import StepDescription from "../StepDescription";
+import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
 import AddToMetamask from "./AddToMetamask";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
 
@@ -112,6 +114,9 @@ function Redeem() {
     <>
       <StepDescription>Receive the tokens on the target chain</StepDescription>
       <KeyAndBalance chainId={targetChain} />
+      {targetChain === CHAIN_ID_TERRA && (
+        <TerraFeeDenomPicker disabled={disabled} />
+      )}
       {isNativeEligible && (
         <FormControlLabel
           control={

+ 5 - 1
bridge_ui/src/components/Transfer/Send.tsx

@@ -1,4 +1,4 @@
-import { isEVMChain } from "@certusone/wormhole-sdk";
+import { CHAIN_ID_TERRA, isEVMChain } from "@certusone/wormhole-sdk";
 import { Checkbox, FormControlLabel } from "@material-ui/core";
 import { Alert } from "@material-ui/lab";
 import { ethers } from "ethers";
@@ -23,6 +23,7 @@ import ButtonWithLoader from "../ButtonWithLoader";
 import KeyAndBalance from "../KeyAndBalance";
 import ShowTx from "../ShowTx";
 import StepDescription from "../StepDescription";
+import TerraFeeDenomPicker from "../TerraFeeDenomPicker";
 import TransactionProgress from "../TransactionProgress";
 import SendConfirmationDialog from "./SendConfirmationDialog";
 import WaitingForWalletMessage from "./WaitingForWalletMessage";
@@ -130,6 +131,9 @@ function Send() {
         Transfer the tokens to the Wormhole Token Bridge.
       </StepDescription>
       <KeyAndBalance chainId={sourceChain} />
+      {sourceChain === CHAIN_ID_TERRA && (
+        <TerraFeeDenomPicker disabled={disabled} />
+      )}
       <Alert severity="info" variant="outlined">
         This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
         wait for finalization. If you navigate away from this page before

+ 14 - 4
bridge_ui/src/components/WithdrawTokensTerra.tsx

@@ -22,6 +22,9 @@ import { postWithFees, waitForTerraExecution } from "../utils/terra";
 import ButtonWithLoader from "./ButtonWithLoader";
 import { useSnackbar } from "notistack";
 import { Alert } from "@material-ui/lab";
+import { useSelector } from "react-redux";
+import { selectTerraFeeDenom } from "../store/selectors";
+import TerraFeeDenomPicker from "./TerraFeeDenomPicker";
 
 const useStyles = makeStyles((theme) => ({
   formControl: {
@@ -33,7 +36,11 @@ const useStyles = makeStyles((theme) => ({
   },
 }));
 
-const withdraw = async (wallet: ConnectedWallet, token: string) => {
+const withdraw = async (
+  wallet: ConnectedWallet,
+  token: string,
+  feeDenom: string
+) => {
   const withdraw = new MsgExecuteContract(
     wallet.walletAddress,
     TERRA_TOKEN_BRIDGE_ADDRESS,
@@ -51,7 +58,8 @@ const withdraw = async (wallet: ConnectedWallet, token: string) => {
   const txResult = await postWithFees(
     wallet,
     [withdraw],
-    "Wormhole - Withdraw Tokens"
+    "Wormhole - Withdraw Tokens",
+    [feeDenom]
   );
   await waitForTerraExecution(txResult);
 };
@@ -62,13 +70,14 @@ export default function WithdrawTokensTerra() {
   const [isLoading, setIsLoading] = useState(false);
   const classes = useStyles();
   const { enqueueSnackbar } = useSnackbar();
+  const feeDenom = useSelector(selectTerraFeeDenom);
 
   const handleClick = useCallback(() => {
     if (wallet) {
       (async () => {
         setIsLoading(true);
         try {
-          await withdraw(wallet, token);
+          await withdraw(wallet, token, feeDenom);
           enqueueSnackbar(null, {
             content: <Alert severity="success">Transaction confirmed.</Alert>,
           });
@@ -81,7 +90,7 @@ export default function WithdrawTokensTerra() {
         setIsLoading(false);
       })();
     }
-  }, [wallet, token, enqueueSnackbar]);
+  }, [wallet, token, enqueueSnackbar, feeDenom]);
 
   return (
     <Container maxWidth="md">
@@ -103,6 +112,7 @@ export default function WithdrawTokensTerra() {
             </MenuItem>
           ))}
         </Select>
+        <TerraFeeDenomPicker disabled={isLoading} />
         <ButtonWithLoader
           onClick={handleClick}
           disabled={!wallet || isLoading}

+ 9 - 3
bridge_ui/src/hooks/useHandleAttest.tsx

@@ -38,6 +38,7 @@ import {
   selectAttestIsTargetComplete,
   selectAttestSourceAsset,
   selectAttestSourceChain,
+  selectTerraFeeDenom,
 } from "../store/selectors";
 import {
   getBridgeAddressForChain,
@@ -156,7 +157,8 @@ async function terra(
   dispatch: any,
   enqueueSnackbar: any,
   wallet: ConnectedWallet,
-  asset: string
+  asset: string,
+  feeDenom: string
 ) {
   dispatch(setIsSending(true));
   try {
@@ -165,7 +167,9 @@ async function terra(
       wallet.terraAddress,
       asset
     );
-    const result = await postWithFees(wallet, [msg], "Create Wrapped");
+    const result = await postWithFees(wallet, [msg], "Create Wrapped", [
+      feeDenom,
+    ]);
     const info = await waitForTerraExecution(result);
     dispatch(setAttestTx({ id: info.txhash, block: info.height }));
     enqueueSnackbar(null, {
@@ -211,6 +215,7 @@ export function useHandleAttest() {
   const solanaWallet = useSolanaWallet();
   const solPK = solanaWallet?.publicKey;
   const terraWallet = useConnectedWallet();
+  const terraFeeDenom = useSelector(selectTerraFeeDenom);
   const disabled = !isTargetComplete || isSending || isSendComplete;
   const handleAttestClick = useCallback(() => {
     if (isEVMChain(sourceChain) && !!signer) {
@@ -218,7 +223,7 @@ export function useHandleAttest() {
     } else if (sourceChain === CHAIN_ID_SOLANA && !!solanaWallet && !!solPK) {
       solana(dispatch, enqueueSnackbar, solPK, sourceAsset, solanaWallet);
     } else if (sourceChain === CHAIN_ID_TERRA && !!terraWallet) {
-      terra(dispatch, enqueueSnackbar, terraWallet, sourceAsset);
+      terra(dispatch, enqueueSnackbar, terraWallet, sourceAsset, terraFeeDenom);
     } else {
     }
   }, [
@@ -230,6 +235,7 @@ export function useHandleAttest() {
     solPK,
     terraWallet,
     sourceAsset,
+    terraFeeDenom,
   ]);
   return useMemo(
     () => ({

+ 15 - 3
bridge_ui/src/hooks/useHandleCreateWrapped.tsx

@@ -28,6 +28,7 @@ import { setCreateTx, setIsCreating } from "../store/attestSlice";
 import {
   selectAttestIsCreating,
   selectAttestTargetChain,
+  selectTerraFeeDenom,
 } from "../store/selectors";
 import {
   getTokenBridgeAddressForChain,
@@ -133,7 +134,8 @@ async function terra(
   enqueueSnackbar: any,
   wallet: ConnectedWallet,
   signedVAA: Uint8Array,
-  shouldUpdate: boolean
+  shouldUpdate: boolean,
+  feeDenom: string
 ) {
   dispatch(setIsCreating(true));
   try {
@@ -151,7 +153,8 @@ async function terra(
     const result = await postWithFees(
       wallet,
       [msg],
-      "Wormhole - Create Wrapped"
+      "Wormhole - Create Wrapped",
+      [feeDenom]
     );
     dispatch(
       setCreateTx({ id: result.result.txhash, block: result.result.height })
@@ -177,6 +180,7 @@ export function useHandleCreateWrapped(shouldUpdate: boolean) {
   const isCreating = useSelector(selectAttestIsCreating);
   const { signer } = useEthereumProvider();
   const terraWallet = useConnectedWallet();
+  const terraFeeDenom = useSelector(selectTerraFeeDenom);
   const handleCreateClick = useCallback(() => {
     if (isEVMChain(targetChain) && !!signer && !!signedVAA) {
       evm(
@@ -202,7 +206,14 @@ export function useHandleCreateWrapped(shouldUpdate: boolean) {
         shouldUpdate
       );
     } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && !!signedVAA) {
-      terra(dispatch, enqueueSnackbar, terraWallet, signedVAA, shouldUpdate);
+      terra(
+        dispatch,
+        enqueueSnackbar,
+        terraWallet,
+        signedVAA,
+        shouldUpdate,
+        terraFeeDenom
+      );
     } else {
       // enqueueSnackbar(
       //   "Creating wrapped tokens on this chain is not yet supported",
@@ -221,6 +232,7 @@ export function useHandleCreateWrapped(shouldUpdate: boolean) {
     signedVAA,
     signer,
     shouldUpdate,
+    terraFeeDenom,
   ]);
   return useMemo(
     () => ({

+ 10 - 4
bridge_ui/src/hooks/useHandleRedeem.tsx

@@ -24,6 +24,7 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
 import useTransferSignedVAA from "./useTransferSignedVAA";
 import {
+  selectTerraFeeDenom,
   selectTransferIsRedeeming,
   selectTransferTargetChain,
 } from "../store/selectors";
@@ -132,7 +133,8 @@ async function terra(
   dispatch: any,
   enqueueSnackbar: any,
   wallet: ConnectedWallet,
-  signedVAA: Uint8Array
+  signedVAA: Uint8Array,
+  feeDenom: string
 ) {
   dispatch(setIsRedeeming(true));
   try {
@@ -144,7 +146,8 @@ async function terra(
     const result = await postWithFees(
       wallet,
       [msg],
-      "Wormhole - Complete Transfer"
+      "Wormhole - Complete Transfer",
+      [feeDenom]
     );
     dispatch(
       setRedeemTx({ id: result.result.txhash, block: result.result.height })
@@ -168,6 +171,7 @@ export function useHandleRedeem() {
   const solPK = solanaWallet?.publicKey;
   const { signer } = useEthereumProvider();
   const terraWallet = useConnectedWallet();
+  const terraFeeDenom = useSelector(selectTerraFeeDenom);
   const signedVAA = useTransferSignedVAA();
   const isRedeeming = useSelector(selectTransferIsRedeeming);
   const handleRedeemClick = useCallback(() => {
@@ -188,7 +192,7 @@ export function useHandleRedeem() {
         false
       );
     } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
-      terra(dispatch, enqueueSnackbar, terraWallet, signedVAA);
+      terra(dispatch, enqueueSnackbar, terraWallet, signedVAA, terraFeeDenom);
     } else {
     }
   }, [
@@ -200,6 +204,7 @@ export function useHandleRedeem() {
     solanaWallet,
     solPK,
     terraWallet,
+    terraFeeDenom,
   ]);
 
   const handleRedeemNativeClick = useCallback(() => {
@@ -220,7 +225,7 @@ export function useHandleRedeem() {
         true
       );
     } else if (targetChain === CHAIN_ID_TERRA && !!terraWallet && signedVAA) {
-      terra(dispatch, enqueueSnackbar, terraWallet, signedVAA); //TODO isNative = true
+      terra(dispatch, enqueueSnackbar, terraWallet, signedVAA, terraFeeDenom); //TODO isNative = true
     } else {
     }
   }, [
@@ -232,6 +237,7 @@ export function useHandleRedeem() {
     solanaWallet,
     solPK,
     terraWallet,
+    terraFeeDenom,
   ]);
 
   return useMemo(

+ 9 - 3
bridge_ui/src/hooks/useHandleTransfer.tsx

@@ -32,6 +32,7 @@ import { useDispatch, useSelector } from "react-redux";
 import { useEthereumProvider } from "../contexts/EthereumProviderContext";
 import { useSolanaWallet } from "../contexts/SolanaWalletContext";
 import {
+  selectTerraFeeDenom,
   selectTransferAmount,
   selectTransferIsSendComplete,
   selectTransferIsSending,
@@ -216,7 +217,8 @@ async function terra(
   amount: string,
   decimals: number,
   targetChain: ChainId,
-  targetAddress: Uint8Array
+  targetAddress: Uint8Array,
+  feeDenom: string
 ) {
   dispatch(setIsSending(true));
   try {
@@ -233,7 +235,8 @@ async function terra(
     const result = await postWithFees(
       wallet,
       msgs,
-      "Wormhole - Initiate Transfer"
+      "Wormhole - Initiate Transfer",
+      [feeDenom]
     );
 
     const info = await waitForTerraExecution(result);
@@ -286,6 +289,7 @@ export function useHandleTransfer() {
   const solanaWallet = useSolanaWallet();
   const solPK = solanaWallet?.publicKey;
   const terraWallet = useConnectedWallet();
+  const terraFeeDenom = useSelector(selectTerraFeeDenom);
   const sourceParsedTokenAccount = useSelector(
     selectTransferSourceParsedTokenAccount
   );
@@ -353,7 +357,8 @@ export function useHandleTransfer() {
         amount,
         decimals,
         targetChain,
-        targetAddress
+        targetAddress,
+        terraFeeDenom
       );
     } else {
     }
@@ -374,6 +379,7 @@ export function useHandleTransfer() {
     originAsset,
     originChain,
     isNative,
+    terraFeeDenom,
   ]);
   return useMemo(
     () => ({

+ 58 - 17
bridge_ui/src/hooks/useTransactionFees.tsx

@@ -37,9 +37,14 @@ export type MethodType = "nft" | "createWrapped" | "transfer";
 //rather than a hardcoded value.
 const SOLANA_THRESHOLD_LAMPORTS: bigint = BigInt(300000);
 const ETHEREUM_THRESHOLD_WEI: bigint = BigInt(35000000000000000);
-const TERRA_THRESHOLD_ULUNA: bigint = BigInt(500000);
+const TERRA_THRESHOLD_ULUNA: bigint = BigInt(100000);
+const TERRA_THRESHOLD_UUSD: bigint = BigInt(10000000);
 
-const isSufficientBalance = (chainId: ChainId, balance: bigint | undefined) => {
+const isSufficientBalance = (
+  chainId: ChainId,
+  balance: bigint | undefined,
+  terraFeeDenom?: string
+) => {
   if (balance === undefined || !chainId) {
     return true;
   }
@@ -49,13 +54,33 @@ const isSufficientBalance = (chainId: ChainId, balance: bigint | undefined) => {
   if (isEVMChain(chainId)) {
     return balance > ETHEREUM_THRESHOLD_WEI;
   }
-  if (CHAIN_ID_TERRA === chainId) {
+  if (terraFeeDenom === "uluna") {
     return balance > TERRA_THRESHOLD_ULUNA;
   }
+  if (terraFeeDenom === "uusd") {
+    return balance > TERRA_THRESHOLD_UUSD;
+  }
 
   return true;
 };
 
+type TerraBalance = {
+  denom: string;
+  balance: bigint;
+};
+
+const isSufficientBalanceTerra = (balances: TerraBalance[]) => {
+  return balances.some(({ denom, balance }) => {
+    if (denom === "uluna") {
+      return balance > TERRA_THRESHOLD_ULUNA;
+    }
+    if (denom === "uusd") {
+      return balance > TERRA_THRESHOLD_UUSD;
+    }
+    return false;
+  });
+};
+
 //TODO move to more generic location
 const getBalanceSolana = async (walletAddress: string) => {
   const connection = new Connection(SOLANA_HOST);
@@ -77,18 +102,25 @@ const getBalanceEvm = async (walletAddress: string, provider: Provider) => {
   return provider.getBalance(walletAddress).then((result) => result.toBigInt());
 };
 
-const getBalanceTerra = async (walletAddress: string) => {
-  const TARGET_DENOM = "uluna";
+const getBalancesTerra = async (walletAddress: string) => {
+  const TARGET_DENOMS = ["uluna", "uusd"];
 
   const lcd = new LCDClient(TERRA_HOST);
   return lcd.bank
     .balance(walletAddress)
     .then((coins) => {
-      // coins doesn't support reduce
-      const balancePairs = coins.map(({ amount, denom }) => [denom, amount]);
-      const targetCoin = balancePairs.find((coin) => coin[0] === TARGET_DENOM);
-      if (targetCoin) {
-        return BigInt(targetCoin[1].toString());
+      const balances = coins
+        .filter(({ denom }) => {
+          return TARGET_DENOMS.includes(denom);
+        })
+        .map(({ amount, denom }) => {
+          return {
+            denom,
+            balance: BigInt(amount.toString()),
+          };
+        });
+      if (balances) {
+        return balances;
       } else {
         return Promise.reject();
       }
@@ -115,6 +147,7 @@ export default function useTransactionFees(chainId: ChainId) {
   const { walletAddress, isReady } = useIsWalletReady(chainId);
   const { provider } = useEthereumProvider();
   const [balance, setBalance] = useState<bigint | undefined>(undefined);
+  const [terraBalances, setTerraBalances] = useState<TerraBalance[]>([]);
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState("");
 
@@ -157,12 +190,17 @@ export default function useTransactionFees(chainId: ChainId) {
       }
     } else if (chainId === CHAIN_ID_TERRA && isReady && walletAddress) {
       loadStart();
-      getBalanceTerra(walletAddress).then(
-        (result) => {
-          const adjustedresult =
-            result === undefined || result === null ? BigInt(0) : result;
+      getBalancesTerra(walletAddress).then(
+        (results) => {
+          const adjustedResults = results.map(({ denom, balance }) => {
+            return {
+              denom,
+              balance:
+                balance === undefined || balance === null ? BigInt(0) : balance,
+            };
+          });
           setIsLoading(false);
-          setBalance(adjustedresult);
+          setTerraBalances(adjustedResults);
         },
         (error) => {
           setIsLoading(false);
@@ -174,13 +212,16 @@ export default function useTransactionFees(chainId: ChainId) {
 
   const results = useMemo(() => {
     return {
-      isSufficientBalance: isSufficientBalance(chainId, balance),
+      isSufficientBalance:
+        chainId === CHAIN_ID_TERRA
+          ? isSufficientBalanceTerra(terraBalances)
+          : isSufficientBalance(chainId, balance),
       balance,
       balanceString: toBalanceString(balance, chainId),
       isLoading,
       error,
     };
-  }, [balance, chainId, isLoading, error]);
+  }, [balance, terraBalances, chainId, isLoading, error]);
 
   return results;
 }

+ 25 - 0
bridge_ui/src/store/feeSlice.ts

@@ -0,0 +1,25 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { TERRA_DEFAULT_FEE_DENOM } from "../utils/consts";
+
+export interface FeeSliceState {
+  terraFeeDenom: string;
+}
+
+const initialState: FeeSliceState = {
+  terraFeeDenom: TERRA_DEFAULT_FEE_DENOM,
+};
+
+export const feeSlice = createSlice({
+  name: "fee",
+  initialState,
+  reducers: {
+    setTerraFeeDenom: (state, action: PayloadAction<string>) => {
+      state.terraFeeDenom = action.payload;
+    },
+    reset: () => initialState,
+  },
+});
+
+export const { setTerraFeeDenom, reset } = feeSlice.actions;
+
+export default feeSlice.reducer;

+ 2 - 0
bridge_ui/src/store/index.ts

@@ -3,6 +3,7 @@ import attestReducer from "./attestSlice";
 import nftReducer from "./nftSlice";
 import transferReducer from "./transferSlice";
 import tokenReducer from "./tokenSlice";
+import feeReducer from "./feeSlice";
 
 export const store = configureStore({
   reducer: {
@@ -10,6 +11,7 @@ export const store = configureStore({
     nft: nftReducer,
     transfer: transferReducer,
     tokens: tokenReducer,
+    fee: feeReducer,
   },
 });
 

+ 4 - 0
bridge_ui/src/store/selectors.ts

@@ -307,3 +307,7 @@ export const selectTerraTokenMap = (state: RootState) => {
 export const selectMarketsMap = (state: RootState) => {
   return state.tokens.marketsMap;
 };
+
+export const selectTerraFeeDenom = (state: RootState) => {
+  return state.fee.terraFeeDenom;
+};

+ 1 - 0
bridge_ui/src/utils/consts.ts

@@ -789,6 +789,7 @@ export const getMigrationAssetMap = (chainId: ChainId) => {
 };
 
 export const SUPPORTED_TERRA_TOKENS = ["uluna", "uusd"];
+export const TERRA_DEFAULT_FEE_DENOM = SUPPORTED_TERRA_TOKENS[0];
 
 export const TERRA_FCD_BASE =
   CLUSTER === "mainnet"

+ 4 - 3
bridge_ui/src/utils/terra.ts

@@ -66,7 +66,8 @@ export const isValidTerraAddress = (address: string) => {
 export async function postWithFees(
   wallet: ConnectedWallet,
   msgs: any[],
-  memo: string
+  memo: string,
+  feeDenoms: string[]
 ) {
   // don't try/catch, let errors propagate
   const lcd = new LCDClient(TERRA_HOST);
@@ -81,7 +82,7 @@ export async function postWithFees(
     [...msgs],
     {
       memo,
-      feeDenoms: ["uluna"],
+      feeDenoms,
       gasPrices,
     }
   );
@@ -89,7 +90,7 @@ export async function postWithFees(
   const result = await wallet.post({
     msgs: [...msgs],
     memo,
-    feeDenoms: ["uluna"],
+    feeDenoms,
     gasPrices,
     fee: feeEstimate,
   });