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 { 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,6 +113,32 @@ function Send() {
|
|||
completing Step 4, you will have to perform the recovery workflow to
|
||||
complete the transfer.
|
||||
</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
|
||||
disabled={isDisabled}
|
||||
onClick={handleClick}
|
||||
|
@ -50,6 +147,7 @@ function Send() {
|
|||
>
|
||||
Transfer
|
||||
</ButtonWithLoader>
|
||||
)}
|
||||
<WaitingForWalletMessage />
|
||||
<TransferProgress />
|
||||
</>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
export const selectTransferRedeemTx = (state: RootState) =>
|
||||
state.transfer.redeemTx;
|
||||
export const selectTransferIsApproving = (state: RootState) =>
|
||||
state.transfer.isApproving;
|
||||
export const selectTransferSourceError = (
|
||||
state: RootState
|
||||
): string | undefined => {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue