diff --git a/src/custom/components/TransactionConfirmationModal/index.tsx b/src/custom/components/TransactionConfirmationModal/index.tsx index 67b0924df..ae6c4a449 100644 --- a/src/custom/components/TransactionConfirmationModal/index.tsx +++ b/src/custom/components/TransactionConfirmationModal/index.tsx @@ -355,6 +355,7 @@ export enum OperationType { WRAP_ETHER, UNWRAP_WETH, APPROVE_TOKEN, + REVOKE_APPROVE_TOKEN, ORDER_SIGN, ORDER_CANCEL, } @@ -382,6 +383,8 @@ function getOperationMessage(operationType: OperationType, chainId: number): str return 'Approving token' case OperationType.ORDER_CANCEL: return 'Soft canceling your order' + case OperationType.REVOKE_APPROVE_TOKEN: + return 'Revoking token approval' default: return 'Almost there!' @@ -396,6 +399,8 @@ function getOperationLabel(operationType: OperationType): string { return t`unwrapping` case OperationType.APPROVE_TOKEN: return t`token approval` + case OperationType.REVOKE_APPROVE_TOKEN: + return t`revoking token approval` case OperationType.ORDER_SIGN: return t`order` case OperationType.ORDER_CANCEL: diff --git a/src/custom/hooks/useApproveCallback/index.ts b/src/custom/hooks/useApproveCallback/index.ts index 204b85082..2b958691a 100644 --- a/src/custom/hooks/useApproveCallback/index.ts +++ b/src/custom/hooks/useApproveCallback/index.ts @@ -51,7 +51,8 @@ export function useApproveCallbackFromTrade({ } export type OptionalApproveCallbackParams = { - transactionSummary: string + transactionSummary?: string + modalMessage?: string } type ApproveCallbackFromClaimParams = Omit< diff --git a/src/custom/hooks/useApproveCallback/useApproveCallbackMod.ts b/src/custom/hooks/useApproveCallback/useApproveCallbackMod.ts index 77242d376..823d80319 100644 --- a/src/custom/hooks/useApproveCallback/useApproveCallbackMod.ts +++ b/src/custom/hooks/useApproveCallback/useApproveCallbackMod.ts @@ -13,9 +13,11 @@ import { useTokenContract } from 'hooks/useContract' import { useTokenAllowance } from 'hooks/useTokenAllowance' import { useActiveWeb3React } from 'hooks/web3' import { OptionalApproveCallbackParams } from '.' +import { useCurrency } from 'hooks/Tokens' +import { OperationType } from 'components/TransactionConfirmationModal' // Use a 150K gas as a fallback if there's issue calculating the gas estimation (fixes some issues with some nodes failing to calculate gas costs for SC wallets) -const APPROVE_GAS_LIMIT_DEFAULT = BigNumber.from('150000') +export const APPROVE_GAS_LIMIT_DEFAULT = BigNumber.from('150000') export enum ApprovalState { UNKNOWN = 'UNKNOWN', @@ -25,7 +27,7 @@ export enum ApprovalState { } export interface ApproveCallbackParams { - openTransactionConfirmationModal: (message: string) => void + openTransactionConfirmationModal: (message: string, operationType: OperationType) => void closeModals: () => void amountToApprove?: CurrencyAmount spender?: string @@ -39,11 +41,12 @@ export function useApproveCallback({ amountToApprove, spender, amountToCheckAgainstAllowance, -}: ApproveCallbackParams): [ApprovalState, (optionalParams?: OptionalApproveCallbackParams) => Promise] { +}: ApproveCallbackParams) { const { account, chainId } = useActiveWeb3React() const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined const currentAllowance = useTokenAllowance(token, account ?? undefined, spender) const pendingApproval = useHasPendingApproval(token?.address, spender) + const spenderCurrency = useCurrency(spender) // TODO: Nice to have, can be deleted { @@ -133,7 +136,10 @@ export function useApproveCallback({ }) }) - openTransactionConfirmationModal(`Approving ${amountToApprove.currency.symbol} for trading`) + openTransactionConfirmationModal( + optionalParams?.modalMessage || `Approving ${amountToApprove.currency.symbol} for trading`, + OperationType.APPROVE_TOKEN + ) return ( tokenContract .approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, { @@ -166,7 +172,81 @@ export function useApproveCallback({ ] ) - return [approvalState, approve] + const revokeApprove = useCallback( + async (optionalParams?: OptionalApproveCallbackParams): Promise => { + if (approvalState === ApprovalState.NOT_APPROVED) { + console.error('Revoke approve was called unnecessarily') + return + } + if (!chainId) { + console.error('no chainId') + return + } + + if (!token) { + console.error('no token') + return + } + + if (!tokenContract) { + console.error('tokenContract is null') + return + } + + if (!spender) { + console.error('no spender') + return + } + + const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => { + // general fallback for tokens who restrict approval amounts + return tokenContract.estimateGas.approve(spender, '0').catch((error) => { + console.log( + '[useApproveCallbackMod] Error estimating gas for revoking approval. Using default gas limit ' + + APPROVE_GAS_LIMIT_DEFAULT.toString(), + error + ) + return APPROVE_GAS_LIMIT_DEFAULT + }) + }) + + openTransactionConfirmationModal( + optionalParams?.modalMessage || `Revoke ${token.symbol} approval from ${spenderCurrency?.symbol || spender}`, + OperationType.REVOKE_APPROVE_TOKEN + ) + return ( + tokenContract + .approve(spender, '0', { + gasLimit: calculateGasMargin(chainId, estimatedGas), + }) + .then((response: TransactionResponse) => { + addTransaction({ + hash: response.hash, + summary: optionalParams?.transactionSummary || `Revoke ${token.symbol} approval from ${spender}`, + approval: { tokenAddress: token.wrapped.address, spender }, + }) + }) + // .catch((error: Error) => { + // console.debug('Failed to approve token', error) + // throw error + // }) + .finally(closeModals) + ) + }, + [ + approvalState, + chainId, + token, + tokenContract, + spender, + spenderCurrency?.symbol, + openTransactionConfirmationModal, + closeModals, + addTransaction, + ] + ) + + return { approvalState, approve, revokeApprove, isPendingApproval: pendingApproval } } // wraps useApproveCallback in the context of a swap diff --git a/src/custom/pages/Claim/InvestmentFlow/InvestOption.tsx b/src/custom/pages/Claim/InvestmentFlow/InvestOption.tsx index 6391f0594..acfe1df63 100644 --- a/src/custom/pages/Claim/InvestmentFlow/InvestOption.tsx +++ b/src/custom/pages/Claim/InvestmentFlow/InvestOption.tsx @@ -3,7 +3,7 @@ import { CurrencyAmount, Percent } from '@uniswap/sdk-core' import { BigNumber } from '@ethersproject/bignumber' import CowProtocolLogo from 'components/CowProtocolLogo' -import { InvestTokenGroup, TokenLogo, InvestSummary, InvestInput, InvestAvailableBar } from '../styled' +import { InvestTokenGroup, TokenLogo, InvestSummary, InvestInput, InvestAvailableBar, UnderlineButton } from '../styled' import { formatSmartLocaleAware } from 'utils/format' import Row from 'components/Row' import CheckCircle from 'assets/cow-swap/check.svg' @@ -11,7 +11,7 @@ import { InvestmentFlowProps } from '.' import { ApprovalState, useApproveCallbackFromClaim } from 'hooks/useApproveCallback' import { useCurrencyBalance } from 'state/wallet/hooks' import { useActiveWeb3React } from 'hooks/web3' -import { ClaimType, useClaimDispatchers, useClaimState } from 'state/claim/hooks' +import { useClaimDispatchers, useClaimState } from 'state/claim/hooks' import { StyledNumericalInput } from 'components/CurrencyInputPanel/CurrencyInputPanelMod' import { ButtonConfirmed } from 'components/Button' @@ -42,18 +42,6 @@ type InvestOptionProps = { closeModal: InvestmentFlowProps['modalCbs']['closeModal'] } -const _claimApproveMessageMap = (type: ClaimType) => { - switch (type) { - case ClaimType.GnoOption: - return 'Approving GNO for investing in vCOW' - case ClaimType.Investor: - return 'Approving USDC for investing in vCOW' - // Shouldn't happen, type safe - default: - return 'Unknown token approval. Please check configuration.' - } -} - export default function InvestOption({ claim, optionIndex, openModal, closeModal }: InvestOptionProps) { const { currencyAmount, price, cost: maxCost } = claim @@ -64,8 +52,14 @@ export default function InvestOption({ claim, optionIndex, openModal, closeModal const investmentAmount = investFlowData[optionIndex].investedAmount // Approve hooks - const [approveState, approveCallback] = useApproveCallbackFromClaim({ - openTransactionConfirmationModal: () => openModal(_claimApproveMessageMap(claim.type), OperationType.APPROVE_TOKEN), + const { + approvalState: approveState, + approve: approveCallback, + // revokeApprove: revokeApprovalCallback, // CURRENTLY TEST ONLY + // isPendingApproval, // CURRENTLY TEST ONLY + } = useApproveCallbackFromClaim({ + openTransactionConfirmationModal: (message: string, operationType: OperationType) => + openModal(message, operationType), closeModals: closeModal, claim, }) @@ -138,9 +132,9 @@ export default function InvestOption({ claim, optionIndex, openModal, closeModal if (!approveCallback) return try { - // for pending state pre-BC setApproving(true) - await approveCallback({ transactionSummary: `Approve ${token?.symbol || 'token'} for investing in vCOW` }) + const summary = `Approve ${token?.symbol || 'token'} for investing in vCOW` + await approveCallback({ modalMessage: summary, transactionSummary: summary }) } catch (error) { console.error('[InvestOption]: Issue approving.', error) handleSetError(error?.message) @@ -149,6 +143,29 @@ export default function InvestOption({ claim, optionIndex, openModal, closeModal } }, [approveCallback, handleCloseError, handleSetError, token?.symbol]) + /* // CURRENTLY TEST ONLY + const handleRevokeApproval = useCallback(async () => { + // reset errors and close any modals + handleCloseError() + + if (!revokeApprovalCallback) return + + try { + setApproving(true) + const summary = `Revoke ${token?.symbol || 'token'} approval for vCOW contract` + await revokeApprovalCallback({ + modalMessage: summary, + transactionSummary: summary, + }) + } catch (error) { + console.error('[InvestOption]: Issue revoking approval.', error) + handleSetError(error?.message) + } finally { + setApproving(false) + } + }, [handleCloseError, handleSetError, revokeApprovalCallback, token?.symbol]) + */ + const vCowAmount = useMemo( () => calculateInvestmentAmounts(claim, investmentAmount)?.vCowAmount, [claim, investmentAmount] @@ -303,6 +320,14 @@ export default function InvestOption({ claim, optionIndex, openModal, closeModal )} )} + {/* + // CURRENTLY TEST ONLY + approveState === ApprovalState.APPROVED && ( + + Revoke approval {approving || (isPendingApproval && )} + + ) + */} @@ -323,9 +348,10 @@ export default function InvestOption({ claim, optionIndex, openModal, closeModal {/* Button should use the max possible amount the user can invest, considering their balance + max investment allowed */} {!noBalance && isSelfClaiming && ( - + )} theme.primary4}; + text-decoration: underline; + text-align: left; + padding: 0; + + &:hover { + color: ${({ theme }) => theme.text1}; + } + + &:disabled { + text-decoration: none; + color: ${({ theme }) => theme.disabled}; + cursor: auto; + } +` + export const InvestInput = styled.span` display: flex; flex-flow: column wrap; @@ -984,16 +1008,8 @@ export const InvestInput = styled.span` font-style: normal; } - > div > label > span > button { - background: none; - border: 0; - cursor: pointer; - color: ${({ theme }) => theme.primary4}; - text-decoration: underline; - - &:hover { - color: ${({ theme }) => theme.text1}; - } + > div > label > span > ${UnderlineButton} { + margin-left: 4px; } ` diff --git a/src/custom/pages/Swap/SwapMod.tsx b/src/custom/pages/Swap/SwapMod.tsx index ce4eed56b..4011cff78 100644 --- a/src/custom/pages/Swap/SwapMod.tsx +++ b/src/custom/pages/Swap/SwapMod.tsx @@ -329,7 +329,7 @@ export default function Swap({ const isLoadingRoute = toggledVersion === Version.v3 && V3TradeState.LOADING === v3TradeState */ // check whether the user has approved the router on the input token - const [approvalState, approveCallback] = useApproveCallbackFromTrade({ + const { approvalState, approve: approveCallback } = useApproveCallbackFromTrade({ openTransactionConfirmationModal: (message: string) => openTransactionConfirmationModal(message, OperationType.APPROVE_TOKEN), closeModals,