approve unlimited functionality in eth
Change-Id: Id268205f540cad6b3936570c650e209fe0220339
This commit is contained in:
parent
3eaff1be7f
commit
0dc9f28bfd
|
@ -1,10 +1,19 @@
|
||||||
|
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
|
||||||
|
import { Checkbox, FormControlLabel } from "@material-ui/core";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import { ethers } from "ethers";
|
||||||
|
import { parseUnits } from "ethers/lib/utils";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import useAllowance from "../../hooks/useAllowance";
|
||||||
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import {
|
import {
|
||||||
selectSourceWalletAddress,
|
selectSourceWalletAddress,
|
||||||
|
selectTransferAmount,
|
||||||
|
selectTransferSourceAsset,
|
||||||
selectTransferSourceChain,
|
selectTransferSourceChain,
|
||||||
|
selectTransferSourceParsedTokenAccount,
|
||||||
selectTransferTargetError,
|
selectTransferTargetError,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
|
@ -16,8 +25,25 @@ import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
|
||||||
function Send() {
|
function Send() {
|
||||||
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
const { handleClick, disabled, showLoader } = useHandleTransfer();
|
||||||
|
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const sourceChain = useSelector(selectTransferSourceChain);
|
||||||
|
const sourceAsset = useSelector(selectTransferSourceAsset);
|
||||||
|
const sourceAmount = useSelector(selectTransferAmount);
|
||||||
|
const sourceDecimals = useSelector(
|
||||||
|
selectTransferSourceParsedTokenAccount
|
||||||
|
)?.decimals;
|
||||||
|
const sourceAmountParsed =
|
||||||
|
sourceDecimals !== undefined &&
|
||||||
|
sourceDecimals !== null &&
|
||||||
|
sourceAmount &&
|
||||||
|
parseUnits(sourceAmount, sourceDecimals).toBigInt();
|
||||||
|
const oneParsed =
|
||||||
|
sourceDecimals !== undefined &&
|
||||||
|
sourceDecimals !== null &&
|
||||||
|
parseUnits("1", sourceDecimals).toBigInt();
|
||||||
|
|
||||||
const error = useSelector(selectTransferTargetError);
|
const error = useSelector(selectTransferTargetError);
|
||||||
|
const [allowanceError, setAllowanceError] = useState("");
|
||||||
const { isReady, statusMessage, walletAddress } =
|
const { isReady, statusMessage, walletAddress } =
|
||||||
useIsWalletReady(sourceChain);
|
useIsWalletReady(sourceChain);
|
||||||
const sourceWalletAddress = useSelector(selectSourceWalletAddress);
|
const sourceWalletAddress = useSelector(selectSourceWalletAddress);
|
||||||
|
@ -26,10 +52,55 @@ function Send() {
|
||||||
sourceWalletAddress &&
|
sourceWalletAddress &&
|
||||||
walletAddress &&
|
walletAddress &&
|
||||||
sourceWalletAddress !== walletAddress;
|
sourceWalletAddress !== walletAddress;
|
||||||
const isDisabled = !isReady || isWrongWallet || disabled;
|
const [shouldApproveUnlimited, setShouldApproveUnlimited] = useState(false);
|
||||||
|
const toggleShouldApproveUnlimited = useCallback(
|
||||||
|
() => setShouldApproveUnlimited(!shouldApproveUnlimited),
|
||||||
|
[shouldApproveUnlimited]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
sufficientAllowance,
|
||||||
|
isAllowanceFetching,
|
||||||
|
isApproveProcessing,
|
||||||
|
approveAmount,
|
||||||
|
} = useAllowance(sourceChain, sourceAsset, sourceAmountParsed || undefined);
|
||||||
|
|
||||||
|
const approveButtonNeeded =
|
||||||
|
sourceChain === CHAIN_ID_ETH && !sufficientAllowance;
|
||||||
|
const notOne = shouldApproveUnlimited || sourceAmountParsed !== oneParsed;
|
||||||
|
const isDisabled =
|
||||||
|
!isReady ||
|
||||||
|
isWrongWallet ||
|
||||||
|
disabled ||
|
||||||
|
isAllowanceFetching ||
|
||||||
|
isApproveProcessing;
|
||||||
const errorMessage = isWrongWallet
|
const errorMessage = isWrongWallet
|
||||||
? "A different wallet is connected than in Step 1."
|
? "A different wallet is connected than in Step 1."
|
||||||
: statusMessage || error || undefined;
|
: statusMessage || error || allowanceError || undefined;
|
||||||
|
|
||||||
|
const approveExactAmount = useMemo(() => {
|
||||||
|
return () => {
|
||||||
|
setAllowanceError("");
|
||||||
|
approveAmount(BigInt(sourceAmountParsed)).then(
|
||||||
|
() => {
|
||||||
|
setAllowanceError("");
|
||||||
|
},
|
||||||
|
(error) => setAllowanceError("Failed to approve the token transfer.")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [approveAmount, sourceAmountParsed]);
|
||||||
|
const approveUnlimited = useMemo(() => {
|
||||||
|
return () => {
|
||||||
|
setAllowanceError("");
|
||||||
|
approveAmount(ethers.constants.MaxUint256.toBigInt()).then(
|
||||||
|
() => {
|
||||||
|
setAllowanceError("");
|
||||||
|
},
|
||||||
|
(error) => setAllowanceError("Failed to approve the token transfer.")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [approveAmount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepDescription>
|
<StepDescription>
|
||||||
|
@ -42,6 +113,32 @@ function Send() {
|
||||||
completing Step 4, you will have to perform the recovery workflow to
|
completing Step 4, you will have to perform the recovery workflow to
|
||||||
complete the transfer.
|
complete the transfer.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
{approveButtonNeeded ? (
|
||||||
|
<>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={shouldApproveUnlimited}
|
||||||
|
onChange={toggleShouldApproveUnlimited}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Approve Unlimited Tokens"
|
||||||
|
/>
|
||||||
|
<ButtonWithLoader
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={
|
||||||
|
shouldApproveUnlimited ? approveUnlimited : approveExactAmount
|
||||||
|
}
|
||||||
|
showLoader={isAllowanceFetching || isApproveProcessing}
|
||||||
|
error={errorMessage}
|
||||||
|
>
|
||||||
|
{"Approve " +
|
||||||
|
(shouldApproveUnlimited ? "Unlimited" : sourceAmount) +
|
||||||
|
` Token${notOne ? "s" : ""}`}
|
||||||
|
</ButtonWithLoader>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<ButtonWithLoader
|
<ButtonWithLoader
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
@ -50,6 +147,7 @@ function Send() {
|
||||||
>
|
>
|
||||||
Transfer
|
Transfer
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
)}
|
||||||
<WaitingForWalletMessage />
|
<WaitingForWalletMessage />
|
||||||
<TransferProgress />
|
<TransferProgress />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
import { Typography, makeStyles } from "@material-ui/core";
|
import { makeStyles, Typography } from "@material-ui/core";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
|
selectTransferIsApproving,
|
||||||
selectTransferIsRedeeming,
|
selectTransferIsRedeeming,
|
||||||
selectTransferIsSending,
|
selectTransferIsSending,
|
||||||
selectTransferRedeemTx,
|
selectTransferRedeemTx,
|
||||||
selectTransferSourceChain,
|
|
||||||
selectTransferTargetChain,
|
selectTransferTargetChain,
|
||||||
selectTransferTransferTx,
|
selectTransferTransferTx,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
|
@ -22,20 +22,19 @@ const WAITING_FOR_WALLET = "Waiting for wallet approval (likely in a popup)...";
|
||||||
|
|
||||||
export default function WaitingForWalletMessage() {
|
export default function WaitingForWalletMessage() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const sourceChain = useSelector(selectTransferSourceChain);
|
const isApproving = useSelector(selectTransferIsApproving);
|
||||||
const isSending = useSelector(selectTransferIsSending);
|
const isSending = useSelector(selectTransferIsSending);
|
||||||
const transferTx = useSelector(selectTransferTransferTx);
|
const transferTx = useSelector(selectTransferTransferTx);
|
||||||
const targetChain = useSelector(selectTransferTargetChain);
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
const isRedeeming = useSelector(selectTransferIsRedeeming);
|
const isRedeeming = useSelector(selectTransferIsRedeeming);
|
||||||
const redeemTx = useSelector(selectTransferRedeemTx);
|
const redeemTx = useSelector(selectTransferRedeemTx);
|
||||||
const showWarning = (isSending && !transferTx) || (isRedeeming && !redeemTx);
|
const showWarning =
|
||||||
|
isApproving || (isSending && !transferTx) || (isRedeeming && !redeemTx);
|
||||||
return showWarning ? (
|
return showWarning ? (
|
||||||
<Typography className={classes.message} variant="body2">
|
<Typography className={classes.message} variant="body2">
|
||||||
{WAITING_FOR_WALLET}{" "}
|
{WAITING_FOR_WALLET}{" "}
|
||||||
{targetChain === CHAIN_ID_SOLANA && isRedeeming
|
{targetChain === CHAIN_ID_SOLANA && isRedeeming
|
||||||
? "Note: there will be several transactions"
|
? "Note: there will be several transactions"
|
||||||
: sourceChain === CHAIN_ID_ETH && isSending
|
|
||||||
? "Note: there will be two transactions"
|
|
||||||
: null}
|
: null}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import {
|
||||||
|
approveEth,
|
||||||
|
ChainId,
|
||||||
|
CHAIN_ID_ETH,
|
||||||
|
getAllowanceEth,
|
||||||
|
} from "@certusone/wormhole-sdk";
|
||||||
|
import { BigNumber } from "ethers";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
|
import { selectTransferIsApproving } from "../store/selectors";
|
||||||
|
import { setIsApproving } from "../store/transferSlice";
|
||||||
|
import { ETH_TOKEN_BRIDGE_ADDRESS } from "../utils/consts";
|
||||||
|
|
||||||
|
export default function useAllowance(
|
||||||
|
chainId: ChainId,
|
||||||
|
tokenAddress?: string,
|
||||||
|
transferAmount?: BigInt
|
||||||
|
) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [allowance, setAllowance] = useState<BigInt | null>(null);
|
||||||
|
const [isAllowanceFetching, setIsAllowanceFetching] = useState(false);
|
||||||
|
const isApproveProcessing = useSelector(selectTransferIsApproving);
|
||||||
|
const { signer } = useEthereumProvider();
|
||||||
|
const sufficientAllowance =
|
||||||
|
chainId !== CHAIN_ID_ETH ||
|
||||||
|
(allowance && transferAmount && allowance >= transferAmount);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
if (
|
||||||
|
chainId === CHAIN_ID_ETH &&
|
||||||
|
tokenAddress &&
|
||||||
|
signer &&
|
||||||
|
!isApproveProcessing
|
||||||
|
) {
|
||||||
|
setIsAllowanceFetching(true);
|
||||||
|
getAllowanceEth(ETH_TOKEN_BRIDGE_ADDRESS, tokenAddress, signer).then(
|
||||||
|
(result) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsAllowanceFetching(false);
|
||||||
|
setAllowance(result.toBigInt());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsAllowanceFetching(false);
|
||||||
|
//setError("Unable to retrieve allowance"); //TODO set an error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [chainId, tokenAddress, signer, isApproveProcessing]);
|
||||||
|
|
||||||
|
const approveAmount: (amount: BigInt) => Promise<any> = useMemo(() => {
|
||||||
|
return chainId !== CHAIN_ID_ETH || !tokenAddress || !signer
|
||||||
|
? (amount: BigInt) => {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
: (amount: BigInt) => {
|
||||||
|
dispatch(setIsApproving(true));
|
||||||
|
return approveEth(
|
||||||
|
ETH_TOKEN_BRIDGE_ADDRESS,
|
||||||
|
tokenAddress,
|
||||||
|
signer,
|
||||||
|
BigNumber.from(amount)
|
||||||
|
).then(
|
||||||
|
() => {
|
||||||
|
dispatch(setIsApproving(false));
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
dispatch(setIsApproving(false));
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [chainId, tokenAddress, signer, dispatch]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
sufficientAllowance,
|
||||||
|
approveAmount,
|
||||||
|
isAllowanceFetching,
|
||||||
|
isApproveProcessing,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
sufficientAllowance,
|
||||||
|
approveAmount,
|
||||||
|
isAllowanceFetching,
|
||||||
|
isApproveProcessing,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
|
@ -181,6 +181,8 @@ export const selectTransferIsRedeeming = (state: RootState) =>
|
||||||
state.transfer.isRedeeming;
|
state.transfer.isRedeeming;
|
||||||
export const selectTransferRedeemTx = (state: RootState) =>
|
export const selectTransferRedeemTx = (state: RootState) =>
|
||||||
state.transfer.redeemTx;
|
state.transfer.redeemTx;
|
||||||
|
export const selectTransferIsApproving = (state: RootState) =>
|
||||||
|
state.transfer.isApproving;
|
||||||
export const selectTransferSourceError = (
|
export const selectTransferSourceError = (
|
||||||
state: RootState
|
state: RootState
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
|
|
|
@ -53,6 +53,7 @@ export interface TransferState {
|
||||||
isSending: boolean;
|
isSending: boolean;
|
||||||
isRedeeming: boolean;
|
isRedeeming: boolean;
|
||||||
redeemTx: Transaction | undefined;
|
redeemTx: Transaction | undefined;
|
||||||
|
isApproving: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TransferState = {
|
const initialState: TransferState = {
|
||||||
|
@ -74,6 +75,7 @@ const initialState: TransferState = {
|
||||||
isSending: false,
|
isSending: false,
|
||||||
isRedeeming: false,
|
isRedeeming: false,
|
||||||
redeemTx: undefined,
|
redeemTx: undefined,
|
||||||
|
isApproving: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transferSlice = createSlice({
|
export const transferSlice = createSlice({
|
||||||
|
@ -203,6 +205,9 @@ export const transferSlice = createSlice({
|
||||||
state.redeemTx = action.payload;
|
state.redeemTx = action.payload;
|
||||||
state.isRedeeming = false;
|
state.isRedeeming = false;
|
||||||
},
|
},
|
||||||
|
setIsApproving: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isApproving = action.payload;
|
||||||
|
},
|
||||||
reset: (state) => ({
|
reset: (state) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
sourceChain: state.sourceChain,
|
sourceChain: state.sourceChain,
|
||||||
|
@ -233,6 +238,7 @@ export const {
|
||||||
setIsSending,
|
setIsSending,
|
||||||
setIsRedeeming,
|
setIsRedeeming,
|
||||||
setRedeemTx,
|
setRedeemTx,
|
||||||
|
setIsApproving,
|
||||||
reset,
|
reset,
|
||||||
} = transferSlice.actions;
|
} = transferSlice.actions;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,28 @@ import {
|
||||||
import { getBridgeFeeIx, ixFromRust } from "../solana";
|
import { getBridgeFeeIx, ixFromRust } from "../solana";
|
||||||
import { ChainId, CHAIN_ID_SOLANA, createNonce } from "../utils";
|
import { ChainId, CHAIN_ID_SOLANA, createNonce } from "../utils";
|
||||||
|
|
||||||
|
export async function getAllowanceEth(
|
||||||
|
tokenBridgeAddress: string,
|
||||||
|
tokenAddress: string,
|
||||||
|
signer: ethers.Signer
|
||||||
|
) {
|
||||||
|
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
||||||
|
const signerAddress = await signer.getAddress();
|
||||||
|
const allowance = await token.allowance(signerAddress, tokenBridgeAddress);
|
||||||
|
|
||||||
|
return allowance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function approveEth(
|
||||||
|
tokenBridgeAddress: string,
|
||||||
|
tokenAddress: string,
|
||||||
|
signer: ethers.Signer,
|
||||||
|
amount: ethers.BigNumberish
|
||||||
|
) {
|
||||||
|
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
||||||
|
return await (await token.approve(tokenBridgeAddress, amount)).wait();
|
||||||
|
}
|
||||||
|
|
||||||
export async function transferFromEth(
|
export async function transferFromEth(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
signer: ethers.Signer,
|
signer: ethers.Signer,
|
||||||
|
@ -19,13 +41,6 @@ export async function transferFromEth(
|
||||||
) {
|
) {
|
||||||
//TODO: should we check if token attestation exists on the target chain
|
//TODO: should we check if token attestation exists on the target chain
|
||||||
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
const token = TokenImplementation__factory.connect(tokenAddress, signer);
|
||||||
//TODO: implement / separate allowance check
|
|
||||||
// const signerAddress = await signer.getAddress();
|
|
||||||
// const allowance = await token.allowance(
|
|
||||||
// signerAddress,
|
|
||||||
// tokenBridgeAddress
|
|
||||||
// );
|
|
||||||
await (await token.approve(tokenBridgeAddress, amount)).wait();
|
|
||||||
const fee = 0; // for now, this won't do anything, we may add later
|
const fee = 0; // for now, this won't do anything, we may add later
|
||||||
const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
|
const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
|
||||||
const v = await bridge.transferTokens(
|
const v = await bridge.transferTokens(
|
||||||
|
|
Loading…
Reference in New Issue