diff --git a/projects/ui/src/components/Common/Form/FormTxn/AddPlantTxnToggle.tsx b/projects/ui/src/components/Common/Form/FormTxn/AddPlantTxnToggle.tsx index 6ffaea2189..68ac447cfd 100644 --- a/projects/ui/src/components/Common/Form/FormTxn/AddPlantTxnToggle.tsx +++ b/projects/ui/src/components/Common/Form/FormTxn/AddPlantTxnToggle.tsx @@ -24,7 +24,7 @@ import useFarmerFormTxnsSummary from '~/hooks/farmer/form-txn/useFarmerFormTxnsS import MergeIcon from '~/img/misc/merge-icon.svg'; import { FormTxnsFormState } from '~/components/Common/Form'; -import { FormTxn } from '~/lib/Txn'; +import { FormTxn, PlantAndDoX } from '~/lib/Txn'; import SiloVestingWarningAlert from '~/components/Silo/SiloVestingWarningAlert'; const sx = { @@ -51,7 +51,9 @@ const sx = { * * NOTE: Used within Formik Context */ -const AddPlantTxnToggle: React.FC<{}> = () => { +const AddPlantTxnToggle: React.FC<{ + plantAndDoX: PlantAndDoX | undefined; +}> = ({ plantAndDoX }) => { /// Local State const [open, show, hide] = useToggle(); @@ -95,7 +97,7 @@ const AddPlantTxnToggle: React.FC<{}> = () => { }, [open, isPlant, isPlanting, show, hide]); /// If there is nothing to plant or if the preset isn't plant, return nothing - if (!isPlant || !plantEnabled) return null; + if (!isPlant || !plantEnabled || !plantAndDoX) return null; return ( <> diff --git a/projects/ui/src/components/Common/Form/FormTxnProvider.tsx b/projects/ui/src/components/Common/Form/FormTxnProvider.tsx index 6038f1b504..aaf572c598 100644 --- a/projects/ui/src/components/Common/Form/FormTxnProvider.tsx +++ b/projects/ui/src/components/Common/Form/FormTxnProvider.tsx @@ -12,7 +12,6 @@ import { useFetchFarmerBalances } from '~/state/farmer/balances/updater'; import { useFetchFarmerBarn } from '~/state/farmer/barn/updater'; import { useFetchFarmerField } from '~/state/farmer/field/updater'; import { useFetchFarmerSilo } from '~/state/farmer/silo/updater'; -import useSeason from '~/hooks/beanstalk/useSeason'; import { FormTxnBundler, ClaimFarmStep, @@ -21,9 +20,9 @@ import { MowFarmStep, PlantFarmStep, RinseFarmStep, - PlantAndDoX, FormTxn, } from '~/lib/Txn'; +import usePlantAndDoX from '~/hooks/farmer/form-txn/usePlantAndDoX'; // ------------------------------------------------------------------------- @@ -68,7 +67,6 @@ const useInitFormTxnContext = () => { const farmerSilo = useFarmerSilo(); const farmerField = useFarmerField(); const farmerBarn = useFarmerFertilizer(); - const season = useSeason(); /// Refetch functions const [refetchFarmerSilo] = useFetchFarmerSilo(); @@ -79,17 +77,11 @@ const useInitFormTxnContext = () => { /// Helpers const getBDV = useBDV(); + const plantAndDoX = usePlantAndDoX(); + /// Context State const [txnBundler, setTxnBundler] = useState(new FormTxnBundler(sdk, {})); - const plantAndDoX = useMemo(() => { - const earnedBeans = sdk.tokens.BEAN.amount( - farmerSilo.beans.earned.toString() - ); - - return new PlantAndDoX(sdk, earnedBeans, season.toNumber()); - }, [farmerSilo.beans.earned, sdk, season]); - /// On any change, update the txn bundler useEffect(() => { const { BEAN } = sdk.tokens; @@ -204,7 +196,7 @@ const useInitFormTxnContext = () => { txnBundler, plantAndDoX, refetch, - }; + } as const; }; export const FormTxnBuilderContext = React.createContext< diff --git a/projects/ui/src/components/Silo/Actions/Convert.tsx b/projects/ui/src/components/Silo/Actions/Convert.tsx index c65f9651dc..c2699a4934 100644 --- a/projects/ui/src/components/Silo/Actions/Convert.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert.tsx @@ -45,7 +45,6 @@ import WarningAlert from '~/components/Common/Alert/WarningAlert'; import TokenOutput from '~/components/Common/Form/TokenOutput'; import TxnAccordion from '~/components/Common/TxnAccordion'; -import useFarmerDepositCrateFromPlant from '~/hooks/farmer/useFarmerDepositCrateFromPlant'; import AdditionalTxnsAccordion from '~/components/Common/Form/FormTxn/AdditionalTxnsAccordion'; import useFarmerFormTxnsActions from '~/hooks/farmer/form-txn/useFarmerFormTxnActions'; import useAsyncMemo from '~/hooks/display/useAsyncMemo'; @@ -53,6 +52,7 @@ import AddPlantTxnToggle from '~/components/Common/Form/FormTxn/AddPlantTxnToggl import FormTxnProvider from '~/components/Common/Form/FormTxnProvider'; import useFormTxnContext from '~/hooks/sdk/useFormTxnContext'; import { FormTxn, ConvertFarmStep } from '~/lib/Txn'; +import usePlantAndDoX from '~/hooks/farmer/form-txn/usePlantAndDoX'; // ----------------------------------------------------------------------- @@ -90,12 +90,14 @@ const ConvertForm: FC< /** other */ sdk: BeanstalkSDK; conversion: ConvertDetails; + plantAndDoX: ReturnType; } > = ({ tokenList, siloBalances, handleQuote, currentSeason, + plantAndDoX, sdk, // Formik values, @@ -107,7 +109,7 @@ const ConvertForm: FC< const [isTokenSelectVisible, showTokenSelect, hideTokenSelect] = useToggle(); const getBDV = useBDV(); - const { crate: plantCrate } = useFarmerDepositCrateFromPlant(); + const plantCrate = plantAndDoX?.crate?.bn; /// Extract values from form state const tokenIn = values.tokens[0].token; // converting from token @@ -129,9 +131,10 @@ const ConvertForm: FC< sdk.tokens.BEAN.equals(tokenIn) ); - const totalAmountIn = isUsingPlanted - ? (amountIn || ZERO_BN).plus(plantCrate.asBN.amount) - : amountIn; + const totalAmountIn = + isUsingPlanted && plantCrate + ? (amountIn || ZERO_BN).plus(plantCrate.amount) + : amountIn; /// Derived form state let isReady = false; @@ -268,7 +271,9 @@ const ConvertForm: FC< } params={quoteHandlerParams} /> - {!canConvert && tokenOut && maxAmountIn ? null : } + {!canConvert && tokenOut && maxAmountIn ? null : ( + + )} {/* User Input: destination token */} {depositedAmount.gt(0) ? ( @@ -490,6 +495,9 @@ const ConvertPropProvider: FC<{ if (!farmerBalances?.deposits) { throw new Error('No balances found'); } + const { plantAction } = plantAndDoX; + + const includePlant = !!(isConvertingPlanted && plantAction); const result = await ConvertFarmStep._handleConversion( sdk, @@ -499,7 +507,7 @@ const ConvertPropProvider: FC<{ tokenIn.amount(_amountIn.toString()), season.toNumber(), slippage, - isConvertingPlanted ? plantAndDoX : undefined + includePlant ? plantAction : undefined ); setConversion(result.conversion); @@ -540,8 +548,11 @@ const ConvertPropProvider: FC<{ success: 'Convert successful.', }); + const { plantAction } = plantAndDoX; + const amountIn = tokenIn.amount(_amountIn.toString()); // amount of from token - const isPlanting = values.farmActions.primary?.includes(FormTxn.PLANT); + const isPlanting = + plantAndDoX && values.farmActions.primary?.includes(FormTxn.PLANT); const convertTxn = new ConvertFarmStep( sdk, @@ -553,7 +564,7 @@ const ConvertPropProvider: FC<{ const { getEncoded, minAmountOut } = await convertTxn.handleConversion( amountIn, slippage, - isPlanting ? plantAndDoX : undefined + isPlanting ? plantAction : undefined ); convertTxn.build(getEncoded, minAmountOut); @@ -602,17 +613,17 @@ const ConvertPropProvider: FC<{ } }, [ - middleware, - account, - farmerBalances, sdk, season, - plantAndDoX, + account, txnBundler, + middleware, + plantAndDoX, + initialValues, + farmerBalances, refetch, refetchPools, refetchFarmerBalances, - initialValues, ] ); @@ -634,6 +645,7 @@ const ConvertPropProvider: FC<{ currentSeason={season} sdk={sdk} conversion={conversion} + plantAndDoX={plantAndDoX} {...formikProps} /> diff --git a/projects/ui/src/components/Silo/Actions/Transfer.tsx b/projects/ui/src/components/Silo/Actions/Transfer.tsx index 6e0a56c0fe..19a5e79ef7 100644 --- a/projects/ui/src/components/Silo/Actions/Transfer.tsx +++ b/projects/ui/src/components/Silo/Actions/Transfer.tsx @@ -65,7 +65,7 @@ const TransferForm: FC< token: Token; siloBalance: TokenSiloBalance | undefined; season: BigNumber; - plantAndDoX: PlantAndDoX; + plantAndDoX: PlantAndDoX | undefined; } > = ({ // Formik @@ -84,7 +84,8 @@ const TransferForm: FC< const txnActions = useFarmerFormTxnsActions(); const isUsingPlant = Boolean( values.farmActions.primary?.includes(FormTxn.PLANT) && - BEAN.equals(whitelistedToken) + BEAN.equals(whitelistedToken) && + plantAndDoX ); // Results @@ -93,7 +94,7 @@ const TransferForm: FC< const deposits = siloBalance?.deposits || []; if (!isUsingPlant && (amount.lte(0) || !deposits.length)) return null; - if (isUsingPlant && plantAndDoX.getAmount().lte(0)) return null; + if (isUsingPlant && plantAndDoX?.getAmount().lte(0)) return null; // FIXME: stems return WithdrawFarmStep.calculateWithdraw( @@ -187,7 +188,7 @@ const TransferForm: FC< balanceLabel="Deposited Balance" InputProps={InputProps} /> - + {depositedBalance?.gt(0) && ( <> @@ -343,15 +344,19 @@ const TransferPropProvider: FC<{ const formData = values.tokens[0]; const primaryActions = values.farmActions.primary; + const { plantAction } = plantAndDoX; + const isPlanting = + plantAndDoX && primaryActions?.includes(FormTxn.PLANT) && sdk.tokens.BEAN.equals(token); const baseAmount = token.amount((formData?.amount || 0).toString()); - const totalAmount = isPlanting - ? baseAmount.add(plantAndDoX.getAmount()) - : baseAmount; + const totalAmount = + isPlanting && plantAction + ? baseAmount.add(plantAction.getAmount()) + : baseAmount; if (totalAmount.lte(0)) throw new Error('Invalid amount.'); @@ -363,7 +368,7 @@ const TransferPropProvider: FC<{ values.to, baseAmount, season.toNumber(), - isPlanting ? plantAndDoX : undefined + isPlanting ? plantAction : undefined ); if (!transferTxn.withdrawResult) { @@ -435,7 +440,7 @@ const TransferPropProvider: FC<{ token={token} siloBalance={farmerBalances} season={season} - plantAndDoX={plantAndDoX} + plantAndDoX={plantAndDoX.plantAction} {...formikProps} /> )} diff --git a/projects/ui/src/components/Silo/Actions/Withdraw.tsx b/projects/ui/src/components/Silo/Actions/Withdraw.tsx index c889a83f8e..e2d932076f 100644 --- a/projects/ui/src/components/Silo/Actions/Withdraw.tsx +++ b/projects/ui/src/components/Silo/Actions/Withdraw.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from 'react'; -import { Box, Divider, Stack } from '@mui/material'; +import { Box, Divider, Stack, Typography } from '@mui/material'; import BigNumber from 'bignumber.js'; import { Form, Formik, FormikHelpers, FormikProps } from 'formik'; import { useSelector } from 'react-redux'; @@ -10,19 +10,22 @@ import { TokenValue, BeanstalkSDK, FarmToMode, + FarmFromMode, + StepGenerator, } from '@beanstalk/sdk'; import { SEEDS, STALK } from '~/constants/tokens'; import { TxnPreview, - TokenInputField, TokenAdornment, TxnSeparator, SmartSubmitButton, FormStateNew, FormTxnsFormState, + SettingInput, + TxnSettings, } from '~/components/Common/Form'; import useSeason from '~/hooks/beanstalk/useSeason'; -import { displayFullBN, tokenValueToBN } from '~/util'; +import { displayFullBN, displayTokenAmount, tokenValueToBN } from '~/util'; import TransactionToast from '~/components/Common/TxnToast'; import { AppState } from '~/state'; import { ActionType } from '~/util/Actions'; @@ -42,6 +45,14 @@ import FormTxnProvider from '~/components/Common/Form/FormTxnProvider'; import useFormTxnContext from '~/hooks/sdk/useFormTxnContext'; import { FormTxn, PlantAndDoX, WithdrawFarmStep } from '~/lib/Txn'; import FarmModeField from '~/components/Common/Form/FarmModeField'; +import useToggle from '~/hooks/display/useToggle'; +import PillRow from '~/components/Common/Form/PillRow'; +import { TokenSelectMode } from '~/components/Common/Form/TokenSelectDialog'; +import TokenSelectDialogNew from '~/components/Common/Form/TokenSelectDialogNew'; +import TokenIcon from '~/components/Common/TokenIcon'; +import { QuoteHandlerWithParams } from '~/hooks/ledger/useQuoteWithParams'; +import TokenQuoteProviderWithParams from '~/components/Common/Form/TokenQuoteProviderWithParams'; +import copy from '~/constants/copy'; // ----------------------------------------------------------------------- @@ -49,11 +60,17 @@ import FarmModeField from '~/components/Common/Form/FarmModeField'; /// remove me when we migrate everything to TokenValue & DecimalBigNumber const toBN = tokenValueToBN; +type WithdrawQuoteHandlerParams = { + destination: FarmToMode | undefined; +}; + type WithdrawFormValues = FormStateNew & FormTxnsFormState & { settings: { - destination: FarmToMode; + slippage: number; }; + destination: FarmToMode | undefined; + tokenOut: ERC20Token | undefined; }; const WithdrawForm: FC< @@ -63,7 +80,7 @@ const WithdrawForm: FC< withdrawSeasons: BigNumber; season: BigNumber; sdk: BeanstalkSDK; - plantAndDoX: PlantAndDoX; + plantAndDoX: PlantAndDoX | undefined; } > = ({ // Formik @@ -76,8 +93,53 @@ const WithdrawForm: FC< season, sdk, plantAndDoX, + setFieldValue, }) => { - const { BEAN } = sdk.tokens; + const pool = useMemo( + () => sdk.pools.getPoolByLPToken(whitelistedToken), + [sdk.pools, whitelistedToken] + ); + + const claimableTokens = useMemo( + () => [ + whitelistedToken, + ...((whitelistedToken.isLP && pool?.tokens) || []), + ], + [pool, whitelistedToken] + ); + + const handleQuote = useCallback< + QuoteHandlerWithParams + >( + async (_tokenIn, _amountIn, _tokenOut, { destination }) => { + const amountIn = _tokenIn.amount(_amountIn.toString()); + + const { curve } = sdk.contracts; + + if (!pool || !_tokenIn.isLP || !_tokenOut) + return { + amountOut: ZERO_BN, + steps: [], + }; + const work = sdk.farm.create(); + work.add( + new sdk.farm.actions.RemoveLiquidityOneToken( + pool.address, + curve.registries.metaFactory.address, + _tokenOut.address, + FarmFromMode.INTERNAL, + destination || FarmToMode.INTERNAL + ) + ); + const estimate = await work.estimate(amountIn); + + return { + amountOut: tokenValueToBN(_tokenOut.fromBlockchain(estimate)), + steps: work.generators as StepGenerator[], + }; + }, + [pool, sdk.contracts, sdk.farm] + ); // Input props const InputProps = useMemo( @@ -94,13 +156,25 @@ const WithdrawForm: FC< sdk.tokens.BEAN.equals(whitelistedToken) ); + const [isTokenSelectVisible, showTokenSelect, hideTokenSelect] = useToggle(); + + const handleSelectTokens = useCallback( + (_tokens: Set) => { + const _token = Array.from(_tokens)[0]; + setFieldValue('tokenOut', _token); + }, + [setFieldValue] + ); + // Results const withdrawResult = useMemo(() => { - const amount = BEAN.amount(values.tokens[0].amount?.toString() || '0'); + const amount = sdk.tokens.BEAN.amount( + values.tokens[0]?.amount?.toString() || '0' + ); const crates = siloBalance?.deposits || []; if (!isUsingPlant && (amount.lte(0) || !crates.length)) return null; - if (isUsingPlant && plantAndDoX.getAmount().lte(0)) return null; + if (isUsingPlant && plantAndDoX?.getAmount().lte(0)) return null; return WithdrawFarmStep.calculateWithdraw( sdk.silo.siloWithdraw, @@ -111,7 +185,7 @@ const WithdrawForm: FC< isUsingPlant ? plantAndDoX : undefined ); }, [ - BEAN, + sdk.tokens.BEAN, isUsingPlant, plantAndDoX, sdk.silo.siloWithdraw, @@ -124,29 +198,83 @@ const WithdrawForm: FC< /// derived const depositedBalance = siloBalance?.amount; - const isReady = withdrawResult && !withdrawResult.amount.lt(0); + const isReady = + withdrawResult && !withdrawResult.amount.lt(0) && values.destination; + + const isLPReady = whitelistedToken.isLP + ? values.tokenOut !== undefined + : true; + + const removingLiquidity = + whitelistedToken.isLP && + values.tokenOut && + !whitelistedToken.equals(values.tokenOut); const disabledActions = useMemo( () => (whitelistedToken.isUnripe ? [FormTxn.ENROOT] : undefined), [whitelistedToken.isUnripe] ); + const quoteHandlerParams = useMemo( + () => ({ + quoterSettings: { + ignoreSameToken: true, + onReset: () => ({ amountOut: ZERO_BN }), + }, + params: { + destination: values.destination || FarmToMode.INTERNAL, + }, + }), + [values.destination] + ); + + const amountOut = values.tokens[0]?.amountOut; + return (
{/* Form Content */} {/* Input Field */} - + name="tokens.0" token={whitelistedToken} - disabled={!depositedBalance || depositedBalance.eq(0)} + state={values.tokens[0]} + tokenOut={values.tokenOut || values.tokens[0].token} + handleQuote={handleQuote} balance={toBN(depositedBalance || TokenValue.ZERO) || ZERO_BN} balanceLabel="Deposited Balance" InputProps={InputProps} + {...quoteHandlerParams} /> - {/** Setting: Destination */} - - + + {/** Setting: Destination */} + + {/** Token Out (If LP) */} + <> + {whitelistedToken.isLP ? ( + + {values.tokenOut && } + + {values.tokenOut?.symbol || 'Select Output'} + + + ) : null} + + + + {isReady ? ( @@ -197,6 +325,15 @@ const WithdrawForm: FC< amount: toBN(withdrawResult.amount), token: getNewToOldToken(whitelistedToken), }, + removingLiquidity && amountOut && values.tokenOut + ? { + type: ActionType.SWAP, + amountIn: toBN(withdrawResult.amount), + tokenIn: getNewToOldToken(whitelistedToken), + amountOut: toBN(amountOut), + tokenOut: getNewToOldToken(values.tokenOut), + } + : undefined, { type: ActionType.UPDATE_SILO_REWARDS, stalk: toBN(withdrawResult.stalk.mul(-1)), @@ -217,7 +354,7 @@ const WithdrawForm: FC< ) : null} ({ + settings: { + slippage: 0.1, + }, tokens: [ { token: token, amount: undefined, + amountOut: undefined, + quoting: false, }, ], + destination: undefined, + tokenOut: undefined, farmActions: { preset: sdk.tokens.BEAN.equals(token) ? 'plant' : 'noPrimary', primary: undefined, secondary: undefined, implied: [FormTxn.MOW], }, - settings: { - destination: FarmToMode.INTERNAL, - }, }), [sdk.tokens.BEAN, token] ); @@ -291,31 +432,48 @@ const WithdrawPropProvider: FC<{ const formData = values.tokens[0]; const primaryActions = values.farmActions.primary; - const destination = values.settings.destination; + const destination = values.destination; + const tokenIn = formData.token; + const tokenOut = values.tokenOut; + const amountOut = + tokenOut && tokenOut.equals(tokenIn) + ? formData.amount + : formData.amountOut; + + const { plantAction } = plantAndDoX; const addPlant = + plantAndDoX && primaryActions?.includes(FormTxn.PLANT) && - sdk.tokens.BEAN.equals(token); + sdk.tokens.BEAN.equals(tokenIn); - const baseAmount = token.amount((formData?.amount || 0).toString()); + const baseAmount = tokenIn.amount((formData?.amount || 0).toString()); - const totalAmount = addPlant - ? baseAmount.add(plantAndDoX.getAmount()) - : baseAmount; + const totalAmount = + addPlant && plantAction + ? baseAmount.add(plantAction.getAmount()) + : baseAmount; if (totalAmount.lte(0)) throw new Error('Invalid amount.'); if (!destination) { throw new Error("Missing 'Destination' setting."); } + if (tokenIn.isLP && !tokenOut) { + throw new Error('Missing Output Token'); + } const withdrawTxn = new WithdrawFarmStep(sdk, token, [ ...farmerBalances.deposits, ]); + console.log('baseAmount: ', baseAmount); + withdrawTxn.build( baseAmount, season.toNumber(), - addPlant ? plantAndDoX : undefined + destination, + tokenOut, + addPlant ? plantAction : undefined ); if (!withdrawTxn.withdrawResult) { @@ -328,19 +486,21 @@ const WithdrawPropProvider: FC<{ token.displayDecimals ); + const messageAmount = + values.tokenOut && amountOut + ? displayTokenAmount(amountOut, values.tokenOut) + : displayTokenAmount(totalAmount, token); + txToast = new TransactionToast({ - loading: `Withdrawing ${withdrawAmtStr} ${token.name} to your ${ - destination === FarmToMode.EXTERNAL ? 'wallet' : 'Farm balance' - }...`, - success: `Withdraw successful.`, + loading: `Withdrawing ${withdrawAmtStr} ${token.name} from the Silo...`, + success: `Withdraw successful. Added ${messageAmount} to your ${copy.MODES[destination]}`, }); const actionsPerformed = txnBundler.setFarmSteps(values.farmActions); const { execute } = await txnBundler.bundle( withdrawTxn, - // we can pass in 0 here b/c WithdrawFarmStep already receives it's input amount in build(); - token.amount(0), - 0.1 + totalAmount, + values.settings.slippage ); const txn = await execute(); @@ -348,10 +508,11 @@ const WithdrawPropProvider: FC<{ txToast.confirming(txn); const receipt = await txn.wait(); - await refetch(actionsPerformed, { farmerSilo: true }, [ - refetchSilo, - fetchFarmerBalances, - ]); + await refetch( + actionsPerformed, + { farmerSilo: true, farmerBalances: true }, + [refetchSilo, fetchFarmerBalances] + ); txToast.success(receipt); formActions.resetForm(); @@ -366,14 +527,14 @@ const WithdrawPropProvider: FC<{ } }, [ - middleware, - account, - farmerBalances?.deposits, sdk, token, - plantAndDoX, season, + account, + middleware, txnBundler, + plantAndDoX, + farmerBalances?.deposits, refetch, refetchSilo, fetchFarmerBalances, @@ -383,15 +544,26 @@ const WithdrawPropProvider: FC<{ return ( {(formikProps) => ( - + <> + {token.isLP && ( + + + + )} + + )} ); diff --git a/projects/ui/src/hooks/beanstalk/useStemTipForToken.ts b/projects/ui/src/hooks/beanstalk/useStemTipForToken.ts new file mode 100644 index 0000000000..65e1c94d97 --- /dev/null +++ b/projects/ui/src/hooks/beanstalk/useStemTipForToken.ts @@ -0,0 +1,14 @@ +import { Token } from '@beanstalk/sdk'; +import { ethers } from 'ethers'; +import { useMemo } from 'react'; +import useSilo from '~/hooks/beanstalk/useSilo'; + +export default function useStemTipForToken( + token: Token +): ethers.BigNumber | null { + const silo = useSilo(); + return useMemo( + () => silo.balances[token.address]?.stemTip ?? null, + [silo, token.address] + ); +} diff --git a/projects/ui/src/hooks/display/useAsyncMemo.ts b/projects/ui/src/hooks/display/useAsyncMemo.ts index 3df9092fc7..9e6682a2b1 100644 --- a/projects/ui/src/hooks/display/useAsyncMemo.ts +++ b/projects/ui/src/hooks/display/useAsyncMemo.ts @@ -5,11 +5,28 @@ export default function useAsyncMemo( deps: React.DependencyList ) { const [state, setState] = useState(undefined); + const memoizedState = useMemo(() => state, [state]); useEffect(() => { - asyncCallback() - .then(setState) - .catch(() => setState(undefined)); + let isMounted = true; + + const run = async () => { + try { + const result = await asyncCallback(); + if (isMounted) { + setState(result); + } + } catch (e) { + console.error(e); + setState(undefined); + } + }; + + run(); + + return () => { + isMounted = false; + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); @@ -22,5 +39,8 @@ export default function useAsyncMemo( } }, [asyncCallback]); - return useMemo(() => [state, refetch] as const, [refetch, state]); + return useMemo( + () => [memoizedState, refetch] as const, + [refetch, memoizedState] + ); } diff --git a/projects/ui/src/hooks/farmer/form-txn/usePlantAndDoX.ts b/projects/ui/src/hooks/farmer/form-txn/usePlantAndDoX.ts new file mode 100644 index 0000000000..100c5ac508 --- /dev/null +++ b/projects/ui/src/hooks/farmer/form-txn/usePlantAndDoX.ts @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; + +import BigNumberJS from 'bignumber.js'; +import { PlantAndDoX } from '~/lib/Txn'; + +import useSdk from '~/hooks/sdk'; +import useStemTipForToken from '~/hooks/beanstalk/useStemTipForToken'; +import useFarmerSilo from '~/hooks/farmer/useFarmerSilo'; + +export default function usePlantAndDoX(): { + plantAction: PlantAndDoX | undefined; + earnedBeans: BigNumberJS; + crate: { + tv: ReturnType | undefined; + bn: ReturnType | undefined; + }; +} { + const sdk = useSdk(); + const farmerSilo = useFarmerSilo(); + const beanStem = useStemTipForToken(sdk.tokens.BEAN); + + const earnedBeans = farmerSilo.beans.earned; + + return useMemo(() => { + if (earnedBeans.lte(0) || !beanStem || beanStem?.lte(0)) { + return { + plantAction: undefined, + earnedBeans, + crate: { + tv: undefined, + bn: undefined, + }, + }; + } + + const _earnedBeans = sdk.tokens.BEAN.amount(earnedBeans.toString()); + const plantAndDoX = new PlantAndDoX(sdk, _earnedBeans, beanStem); + + return { + plantAction: plantAndDoX, + earnedBeans, + crate: { + tv: plantAndDoX.makePlantCrate(), + bn: plantAndDoX.makePlantCrateBN(), + }, + }; + }, [beanStem, earnedBeans, sdk]); +} diff --git a/projects/ui/src/hooks/farmer/useFarmerBalancesWithFiatValue.ts b/projects/ui/src/hooks/farmer/useFarmerBalancesWithFiatValue.ts index a605dd9cf0..0818191f88 100644 --- a/projects/ui/src/hooks/farmer/useFarmerBalancesWithFiatValue.ts +++ b/projects/ui/src/hooks/farmer/useFarmerBalancesWithFiatValue.ts @@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'; import { useCallback, useMemo } from 'react'; import { ERC20Token, NativeToken } from '~/classes/Token'; import { TokenMap, ZERO_BN } from '~/constants'; -import { CRV3_UNDERLYING, ETH } from '~/constants/tokens'; +import { ERC20_TOKENS, ETH } from '~/constants/tokens'; import useDataFeedTokenPrices from '../beanstalk/useDataFeedTokenPrices'; import useWhitelist from '../beanstalk/useWhitelist'; import useTokenMap from '../chain/useTokenMap'; @@ -45,7 +45,7 @@ export default function useFarmerBalancesWithFiatValue(includeZero?: boolean) { // data const tokenMap = useTokenMap([ - ...CRV3_UNDERLYING, + ...ERC20_TOKENS, ETH, ]); const tokenList = useMemo(() => Object.values(tokenMap), [tokenMap]); diff --git a/projects/ui/src/hooks/farmer/useFarmerDepositCrateFromPlant.ts b/projects/ui/src/hooks/farmer/useFarmerDepositCrateFromPlant.ts deleted file mode 100644 index ec1bdbc372..0000000000 --- a/projects/ui/src/hooks/farmer/useFarmerDepositCrateFromPlant.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useMemo } from 'react'; -import { ethers } from 'ethers'; -import useSdk from '~/hooks/sdk'; -import useSeason from '../beanstalk/useSeason'; -import useFarmerSilo from './useFarmerSilo'; -import { LegacyDepositCrate } from '~/state/farmer/silo'; -import { tokenValueToBN } from '~/util'; - -/// Returns the deposit crate which will be created via calling 'plant' -export default function useFarmerDepositCrateFromPlant() { - /// - const sdk = useSdk(); - - /// Beanstalk - const season = useSeason(); - - /// Farmer - const farmerSilo = useFarmerSilo(); - - const crate = useMemo(() => { - const { STALK, BEAN } = sdk.tokens; - const earned = farmerSilo.beans.earned; - const earnedTV = BEAN.amount(earned.toString()); - - const stalk = BEAN.getStalk(earnedTV); - const seeds = BEAN.getSeeds(earnedTV); - // no stalk is grown yet as it is a new deposit from the current season - const grownStalk = STALK.amount(0); - - // asBN => as DepositCrate from UI; - const asBN: LegacyDepositCrate = { - season, - amount: earned, - bdv: earned, - stalk: tokenValueToBN(stalk), - seeds: tokenValueToBN(seeds), - }; - - // asTV => as DepositCrate from SDK; - const asTV = { - season: ethers.BigNumber.from(season.toString()), - amount: earnedTV, - bdv: earnedTV, - stalk, - baseStalk: stalk, - grownStalk, - seeds, - }; - - return { - asBN, - asTV, - }; - }, [farmerSilo.beans.earned, sdk.tokens, season]); - - return { - crate, - }; -} diff --git a/projects/ui/src/hooks/farmer/useFarmerSiloBalancesAsync.ts b/projects/ui/src/hooks/farmer/useFarmerSiloBalancesAsync.ts index 8f45db6e63..4aff4adce3 100644 --- a/projects/ui/src/hooks/farmer/useFarmerSiloBalancesAsync.ts +++ b/projects/ui/src/hooks/farmer/useFarmerSiloBalancesAsync.ts @@ -11,8 +11,11 @@ export default function useFarmerSiloBalancesAsync(token: Token | undefined) { const account = useAccount(); const [farmerBalances, refetchFarmerBalances] = useAsyncMemo(async () => { - if (!account || !token) return undefined; - console.debug(`[Transfer] Fetching silo balances for SILO:${token.symbol}`); + if (!account || !token || !sdk.tokens.siloWhitelist.has(token)) + return undefined; + console.debug( + `[useFarmerSiloBalancesAsync] Fetching silo balances for SILO:${token.symbol}` + ); return sdk.silo.getBalance( token, account, diff --git a/projects/ui/src/lib/Txn/FarmSteps/silo/WithdrawFarmStep.ts b/projects/ui/src/lib/Txn/FarmSteps/silo/WithdrawFarmStep.ts index 3d06d7fe3e..2d2a0edc1f 100644 --- a/projects/ui/src/lib/Txn/FarmSteps/silo/WithdrawFarmStep.ts +++ b/projects/ui/src/lib/Txn/FarmSteps/silo/WithdrawFarmStep.ts @@ -1,4 +1,12 @@ -import { BeanstalkSDK, Deposit, Token, TokenValue } from '@beanstalk/sdk'; +import { + BeanstalkSDK, + Deposit, + FarmToMode, + ERC20Token, + Token, + TokenValue, + FarmFromMode, +} from '@beanstalk/sdk'; import { FarmStep, PlantAndDoX } from '~/lib/Txn/Interface'; type WithdrawResult = ReturnType; @@ -8,7 +16,7 @@ export class WithdrawFarmStep extends FarmStep { constructor( _sdk: BeanstalkSDK, - private _token: Token, + private _token: ERC20Token, private _crates: Deposit[] ) { super(_sdk); @@ -24,6 +32,8 @@ export class WithdrawFarmStep extends FarmStep { // amountIn excluding plant amount _amountIn: TokenValue, season: number, + toMode: FarmToMode, + tokenOut?: ERC20Token, plant?: PlantAndDoX ) { this.clear(); @@ -43,6 +53,20 @@ export class WithdrawFarmStep extends FarmStep { if (!result || !result.crates.length) { throw new Error('Nothing to Withdraw.'); } + if (!tokenOut && this._token.isLP) { + throw new Error('Must specify Output Token'); + } + + const removeLiquidity = + this._token.isLP && tokenOut && !this._token.equals(tokenOut); + + const pool = removeLiquidity + ? this._sdk.pools.getPoolByLPToken(this._token) + : undefined; + + const withdrawToMode = removeLiquidity ? FarmToMode.INTERNAL : toMode; + + console.log('removeLIquidity: ', removeLiquidity); // FIXME const stems = result.crates.map((crate) => crate.stem.toString()); @@ -55,7 +79,8 @@ export class WithdrawFarmStep extends FarmStep { input: new this._sdk.farm.actions.WithdrawDeposit( this._token.address, stems[0], - amounts[0] + amounts[0], + withdrawToMode ), }); } else { @@ -63,11 +88,23 @@ export class WithdrawFarmStep extends FarmStep { input: new this._sdk.farm.actions.WithdrawDeposits( this._token.address, stems, - amounts + amounts, + withdrawToMode ), }); } + if (removeLiquidity && tokenOut && pool) { + const removeStep = new this._sdk.farm.actions.RemoveLiquidityOneToken( + pool.address, + this._sdk.contracts.curve.registries.metaFactory.address, + tokenOut.address, + FarmFromMode.INTERNAL_TOLERANT, + toMode + ); + this.pushInput({ input: removeStep }); + console.debug('[WithdrawFarmStep][build] removing liquidity', removeStep); + } console.debug('[WithdrawFarmStep][build]: ', this.getFarmInput()); return this; diff --git a/projects/ui/src/lib/Txn/Interface/PlantAndDoX.ts b/projects/ui/src/lib/Txn/Interface/PlantAndDoX.ts index c87d288f38..ef42064372 100644 --- a/projects/ui/src/lib/Txn/Interface/PlantAndDoX.ts +++ b/projects/ui/src/lib/Txn/Interface/PlantAndDoX.ts @@ -8,15 +8,17 @@ import { ethers } from 'ethers'; import BigNumber from 'bignumber.js'; import { LegacyDepositCrate } from '~/state/farmer/silo'; import { tokenValueToBN } from '~/util'; +import { ZERO_BN } from '~/constants'; export default class PlantAndDoX { constructor( private _sdk: BeanstalkSDK, private _earnedBeans: TokenValue, - private _season: number + private _stem: ethers.BigNumber ) { + this._sdk = _sdk; this._earnedBeans = _earnedBeans; - this._season = _season; + this._stem = _stem; } /// Returns whether 'plant' can be called @@ -33,7 +35,7 @@ export default class PlantAndDoX { return PlantAndDoX.makeCrate.tokenValue( this._sdk, this._earnedBeans, - this._season + this._stem ); } @@ -42,7 +44,7 @@ export default class PlantAndDoX { return PlantAndDoX.makeCrate.bigNumber( this._sdk, tokenValueToBN(this._earnedBeans), - new BigNumber(this._season) + this._stem ); } @@ -51,12 +53,8 @@ export default class PlantAndDoX { tokenValue( sdk: BeanstalkSDK, earnedBeans: TokenValue, - _season: number | BigNumber + stem: ethers.BigNumber ) { - const season = BigNumber.isBigNumber(_season) - ? _season.toNumber() - : _season; - const { STALK, BEAN } = sdk.tokens; const stalk = BEAN.getStalk(earnedBeans); @@ -66,7 +64,8 @@ export default class PlantAndDoX { // asTV => as DepositCrate from SDK; const crate: TokenSiloBalance['deposits'][number] = { - season: ethers.BigNumber.from(season), + stem: stem, + // season: ethers.BigNumber.from(season), amount: earnedBeans, bdv: earnedBeans, stalk: { @@ -74,8 +73,6 @@ export default class PlantAndDoX { base: stalk, grown: grownStalk, }, - baseStalk: stalk, - grownStalk, seeds, }; @@ -86,7 +83,7 @@ export default class PlantAndDoX { bigNumber( sdk: BeanstalkSDK, earnedBeans: BigNumber, - season: BigNumber + stem: ethers.BigNumber ): LegacyDepositCrate { const { BEAN } = sdk.tokens; const earnedTV = BEAN.amount(earnedBeans.toString()); @@ -95,10 +92,14 @@ export default class PlantAndDoX { const seeds = BEAN.getSeeds(earnedTV); const crate: LegacyDepositCrate = { - season, amount: earnedBeans, + stem: stem, bdv: earnedBeans, - stalk: tokenValueToBN(stalk), + stalk: { + total: tokenValueToBN(stalk), + base: tokenValueToBN(stalk), + grown: ZERO_BN, + }, seeds: tokenValueToBN(seeds), };