approve unlimited functionality in eth

Change-Id: Id268205f540cad6b3936570c650e209fe0220339
This commit is contained in:
Chase Moran 2021-09-14 00:45:26 -04:00 committed by Evan Gray
parent 3eaff1be7f
commit 0dc9f28bfd
6 changed files with 242 additions and 24 deletions

View File

@ -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 { ethers } from "ethers";
import { parseUnits } from "ethers/lib/utils";
import { useCallback, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import useAllowance from "../../hooks/useAllowance";
import { useHandleTransfer } from "../../hooks/useHandleTransfer";
import useIsWalletReady from "../../hooks/useIsWalletReady";
import {
selectSourceWalletAddress,
selectTransferAmount,
selectTransferSourceAsset,
selectTransferSourceChain,
selectTransferSourceParsedTokenAccount,
selectTransferTargetError,
} from "../../store/selectors";
import { CHAINS_BY_ID } from "../../utils/consts";
@ -16,8 +25,25 @@ import WaitingForWalletMessage from "./WaitingForWalletMessage";
function Send() {
const { handleClick, disabled, showLoader } = useHandleTransfer();
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 [allowanceError, setAllowanceError] = useState("");
const { isReady, statusMessage, walletAddress } =
useIsWalletReady(sourceChain);
const sourceWalletAddress = useSelector(selectSourceWalletAddress);
@ -26,10 +52,55 @@ function Send() {
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
? "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 (
<>
<StepDescription>
@ -42,14 +113,41 @@ function Send() {
completing Step 4, you will have to perform the recovery workflow to
complete the transfer.
</Alert>
<ButtonWithLoader
disabled={isDisabled}
onClick={handleClick}
showLoader={showLoader}
error={errorMessage}
>
Transfer
</ButtonWithLoader>
{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
disabled={isDisabled}
onClick={handleClick}
showLoader={showLoader}
error={errorMessage}
>
Transfer
</ButtonWithLoader>
)}
<WaitingForWalletMessage />
<TransferProgress />
</>

View File

@ -1,11 +1,11 @@
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { Typography, makeStyles } from "@material-ui/core";
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { makeStyles, Typography } from "@material-ui/core";
import { useSelector } from "react-redux";
import {
selectTransferIsApproving,
selectTransferIsRedeeming,
selectTransferIsSending,
selectTransferRedeemTx,
selectTransferSourceChain,
selectTransferTargetChain,
selectTransferTransferTx,
} from "../../store/selectors";
@ -22,20 +22,19 @@ const WAITING_FOR_WALLET = "Waiting for wallet approval (likely in a popup)...";
export default function WaitingForWalletMessage() {
const classes = useStyles();
const sourceChain = useSelector(selectTransferSourceChain);
const isApproving = useSelector(selectTransferIsApproving);
const isSending = useSelector(selectTransferIsSending);
const transferTx = useSelector(selectTransferTransferTx);
const targetChain = useSelector(selectTransferTargetChain);
const isRedeeming = useSelector(selectTransferIsRedeeming);
const redeemTx = useSelector(selectTransferRedeemTx);
const showWarning = (isSending && !transferTx) || (isRedeeming && !redeemTx);
const showWarning =
isApproving || (isSending && !transferTx) || (isRedeeming && !redeemTx);
return showWarning ? (
<Typography className={classes.message} variant="body2">
{WAITING_FOR_WALLET}{" "}
{targetChain === CHAIN_ID_SOLANA && isRedeeming
? "Note: there will be several transactions"
: sourceChain === CHAIN_ID_ETH && isSending
? "Note: there will be two transactions"
: null}
</Typography>
) : null;

View File

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

View File

@ -181,6 +181,8 @@ export const selectTransferIsRedeeming = (state: RootState) =>
state.transfer.isRedeeming;
export const selectTransferRedeemTx = (state: RootState) =>
state.transfer.redeemTx;
export const selectTransferIsApproving = (state: RootState) =>
state.transfer.isApproving;
export const selectTransferSourceError = (
state: RootState
): string | undefined => {

View File

@ -53,6 +53,7 @@ export interface TransferState {
isSending: boolean;
isRedeeming: boolean;
redeemTx: Transaction | undefined;
isApproving: boolean;
}
const initialState: TransferState = {
@ -74,6 +75,7 @@ const initialState: TransferState = {
isSending: false,
isRedeeming: false,
redeemTx: undefined,
isApproving: false,
};
export const transferSlice = createSlice({
@ -203,6 +205,9 @@ export const transferSlice = createSlice({
state.redeemTx = action.payload;
state.isRedeeming = false;
},
setIsApproving: (state, action: PayloadAction<boolean>) => {
state.isApproving = action.payload;
},
reset: (state) => ({
...initialState,
sourceChain: state.sourceChain,
@ -233,6 +238,7 @@ export const {
setIsSending,
setIsRedeeming,
setRedeemTx,
setIsApproving,
reset,
} = transferSlice.actions;

View File

@ -9,6 +9,28 @@ import {
import { getBridgeFeeIx, ixFromRust } from "../solana";
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(
tokenBridgeAddress: string,
signer: ethers.Signer,
@ -19,13 +41,6 @@ export async function transferFromEth(
) {
//TODO: should we check if token attestation exists on the target chain
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 bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
const v = await bridge.transferTokens(