bridge_ui: Terra fee denomination support

This commit is contained in:
Kevin Peters 2022-01-07 16:06:33 +00:00 committed by Evan Gray
parent 8eea035490
commit 7882eccac4
19 changed files with 303 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

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