From 33c34542b48afc90f276eff88da3ec0ed168bd77 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 13:45:15 -0600 Subject: [PATCH] fix pipe convert in UI --- projects/sdk/src/lib/silo/PipelineConvert.ts | 10 +- .../Actions/Convert/PipelineConvertForm.tsx | 341 ++++++++---------- .../components/Silo/Actions/Convert/index.tsx | 18 +- 3 files changed, 163 insertions(+), 206 deletions(-) diff --git a/projects/sdk/src/lib/silo/PipelineConvert.ts b/projects/sdk/src/lib/silo/PipelineConvert.ts index ace816b15..cba922a1e 100644 --- a/projects/sdk/src/lib/silo/PipelineConvert.ts +++ b/projects/sdk/src/lib/silo/PipelineConvert.ts @@ -33,16 +33,14 @@ export class PipelineConvert { stems: BigNumber[], amounts: BigNumber[], tokenOut: ERC20Token, - advPipeCalls: AdvancedPipeCallStruct[], - overrides?: ethers.PayableOverrides + advPipeCalls: AdvancedPipeCallStruct[] ) { return PipelineConvert.sdk.contracts.beanstalk.pipelineConvert( tokenIn.address, stems, amounts, tokenOut.address, - advPipeCalls, - overrides + advPipeCalls ); } @@ -183,9 +181,7 @@ export class PipelineConvert { const pipe: AdvancedPipeCallStruct[] = []; // 0: approve from.well.lpToken to use from.well.lpToken - pipe.push( - PipelineConvert.snippets.erc20Approve(source.well.lpToken, source.well.lpToken.address) - ); + pipe.push(PipelineConvert.snippets.erc20Approve(source.well.lpToken, source.well.address)); // 1: remove liquidity from from.well pipe.push( diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index fbc3e0b81..60ff40e0d 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -1,11 +1,10 @@ -import React from 'react'; - +import React, { useEffect, useCallback, useMemo, useState } from 'react'; +import { ethers } from 'ethers'; import { Form } from 'formik'; import BigNumber from 'bignumber.js'; import { useQuery } from '@tanstack/react-query'; import { Box, - Button, CircularProgress, Stack, Tooltip, @@ -23,6 +22,7 @@ import TokenSelectDialog, { TokenSelectMode, } from '~/components/Common/Form/TokenSelectDialogNew'; import { + SmartSubmitButton, TokenAdornment, TokenInputField, TxnPreview, @@ -34,8 +34,8 @@ import WarningAlert from '~/components/Common/Alert/WarningAlert'; import TxnAccordion from '~/components/Common/TxnAccordion'; import StatHorizontal from '~/components/Common/StatHorizontal'; -import { ActionType, displayFullBN } from '~/util'; -import { PipelineConvertUtil } from '~/lib/PipelineConvert/PipelineConvert'; +import { ActionType, displayFullBN, transform } from '~/util'; +import { useAppSelector } from '~/state'; import { BaseConvertFormProps } from './types'; interface Props extends BaseConvertFormProps { @@ -47,6 +47,13 @@ interface PipelineConvertFormProps extends Props { targetWell: BasinWell; } +interface PipeConvertResult { + toAmount: ethers.BigNumber; + fromBdv: ethers.BigNumber; + toBdv: ethers.BigNumber; + toStem: ethers.BigNumber; +} + const baseQueryOptions = { staleTime: 20_000, // 20 seconds stale time refetchOnWindowFocus: false, @@ -66,17 +73,32 @@ const PipelineConvertFormInner = ({ isSubmitting, setFieldValue, }: PipelineConvertFormProps) => { + const beanstalkSiloBalances = useAppSelector( + (s) => s._beanstalk.silo.balances + ); + const [tokenSelectOpen, showTokenSelect, hideTokenSelect] = useToggle(); + const [convertResults, setConvertResults] = useState< + PipeConvertResult | undefined + >(undefined); + const getBDV = useBDV(); const sourceToken = sourceWell.lpToken; // LP token of source well const targetToken = targetWell.lpToken; // LP token of target well - const BEAN = sdk.tokens.BEAN; + const slippage = values.settings.slippage; + + const sourceTokenStemTip = + beanstalkSiloBalances[sourceToken.address]?.stemTip; + const targetTokenStemTip = + beanstalkSiloBalances[targetToken.address]?.stemTip; const debouncedAmountIn = useDebounce(values.tokens[0].amount ?? ZERO_BN); // - const maxConvertableBN = new BigNumber( - (balance?.convertibleAmount || TokenValue.ZERO).toHuman() + const maxConvertableBN = useMemo( + () => + new BigNumber((balance?.convertibleAmount || TokenValue.ZERO).toHuman()), + [balance?.convertibleAmount] ); const pickedDeposits = sdk.silo.siloConvert.calculateConvert( @@ -87,150 +109,105 @@ const PipelineConvertFormInner = ({ 0 ); - React.useEffect(() => { - console.debug('[pipelineConvert] pickedDeposits:', pickedDeposits); - }, [pickedDeposits]); - - const sourceIdx = getWellTokenIndexes(sourceWell, BEAN); // token indexes of source well - const targetIdx = getWellTokenIndexes(targetWell, BEAN); // token indexes of target well - - const sellToken = sourceWell.tokens[sourceIdx.nonBeanIndex]; // token we will sell when after removing liquidity in equal parts - const buyToken = targetWell.tokens[targetIdx.nonBeanIndex]; // token we will buy to add liquidity - - const slippage = values.settings.slippage; - - // const amountOut = values.tokens[0].amountOut; // amount of to token - // const maxAmountIn = values.maxAmountIn; - // const canConvert = maxAmountIn?.gt(0) || false; - // const plantCrate = plantAndDoX?.crate?.bn; + // same as query.isFetching & query.isLoading + const isQuoting = values.tokens?.[0]?.quoting; // prettier-ignore - const { data, ...restQuery } = useQuery({ - queryKey: ['pipelineConvert', sourceWell.address, targetWell.address, debouncedAmountIn.toString()], + const { data, ...query } = useQuery({ + queryKey: [ + 'pipelineConvert', + sourceWell.address, + targetWell.address, + debouncedAmountIn.toString(), + pickedDeposits.crates, + slippage, + ], queryFn: async () => { - setFieldValue('tokens.0.quoting', true); + console.log({ + sourceToken, + targetToken, + debouncedAmountIn, + maxConvertableBN, + pickedDeposits, + }); + try { - const lpIn = sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()); - const sourceLPAmountOut = await sourceWell.getRemoveLiquidityOutEqual( - lpIn + setFieldValue('tokens.0.quoting', true); + console.log("debouncedAmountIn: ", debouncedAmountIn.toString()); + const { + amountOut, + advPipeCalls + } = await sdk.silo.pipelineConvert.removeEqual2AddEqualQuote( + sourceWell, + targetWell, + sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()), + slippage / 100 // 0x uses a different slippage format ); - console.debug(`[pipelineConvert/removeLiquidity (1)] result:`, { - BEAN: sourceLPAmountOut[sourceIdx.beanIndex].toNumber(), - [`${sellToken.symbol}`]: sourceLPAmountOut[sourceIdx.nonBeanIndex].toNumber(), - }); - - const beanAmountOut = sourceLPAmountOut[sourceIdx.beanIndex]; - const swapAmountIn = sourceLPAmountOut[sourceIdx.nonBeanIndex]; - - console.debug( - '[pipelineConvert/0xQuote] slippage:,', - slippage / 100 - ) - - const quote = await sdk.zeroX.fetchSwapQuote({ - sellToken: sellToken.address, - buyToken: buyToken.address, - sellAmount: swapAmountIn.blockchainString, - takerAddress: sdk.contracts.pipeline.address, - shouldSellEntireBalance: true, - // 0x requests are formatted such that 0.01 = 1%. Everywhere else in the UI we use 0.01 = 0.01% ?? BS3TODO: VALIDATE ME - slippagePercentage: (slippage / 10).toString(), - - // 0.05% => 0.0005 - }); - - console.debug(`[pipelineConvert/0xQuote (2)] result:`, { quote }); - - const swapAmountOut = buyToken.fromBlockchain(quote?.buyAmount || '0'); - const targetLPAmountOut = await targetWell.getAddLiquidityOut([ - beanAmountOut, - swapAmountOut, - ]); - console.debug(`[pipelineConvert/addLiquidity (3)] result:`, { - amount: targetLPAmountOut.toNumber(), - }); - - setFieldValue('amountOut', new BigNumber(targetLPAmountOut.toHuman())); + setFieldValue('pipe.structs', advPipeCalls); + return { - amountIn: lpIn, - beanAmountOut, - swapAmountIn, - swapAmountOut, - quote, - targetLPAmountOut, + amountOut, + advPipeCalls }; } catch (e) { console.debug('[pipelineConvert/query] FAILED: ', e); - throw e; - } finally { setFieldValue('tokens.0.quoting', false); - } - }, - enabled: maxConvertableBN.gt(0) && debouncedAmountIn?.gt(0), - ...baseQueryOptions, - }); - - const { data: staticCallData } = useQuery({ - queryKey: [ - 'pipelineConvert/callStatic', - sourceWell.address, - targetWell.address, - data?.targetLPAmountOut?.toHuman(), - ], - queryFn: async () => { - if (!data) return; - try { - const advPipeCalls = PipelineConvertUtil.buildEqual2Equal({ - sdk, - source: { - well: sourceWell, - lpAmountIn: data.amountIn, - beanAmountOut: data.beanAmountOut, - nonBeanAmountOut: data.swapAmountIn, - }, - swap: { - buyToken, - sellToken, - buyAmount: data.swapAmountOut, - quote: data.quote, - }, - target: { - well: targetWell, - amountOut: data.targetLPAmountOut, - }, - slippage, - }); - - const datas = await sdk.contracts.beanstalk.callStatic - .pipelineConvert( - sourceToken.address, - pickedDeposits.crates.map((c) => c.stem), - pickedDeposits.crates.map((c) => c.amount.toBigNumber()), - targetToken.address, - advPipeCalls, - { gasLimit: 1_000_000 } - ) - .then((result) => ({ - toStem: result.toStem, - fromAmount: result.fromAmount, - toAmount: result.toAmount, - fromBdv: result.fromBdv, - toBdv: result.toBdv, - })); - - console.debug(`[pipelineConvert/callStatic] result:`, datas); - return datas; - } catch (e) { - console.debug('[pipelineConvert/callStatic] FAILED: ', e); throw e; } }, - retry: false, - enabled: !!data && debouncedAmountIn?.gt(0), + enabled: maxConvertableBN.gt(0) && debouncedAmountIn?.gt(0) && pickedDeposits.crates.length > 0, ...baseQueryOptions, }); + const handleQuoteResult = useCallback(async () => { + if ( + !data || + data.amountOut.lte(0) || + !data.advPipeCalls.length || + query.isLoading || + query.isFetching + ) { + return; + } + try { + const result = await sdk.contracts.beanstalk.callStatic.pipelineConvert( + sourceToken.address, + pickedDeposits.crates.map((c) => c.stem), + pickedDeposits.crates.map((c) => c.amount.toBigNumber()), + targetToken.address, + data.advPipeCalls + ); + const toAmount = transform(result.toAmount, 'bnjs', targetToken); + setFieldValue('pipe.amountOut', toAmount); + + setConvertResults({ + toAmount: result.toAmount, + fromBdv: result.fromBdv, + toBdv: result.toBdv, + toStem: result.toStem, + }); + } catch (e) { + console.error('[pipelineConvert/handleQuoteResult] FAILED: ', e); + throw e; + } finally { + setFieldValue('tokens.0.quoting', false); + } + }, [ + sdk.contracts.beanstalk, + query.isFetching, + query.isLoading, + data, + pickedDeposits.crates, + targetToken, + sourceToken, + setFieldValue, + ]); + + useEffect(() => { + handleQuoteResult(); + }, [handleQuoteResult]); + /// When a new output token is selected, reset maxAmountIn. const handleSelectTokenOut = async (_selectedTokens: Set) => { const selected = [..._selectedTokens]?.[0]; @@ -246,8 +223,30 @@ const PipelineConvertFormInner = ({ } }; - // same as query.isFetching & query.isLoading - const isQuoting = values.tokens?.[0]?.quoting; + const deltaStemTip = + convertResults && + targetTokenStemTip && + convertResults.toStem.sub(targetTokenStemTip); + + const grownStalk = + convertResults && + deltaStemTip && + sdk.tokens.STALK.fromBlockchain( + deltaStemTip.mul(convertResults.toBdv.toString()) + ); + + const baseStalk = + convertResults && + sdk.tokens.STALK.fromHuman(convertResults.toBdv.toString()); + + const totalStalk = baseStalk && grownStalk && baseStalk.add(grownStalk); + + const getButtonContents = () => { + if (maxConvertableBN.eq(0)) { + return 'Nothing to Convert'; + } + return 'Convert'; + }; return ( <> @@ -332,25 +331,24 @@ const PipelineConvertFormInner = ({ ) : null} - {data && ( <> values from pipe convert: - Amount Out: {data?.targetLPAmountOut.toHuman()} + Amount Out: {data?.amountOut.toString()} )} - {staticCallData - ? Object.entries(staticCallData).map(([k, v]) => ( - - {k}: {v.toString()} - - )) - : 'Failed to load results from static call'} + {convertResults && ( + <> + base Stalk: {baseStalk?.toHuman()} + grown Stalk: {grownStalk?.toHuman()} + Total Stalk: {totalStalk?.toHuman()} + + )} {/* You may Lose Grown Stalk warning here */} @@ -358,7 +356,7 @@ const PipelineConvertFormInner = ({ You may lose Grown Stalk through this transaction. - {debouncedAmountIn?.gt(0) && data?.targetLPAmountOut?.gt(0) && ( + {debouncedAmountIn?.gt(0) && data?.amountOut?.gt(0) && ( )} + + {getButtonContents()} + @@ -409,38 +419,3 @@ export const PipelineConvertForm = ({ values, sdk, ...restProps }: Props) => { /> ); }; - -// ------------------------------------------ -// Utils -// ------------------------------------------ - -function getWellTokenIndexes(well: BasinWell | undefined, bean: Token) { - const beanIndex = well?.tokens?.[0].equals(bean) ? 0 : 1; - const nonBeanIndex = beanIndex === 0 ? 1 : 0; - - return { - beanIndex, - nonBeanIndex, - } as const; -} - -// const swapAmountIn = removeOutQuery.data?.[sourceWellNonBeanIndex]; - -// const swapOutQuery = useQuery({ -// queryKey: queryKeys.swapOut(swapTokenIn, swapTokenOut, swapAmountIn), -// queryFn: ({ signal }) => { -// if (!swapTokenIn || !swapTokenOut || !swapAmountIn) return TokenValue.ZERO; -// const controller = new AbortController(); -// signal.addEventListener('abort', () => controller.abort()); - -// const params = sdk.zeroX.fetchQuote({ -// slippagePercentage: values.settings.slippage.toString(), -// buyToken: swapTokenIn.address, -// sellToken: swapTokenOut.address, -// sellAmount: swapAmountIn.blockchainString, -// mode: "" -// }) -// }, -// enabled: !!swapTokenIn && !!swapTokenOut && swapAmountIn?.gt(0), -// initialData: TokenValue.ZERO, -// }); diff --git a/projects/ui/src/components/Silo/Actions/Convert/index.tsx b/projects/ui/src/components/Silo/Actions/Convert/index.tsx index 6c2f81f0e..c4f84589e 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/index.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/index.tsx @@ -28,7 +28,6 @@ import { FormTxn, ConvertFarmStep } from '~/lib/Txn'; import { useWhitelistedTokens } from '~/hooks/beanstalk/useTokens'; import { useFetchFarmerSilo } from '~/state/farmer/silo/updater'; import { DefaultConvertForm } from './DefaultConvertForm'; -import { PipelineConvertFormV2 } from './PipelineconvertFormV2'; // import { PipelineConvertForm } from './PipelineConvertForm'; import { BaseConvertFormProps, @@ -37,6 +36,7 @@ import { ConvertProps, ConvertQuoteHandlerParams, } from './types'; +import { PipelineConvertForm } from './PipelineConvertForm'; interface Props extends BaseConvertFormProps { farmerBalances: TokenSiloBalance | undefined; @@ -100,20 +100,6 @@ const DefaultConvertFormWrapper = (props: Props) => { ); }; -const PipelineConvertFormWrapper = (props: Props) => { - const [conversion, setConversion] = useState({ - // pull this to the parent? - actions: [], - amount: TokenValue.ZERO, - bdv: TokenValue.ZERO, - crates: [], - seeds: TokenValue.ZERO, - stalk: TokenValue.ZERO, - }); - - return ; -}; - // ---------- Convert Form Router ---------- /** * Depending on whether the conversion requires a pipeline convert, @@ -125,7 +111,7 @@ const ConvertFormRouter = (props: Props) => { if (!tokenOut) return null; if (isPipelineConvert(props.fromToken, tokenOut)) { - return ; + return ; } return ;