diff --git a/src/abi/staticTokenAdapter.ts b/src/abi/staticTokenAdapter.ts new file mode 100644 index 00000000..6beb5510 --- /dev/null +++ b/src/abi/staticTokenAdapter.ts @@ -0,0 +1,426 @@ +export const staticTokenAdapterAbi = [ + { + inputs: [ + { internalType: "address", name: "lendingPool", type: "address" }, + { internalType: "address", name: "rewardsController", type: "address" }, + { internalType: "address", name: "aToken", type: "address" }, + { internalType: "address", name: "_rewardCollector", type: "address" }, + { internalType: "string", name: "wrappedTokenName", type: "string" }, + { internalType: "string", name: "wrappedTokenSymbol", type: "string" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { internalType: "address", name: "target", type: "address" }, + { internalType: "bool", name: "success", type: "bool" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "ERC20CallFailed", + type: "error", + }, + { inputs: [], name: "Unauthorized", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + inputs: [], + name: "ASSET", + outputs: [{ internalType: "contract IERC20", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ATOKEN", + outputs: [{ internalType: "contract IERC20", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EIP712_REVISION", + outputs: [{ internalType: "bytes", name: "", type: "bytes" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "LENDING_POOL", + outputs: [ + { internalType: "contract ILendingPool", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "METADEPOSIT_TYPEHASH", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "METAWITHDRAWAL_TYPEHASH", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PERMIT_TYPEHASH", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "REWARDS_CONTROLLER", + outputs: [ + { + internalType: "contract IRewardsController", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "_nonces", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acceptAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "admin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "claimRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "subtractedValue", type: "uint256" }, + ], + name: "decreaseAllowance", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint16", name: "referralCode", type: "uint16" }, + { internalType: "bool", name: "fromUnderlying", type: "bool" }, + ], + name: "deposit", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "dynamicBalanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + name: "dynamicToStaticAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "chainId", type: "uint256" }], + name: "getDomainSeparator", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "addedValue", type: "uint256" }, + ], + name: "increaseAllowance", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "depositor", type: "address" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "uint16", name: "referralCode", type: "uint16" }, + { internalType: "bool", name: "fromUnderlying", type: "bool" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + components: [ + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + internalType: "struct StaticATokenV3.SignatureParams", + name: "sigParams", + type: "tuple", + }, + { internalType: "uint256", name: "chainId", type: "uint256" }, + ], + name: "metaDeposit", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "staticAmount", type: "uint256" }, + { internalType: "uint256", name: "dynamicAmount", type: "uint256" }, + { internalType: "bool", name: "toUnderlying", type: "bool" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + components: [ + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + internalType: "struct StaticATokenV3.SignatureParams", + name: "sigParams", + type: "tuple", + }, + { internalType: "uint256", name: "chainId", type: "uint256" }, + ], + name: "metaWithdraw", + outputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + { internalType: "uint256", name: "chainId", type: "uint256" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "rate", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "rewardCollector", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newAdmin", type: "address" }], + name: "setPendingAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_rewardCollector", type: "address" }, + ], + name: "setRewardCollector", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + name: "staticToDynamicAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bool", name: "toUnderlying", type: "bool" }, + ], + name: "withdraw", + outputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bool", name: "toUnderlying", type: "bool" }, + ], + name: "withdrawDynamicAmount", + outputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/src/components/common/CtaButton.tsx b/src/components/common/CtaButton.tsx new file mode 100644 index 00000000..ec39193b --- /dev/null +++ b/src/components/common/CtaButton.tsx @@ -0,0 +1,21 @@ +import { useAccount } from "wagmi"; +import { Button } from "../ui/button"; +import { useConnectModal } from "@rainbow-me/rainbowkit"; +import { ComponentPropsWithRef } from "react"; + +/** + * Button that opens connect modal if the user is not connected. + */ +export const CtaButton = (props: ComponentPropsWithRef) => { + const { address } = useAccount(); + const { openConnectModal } = useConnectModal(); + return ( + + ); +}; diff --git a/src/components/common/input/VaultWithdrawTokenInput.tsx b/src/components/common/input/VaultWithdrawTokenInput.tsx index d45fc1fc..0c424a21 100644 --- a/src/components/common/input/VaultWithdrawTokenInput.tsx +++ b/src/components/common/input/VaultWithdrawTokenInput.tsx @@ -4,8 +4,8 @@ import { Vault } from "@/lib/types"; import { alchemistV2Abi } from "@/abi/alchemistV2"; import { formatEther, formatUnits, parseUnits, zeroAddress } from "viem"; import { useWatchQuery } from "@/hooks/useWatchQuery"; -import { useMemo } from "react"; import { TokenInput } from "./TokenInput"; +import { useStaticTokenAdapterWithdraw } from "@/hooks/useStaticTokenAdapterWithdraw"; export const VaultWithdrawTokenInput = ({ amount, @@ -74,16 +74,7 @@ export const VaultWithdrawTokenInput = ({ }, }); - useWatchQuery({ - queryKeys: [sharesBalanceQueryKey, totalCollateralInDebtTokenQueryKey], - }); - - const otherCoverInDebt = - totalCollateralInDebtToken !== undefined && - collateralInDebtToken !== undefined - ? totalCollateralInDebtToken - collateralInDebtToken - : 0n; - const balanceInDebt = useMemo(() => { + const balanceInDebt = (() => { if (collateralInDebtToken === undefined) { return 0n; } @@ -93,12 +84,18 @@ export const VaultWithdrawTokenInput = ({ const maxWithdrawAmount = collateralInDebtToken - requiredCoverInDebt; + const otherCoverInDebt = + totalCollateralInDebtToken !== undefined && + collateralInDebtToken !== undefined + ? totalCollateralInDebtToken - collateralInDebtToken + : 0n; + if (otherCoverInDebt >= requiredCoverInDebt) { return collateralInDebtToken; } else { return maxWithdrawAmount; } - }, [collateralInDebtToken, otherCoverInDebt, vault]); + })(); const { data: balanceForUnderlying } = useReadContract({ address: vault.alchemist.address, @@ -112,27 +109,52 @@ export const VaultWithdrawTokenInput = ({ }, }); - const { data: balanceForYieldToken } = useReadContract({ - address: vault.alchemist.address, - chainId: chain.id, - abi: alchemistV2Abi, - functionName: "convertUnderlyingTokensToYield", - args: [ - vault.yieldToken, - parseUnits( - balanceForUnderlying ?? "0", - vault.underlyingTokensParams.decimals, - ), + const { data: balanceForYieldToken, queryKey: balanceForYieldTokenQueryKey } = + useReadContract({ + address: vault.alchemist.address, + chainId: chain.id, + abi: alchemistV2Abi, + functionName: "convertUnderlyingTokensToYield", + args: [ + vault.yieldToken, + parseUnits( + balanceForUnderlying ?? "0", + vault.underlyingTokensParams.decimals, + ), + ], + query: { + enabled: balanceForUnderlying !== undefined, + select: (balance) => + formatUnits(balance, vault.yieldTokenParams.decimals), + }, + }); + + const { balanceForYieldTokenAdapter } = useStaticTokenAdapterWithdraw({ + typeGuard: "withdrawInput", + balanceForYieldToken, + isSelectedTokenYieldToken, + vault, + }); + + /** + * NOTE: Watch queries for changes in sharesBalance, totalCollateral, and balanceForYieldToken. + * sharesBalanceQueryKey - if user deposited or withdrawed from vault for yield token; + * totalCollateralInDebtTokenQueryKey - if user deposited or withdrawed from vault for yield token; + * balanceForYieldTokenQueryKey - because shares to yield token uses price which changes each block. + */ + useWatchQuery({ + queryKeys: [ + sharesBalanceQueryKey, + totalCollateralInDebtTokenQueryKey, + balanceForYieldTokenQueryKey, ], - query: { - enabled: balanceForUnderlying !== undefined, - select: (balance) => - formatUnits(balance, vault.yieldTokenParams.decimals), - }, }); const balance = isSelectedTokenYieldToken - ? balanceForYieldToken + ? vault.metadata.api.provider === "aave" && + vault.metadata.yieldTokenOverride + ? balanceForYieldTokenAdapter + : balanceForYieldToken : balanceForUnderlying; return ( diff --git a/src/components/farms/GAlcxWrapper.tsx b/src/components/farms/GAlcxWrapper.tsx index 7e818ab7..095ceec0 100644 --- a/src/components/farms/GAlcxWrapper.tsx +++ b/src/components/farms/GAlcxWrapper.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Switch } from "../ui/switch"; import { Button } from "../ui/button"; import { @@ -122,7 +122,7 @@ export const GAlcsWrapper = () => { setAmount(""); }; - const onWrap = useCallback(() => { + const onWrap = () => { if (isApprovalNeeded) { approveConfig && approve(approveConfig.request); return; @@ -145,9 +145,9 @@ export const GAlcsWrapper = () => { description: "Unexpected error. Please contact Alchemix team.", }); } - }, [approve, approveConfig, isApprovalNeeded, wrap, wrapConfig, wrapError]); + }; - const onUnwrap = useCallback(() => { + const onUnwrap = () => { if (unwrapError) { toast.error("Error unwrapping ALCX", { description: @@ -165,7 +165,7 @@ export const GAlcsWrapper = () => { description: "Unexpected error. Please contact Alchemix team.", }); } - }, [unwrap, unwrapConfig, unwrapError]); + }; const handleOpen = () => { setOpen((prev) => !prev); diff --git a/src/components/governance/row/ProposalAccordionRow.tsx b/src/components/governance/row/ProposalAccordionRow.tsx index 3fd25733..707b2ff8 100644 --- a/src/components/governance/row/ProposalAccordionRow.tsx +++ b/src/components/governance/row/ProposalAccordionRow.tsx @@ -1,5 +1,5 @@ import { getAddress } from "viem"; -import { Fragment, useMemo, useState } from "react"; +import { Fragment, useState } from "react"; import { useAccount, useWalletClient } from "wagmi"; import { toast } from "sonner"; import { @@ -68,24 +68,21 @@ export const ProposalsAccordionRow = ({ proposal }: { proposal: Proposal }) => { const isSupported = supportedTypes.indexOf(proposal.type) !== -1; - const message = useMemo(() => { - const choice = proposal.choices.indexOf(selectedChoice) + 1; - return { - choice, - proposal: proposal.id, - app: "alchemix", - space: "alchemixstakers.eth", - type: proposal.type, - metadata: "{}", - reason: "", - }; - }, [proposal, selectedChoice]); - const { mutate: writeVote, isPending } = useMutation({ mutationFn: async () => { if (!address) throw new Error("Not connected."); if (!walletClient) throw new Error("No wallet."); + const message = { + choice: proposal.choices.indexOf(selectedChoice) + 1, + proposal: proposal.id, + app: "alchemix", + space: "alchemixstakers.eth", + type: proposal.type, + metadata: "{}", + reason: "", + }; + const type2 = message.proposal.startsWith("0x"); const types = type2 ? { diff --git a/src/components/transmuters/row/Claim.tsx b/src/components/transmuters/row/Claim.tsx index 60b2cf1f..a121fd84 100644 --- a/src/components/transmuters/row/Claim.tsx +++ b/src/components/transmuters/row/Claim.tsx @@ -1,13 +1,13 @@ import { transmuterV2Abi } from "@/abi/transmuterV2"; +import { CtaButton } from "@/components/common/CtaButton"; import { TransmuterInput } from "@/components/common/input/TransmuterInput"; -import { Button } from "@/components/ui/button"; import { useChain } from "@/hooks/useChain"; import { useWriteContractMutationCallback } from "@/hooks/useWriteContractMutationCallback"; import { QueryKeys } from "@/lib/queries/queriesSchema"; import { Token, Transmuter } from "@/lib/types"; import { isInputZero } from "@/utils/inputNotZero"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { parseUnits } from "viem"; import { @@ -34,7 +34,7 @@ export const Claim = ({ const { data: claimConfig, - isFetching, + isPending, error: claimConfigError, } = useSimulateContract({ address: transmuter.address, @@ -64,7 +64,7 @@ export const Claim = ({ } }, [claimReceipt, queryClient]); - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (claimConfigError) { toast.error("Claim failed", { description: @@ -81,7 +81,7 @@ export const Claim = ({ description: "Unknown error occurred. Please contact Alchemix team.", }); } - }, [claim, claimConfig, claimConfigError]); + }; return ( <> @@ -95,13 +95,13 @@ export const Claim = ({ tokenDecimals={underlyingToken.decimals} /> - + ); }; diff --git a/src/components/transmuters/row/Deposit.tsx b/src/components/transmuters/row/Deposit.tsx index 872f4037..69bd1e66 100644 --- a/src/components/transmuters/row/Deposit.tsx +++ b/src/components/transmuters/row/Deposit.tsx @@ -1,6 +1,6 @@ import { transmuterV2Abi } from "@/abi/transmuterV2"; +import { CtaButton } from "@/components/common/CtaButton"; import { TransmuterInput } from "@/components/common/input/TransmuterInput"; -import { Button } from "@/components/ui/button"; import { useAllowance } from "@/hooks/useAllowance"; import { useChain } from "@/hooks/useChain"; import { useWriteContractMutationCallback } from "@/hooks/useWriteContractMutationCallback"; @@ -8,7 +8,7 @@ import { QueryKeys } from "@/lib/queries/queriesSchema"; import { Token, Transmuter } from "@/lib/types"; import { isInputZero } from "@/utils/inputNotZero"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { parseEther } from "viem"; import { @@ -33,7 +33,13 @@ export const Deposit = ({ const [depositAmount, setDepositAmount] = useState(""); - const { isApprovalNeeded, approve, approveConfig } = useAllowance({ + const { + isApprovalNeeded, + approve, + approveConfig, + isPending: isPendingAllowance, + isFetching: isFetchingAllowance, + } = useAllowance({ tokenAddress: syntheticToken.address, spender: transmuter.address, amount: depositAmount, @@ -42,7 +48,7 @@ export const Deposit = ({ const { data: depositConfig, - isFetching, + isPending: isPendingDepositConfig, error: depositConfigError, } = useSimulateContract({ address: transmuter.address, @@ -73,7 +79,7 @@ export const Deposit = ({ } }, [depositReceipt, queryClient]); - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (isApprovalNeeded === true) { approveConfig && approve(approveConfig.request); return; @@ -96,14 +102,12 @@ export const Deposit = ({ description: "Unkown error. Please contact Alchemix team.", }); } - }, [ - isApprovalNeeded, - depositConfigError, - depositConfig, - approveConfig, - approve, - deposit, - ]); + }; + + const isPending = + isApprovalNeeded === false + ? isPendingDepositConfig + : isPendingAllowance || isFetchingAllowance; return ( <> @@ -116,13 +120,14 @@ export const Deposit = ({ type="Balance" transmuterAddress={transmuter.address} /> - + ); }; diff --git a/src/components/transmuters/row/Withdraw.tsx b/src/components/transmuters/row/Withdraw.tsx index 6a231d0b..dbd222a4 100644 --- a/src/components/transmuters/row/Withdraw.tsx +++ b/src/components/transmuters/row/Withdraw.tsx @@ -1,13 +1,13 @@ import { transmuterV2Abi } from "@/abi/transmuterV2"; +import { CtaButton } from "@/components/common/CtaButton"; import { TransmuterInput } from "@/components/common/input/TransmuterInput"; -import { Button } from "@/components/ui/button"; import { useChain } from "@/hooks/useChain"; import { useWriteContractMutationCallback } from "@/hooks/useWriteContractMutationCallback"; import { QueryKeys } from "@/lib/queries/queriesSchema"; import { Token, Transmuter } from "@/lib/types"; import { isInputZero } from "@/utils/inputNotZero"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { parseEther } from "viem"; import { @@ -34,7 +34,7 @@ export const Withdraw = ({ const { data: withdrawConfig, - isFetching, + isPending, error: withdrawConfigError, } = useSimulateContract({ address: transmuter.address, @@ -64,7 +64,7 @@ export const Withdraw = ({ } }, [withdrawReceipt, queryClient]); - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (withdrawConfigError) { toast.error("Withdraw failed", { description: @@ -81,7 +81,7 @@ export const Withdraw = ({ description: "Unknown error occurred. Please contact Alchemix team.", }); } - }, [withdraw, withdrawConfig, withdrawConfigError]); + }; return ( <> @@ -95,13 +95,13 @@ export const Withdraw = ({ tokenDecimals={syntheticToken.decimals} /> - + ); }; diff --git a/src/components/vaults/common_actions/Borrow.tsx b/src/components/vaults/common_actions/Borrow.tsx index 30160725..87404d61 100644 --- a/src/components/vaults/common_actions/Borrow.tsx +++ b/src/components/vaults/common_actions/Borrow.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { BorrowInput } from "@/components/common/input/BorrowInput"; import { Select, @@ -29,6 +29,7 @@ import { Switch } from "@/components/ui/switch"; import { AnimatePresence, m } from "framer-motion"; import { Input } from "@/components/ui/input"; import { accordionTransition, accordionVariants } from "@/lib/motion/motion"; +import { CtaButton } from "@/components/common/CtaButton"; export const Borrow = () => { const queryClient = useQueryClient(); @@ -76,7 +77,7 @@ export const Borrow = () => { const { data: borrowConfig, error: borrowError, - isFetching, + isPending, } = useSimulateContract({ address: alchemistForDebtTokenAddress, abi: alchemistV2Abi, @@ -123,7 +124,7 @@ export const Borrow = () => { setConfirmedDifferentAddress(checked); }; - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (borrowError) { toast.error("Borrow failed", { description: @@ -141,7 +142,7 @@ export const Borrow = () => { "Borrow failed. Unexpected. Please contract Alchemix team.", }); } - }, [borrow, borrowConfig, borrowError]); + }; return (
@@ -239,19 +240,19 @@ export const Borrow = () => { )}
- + )} diff --git a/src/components/vaults/common_actions/Liquidate.tsx b/src/components/vaults/common_actions/Liquidate.tsx index 51636506..bdda7f5d 100644 --- a/src/components/vaults/common_actions/Liquidate.tsx +++ b/src/components/vaults/common_actions/Liquidate.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Select, SelectTrigger, @@ -7,7 +7,6 @@ import { SelectItem, } from "@/components/ui/select"; import { useTokensQuery } from "@/lib/queries/useTokensQuery"; -import { Button } from "@/components/ui/button"; import { useReadContract, useSimulateContract, @@ -31,6 +30,7 @@ import { useWriteContractMutationCallback } from "@/hooks/useWriteContractMutati import { Switch } from "@/components/ui/switch"; import { SlippageInput } from "@/components/common/input/SlippageInput"; import { MAX_UINT256_BN } from "@/lib/constants"; +import { CtaButton } from "@/components/common/CtaButton"; export const Liquidate = () => { const queryClient = useQueryClient(); @@ -120,7 +120,7 @@ export const Liquidate = () => { const { data: liquidateConfig, - isFetching, + isPending, error: liquidateError, } = useSimulateContract({ address: ALCHEMISTS_METADATA[chain.id][selectedSynthAsset], @@ -186,7 +186,7 @@ export const Liquidate = () => { setConfirmedLiquidation(checked); }; - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (liquidateError) { toast.error("Liquidate failed", { description: @@ -204,7 +204,7 @@ export const Liquidate = () => { description: "Unknown error. Please contact Alchemix team.", }); } - }, [liquidate, liquidateConfig, liquidateError]); + }; return (
@@ -270,16 +270,14 @@ export const Liquidate = () => { repay the outstanding debt
- + {isPending ? "Preparing..." : "Liquidate"} + )} diff --git a/src/components/vaults/common_actions/Repay.tsx b/src/components/vaults/common_actions/Repay.tsx index 748e4e0b..92be5dbb 100644 --- a/src/components/vaults/common_actions/Repay.tsx +++ b/src/components/vaults/common_actions/Repay.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Select, SelectTrigger, @@ -7,7 +7,6 @@ import { SelectItem, } from "@/components/ui/select"; import { useTokensQuery } from "@/lib/queries/useTokensQuery"; -import { Button } from "@/components/ui/button"; import { useAccount, useSimulateContract, @@ -28,6 +27,7 @@ import { isInputZero } from "@/utils/inputNotZero"; import { QueryKeys } from "@/lib/queries/queriesSchema"; import { useWriteContractMutationCallback } from "@/hooks/useWriteContractMutationCallback"; import { RepayInput } from "@/components/common/input/RepayInput"; +import { CtaButton } from "@/components/common/CtaButton"; export const Repay = () => { const queryClient = useQueryClient(); @@ -74,17 +74,23 @@ export const Repay = () => { (token) => token.address === repaymentTokenAddress, ); - const { isApprovalNeeded, approve, approveConfig, approveUsdtEthConfig } = - useAllowance({ - tokenAddress: repaymentToken?.address, - spender: ALCHEMISTS_METADATA[chain.id][selectedSynthAsset], - amount, - decimals: repaymentToken?.decimals, - }); + const { + isApprovalNeeded, + approve, + approveConfig, + approveUsdtEthConfig, + isPending: isPendingAllowance, + isFetching: isFetchingAllowance, + } = useAllowance({ + tokenAddress: repaymentToken?.address, + spender: ALCHEMISTS_METADATA[chain.id][selectedSynthAsset], + amount, + decimals: repaymentToken?.decimals, + }); const { data: burnConfig, - isFetching: isFetchingBurnConfig, + isPending: isPendingBurnConfig, error: burnConfigError, } = useSimulateContract({ address: ALCHEMISTS_METADATA[chain.id][selectedSynthAsset], @@ -105,7 +111,7 @@ export const Repay = () => { const { data: repayConfig, - isFetching: isFetchingRepayConfig, + isPending: isPendingRepayConfig, error: repayConfigError, } = useSimulateContract({ address: ALCHEMISTS_METADATA[chain.id][selectedSynthAsset], @@ -161,7 +167,7 @@ export const Repay = () => { setSelectedSynthAsset(newSynthAsset); }; - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (isApprovalNeeded) { if (approveUsdtEthConfig?.request) { approve(approveUsdtEthConfig.request); @@ -211,24 +217,15 @@ export const Repay = () => { "Repay failed. Unknown error. Please contact Alchemix team.", }); } - }, [ - approve, - approveConfig?.request, - approveUsdtEthConfig?.request, - burnConfig, - burnConfigError, - isApprovalNeeded, - repay, - repayConfig, - repayConfigError, - repaymentToken?.symbol, - selectedSynthAsset, - ]); + }; - const isFetching = - repaymentToken?.symbol.toLowerCase() === selectedSynthAsset.toLowerCase() - ? isFetchingBurnConfig - : isFetchingRepayConfig; + const isPending = + isApprovalNeeded === false + ? repaymentToken?.symbol.toLowerCase() === + selectedSynthAsset.toLowerCase() + ? isPendingBurnConfig + : isPendingRepayConfig + : isPendingAllowance || isFetchingAllowance; return (
@@ -283,16 +280,16 @@ export const Repay = () => { } />
- + )} diff --git a/src/components/vaults/row/Deposit.tsx b/src/components/vaults/row/Deposit.tsx index 9e527940..2c01aaf8 100644 --- a/src/components/vaults/row/Deposit.tsx +++ b/src/components/vaults/row/Deposit.tsx @@ -1,7 +1,6 @@ import { Token, Vault } from "@/lib/types"; import { TokenInput } from "@/components/common/input/TokenInput"; -import { Button } from "@/components/ui/button"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Select, SelectTrigger, @@ -19,6 +18,7 @@ import { alchemistV2Abi } from "@/abi/alchemistV2"; import { useChain } from "@/hooks/useChain"; import { SlippageInput } from "@/components/common/input/SlippageInput"; import { VaultActionMotionDiv } from "./motion"; +import { CtaButton } from "@/components/common/CtaButton"; export const Deposit = ({ vault, @@ -80,7 +80,7 @@ export const Deposit = ({ const isSelectedTokenYieldToken = token.address.toLowerCase() === yieldTokenData.address.toLowerCase(); - const { isApprovalNeeded, writeApprove, writeDeposit, isFetching } = + const { isApprovalNeeded, writeApprove, writeDeposit, isPending } = useDeposit({ vault, selectedToken: token, @@ -90,13 +90,13 @@ export const Deposit = ({ setAmount, }); - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (token.address !== GAS_ADDRESS && isApprovalNeeded === true) { writeApprove(); } else { writeDeposit(); } - }, [token, isApprovalNeeded, writeApprove, writeDeposit]); + }; return ( @@ -139,20 +139,20 @@ export const Deposit = ({ {!isSelectedTokenYieldToken && ( )} - + ); diff --git a/src/components/vaults/row/Migrate.tsx b/src/components/vaults/row/Migrate.tsx index 5d6c12de..8f94baeb 100644 --- a/src/components/vaults/row/Migrate.tsx +++ b/src/components/vaults/row/Migrate.tsx @@ -6,13 +6,13 @@ import { SelectContent, SelectItem, } from "@/components/ui/select"; -import { Button } from "@/components/ui/button"; -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useMigrate } from "@/lib/mutations/useMigrate"; import { MigrateTokenInput } from "@/components/common/input/MigrateTokenInput"; import { isInputZero } from "@/utils/inputNotZero"; import { useTokensQuery } from "@/lib/queries/useTokensQuery"; import { VaultActionMotionDiv } from "./motion"; +import { CtaButton } from "@/components/common/CtaButton"; export const Migrate = ({ vault, @@ -45,7 +45,7 @@ export const Migrate = ({ writeWithdrawApprove, writeMintApprove, writeMigrate, - isFetching, + isPending, } = useMigrate({ currentVault: vault, amount, @@ -53,7 +53,7 @@ export const Migrate = ({ selectedVault, }); - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (isApprovalNeededWithdraw === true) { writeWithdrawApprove(); return; @@ -63,13 +63,7 @@ export const Migrate = ({ return; } writeMigrate(); - }, [ - isApprovalNeededMint, - isApprovalNeededWithdraw, - writeMigrate, - writeMintApprove, - writeWithdrawApprove, - ]); + }; return ( @@ -120,20 +114,20 @@ export const Migrate = ({ vault={vault} /> - + ); diff --git a/src/components/vaults/row/VaultAccordionRow.tsx b/src/components/vaults/row/VaultAccordionRow.tsx index 6c27bb5f..65de4be1 100644 --- a/src/components/vaults/row/VaultAccordionRow.tsx +++ b/src/components/vaults/row/VaultAccordionRow.tsx @@ -289,7 +289,7 @@ export const CurrencyCell = ({ : 0; return ( -
+

{formatNumber(tokenAmountFormated, { dustToZero: true, tokenDecimals })}{" "} {tokenSymbol} diff --git a/src/components/vaults/row/Withdraw.tsx b/src/components/vaults/row/Withdraw.tsx index 7982d3b0..e4333d21 100644 --- a/src/components/vaults/row/Withdraw.tsx +++ b/src/components/vaults/row/Withdraw.tsx @@ -1,6 +1,5 @@ import { Token, Vault } from "@/lib/types"; -import { Button } from "@/components/ui/button"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Select, SelectTrigger, @@ -17,6 +16,7 @@ import { formatNumber } from "@/utils/number"; import { formatEther } from "viem"; import { SlippageInput } from "@/components/common/input/SlippageInput"; import { VaultActionMotionDiv } from "./motion"; +import { CtaButton } from "@/components/common/CtaButton"; export const Withdraw = ({ vault, @@ -47,7 +47,7 @@ export const Withdraw = ({ const isSelectedTokenYieldToken = token.address.toLowerCase() === yieldTokenData.address.toLowerCase(); - const { isApprovalNeeded, writeApprove, writeWithdraw, isFetching } = + const { isApprovalNeeded, writeApprove, writeWithdraw, isPending } = useWithdraw({ vault, selectedToken: token, @@ -55,15 +55,16 @@ export const Withdraw = ({ slippage, yieldToken: yieldTokenData, setAmount, + isSelectedTokenYieldToken, }); - const onCtaClick = useCallback(() => { + const onCtaClick = () => { if (isApprovalNeeded === true) { writeApprove(); } else { writeWithdraw(); } - }, [isApprovalNeeded, writeApprove, writeWithdraw]); + }; const onSelectChange = (value: string) => { setAmount(""); @@ -121,18 +122,18 @@ export const Withdraw = ({ )}{" "} {vault.alchemist.synthType}

- +
); diff --git a/src/hooks/useAllowance.ts b/src/hooks/useAllowance.ts index b26fca0b..f779bef2 100644 --- a/src/hooks/useAllowance.ts +++ b/src/hooks/useAllowance.ts @@ -38,7 +38,8 @@ export const useAllowance = ({ const { data: allowanceData, queryKey: isApprovalNeededQueryKey, - isFetching, + isPending: isPendingAllowance, + isFetching: isFetchingAllowance, } = useReadContract({ address: tokenAddress, abi: erc20Abi, @@ -57,29 +58,33 @@ export const useAllowance = ({ const { isApprovalNeeded, allowance } = allowanceData ?? {}; - const { data: approveConfig } = useSimulateContract({ - address: tokenAddress, - abi: erc20Abi, - functionName: "approve", - chainId: chain.id, - args: [ - spender, - isInfiniteApproval ? MAX_UINT256_BN : parseUnits(amount, decimals), - ], - query: { - enabled: - !isInputZero(amount) && - !!address && - isApprovalNeeded === true && - tokenAddress !== GAS_ADDRESS && - tokenAddress?.toLowerCase() !== USDT_MAINNET_ADDRESS.toLowerCase(), - }, - }); + const { data: approveConfig, isPending: isPendingApproveConfigToken } = + useSimulateContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "approve", + chainId: chain.id, + args: [ + spender, + isInfiniteApproval ? MAX_UINT256_BN : parseUnits(amount, decimals), + ], + query: { + enabled: + !isInputZero(amount) && + !!address && + isApprovalNeeded === true && + tokenAddress !== GAS_ADDRESS && + tokenAddress?.toLowerCase() !== USDT_MAINNET_ADDRESS.toLowerCase(), + }, + }); /** USDT on Ethereum doesn't follow ERC20 standard. * https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L199 */ - const { data: approveUsdtEthConfig } = useSimulateContract({ + const { + data: approveUsdtEthConfig, + isPending: isPendingApproveConfigUsdtEth, + } = useSimulateContract({ address: tokenAddress, abi: parseAbi(["function approve(address _spender, uint _value) public"]), functionName: "approve", @@ -116,11 +121,21 @@ export const useAllowance = ({ } }, [approvalReceipt, isApprovalNeededQueryKey, resetApprove, queryClient]); + const isPending = (() => { + if (isApprovalNeeded) { + if (tokenAddress?.toLowerCase() === USDT_MAINNET_ADDRESS.toLowerCase()) { + return isPendingApproveConfigUsdtEth || isPendingAllowance; + } + return isPendingApproveConfigToken || isPendingAllowance; + } else return isPendingAllowance; + })(); + return { isApprovalNeeded, approve, approveConfig, approveUsdtEthConfig, - isFetching, + isPending, + isFetching: isFetchingAllowance, }; }; diff --git a/src/hooks/useStaticTokenAdapterWithdraw.ts b/src/hooks/useStaticTokenAdapterWithdraw.ts new file mode 100644 index 00000000..93c6e018 --- /dev/null +++ b/src/hooks/useStaticTokenAdapterWithdraw.ts @@ -0,0 +1,97 @@ +import { keepPreviousData } from "@tanstack/react-query"; +import { useReadContract } from "wagmi"; +import { formatUnits, parseUnits } from "viem"; + +import { staticTokenAdapterAbi } from "@/abi/staticTokenAdapter"; +import { Token, Vault } from "@/lib/types"; +import { isInputZero } from "@/utils/inputNotZero"; +import { useChain } from "./useChain"; + +interface UseStaticTokenAdapterWithdrawAmountArgs { + typeGuard: "withdrawInput"; + + balanceForYieldToken: string | undefined; + isSelectedTokenYieldToken: boolean; + vault: Vault; + + amount?: never; + selectedToken?: never; +} + +interface UseStaticTokenAdapterAdjustedAmountArgs { + typeGuard: "adjustedAmount"; + + amount: string; + selectedToken: Token; + vault: Vault; + isSelectedTokenYieldToken: boolean; + + balanceForYieldToken?: never; +} + +type UseStaticTokenAdapterWithdrawArgs = + | UseStaticTokenAdapterWithdrawAmountArgs + | UseStaticTokenAdapterAdjustedAmountArgs; + +/** + * Adjusted for Aave static token adapter. + * Aim is to get the exact amount of yield token user wants to withdraw. + * We use it in withdraw input to adjust available balance to dynamic aave yield tokens. + * We use it in useWithdraw to adjust amount back to static amount for static token adapter. + * @dev It is safe to assume that static adapter has same decimals as yield token (it inherits from it). + */ +export const useStaticTokenAdapterWithdraw = ({ + typeGuard, + balanceForYieldToken, + isSelectedTokenYieldToken, + vault, + amount, + selectedToken, +}: UseStaticTokenAdapterWithdrawArgs) => { + const chain = useChain(); + + const { data: balanceForYieldTokenAdapter } = useReadContract({ + address: vault.yieldToken, + chainId: chain.id, + abi: staticTokenAdapterAbi, + functionName: "staticToDynamicAmount", + args: [ + parseUnits(balanceForYieldToken ?? "0", vault.yieldTokenParams.decimals), + ], + query: { + enabled: + vault.metadata.api.provider === "aave" && + typeGuard === "withdrawInput" && + balanceForYieldToken !== undefined && + isSelectedTokenYieldToken && + !!vault.metadata.yieldTokenOverride, + select: (balance) => + formatUnits(balance, vault.yieldTokenParams.decimals), + placeholderData: keepPreviousData, + }, + }); + + const { data: aaveAdjustedAmount } = useReadContract({ + address: vault.yieldToken, + abi: staticTokenAdapterAbi, + functionName: "dynamicToStaticAmount", + args: [ + typeGuard === "adjustedAmount" + ? parseUnits(amount, selectedToken.decimals) + : 0n, + ], + query: { + enabled: + vault.metadata.api.provider === "aave" && + typeGuard === "adjustedAmount" && + !isInputZero(amount) && + isSelectedTokenYieldToken && + !!vault.metadata.yieldTokenOverride, + }, + }); + + return { + balanceForYieldTokenAdapter, + aaveAdjustedAmount, + }; +}; diff --git a/src/lib/config/metadataTypes.ts b/src/lib/config/metadataTypes.ts index a5750a7f..785f494d 100644 --- a/src/lib/config/metadataTypes.ts +++ b/src/lib/config/metadataTypes.ts @@ -47,6 +47,15 @@ export type VaultMessage = { learnMoreUrl?: string; }; +type ApiProvider = + | "meltedRewards" + | "aave" + | "yearn" + | "frax" + | "rocket" + | "vesper" + | "lido"; + export interface VaultMetadata { label: string; synthAssetType: SynthAsset; @@ -56,13 +65,19 @@ export interface VaultMetadata { api: { apr: AprFn; yieldType: string; - cacheKey: string; + provider: ApiProvider; bonus: BonusFn; }; disabledDepositTokens: Address[]; wethGateway?: Address; gateway?: Address; migrator?: Address; + /** + * This is the address of the actual yield (bearing for aave) token, + * the regular yield token address in this case becomes a (static token adapter for aave or staking token for yearn), + * that we use for the vaults. + * If it exists, means the vault is using (static token adapter for aave or staking token for yearn). + */ yieldTokenOverride?: Address; strategy?: string; beta?: boolean; diff --git a/src/lib/config/vaults.ts b/src/lib/config/vaults.ts index 47ee963b..3cb6a9f9 100644 --- a/src/lib/config/vaults.ts +++ b/src/lib/config/vaults.ts @@ -48,7 +48,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -63,7 +63,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -78,7 +78,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -94,7 +94,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getAaveBonusData, }, disabledDepositTokens: [], @@ -110,7 +110,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getAaveBonusData, }, disabledDepositTokens: [], @@ -126,7 +126,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getAaveBonusData, }, disabledDepositTokens: [], @@ -141,7 +141,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -156,7 +156,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getVesperApr, yieldType: "APR", - cacheKey: "vesper", + provider: "vesper", bonus: getVesperBonusData, }, disabledDepositTokens: [], @@ -171,7 +171,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getVesperApr, yieldType: "APR", - cacheKey: "vesper", + provider: "vesper", bonus: getVesperBonusData, }, disabledDepositTokens: [], @@ -186,7 +186,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getVesperApr, yieldType: "APR", - cacheKey: "vesper", + provider: "vesper", bonus: getVesperBonusData, }, disabledDepositTokens: [], @@ -202,7 +202,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getAaveBonusData, }, disabledDepositTokens: [], @@ -219,7 +219,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -236,7 +236,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getLidoApy, yieldType: "APR", - cacheKey: "lido", + provider: "lido", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -252,7 +252,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getRocketApr, yieldType: "APR", - cacheKey: "rocket", + provider: "rocket", bonus: getNoBonus, }, disabledDepositTokens: [GAS_ADDRESS, WETH_MAINNET_ADDRESS], @@ -269,7 +269,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getAaveBonusData, }, disabledDepositTokens: [], @@ -285,7 +285,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getVesperApr, yieldType: "APR", - cacheKey: "vesper", + provider: "vesper", bonus: getVesperBonusData, }, disabledDepositTokens: [], @@ -300,7 +300,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getFraxApy, yieldType: "APR", - cacheKey: "frax", + provider: "frax", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -317,7 +317,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -331,7 +331,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -345,7 +345,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -364,7 +364,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -381,7 +381,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getMeltedRewardsBonusData, }, disabledDepositTokens: [], @@ -397,7 +397,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -420,7 +420,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "meltedRewards", + provider: "meltedRewards", bonus: getMeltedRewardsBonusData, }, disabledDepositTokens: [], @@ -443,7 +443,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "yearn", + provider: "yearn", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -462,7 +462,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getNoBonus, }, disabledDepositTokens: [], @@ -478,7 +478,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getLidoApy, yieldType: "APR", - cacheKey: "meltedRewards", + provider: "meltedRewards", bonus: getMeltedRewardsBonusData, }, disabledDepositTokens: [], @@ -502,7 +502,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getYearnApy, yieldType: "APY", - cacheKey: "meltedRewards", + provider: "meltedRewards", bonus: getMeltedRewardsBonusData, }, disabledDepositTokens: [], @@ -521,7 +521,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getAaveApr, yieldType: "APR", - cacheKey: "aave", + provider: "aave", bonus: getMeltedRewardsBonusData, }, disabledDepositTokens: [], @@ -537,7 +537,7 @@ export const VAULTS: VaultsConfig = { api: { apr: getLidoApy, yieldType: "APR", - cacheKey: "lido", + provider: "lido", bonus: getMeltedRewardsBonusData, }, disabledDepositTokens: [], diff --git a/src/lib/mutations/useDeposit.ts b/src/lib/mutations/useDeposit.ts index 34a2ad1a..f1d3f89e 100644 --- a/src/lib/mutations/useDeposit.ts +++ b/src/lib/mutations/useDeposit.ts @@ -5,7 +5,7 @@ import { useAllowance } from "@/hooks/useAllowance"; import { useChain } from "@/hooks/useChain"; import { Token, Vault } from "@/lib/types"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect } from "react"; import { toast } from "sonner"; import { parseUnits } from "viem"; import { @@ -73,6 +73,7 @@ export const useDeposit = ({ approveConfig, isApprovalNeeded, approveUsdtEthConfig, + isPending: isPendingAllowance, isFetching: isFetchingAllowance, } = useAllowance({ amount, @@ -84,7 +85,7 @@ export const useDeposit = ({ const { data: depositGatewayConfig, error: depositGatewayError, - isFetching: isDepositGatewayConfigFetching, + isPending: isDepositGatewayConfigPending, } = useSimulateContract({ address: vault.metadata.gateway, abi: aaveTokenGatewayAbi, @@ -126,7 +127,7 @@ export const useDeposit = ({ const { data: depositAlchemistConfig, error: depositAlchemistError, - isFetching: isDepositAlchemistConfigFetching, + isPending: isDepositAlchemistConfigPending, } = useSimulateContract({ address: vault.alchemist.address, abi: alchemistV2Abi, @@ -164,7 +165,7 @@ export const useDeposit = ({ const { data: depositGasConfig, error: depositGasError, - isFetching: isDepositGasConfigFetching, + isPending: isDepositGasConfigPending, } = useSimulateContract({ address: vault.metadata.wethGateway, abi: wethGatewayAbi, @@ -206,7 +207,7 @@ export const useDeposit = ({ const { data: depositUnderlyingConfig, error: depositUnderlyingError, - isFetching: isDepositUnderlyingConfigFetching, + isPending: isDepositUnderlyingConfigPending, } = useSimulateContract({ address: vault.alchemist.address, abi: alchemistV2Abi, @@ -374,7 +375,7 @@ export const useDeposit = ({ approveConfig?.request && approve(approveConfig.request); }, [approve, approveConfig, approveUsdtEthConfig]); - const isFetching = useMemo(() => { + const isPending = (() => { if (!amount) return; // deposit gateway if ( @@ -383,7 +384,9 @@ export const useDeposit = ({ !!vault.metadata.gateway && !!vault.metadata.yieldTokenOverride ) { - return isDepositGatewayConfigFetching || isFetchingAllowance; + if (isApprovalNeeded === false) { + return isDepositGatewayConfigPending; + } else return isPendingAllowance || isFetchingAllowance; } // deposit alchemist @@ -393,12 +396,14 @@ export const useDeposit = ({ !vault.metadata.gateway && !vault.metadata.yieldTokenOverride ) { - return isDepositAlchemistConfigFetching; + if (isApprovalNeeded === false) { + return isDepositAlchemistConfigPending; + } else return isPendingAllowance || isFetchingAllowance; } // deposit gas if (selectedToken.address === GAS_ADDRESS) { - return isDepositGasConfigFetching; + return isDepositGasConfigPending; } // if depositUnderlyingConfig is available, deposit using alchemist @@ -406,20 +411,11 @@ export const useDeposit = ({ selectedToken.address !== GAS_ADDRESS && selectedToken.address.toLowerCase() !== yieldToken.address.toLowerCase() ) { - return isDepositUnderlyingConfigFetching; + if (isApprovalNeeded === false) { + return isDepositUnderlyingConfigPending; + } else return isPendingAllowance || isFetchingAllowance; } - }, [ - amount, - isDepositAlchemistConfigFetching, - isDepositGasConfigFetching, - isDepositGatewayConfigFetching, - isDepositUnderlyingConfigFetching, - isFetchingAllowance, - selectedToken.address, - vault.metadata.gateway, - vault.metadata.yieldTokenOverride, - yieldToken.address, - ]); + })(); - return { writeDeposit, writeApprove, isApprovalNeeded, isFetching }; + return { writeDeposit, writeApprove, isApprovalNeeded, isPending }; }; diff --git a/src/lib/mutations/useMigrate.ts b/src/lib/mutations/useMigrate.ts index 2fda8ddf..06d24ffb 100644 --- a/src/lib/mutations/useMigrate.ts +++ b/src/lib/mutations/useMigrate.ts @@ -92,6 +92,8 @@ export const useMigrate = ({ const { data: isApprovalNeededWithdraw, + isPending: isPendingApprovalWithdraw, + isFetching: isFetchingApprovalWithdraw, queryKey: isApprovalNeededWithdrawQueryKey, } = useReadContract({ address: currentVault.alchemist.address, @@ -106,20 +108,24 @@ export const useMigrate = ({ }, }); - const { data: isApprovalNeededMint, queryKey: isApprovalNeededMintQueryKey } = - useReadContract({ - address: currentVault.alchemist.address, - abi: alchemistV2Abi, - chainId: chain.id, - functionName: "mintAllowance", - args: [address!, migratorToolAddress], - query: { - enabled: !!address, - select: (allowance) => - allowance === 0n || - (underlyingInDebt !== undefined && allowance < underlyingInDebt), - }, - }); + const { + data: isApprovalNeededMint, + isPending: isPendingApprovalMint, + isFetching: isFetchingApprovalMint, + queryKey: isApprovalNeededMintQueryKey, + } = useReadContract({ + address: currentVault.alchemist.address, + abi: alchemistV2Abi, + chainId: chain.id, + functionName: "mintAllowance", + args: [address!, migratorToolAddress], + query: { + enabled: !!address, + select: (allowance) => + allowance === 0n || + (underlyingInDebt !== undefined && allowance < underlyingInDebt), + }, + }); const { data: approveWithdrawConfig } = useSimulateContract({ address: currentVault.alchemist.address, @@ -138,6 +144,7 @@ export const useMigrate = ({ const { writeContract: writeWithdrawApprovePrepared, data: approveWithdrawHash, + reset: resetApproveWithdraw, } = useWriteContract({ mutation: mutationCallback({ action: "Approve withdraw", @@ -153,8 +160,14 @@ export const useMigrate = ({ queryClient.invalidateQueries({ queryKey: isApprovalNeededWithdrawQueryKey, }); + resetApproveWithdraw(); } - }, [approveWithdrawReceipt, isApprovalNeededWithdrawQueryKey, queryClient]); + }, [ + resetApproveWithdraw, + approveWithdrawReceipt, + isApprovalNeededWithdrawQueryKey, + queryClient, + ]); const { data: approveMintConfig } = useSimulateContract({ address: currentVault.alchemist.address, @@ -166,12 +179,15 @@ export const useMigrate = ({ }, }); - const { writeContract: writeMintApprovePrepared, data: approveMintHash } = - useWriteContract({ - mutation: mutationCallback({ - action: "Approve mint", - }), - }); + const { + writeContract: writeMintApprovePrepared, + data: approveMintHash, + reset: resetApproveMint, + } = useWriteContract({ + mutation: mutationCallback({ + action: "Approve mint", + }), + }); const { data: approveMintReceipt } = useWaitForTransactionReceipt({ hash: approveMintHash, @@ -182,12 +198,18 @@ export const useMigrate = ({ queryClient.invalidateQueries({ queryKey: isApprovalNeededMintQueryKey, }); + resetApproveMint(); } - }, [approveMintReceipt, isApprovalNeededMintQueryKey, queryClient]); + }, [ + resetApproveMint, + approveMintReceipt, + isApprovalNeededMintQueryKey, + queryClient, + ]); const { data: migrateConfig, - isFetching, + isPending: isPendingConfig, error: migrateConfigError, } = useSimulateContract({ address: migratorToolAddress, @@ -271,12 +293,25 @@ export const useMigrate = ({ writeMigratePrepared, ]); + const isPending = (() => { + if (!amount) return; + if (isApprovalNeededWithdraw === false && isApprovalNeededMint === false) { + return isPendingConfig; + } + return ( + isPendingApprovalMint || + isPendingApprovalWithdraw || + isFetchingApprovalMint || + isFetchingApprovalWithdraw + ); + })(); + return { isApprovalNeededWithdraw, isApprovalNeededMint, writeWithdrawApprove, writeMintApprove, writeMigrate, - isFetching, + isPending, }; }; diff --git a/src/lib/mutations/useWithdraw.ts b/src/lib/mutations/useWithdraw.ts index f117f94e..e010a907 100644 --- a/src/lib/mutations/useWithdraw.ts +++ b/src/lib/mutations/useWithdraw.ts @@ -3,7 +3,7 @@ import { alchemistV2Abi } from "@/abi/alchemistV2"; import { useChain } from "@/hooks/useChain"; import { Token, Vault } from "@/lib/types"; import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect } from "react"; import { toast } from "sonner"; import { parseUnits } from "viem"; import { @@ -18,6 +18,8 @@ import { wethGatewayAbi } from "@/abi/wethGateway"; import { calculateMinimumOut } from "@/utils/helpers/minAmountWithSlippage"; import { QueryKeys } from "@/lib/queries/queriesSchema"; import { useWriteContractMutationCallback } from "@/hooks/useWriteContractMutationCallback"; +import { isInputZero } from "@/utils/inputNotZero"; +import { useStaticTokenAdapterWithdraw } from "@/hooks/useStaticTokenAdapterWithdraw"; export const useWithdraw = ({ vault, @@ -26,6 +28,7 @@ export const useWithdraw = ({ slippage, yieldToken, setAmount, + isSelectedTokenYieldToken, }: { amount: string; slippage: string; @@ -33,6 +36,7 @@ export const useWithdraw = ({ selectedToken: Token; yieldToken: Token; setAmount: (amount: string) => void; + isSelectedTokenYieldToken: boolean; }) => { const chain = useChain(); const queryClient = useQueryClient(); @@ -45,17 +49,29 @@ export const useWithdraw = ({ const { address } = useAccount(); - const isSelectedTokenYieldToken = - selectedToken.address.toLowerCase() === yieldToken.address.toLowerCase(); + const { aaveAdjustedAmount } = useStaticTokenAdapterWithdraw({ + typeGuard: "adjustedAmount", + amount, + selectedToken, + vault, + isSelectedTokenYieldToken, + }); + + const withdrawAmount = + vault.metadata.api.provider === "aave" && + isSelectedTokenYieldToken && + !!vault.metadata.yieldTokenOverride + ? aaveAdjustedAmount + : parseUnits(amount, selectedToken.decimals); const { data: sharesFromYieldToken } = useReadContract({ address: vault.alchemist.address, abi: alchemistV2Abi, chainId: chain.id, functionName: "convertYieldTokensToShares", - args: [vault.yieldToken, parseUnits(amount, selectedToken.decimals)], + args: [vault.yieldToken, withdrawAmount ?? 0n], query: { - enabled: isSelectedTokenYieldToken, + enabled: isSelectedTokenYieldToken && !isInputZero(amount), }, }); @@ -66,7 +82,7 @@ export const useWithdraw = ({ functionName: "convertUnderlyingTokensToShares", args: [vault.yieldToken, parseUnits(amount, selectedToken.decimals)], query: { - enabled: !isSelectedTokenYieldToken, + enabled: !isSelectedTokenYieldToken && !isInputZero(amount), }, }); @@ -84,7 +100,8 @@ export const useWithdraw = ({ const { data: isApprovalNeededAaveGateway, queryKey: isApprovalNeededAaveGatewayQueryKey, - isFetching: isApprovalNeededAaveGatewayFetching, + isPending: isPendingApprovalAaveGateway, + isFetching: isFetchingApprovalAaveGateway, } = useReadContract({ address: vault.alchemist.address, abi: alchemistV2Abi, @@ -106,7 +123,8 @@ export const useWithdraw = ({ const { data: isApprovalNeededWethGateway, queryKey: isApprovalNeededWethGatewayQueryKey, - isFetching: isApprovalNeededWethGatewayFetching, + isPending: isPendingApprovalWethGateway, + isFetching: isFetchingApprovalWethGateway, } = useReadContract({ address: vault.alchemist.address, abi: alchemistV2Abi, @@ -185,7 +203,7 @@ export const useWithdraw = ({ const { data: withdrawGatewayConfig, error: withdrawGatewayError, - isFetching: isWithdrawGatewayConfigFetching, + isPending: isWithdrawGatewayConfigPending, } = useSimulateContract({ address: vault.metadata.gateway, abi: aaveTokenGatewayAbi, @@ -224,7 +242,7 @@ export const useWithdraw = ({ const { data: withdrawAlchemistConfig, error: withdrawAlchemistError, - isFetching: isWithdrawAlchemistConfigFetching, + isPending: isWithdrawAlchemistConfigPending, } = useSimulateContract({ address: vault.alchemist.address, abi: alchemistV2Abi, @@ -262,7 +280,7 @@ export const useWithdraw = ({ const { data: withdrawGasConfig, error: withdrawGasError, - isFetching: isWithdrawGasConfigFetching, + isPending: isWithdrawGasConfigPending, } = useSimulateContract({ address: vault.metadata.wethGateway, abi: wethGatewayAbi, @@ -306,7 +324,7 @@ export const useWithdraw = ({ const { data: withdrawUnderlyingConfig, error: withdrawUnderlyingError, - isFetching: isWithdrawUnderlyingConfigFetching, + isPending: isWithdrawUnderlyingConfigPending, } = useSimulateContract({ address: vault.alchemist.address, abi: alchemistV2Abi, @@ -480,7 +498,7 @@ export const useWithdraw = ({ yieldToken.address, ]); - const isFetching = useMemo(() => { + const isPending = (() => { if (!amount) return; // withdraw gateway if ( @@ -489,9 +507,10 @@ export const useWithdraw = ({ !!vault.metadata.gateway && !!vault.metadata.yieldTokenOverride ) { - return ( - isWithdrawGatewayConfigFetching || isApprovalNeededAaveGatewayFetching - ); + if (isApprovalNeededAaveGateway === false) { + return isWithdrawGatewayConfigPending; + } else + return isPendingApprovalAaveGateway || isFetchingApprovalAaveGateway; } // withdraw alchemist @@ -501,14 +520,15 @@ export const useWithdraw = ({ !vault.metadata.gateway && !vault.metadata.yieldTokenOverride ) { - return ( - isWithdrawAlchemistConfigFetching || isApprovalNeededWethGatewayFetching - ); + return isWithdrawAlchemistConfigPending; } // withdraw gas if (selectedToken.address === GAS_ADDRESS) { - return isWithdrawGasConfigFetching; + if (isApprovalNeededWethGateway === false) { + return isWithdrawGasConfigPending; + } else + return isPendingApprovalWethGateway || isFetchingApprovalWethGateway; } // withdraw underlying @@ -516,26 +536,14 @@ export const useWithdraw = ({ selectedToken.address !== GAS_ADDRESS && selectedToken.address.toLowerCase() !== yieldToken.address.toLowerCase() ) { - return isWithdrawUnderlyingConfigFetching; + return isWithdrawUnderlyingConfigPending; } - }, [ - amount, - isWithdrawAlchemistConfigFetching, - isWithdrawGasConfigFetching, - isWithdrawGatewayConfigFetching, - isWithdrawUnderlyingConfigFetching, - isApprovalNeededAaveGatewayFetching, - isApprovalNeededWethGatewayFetching, - selectedToken.address, - vault.metadata.gateway, - vault.metadata.yieldTokenOverride, - yieldToken.address, - ]); + })(); return { isApprovalNeeded, writeApprove, writeWithdraw, - isFetching, + isPending, }; };