From 5586fa089988bc280616c36b34f9abff81bdb3b0 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 22:32:32 -0600 Subject: [PATCH 01/16] feat: update pipe convert ui --- .../src/lib/farm/actions/PipelineConvert.ts | 11 +- .../Actions/Convert/PipelineConvertForm.tsx | 60 ++++---- .../components/Silo/Actions/Convert/index.tsx | 139 +++++++++++++++++- .../components/Silo/Actions/Convert/types.ts | 12 +- 4 files changed, 187 insertions(+), 35 deletions(-) diff --git a/projects/sdk/src/lib/farm/actions/PipelineConvert.ts b/projects/sdk/src/lib/farm/actions/PipelineConvert.ts index 9d891214a..90223d8d3 100644 --- a/projects/sdk/src/lib/farm/actions/PipelineConvert.ts +++ b/projects/sdk/src/lib/farm/actions/PipelineConvert.ts @@ -14,25 +14,26 @@ export class PipelineConvert extends StepClass { public readonly _stems: ethers.BigNumberish[], public readonly _amounts: ethers.BigNumberish[], private _tokenOut: ERC20Token, - private _amountIn: TokenValue, - private _minAmountOut: TokenValue, + private _amountOut: TokenValue, // before slippage public readonly advancedPipeStructs: AdvancedPipeCallStruct[] ) { super(); } async run(_amountInStep: ethers.BigNumber, context: RunContext) { + const slip = context?.data?.slippage || 0.1; + const minAmountOut = this._amountOut.subSlippage(slip).toBigNumber(); + return { name: this.name, - amountOut: _amountInStep, + amountOut: minAmountOut, prepare: () => { PipelineConvert.sdk.debug(`[${this.name}.encode()]`, { tokenIn: this._tokenIn, amounts: this._amounts, stems: this._stems, tokenOut: this._tokenOut, - amountIn: this._amountIn, - minAmountOut: this._minAmountOut, + amountOut: this._amountOut, advancedPipeStructs: this.advancedPipeStructs }); return { diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index 99be73d6f..a686735c9 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -67,7 +67,6 @@ const PipelineConvertFormInner = ({ }: PipelineConvertFormProps) => { const [tokenSelectOpen, showTokenSelect, hideTokenSelect] = useToggle(); const getBDV = useBDV(); - const sourceToken = sourceWell.lpToken; // LP token of source well const targetToken = targetWell.lpToken; // LP token of target well @@ -140,7 +139,7 @@ const PipelineConvertFormInner = ({ amount: targetLPAmountOut.toNumber(), }); - setFieldValue('tokens.0.amountOut', new BigNumber(targetLPAmountOut.toHuman())); + setFieldValue('amountOut', new BigNumber(targetLPAmountOut.toHuman())); return { amountIn: lpIn, beanAmountOut, @@ -161,7 +160,12 @@ const PipelineConvertFormInner = ({ }); const { data: staticCallData } = useQuery({ - queryKey: ['pipelineConvert/callStatic', sourceWell.address, targetWell.address, data?.targetLPAmountOut?.toString()], + queryKey: [ + 'pipelineConvert/callStatic', + sourceWell.address, + targetWell.address, + data?.targetLPAmountOut?.toString(), + ], queryFn: async () => { if (!data) return; try { @@ -186,19 +190,21 @@ const PipelineConvertFormInner = ({ 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 - ).then((result) => ({ - toStem: result.toStem, - fromAmount: result.fromAmount, - toAmount: result.toAmount, - fromBdv: result.fromBdv, - toBdv: result.toBdv, - })); + 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 + ) + .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; @@ -209,8 +215,8 @@ const PipelineConvertFormInner = ({ }, retry: 2, enabled: !!data && debouncedAmountIn?.gt(0), - ...baseQueryOptions - }) + ...baseQueryOptions, + }); /// When a new output token is selected, reset maxAmountIn. const handleSelectTokenOut = async (_selectedTokens: Set) => { @@ -314,15 +320,15 @@ const PipelineConvertFormInner = ({ ) : null} {staticCallData && ( - - values from pipe convert: - - )} - {staticCallData ? (Object.entries(staticCallData).map(([k, v]) => ( - - {k}: {v.toString()} - - ))) : "Failed to load results from static call"} + values from pipe convert: + )} + {staticCallData + ? Object.entries(staticCallData).map(([k, v]) => ( + + {k}: {v.toString()} + + )) + : 'Failed to load results from static call'} {/* You may Lose Grown Stalk warning here */} diff --git a/projects/ui/src/components/Silo/Actions/Convert/index.tsx b/projects/ui/src/components/Silo/Actions/Convert/index.tsx index 5837e4f35..a63dcc4df 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/index.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/index.tsx @@ -169,6 +169,10 @@ const ConvertFormsWrapper = ({ fromToken }: ConvertProps) => { secondary: undefined, implied: [FormTxn.MOW], }, + pipe: { + structs: [], + amountOut: undefined, + }, }), [fromToken, initialTokenOut] ); @@ -185,6 +189,14 @@ const ConvertFormsWrapper = ({ fromToken }: ConvertProps) => { fromToken, }); + const pipelineConvertSubmitHandler = usePipelineConvertSubmitHandler({ + sdk, + farmerSilo, + middleware, + initialValues, + season, + }); + const submitHandler: ConvertFormSubmitHandler = useCallback( async (values, formActions) => { const tokenOut = values.tokenOut; @@ -193,10 +205,12 @@ const ConvertFormsWrapper = ({ fromToken }: ConvertProps) => { } if (isPipelineConvert(fromToken, tokenOut as ERC20Token)) { - return defaultSubmitHandler(values, formActions); + return pipelineConvertSubmitHandler(values, formActions); } + + return defaultSubmitHandler(values, formActions); }, - [defaultSubmitHandler, fromToken] + [defaultSubmitHandler, pipelineConvertSubmitHandler, fromToken] ); return ( @@ -238,6 +252,127 @@ export default Convert; // Helpers // ----------------------------------------------------------------------- +function usePipelineConvertSubmitHandler({ + sdk, + farmerSilo, + middleware, + initialValues, +}: { + sdk: BeanstalkSDK; + farmerSilo: FarmerSilo; + middleware: ReturnType; + initialValues: ConvertFormValues; + season: BigNumber; +}) { + const account = useAccount(); + const [refetchFarmerSilo] = useFetchFarmerSilo(); + const [refetchPools] = useFetchPools(); + + return useCallback( + async ( + values: ConvertFormValues, + formActions: FormikHelpers + ) => { + const txToast = new TransactionToast({ + loading: 'Converting...', + success: 'Convert successful.', + }); + + try { + middleware.before(); + + // form Data + const tokenIn = values.tokens[0].token; + const _amountIn = values.tokens[0].amount; + const tokenOut = values.tokenOut; + const _amountOut = values?.pipe?.amountOut; + + const advPipeStructs = values.pipe?.structs || []; + const slippage = values?.settings?.slippage; + const farmerBalances = farmerSilo.balancesSdk.get(tokenIn); + const amountIn = tokenIn?.amount(_amountIn?.toString() || '0'); // amount of from token + const amountOut = tokenOut?.amount(_amountOut?.toString() || '0'); // amount of to token + + /// Validation + if (!account) throw new Error('Wallet connection required'); + if (!slippage) throw new Error('No slippage value set.'); + if (!tokenOut) throw new Error('Conversion pathway not set'); + if (!farmerBalances) throw new Error('No balances found'); + if (amountIn.lte(0)) throw new Error('Amount must be greater than 0'); + + if (!advPipeStructs?.length) { + throw new Error('No Pipeline Convert data found'); + } + if (!amountOut || amountOut?.lte(0)) { + throw new Error('Conversion invalid'); + } + + const farm = sdk.farm.createAdvancedFarm(); + + const { crates = [] } = sdk.silo.siloConvert.calculateConvert( + tokenIn, + tokenOut, + amountIn, + farmerBalances.convertibleDeposits, + 0 // this isn't being used in the calculation. We should remove it from the function signature + ); + + if (!crates.length) { + throw new Error('Could not find any crates to convert'); + } + + farm.add( + new sdk.farm.actions.PipelineConvert( + tokenIn as ERC20Token, + crates.map((c) => c.stem), + crates.map((c) => c.amount.toBigNumber()), + tokenOut as ERC20Token, + amountOut, + advPipeStructs + ) + ); + + const gasEstimate = await farm.estimateGas(amountIn, { slippage }); + const adjustedGas = Math.round(gasEstimate.toNumber() * 1.2).toString(); + const txn = await farm.execute( + amountIn, + { slippage }, + { gasLimit: adjustedGas } + ); + txToast.confirming(txn); + + const receipt = await txn.wait(); + + await Promise.all([ + refetchFarmerSilo(), + refetchPools(), // update prices to account for pool conversion + ]); + + txToast.success(receipt); + + formActions.resetForm({ + values: { + ...initialValues, + }, + }); + } catch (err) { + console.error(err); + txToast.error(err); + formActions.setSubmitting(false); + } + }, + [ + sdk, + account, + farmerSilo.balancesSdk, + initialValues, + middleware, + refetchFarmerSilo, + refetchPools, + ] + ); +} + function useDefaultConvertSubmitHandler({ sdk, farmerSilo, diff --git a/projects/ui/src/components/Silo/Actions/Convert/types.ts b/projects/ui/src/components/Silo/Actions/Convert/types.ts index c58779dd5..572a28490 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/types.ts +++ b/projects/ui/src/components/Silo/Actions/Convert/types.ts @@ -1,4 +1,10 @@ -import { BeanstalkSDK, ERC20Token, NativeToken, Token } from '@beanstalk/sdk'; +import { + AdvancedPipeStruct, + BeanstalkSDK, + ERC20Token, + NativeToken, + Token, +} from '@beanstalk/sdk'; import BigNumber from 'bignumber.js'; import { FormikHelpers, FormikProps } from 'formik'; import { FormStateNew, FormTxnsFormState } from '~/components/Common/Form'; @@ -12,6 +18,10 @@ export type ConvertFormValues = FormStateNew & { }; maxAmountIn: BigNumber | undefined; tokenOut: Token | undefined; + pipe: { + amountOut: BigNumber | undefined; + structs: AdvancedPipeStruct[]; + }; } & FormTxnsFormState; export type ConvertFormSubmitHandler = ( From 137348a4232e17439439b6b7ff4fe524c6019c7c Mon Sep 17 00:00:00 2001 From: Brean0 Date: Sat, 21 Sep 2024 16:50:43 -0500 Subject: [PATCH 02/16] update lamda_lambda UI --- .../Silo/Token/DepositConvertTable.tsx | 4 +-- .../Silo/Token/TokenLambdaConvert.tsx | 30 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx b/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx index fb9eec4a8..9cf807f64 100644 --- a/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx +++ b/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx @@ -207,7 +207,7 @@ const DepositConvertTable = ({ : BeanstalkPalette.red, }} > - {isGain ? '+' : '-'} {formatTV(params.row.deltaStalk, 0)} + {isGain ? '+' : '-'} {formatTV(params.row.deltaStalk, 3)} ); }, @@ -233,7 +233,7 @@ const DepositConvertTable = ({ : BeanstalkPalette.red, }} > - {isGain ? '+' : '-'} {formatTV(params.row.deltaStalk, 0)} + {isGain ? '+' : '-'} {formatTV(params.row.deltaSeed, 3)} ); }, diff --git a/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx b/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx index 9efa12bfa..7bfeaa6e0 100644 --- a/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx +++ b/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx @@ -16,37 +16,48 @@ const TokenLambdaConvert = ({ token }: { token: ERC20Token }) => { const { setWithIds, depositsById } = useTokenDepositsContext(); const getBDV = useBDV(); - const updatableDeposits: FarmerTokenConvertRow[] = useMemo(() => { + const { updatableDeposits, totalDeltaStalk, totalDeltaSeed } = useMemo(() => { const oneTokenBDV = token.fromHuman(getBDV(token).toString()); const updateable: FarmerTokenConvertRow[] = []; + let totalDeltaStalk = sdk.tokens.STALK.fromHuman('0'); + let totalDeltaSeed = sdk.tokens.SEEDS.fromHuman('0'); Object.entries(depositsById).forEach(([key, deposit]) => { const currentBDV = oneTokenBDV.mul(deposit.amount); const deltaBDV = currentBDV.sub(deposit.bdv); if (deposit.bdv.gte(currentBDV)) return; + + const deltaStalk = deposit.seeds.mul(deltaBDV); + const deltaSeed = deltaBDV.div(deposit.bdv).mul(deposit.seeds); + console.log('deltaStalk', deltaStalk); + console.log('deltaSeed', deltaSeed); + console.log('deposit.id', deposit.id); updateable.push({ - key, + key: '....' + deposit.id.toHexString().slice(-13), currentBDV: currentBDV, deltaBDV: deltaBDV, - deltaStalk: sdk.tokens.STALK.fromHuman('50'), // FIX ME - deltaSeed: sdk.tokens.SEEDS.fromHuman('100'), // FIX ME + deltaStalk: deltaStalk, + deltaSeed: deltaSeed, ...deposit, }); + + totalDeltaStalk = totalDeltaStalk.add(deltaStalk); + totalDeltaSeed = totalDeltaSeed.add(deltaSeed); + console.log('totalDeltaStalk', totalDeltaStalk); + console.log('totalDeltaSeed', totalDeltaSeed); + console.log('deposit.id', deposit.id); }); updateable.sort((a, b) => (a.deltaBDV.gte(b.deltaBDV) ? -1 : 1)); - return updateable; + return { updatableDeposits: updateable, totalDeltaStalk, totalDeltaSeed }; }, [depositsById, getBDV, token, sdk.tokens]); const handleSelectAll = () => { setWithIds(updatableDeposits.map(({ key }) => key)); }; - const deltaStalk = sdk.tokens.STALK.fromHuman('50'); - const deltaSeed = sdk.tokens.SEEDS.fromHuman('100'); - return ( @@ -74,7 +85,8 @@ const TokenLambdaConvert = ({ token }: { token: ERC20Token }) => { color="primary.main" lineHeight="30px" > - + {formatTV(deltaStalk, 0)} STALK and {formatTV(deltaSeed, 0)} SEED + {formatTV(totalDeltaStalk, 2)} Stalk and{' '} + {formatTV(totalDeltaSeed, 3)} Seeds From d25218e69aba6129b9fa14bd3b861de6ca96866a Mon Sep 17 00:00:00 2001 From: Brean0 Date: Sat, 21 Sep 2024 17:44:43 -0500 Subject: [PATCH 03/16] remove logs --- .../ui/src/components/Silo/Token/TokenLambdaConvert.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx b/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx index 7bfeaa6e0..53fff43c6 100644 --- a/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx +++ b/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx @@ -30,9 +30,6 @@ const TokenLambdaConvert = ({ token }: { token: ERC20Token }) => { const deltaStalk = deposit.seeds.mul(deltaBDV); const deltaSeed = deltaBDV.div(deposit.bdv).mul(deposit.seeds); - console.log('deltaStalk', deltaStalk); - console.log('deltaSeed', deltaSeed); - console.log('deposit.id', deposit.id); updateable.push({ key: '....' + deposit.id.toHexString().slice(-13), currentBDV: currentBDV, @@ -44,9 +41,6 @@ const TokenLambdaConvert = ({ token }: { token: ERC20Token }) => { totalDeltaStalk = totalDeltaStalk.add(deltaStalk); totalDeltaSeed = totalDeltaSeed.add(deltaSeed); - console.log('totalDeltaStalk', totalDeltaStalk); - console.log('totalDeltaSeed', totalDeltaSeed); - console.log('deposit.id', deposit.id); }); updateable.sort((a, b) => (a.deltaBDV.gte(b.deltaBDV) ? -1 : 1)); From 363c4981841c5199da61650723db9d57599f28fa Mon Sep 17 00:00:00 2001 From: Spacebean Date: Sat, 21 Sep 2024 18:41:29 -0600 Subject: [PATCH 04/16] updates to pipeline convert + lambda convert --- projects/sdk/src/lib/silo/Convert.ts | 22 +-- .../Actions/Convert/PipelineConvertForm.tsx | 42 +++-- .../components/Silo/Actions/Convert/index.tsx | 5 + .../components/Silo/Actions/LambdaConvert.tsx | 148 +++++++++++++++++- .../Silo/Token/DepositConvertTable.tsx | 5 +- .../components/Silo/Token/DepositsTable.tsx | 2 +- .../Silo/Token/TokenDepositsContext.tsx | 53 +++++-- .../Silo/Token/TokenDepositsOverview.tsx | 68 ++++---- .../Silo/Token/TokenLambdaConvert.tsx | 23 +-- .../lib/PipelineConvert/PipelineConvert.ts | 51 +++--- projects/ui/src/pages/silo/token.tsx | 21 ++- projects/ui/src/state/bean/pools/updater.ts | 12 +- 12 files changed, 345 insertions(+), 107 deletions(-) diff --git a/projects/sdk/src/lib/silo/Convert.ts b/projects/sdk/src/lib/silo/Convert.ts index 0f8e3b08f..3ea73cdaf 100644 --- a/projects/sdk/src/lib/silo/Convert.ts +++ b/projects/sdk/src/lib/silo/Convert.ts @@ -145,16 +145,17 @@ export class Convert { currentSeason: number ): ConvertDetails { if (deposits.length === 0) throw new Error("No crates to withdraw from"); - const sortedCrates = toToken.isLP - ? /// BEAN -> LP: oldest crates are best. Grown stalk is equivalent - /// on both sides of the convert, but having more seeds in older crates - /// allows you to accrue stalk faster after convert. - /// Note that during this convert, BDV is approx. equal after the convert. - sortCratesByStem(deposits, "asc") - : /// LP -> BEAN: use the crates with the lowest [BDV/Amount] ratio first. - /// Since LP deposits can have varying BDV, the best option for the Farmer - /// is to increase the BDV of their existing lowest-BDV crates. - sortCratesByBDVRatio(deposits, "asc"); + const sortedCrates = + !fromToken.isLP && toToken.isLP + ? /// BEAN -> LP: oldest crates are best. Grown stalk is equivalent + /// on both sides of the convert, but having more seeds in older crates + /// allows you to accrue stalk faster after convert. + /// Note that during this convert, BDV is approx. equal after the convert. + sortCratesByStem(deposits, "asc") + : /// X -> LP: use the crates with the lowest [BDV/Amount] ratio first. + /// Since LP deposits can have varying BDV, the best option for the Farmer + /// is to increase the BDV of their existing lowest-BDV crates. + sortCratesByBDVRatio(deposits, "asc"); const pickedCrates = pickCrates(sortedCrates, fromAmount, fromToken, currentSeason); @@ -192,7 +193,6 @@ export class Convert { throw new Error("SDK: Deprecated conversion pathway"); } - // BS3TODO: is this encoding correct ? if (fromToken.equals(toToken)) { return ConvertEncoder.lambdaLambda(amountIn.toBlockchain(), fromToken.address); } diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index a686735c9..fbc3e0b81 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -5,6 +5,7 @@ import BigNumber from 'bignumber.js'; import { useQuery } from '@tanstack/react-query'; import { Box, + Button, CircularProgress, Stack, Tooltip, @@ -48,7 +49,7 @@ interface PipelineConvertFormProps extends Props { const baseQueryOptions = { staleTime: 20_000, // 20 seconds stale time - refetchOnWindowFocus: true, + refetchOnWindowFocus: false, refetchIntervalInBackground: false, } as const; @@ -86,6 +87,10 @@ 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 @@ -100,7 +105,7 @@ const PipelineConvertFormInner = ({ // const plantCrate = plantAndDoX?.crate?.bn; // prettier-ignore - const { data } = useQuery({ + const { data, ...restQuery } = useQuery({ queryKey: ['pipelineConvert', sourceWell.address, targetWell.address, debouncedAmountIn.toString()], queryFn: async () => { setFieldValue('tokens.0.quoting', true); @@ -118,6 +123,11 @@ const PipelineConvertFormInner = ({ 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, @@ -125,7 +135,9 @@ const PipelineConvertFormInner = ({ 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 * 100).toString(), + slippagePercentage: (slippage / 10).toString(), + + // 0.05% => 0.0005 }); console.debug(`[pipelineConvert/0xQuote (2)] result:`, { quote }); @@ -164,7 +176,7 @@ const PipelineConvertFormInner = ({ 'pipelineConvert/callStatic', sourceWell.address, targetWell.address, - data?.targetLPAmountOut?.toString(), + data?.targetLPAmountOut?.toHuman(), ], queryFn: async () => { if (!data) return; @@ -175,7 +187,7 @@ const PipelineConvertFormInner = ({ well: sourceWell, lpAmountIn: data.amountIn, beanAmountOut: data.beanAmountOut, - nonBeanAmountOut: data.swapAmountOut, + nonBeanAmountOut: data.swapAmountIn, }, swap: { buyToken, @@ -196,7 +208,8 @@ const PipelineConvertFormInner = ({ pickedDeposits.crates.map((c) => c.stem), pickedDeposits.crates.map((c) => c.amount.toBigNumber()), targetToken.address, - advPipeCalls + advPipeCalls, + { gasLimit: 1_000_000 } ) .then((result) => ({ toStem: result.toStem, @@ -213,7 +226,7 @@ const PipelineConvertFormInner = ({ throw e; } }, - retry: 2, + retry: false, enabled: !!data && debouncedAmountIn?.gt(0), ...baseQueryOptions, }); @@ -319,9 +332,18 @@ const PipelineConvertFormInner = ({ ) : null} - {staticCallData && ( - values from pipe convert: - )} + + + {data && ( + <> + values from pipe convert: + + Amount Out: {data?.targetLPAmountOut.toHuman()} + + + )} + + {staticCallData ? Object.entries(staticCallData).map(([k, v]) => ( diff --git a/projects/ui/src/components/Silo/Actions/Convert/index.tsx b/projects/ui/src/components/Silo/Actions/Convert/index.tsx index a63dcc4df..ad7dcbf67 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/index.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/index.tsx @@ -670,12 +670,17 @@ function useConvertTokenList( sdk.silo.siloConvert.getConversionPaths(fromToken).map((t) => t.address) ); + // As of now, we don't include unripe PipelineConverts if (!fromToken.isUnripe) { whitelist.forEach((toToken) => { !toToken.isUnripe && pathSet.add(toToken.address); }); } + // As of now, remove the fromToken from the list. + // They can update their deposits via silo/token/updateDeposits + pathSet.delete(fromToken.address); + const list = Array.from(pathSet).map((address) => whitelistLookup[address]); return [ list, // all available tokens to convert to diff --git a/projects/ui/src/components/Silo/Actions/LambdaConvert.tsx b/projects/ui/src/components/Silo/Actions/LambdaConvert.tsx index ea83b5e5e..0484c9644 100644 --- a/projects/ui/src/components/Silo/Actions/LambdaConvert.tsx +++ b/projects/ui/src/components/Silo/Actions/LambdaConvert.tsx @@ -15,16 +15,26 @@ import Row from '~/components/Common/Row'; import stalkIconGrey from '~/img/beanstalk/stalk-icon-grey.svg'; import seedIconGrey from '~/img/beanstalk/seed-icon-grey.svg'; import { formatTV } from '~/util'; -import { TokenValue } from '@beanstalk/sdk'; +import { BeanstalkSDK, ERC20Token, TokenValue } from '@beanstalk/sdk'; import { BeanstalkPalette } from '~/components/App/muiTheme'; import { LongArrowRight } from '~/components/Common/SystemIcons'; -import { useTokenDepositsContext } from '../Token/TokenDepositsContext'; + +import { ethers } from 'ethers'; +import TransactionToast from '~/components/Common/TxnToast'; +import { useFetchFarmerSilo } from '~/state/farmer/silo/updater'; +import { + TokenDepositsContextType, + useTokenDepositsContext, +} from '../Token/TokenDepositsContext'; const LambdaConvert = () => { const sdk = useSdk(); - const { selected, token } = useTokenDepositsContext(); + const { selected, token, updateableDepositsById, clear } = + useTokenDepositsContext(); const [combine, setCombine] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [fetchFarmerSilo] = useFetchFarmerSilo(); const totalDeltaStalk = sdk.tokens.STALK.fromHuman('50'); @@ -40,6 +50,44 @@ const LambdaConvert = () => { } }, [selected, combine]); + const onSubmit = async () => { + if (!selected.size) { + throw new Error('No deposits selected'); + } + + const txToast = new TransactionToast({ + success: `Successfully updated ${selected.size} ${token.symbol} deposit${selected.size === 1 ? '' : 's'}`, + loading: 'Updating your deposits...', + }); + try { + setSubmitting(true); + const farm = constructLambdaConvert( + sdk, + selected, + updateableDepositsById, + token, + combine + ); + + await farm.estimate(ethers.constants.Zero); + const tx = await farm.execute(ethers.constants.Zero, { slippage: 0.1 }); + + txToast.confirming(tx); + + const receipt = await tx.wait(); + await fetchFarmerSilo(); + + txToast.success(receipt); + + clear(); + } catch (e) { + console.error(e); + txToast.error(e); + } finally { + setSubmitting(false); + } + }; + return ( @@ -180,7 +228,11 @@ const LambdaConvert = () => { )} - @@ -188,3 +240,91 @@ const LambdaConvert = () => { }; export default LambdaConvert; + +function constructCombineDepositsCallStruct( + sdk: BeanstalkSDK, + selected: Set, + updateableDepositsById: TokenDepositsContextType['updateableDepositsById'], + token: ERC20Token +) { + let amountIn = token.fromHuman('0'); + const stems: ethers.BigNumber[] = []; + const amounts: ethers.BigNumber[] = []; + + selected.forEach((id) => { + const deposit = updateableDepositsById[id]; + amountIn = amountIn.add(deposit.amount); + stems.push(deposit.stem); + amounts.push(deposit.amount.toBigNumber()); + }); + + const encoding = sdk.silo.siloConvert.calculateEncoding( + token, + token, + amountIn, + amountIn + ); + + const farmCallStruct = { + target: sdk.contracts.beanstalk.address, + callData: sdk.contracts.beanstalk.interface.encodeFunctionData('convert', [ + encoding, + stems, + amounts, + ]), + }; + + return farmCallStruct; +} + +function constructSingleDepositCallStruct( + sdk: BeanstalkSDK, + deposit: TokenDepositsContextType['updateableDepositsById'][string], + token: ERC20Token +) { + const encoding = sdk.silo.siloConvert.calculateEncoding( + token, + token, + deposit.amount, + deposit.amount + ); + + return { + target: sdk.contracts.beanstalk.address, + callData: sdk.contracts.beanstalk.interface.encodeFunctionData('convert', [ + encoding, + [deposit.stem], + [deposit.amount.toBigNumber()], + ]), + }; +} + +function constructLambdaConvert( + sdk: BeanstalkSDK, + selected: Set, + updateableDepositsById: TokenDepositsContextType['updateableDepositsById'], + token: ERC20Token, + combineDeposits: boolean +) { + const farm = sdk.farm.create(); + + const combining = selected.size > 1 && combineDeposits; + + if (combining) { + const callStruct = constructCombineDepositsCallStruct( + sdk, + selected, + updateableDepositsById, + token + ); + farm.add(() => callStruct); + } else { + selected.forEach((id) => { + const deposit = updateableDepositsById[id]; + const callStruct = constructSingleDepositCallStruct(sdk, deposit, token); + farm.add(() => callStruct); + }); + } + + return farm; +} diff --git a/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx b/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx index 9cf807f64..154129dff 100644 --- a/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx +++ b/projects/ui/src/components/Silo/Token/DepositConvertTable.tsx @@ -20,6 +20,7 @@ import { TokenDepositsSelectType, useTokenDepositsContext, } from './TokenDepositsContext'; +import { trimDepositId } from './TokenDepositsOverview'; export type FarmerTokenConvertRow = Deposit & { key: string; @@ -95,7 +96,7 @@ const DepositConvertTable = ({ const allColumns = useMemo>( () => [ { - field: 'deposits', + field: 'deposit', flex: 1, minWidth: 150, headerName: 'Deposits', @@ -110,7 +111,7 @@ const DepositConvertTable = ({ {token.symbol} Deposit - {params.row.key} + {trimDepositId(params.row.key)} diff --git a/projects/ui/src/components/Silo/Token/DepositsTable.tsx b/projects/ui/src/components/Silo/Token/DepositsTable.tsx index 3aaa4fd22..627e71e79 100644 --- a/projects/ui/src/components/Silo/Token/DepositsTable.tsx +++ b/projects/ui/src/components/Silo/Token/DepositsTable.tsx @@ -85,7 +85,7 @@ const DepositsTable = ({ align: 'left', headerAlign: 'left', sortable: true, - valueGetter: (params) => parseFloat(params.row.key), + valueGetter: (params) => params.row.key, renderCell: (params) => { const isMultiSelect = selectType === 'multi'; const isSelected = selected.has(params.row.key); diff --git a/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx b/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx index d21d518fc..b672a5225 100644 --- a/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx +++ b/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx @@ -12,6 +12,7 @@ import React, { useMemo, useState, } from 'react'; +import useBDV from '~/hooks/beanstalk/useBDV'; import useTabs from '~/hooks/display/useTabs'; import useFarmerSiloBalanceSdk from '~/hooks/farmer/useFarmerSiloBalanceSdk'; import { exists } from '~/util/UI'; @@ -20,12 +21,17 @@ export type TokenDepositsSelectType = 'single' | 'multi'; export type SiloTokenSlug = 'token' | 'transfer' | 'lambda' | 'anti-lambda'; +export interface UpdateableDeposit extends Deposit { + newBDV: T; +} + export type TokenDepositsContextType = { selected: Set; token: ERC20Token; slug: SiloTokenSlug; balances: TokenSiloBalance | undefined; depositsById: Record>; + updateableDepositsById: Record>; setSelected: ( depositId: string, selectType: TokenDepositsSelectType, @@ -58,6 +64,9 @@ export const TokenDepositsProvider = (props: { }) => { const [selected, setSelected] = useState>(new Set()); const [slugIndex, setSlugIndex] = useTabs(SLUGS, 'content', 0); + const getBDV = useBDV(); + + const tokenBDV = getBDV(props.token); const siloBalances = useFarmerSiloBalanceSdk(props.token); @@ -70,13 +79,29 @@ export const TokenDepositsProvider = (props: { return map; }, [siloBalances?.deposits]); - const handleSetSelected = ( - depositId: string, - selectType: TokenDepositsSelectType, - callback?: () => void - ) => { - setSelected((prevSelected) => { - const copy = new Set(prevSelected); + const updateableDepositsById = useMemo(() => { + const map: Record> = {}; + (siloBalances?.convertibleDeposits || []).forEach((deposit) => { + const newBDV = deposit.amount.mul(tokenBDV.toNumber()); + + if (deposit.bdv.lt(newBDV.toNumber())) { + map[deposit.id.toString()] = { + ...deposit, + newBDV, + }; + } + }); + + return map; + }, [siloBalances?.convertibleDeposits, tokenBDV]); + + const handleSetSelected = useCallback( + ( + depositId: string, + selectType: TokenDepositsSelectType, + callback?: () => void + ) => { + const copy = new Set(selected); if (selectType === 'single') { const inSelected = copy.has(depositId); copy.clear(); @@ -87,10 +112,11 @@ export const TokenDepositsProvider = (props: { copy.add(depositId); } - return copy; - }); - callback?.(); - }; + setSelected(copy); + callback?.(); + }, + [selected] + ); const handleSetMulti = useCallback((depositIds: string[]) => { setSelected(new Set(depositIds)); @@ -120,6 +146,7 @@ export const TokenDepositsProvider = (props: { token: props.token, balances: siloBalances, depositsById: depositMap, + updateableDepositsById, slug: slugIndexMap[slugIndex] || 'token', setSlug, setSelected: handleSetSelected, @@ -130,11 +157,13 @@ export const TokenDepositsProvider = (props: { clear, depositMap, handleSetMulti, + handleSetSelected, + setSlug, props.token, selected, - setSlug, siloBalances, slugIndex, + updateableDepositsById, ] ); diff --git a/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx b/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx index b14acfa79..785fccfc7 100644 --- a/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx +++ b/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx @@ -8,6 +8,7 @@ import TokenIcon from '~/components/Common/TokenIcon'; import { Token, TokenValue } from '@beanstalk/sdk'; import BigNumber from 'bignumber.js'; import { deliveryBoxIcon, minimizeWindowIcon } from '~/img/icon'; +import { useTokens } from '~/hooks/beanstalk/useTokens'; import DepositsTable from './DepositsTable'; import { useTokenDepositsContext } from './TokenDepositsContext'; @@ -16,7 +17,15 @@ type Props = { }; const TokenDepositsOverview = ({ token }: Props) => { - const { balances, setSlug } = useTokenDepositsContext(); + const { balances, updateableDepositsById, setSlug } = + useTokenDepositsContext(); + const { BEAN } = useTokens(); + + const isBEAN = token.equals(BEAN); + + const hasUpdateableDeposits = Boolean( + Object.keys(updateableDepositsById).length + ); const depositedAmount = balances?.amount || TokenValue.ZERO; const amount = new BigNumber(depositedAmount.toHuman()); @@ -43,41 +52,46 @@ const TokenDepositsOverview = ({ token }: Props) => { - {hasDeposits && ( - - + {' Deposits'} + + + {!isBEAN && ( - - )} + )} + diff --git a/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx b/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx index 53fff43c6..e9ab01b7f 100644 --- a/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx +++ b/projects/ui/src/components/Silo/Token/TokenLambdaConvert.tsx @@ -13,25 +13,24 @@ import DepositConvertTable, { const TokenLambdaConvert = ({ token }: { token: ERC20Token }) => { const sdk = useSdk(); - const { setWithIds, depositsById } = useTokenDepositsContext(); + const { setWithIds, updateableDepositsById } = useTokenDepositsContext(); const getBDV = useBDV(); const { updatableDeposits, totalDeltaStalk, totalDeltaSeed } = useMemo(() => { const oneTokenBDV = token.fromHuman(getBDV(token).toString()); const updateable: FarmerTokenConvertRow[] = []; - let totalDeltaStalk = sdk.tokens.STALK.fromHuman('0'); - let totalDeltaSeed = sdk.tokens.SEEDS.fromHuman('0'); + let ttlDeltaStalk = sdk.tokens.STALK.fromHuman('0'); + let ttlDeltaSeed = sdk.tokens.SEEDS.fromHuman('0'); - Object.entries(depositsById).forEach(([key, deposit]) => { + Object.entries(updateableDepositsById).forEach(([_, deposit]) => { const currentBDV = oneTokenBDV.mul(deposit.amount); const deltaBDV = currentBDV.sub(deposit.bdv); if (deposit.bdv.gte(currentBDV)) return; - const deltaStalk = deposit.seeds.mul(deltaBDV); const deltaSeed = deltaBDV.div(deposit.bdv).mul(deposit.seeds); updateable.push({ - key: '....' + deposit.id.toHexString().slice(-13), + key: `...${deposit.id.toHexString().slice(-13)}`, currentBDV: currentBDV, deltaBDV: deltaBDV, deltaStalk: deltaStalk, @@ -39,14 +38,18 @@ const TokenLambdaConvert = ({ token }: { token: ERC20Token }) => { ...deposit, }); - totalDeltaStalk = totalDeltaStalk.add(deltaStalk); - totalDeltaSeed = totalDeltaSeed.add(deltaSeed); + ttlDeltaStalk = ttlDeltaStalk.add(deltaStalk); + ttlDeltaSeed = ttlDeltaSeed.add(deltaSeed); }); updateable.sort((a, b) => (a.deltaBDV.gte(b.deltaBDV) ? -1 : 1)); - return { updatableDeposits: updateable, totalDeltaStalk, totalDeltaSeed }; - }, [depositsById, getBDV, token, sdk.tokens]); + return { + updatableDeposits: updateable, + totalDeltaStalk: ttlDeltaStalk, + totalDeltaSeed: ttlDeltaSeed, + }; + }, [updateableDepositsById, getBDV, token, sdk.tokens]); const handleSelectAll = () => { setWithIds(updatableDeposits.map(({ key }) => key)); diff --git a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts index c31d8fba1..35c22d770 100644 --- a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts +++ b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts @@ -43,14 +43,14 @@ export class PipelineConvertUtil { token: ERC20Token, spender: string, amount: ethers.BigNumberish = ethers.constants.MaxUint256, - clipboard: string = ethers.constants.HashZero + clipboard: string = Clipboard.encode([]) ): AdvancedPipeStruct { return { target: token.address, callData: token .getContract() .interface.encodeFunctionData('approve', [spender, amount]), - clipboard, + clipboard: clipboard, }; } @@ -59,7 +59,7 @@ export class PipelineConvertUtil { amountIn: TokenValue, minAmountsOut: TokenValue[], recipient: string, - clipboard: string = ethers.constants.HashZero + clipboard: string = Clipboard.encode([]) ): AdvancedPipeStruct { return { target: sourceWell.address, @@ -71,7 +71,7 @@ export class PipelineConvertUtil { recipient, ethers.constants.MaxUint256, ]), - clipboard, + clipboard: clipboard, }; } @@ -79,7 +79,7 @@ export class PipelineConvertUtil { well: BasinWell, recipient: string, amount: ethers.BigNumberish, - clipboard: string = ethers.constants.HashZero + clipboard: string = Clipboard.encode([]) ): AdvancedPipeStruct { return { target: well.address, @@ -94,26 +94,29 @@ export class PipelineConvertUtil { token: ERC20Token, recipient: string, amount: ethers.BigNumberish, - clipboard: string = ethers.constants.HashZero + clipboard: string = Clipboard.encode([]) ): AdvancedPipeStruct { return { target: token.address, callData: token .getContract() .interface.encodeFunctionData('transfer', [recipient, amount]), - clipboard, + clipboard: clipboard, }; } private static junctionGte( junction: BeanstalkSDK['contracts']['junction'], - left: ethers.BigNumberish, - right: ethers.BigNumberish, - clipboard: string = ethers.constants.HashZero + value: ethers.BigNumberish, + expectedGteValue: ethers.BigNumberish, + clipboard: string = Clipboard.encode([]) ): AdvancedPipeStruct { return { target: junction.address, - callData: junction.interface.encodeFunctionData('gte', [left, right]), + callData: junction.interface.encodeFunctionData('gte', [ + value, + expectedGteValue, + ]), clipboard: clipboard, }; } @@ -121,12 +124,12 @@ export class PipelineConvertUtil { private static junctionCheck( junction: BeanstalkSDK['contracts']['junction'], value: boolean, - clipboard: string = ethers.constants.HashZero + clipboard: string = Clipboard.encode([]) ): AdvancedPipeStruct { return { target: junction.address, callData: junction.interface.encodeFunctionData('check', [value]), - clipboard, + clipboard: clipboard, }; } @@ -143,7 +146,12 @@ export class PipelineConvertUtil { source.beanAmountOut, source.nonBeanAmountOut, ]; - if (!source.well.tokens[0].equals(sdk.tokens.BEAN)) { + + const sourceWellBeanIndex = source.well.tokens.findIndex((t) => + t.equals(sdk.tokens.BEAN) + ); + + if (sourceWellBeanIndex !== 0) { sourceWellAmountsOut.reverse(); } @@ -157,14 +165,17 @@ export class PipelineConvertUtil { PipelineConvertUtil.getRemoveLiquidityEqual( source.well, source.lpAmountIn, - sourceWellAmountsOut.map((a) => a.subSlippage(slippage)), + sourceWellAmountsOut, sdk.contracts.pipeline.address ) ); // 2. Approve 0x pipe.push( - PipelineConvertUtil.erc20Approve(swap.sellToken, swap.quote.allowanceTarget) + PipelineConvertUtil.erc20Approve( + swap.sellToken, + swap.quote.allowanceTarget + ) ); // 3. Swap nonBeanToken1 for nonBeanToken2. recipient MUST be Pipeline or this will fail. @@ -187,10 +198,10 @@ export class PipelineConvertUtil { // 5. Transfer well.tokens[0] to target well pipe.push( PipelineConvertUtil.transferToken( - sdk.tokens.BEAN, + source.well.tokens[sourceWellBeanIndex], target.well.address, ethers.constants.Zero, - Clipboard.encodeSlot(1, 3, 1) + Clipboard.encodeSlot(1, 2, 1) ) ); @@ -209,7 +220,7 @@ export class PipelineConvertUtil { pipe.push( PipelineConvertUtil.junctionGte( sdk.contracts.junction, - ethers.constants.Zero, + minLPOut.add(1), // add 1 to ensure copy clip gte is true minLPOut, Clipboard.encodeSlot(6, 0, 0) ) @@ -219,7 +230,7 @@ export class PipelineConvertUtil { pipe.push( PipelineConvertUtil.junctionCheck( sdk.contracts.junction, - true, + false, // default false Clipboard.encodeSlot(7, 0, 0) ) ); diff --git a/projects/ui/src/pages/silo/token.tsx b/projects/ui/src/pages/silo/token.tsx index 62933d795..180a31443 100644 --- a/projects/ui/src/pages/silo/token.tsx +++ b/projects/ui/src/pages/silo/token.tsx @@ -37,6 +37,7 @@ import Row from '~/components/Common/Row'; import CloseIcon from '@mui/icons-material/Close'; import LambdaConvert from '~/components/Silo/Actions/LambdaConvert'; import { useWhitelistedTokens } from '~/hooks/beanstalk/useTokens'; +import useBDV from '~/hooks/beanstalk/useBDV'; const guides = [ HOW_TO_DEPOSIT_IN_THE_SILO, @@ -132,13 +133,16 @@ const LambdaConvertContent = ({ }: Props & { handleClose: () => void; }) => { - const { selected } = useTokenDepositsContext(); + const { selected, balances } = useTokenDepositsContext(); const hasSelected = Boolean(selected.size); + const getBDV = useBDV(); + + const tokenBDV = getBDV(token); return ( - + @@ -160,14 +164,19 @@ const LambdaConvertContent = ({ - {hasSelected && ( + @@ -179,7 +188,7 @@ const LambdaConvertContent = ({ - )} + ); }; diff --git a/projects/ui/src/state/bean/pools/updater.ts b/projects/ui/src/state/bean/pools/updater.ts index 021cfc2c4..11301018a 100644 --- a/projects/ui/src/state/bean/pools/updater.ts +++ b/projects/ui/src/state/bean/pools/updater.ts @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'; import throttle from 'lodash/throttle'; import { multicall } from '@wagmi/core'; -import { displayBeanPrice, tokenResult, exists } from '~/util'; +import { displayBeanPrice, tokenResult } from '~/util'; import useSdk from '~/hooks/sdk'; import { ContractFunctionParameters, erc20Abi } from 'viem'; import BEANSTALK_ABI_SNIPPETS from '~/constants/abi/Beanstalk/abiSnippets'; @@ -94,7 +94,7 @@ export const useFetchPools = () => { const pool = sdk.pools.getPoolByLPToken(address); const lpResult = lpResults[address]; - if (pool && exists(lpResult.deltaB) && exists(lpResult.supply)) { + if (pool) { const payload: UpdatePoolPayload = { address: address, pool: { @@ -104,14 +104,18 @@ export const useFetchPools = () => { transform(poolData.balances[1], 'bnjs', pool.tokens[1]), ], deltaB: transform(poolData.deltaB, 'bnjs', BEAN), - supply: transform(lpResult.supply, 'bnjs', pool.lpToken), + supply: lpResult.supply + ? transform(lpResult.supply, 'bnjs', pool.lpToken) + : new BigNumber(0), // Liquidity: always denominated in USD for the price contract liquidity: transform(poolData.liquidity, 'bnjs', BEAN), // USD value of 1 LP token == liquidity / supply totalCrosses: new BigNumber(0), lpUsd: transform(poolData.lpUsd, 'bnjs', BEAN), lpBdv: transform(poolData.lpBdv, 'bnjs', BEAN), - twaDeltaB: transform(lpResult.deltaB, 'bnjs', BEAN), + twaDeltaB: lpResult.deltaB + ? transform(lpResult.deltaB, 'bnjs', BEAN) + : null, }, } as UpdatePoolPayload; acc.push(payload); From 99835dbaba7e1322cadb337689148aab679279b2 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Sat, 21 Sep 2024 19:17:46 -0600 Subject: [PATCH 05/16] feat: update lambda convert --- .../Silo/Token/TokenDepositsContext.tsx | 77 +++++++++---------- .../Silo/Token/TokenDepositsOverview.tsx | 18 +---- .../Silo/Token/TokenLambdaConvert.tsx | 27 ++++--- projects/ui/src/pages/silo/token.tsx | 71 +++++++++-------- 4 files changed, 90 insertions(+), 103 deletions(-) diff --git a/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx b/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx index b672a5225..66885bed5 100644 --- a/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx +++ b/projects/ui/src/components/Silo/Token/TokenDepositsContext.tsx @@ -11,6 +11,7 @@ import React, { useContext, useMemo, useState, + useEffect, } from 'react'; import useBDV from '~/hooks/beanstalk/useBDV'; import useTabs from '~/hooks/display/useTabs'; @@ -58,12 +59,16 @@ const TokenDepositsContext = createContext( null ); +const emptyObj = {}; + export const TokenDepositsProvider = (props: { children: React.ReactNode; token: ERC20Token; }) => { const [selected, setSelected] = useState>(new Set()); const [slugIndex, setSlugIndex] = useTabs(SLUGS, 'content', 0); + const [updateableDepositsById, setUpdateableDepositsById] = + useState(emptyObj); const getBDV = useBDV(); const tokenBDV = getBDV(props.token); @@ -79,21 +84,27 @@ export const TokenDepositsProvider = (props: { return map; }, [siloBalances?.deposits]); - const updateableDepositsById = useMemo(() => { - const map: Record> = {}; - (siloBalances?.convertibleDeposits || []).forEach((deposit) => { - const newBDV = deposit.amount.mul(tokenBDV.toNumber()); - - if (deposit.bdv.lt(newBDV.toNumber())) { - map[deposit.id.toString()] = { + useEffect(() => { + if (!siloBalances?.convertibleDeposits?.length) { + setUpdateableDepositsById(emptyObj); + return; + } + + const map = siloBalances.convertibleDeposits.reduce( + (prev, deposit) => { + const newBDV = deposit.amount.mul(tokenBDV.toNumber()); + if (!deposit.bdv.lt(newBDV.toNumber())) return prev; + prev[deposit.id.toString()] = { ...deposit, newBDV, }; - } - }); + return prev; + }, + {} as typeof updateableDepositsById + ); - return map; - }, [siloBalances?.convertibleDeposits, tokenBDV]); + setUpdateableDepositsById(map); + }, [siloBalances, tokenBDV]); const handleSetSelected = useCallback( ( @@ -101,7 +112,7 @@ export const TokenDepositsProvider = (props: { selectType: TokenDepositsSelectType, callback?: () => void ) => { - const copy = new Set(selected); + const copy = new Set([...selected]); if (selectType === 'single') { const inSelected = copy.has(depositId); copy.clear(); @@ -140,35 +151,21 @@ export const TokenDepositsProvider = (props: { [setSlugIndex] ); - const contextValue = useMemo( - () => ({ - selected, - token: props.token, - balances: siloBalances, - depositsById: depositMap, - updateableDepositsById, - slug: slugIndexMap[slugIndex] || 'token', - setSlug, - setSelected: handleSetSelected, - setWithIds: handleSetMulti, - clear, - }), - [ - clear, - depositMap, - handleSetMulti, - handleSetSelected, - setSlug, - props.token, - selected, - siloBalances, - slugIndex, - updateableDepositsById, - ] - ); - return ( - + {props.children} ); diff --git a/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx b/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx index 785fccfc7..ebbe23bc8 100644 --- a/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx +++ b/projects/ui/src/components/Silo/Token/TokenDepositsOverview.tsx @@ -17,20 +17,11 @@ type Props = { }; const TokenDepositsOverview = ({ token }: Props) => { - const { balances, updateableDepositsById, setSlug } = - useTokenDepositsContext(); + const { balances, setSlug } = useTokenDepositsContext(); const { BEAN } = useTokens(); - const isBEAN = token.equals(BEAN); - const hasUpdateableDeposits = Boolean( - Object.keys(updateableDepositsById).length - ); - - const depositedAmount = balances?.amount || TokenValue.ZERO; - const amount = new BigNumber(depositedAmount.toHuman()); - - const hasDeposits = Boolean(balances?.deposits?.length); + const amount = new BigNumber((balances?.amount || TokenValue.ZERO).toHuman()); return ( @@ -52,20 +43,17 @@ const TokenDepositsOverview = ({ token }: Props) => { - - - - - - - - - + + + + + + Update Deposits + + + + + + + + + {!!hasUpdateableDeposits && ( - + )} ); }; From 81863f6b6d8b222307303103d5b292eae53e48d3 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Sat, 21 Sep 2024 19:25:35 -0600 Subject: [PATCH 06/16] feat: update lambda convert styling --- projects/ui/src/pages/silo/token.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/projects/ui/src/pages/silo/token.tsx b/projects/ui/src/pages/silo/token.tsx index afd135e51..b0499a794 100644 --- a/projects/ui/src/pages/silo/token.tsx +++ b/projects/ui/src/pages/silo/token.tsx @@ -37,7 +37,6 @@ import Row from '~/components/Common/Row'; import CloseIcon from '@mui/icons-material/Close'; import LambdaConvert from '~/components/Silo/Actions/LambdaConvert'; import { useWhitelistedTokens } from '~/hooks/beanstalk/useTokens'; -import useBDV from '~/hooks/beanstalk/useBDV'; const guides = [ HOW_TO_DEPOSIT_IN_THE_SILO, @@ -138,18 +137,22 @@ const LambdaConvertContent = ({ const hasUpdateableDeposits = Boolean( Object.keys(updateableDepositsById).length ); - const getBDV = useBDV(); - - const tokenBDV = getBDV(token); return ( - + From b450f516d9d8d9ac01749e22e5079e2400186948 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 00:57:21 -0600 Subject: [PATCH 07/16] change PipelineConvert implementation --- projects/sdk/src/index.ts | 2 +- .../src/lib/farm/actions/PipelineConvert.ts | 65 ------ projects/sdk/src/lib/farm/actions/index.ts | 2 - projects/sdk/src/lib/matcha/types.ts | 40 +++- projects/sdk/src/lib/silo.ts | 5 +- projects/sdk/src/lib/silo/PipelineConvert.ts | 190 ++++++++++++++++++ 6 files changed, 230 insertions(+), 74 deletions(-) delete mode 100644 projects/sdk/src/lib/farm/actions/PipelineConvert.ts create mode 100644 projects/sdk/src/lib/silo/PipelineConvert.ts diff --git a/projects/sdk/src/index.ts b/projects/sdk/src/index.ts index 633145b92..7101be287 100644 --- a/projects/sdk/src/index.ts +++ b/projects/sdk/src/index.ts @@ -28,7 +28,7 @@ export type { } from "src/lib/depot"; export type { - ZeroExQuoteParams, + MinimumViableSwapQuote, ZeroExQuoteResponse, ZeroExAPIRequestParams } from "src/lib/matcha/types"; diff --git a/projects/sdk/src/lib/farm/actions/PipelineConvert.ts b/projects/sdk/src/lib/farm/actions/PipelineConvert.ts deleted file mode 100644 index 90223d8d3..000000000 --- a/projects/sdk/src/lib/farm/actions/PipelineConvert.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ethers } from "ethers"; -import { BasicPreparedResult, RunContext, StepClass } from "src/classes/Workflow"; -import { BeanstalkSDK } from "src/lib/BeanstalkSDK"; -import { ERC20Token } from "src/classes/Token"; -import { TokenValue } from "@beanstalk/sdk-core"; -import { AdvancedPipeCallStruct } from "src/lib/depot"; - -export class PipelineConvert extends StepClass { - static sdk: BeanstalkSDK; - public name: string = "pipeline-convert"; - - constructor( - private _tokenIn: ERC20Token, - public readonly _stems: ethers.BigNumberish[], - public readonly _amounts: ethers.BigNumberish[], - private _tokenOut: ERC20Token, - private _amountOut: TokenValue, // before slippage - public readonly advancedPipeStructs: AdvancedPipeCallStruct[] - ) { - super(); - } - - async run(_amountInStep: ethers.BigNumber, context: RunContext) { - const slip = context?.data?.slippage || 0.1; - const minAmountOut = this._amountOut.subSlippage(slip).toBigNumber(); - - return { - name: this.name, - amountOut: minAmountOut, - prepare: () => { - PipelineConvert.sdk.debug(`[${this.name}.encode()]`, { - tokenIn: this._tokenIn, - amounts: this._amounts, - stems: this._stems, - tokenOut: this._tokenOut, - amountOut: this._amountOut, - advancedPipeStructs: this.advancedPipeStructs - }); - return { - target: PipelineConvert.sdk.contracts.beanstalk.address, - callData: PipelineConvert.sdk.contracts.beanstalk.interface.encodeFunctionData( - "pipelineConvert", - [ - this._tokenIn.address, - this._stems, - this._amounts, - this._tokenOut.address, - this.advancedPipeStructs - ] - ) - }; - }, - decode: (data: string) => - PipelineConvert.sdk.contracts.beanstalk.interface.decodeFunctionData( - "pipelineConvert", - data - ), - decodeResult: (result: string) => - PipelineConvert.sdk.contracts.beanstalk.interface.decodeFunctionResult( - "pipelineConvert", - result - ) - }; - } -} diff --git a/projects/sdk/src/lib/farm/actions/index.ts b/projects/sdk/src/lib/farm/actions/index.ts index b01431133..a754af007 100644 --- a/projects/sdk/src/lib/farm/actions/index.ts +++ b/projects/sdk/src/lib/farm/actions/index.ts @@ -20,7 +20,6 @@ import { UniswapV3Swap } from "./UniswapV3Swap"; import { DevDebug } from "./_DevDebug"; import { LidoWrapSteth } from "./LidoWrapSteth"; import { LidoUnwrapWstETH } from "./LidoUnwrapWstETH"; -import { PipelineConvert } from "./PipelineConvert"; export { // Approvals @@ -45,7 +44,6 @@ export { ClaimWithdrawal, TransferDeposits, TransferDeposit, - PipelineConvert, // Lido LidoWrapSteth, diff --git a/projects/sdk/src/lib/matcha/types.ts b/projects/sdk/src/lib/matcha/types.ts index 9f2bef486..7adeaed61 100644 --- a/projects/sdk/src/lib/matcha/types.ts +++ b/projects/sdk/src/lib/matcha/types.ts @@ -1,8 +1,3 @@ -export interface ZeroExQuoteParams extends ZeroExAPIRequestParams { - mode: "exactInput" | "exactOutput"; - enabled: boolean; -} - export interface ZeroExAPIRequestParams { /** * The ERC20 token address of the token you want to sell. It is recommended to always use the token address @@ -148,6 +143,41 @@ interface ZeroExSource { proportion: string; } +export interface MinimumViableSwapQuote { + /** + * The ERC20 token address of the token you want to receive in quote. + */ + buyTokenAddress: string; + /** + * The ERC20 token address of the token you want to sell with quote. + */ + sellTokenAddress: string; + /** + * The address of the contract to send call data to. + */ + to: string; + /** + * The target contract address for which the user needs to have an allowance in order to be able to complete the swap. + */ + allowanceTarget: string; + /** + * The amount of buyToken (in buyToken units) that would be bought in this swap. + */ + buyAmount: string; + /** + * The amount of sellToken (in sellToken units) that would be sold in this swap. + */ + sellAmount: string; + /** + * The amount of ether (in wei) that should be sent with the transaction. + */ + value: string; + /** + * The call data + */ + data: string; +} + /** * Response type from 0x quote-v1 swap API. * diff --git a/projects/sdk/src/lib/silo.ts b/projects/sdk/src/lib/silo.ts index 46995eced..338efdc19 100644 --- a/projects/sdk/src/lib/silo.ts +++ b/projects/sdk/src/lib/silo.ts @@ -18,6 +18,7 @@ import { } from "./silo/types"; import { Transfer } from "./silo/Transfer"; import { Convert, ConvertDetails } from "./silo/Convert"; +import { PipelineConvert } from "./silo/PipelineConvert"; export class Silo { static sdk: BeanstalkSDK; @@ -25,10 +26,11 @@ export class Silo { siloWithdraw: Withdraw; siloTransfer: Transfer; siloConvert: Convert; + pipelineConvert: PipelineConvert; // 1 Seed grows 1 / 10_000 Stalk per Season. // 1/10_000 = 1E-4 - // FIXME + // BS3TODO: FIXME. static STALK_PER_SEED_PER_SEASON = TokenValue.fromHuman(1e-4, 10); constructor(sdk: BeanstalkSDK) { @@ -37,6 +39,7 @@ export class Silo { this.siloWithdraw = new Withdraw(sdk); this.siloTransfer = new Transfer(sdk); this.siloConvert = new Convert(sdk); + this.pipelineConvert = new PipelineConvert(sdk); } public calculateGrownStalk = utils.calculateGrownStalkStems; diff --git a/projects/sdk/src/lib/silo/PipelineConvert.ts b/projects/sdk/src/lib/silo/PipelineConvert.ts new file mode 100644 index 000000000..724c601a3 --- /dev/null +++ b/projects/sdk/src/lib/silo/PipelineConvert.ts @@ -0,0 +1,190 @@ +import { TokenValue } from "@beanstalk/sdk-core"; +import { BeanstalkSDK } from "src/lib/BeanstalkSDK"; +import { MinimumViableSwapQuote } from "src/lib/matcha"; +import { AdvancedPipeCallStruct, Clipboard } from "src/lib/depot"; +import { ERC20Token } from "src/classes/Token"; +import { ethers } from "ethers"; +import { BasinWell } from "src/classes/Pool/BasinWell"; + +export class PipelineConvert { + static sdk: BeanstalkSDK; + + constructor(sdk: BeanstalkSDK) { + PipelineConvert.sdk = sdk; + } + + /** + * Equal2Equal + * - remove in equal parts from Well 1 + * - swap non-bean token of well 1 for non-bean token of well 2 + * - add in equal parts to well 2 + * Builds the advanced pipe calls for the pipeline convert + * @param quote + */ + public buildEq2EqAdvancedPipeCalls( + from: { + well: BasinWell; + amountIn: TokenValue; + amountsOut: TokenValue[]; + }, + swap: { + buyToken: ERC20Token; + sellToken: ERC20Token; + quote: MinimumViableSwapQuote; + }, + to: { + well: BasinWell; + amountOut: TokenValue; + } + ) { + const sellTokenIndex = from.well.tokens.findIndex( + (t) => t.address.toLowerCase() === swap.sellToken.address.toLowerCase() + ); + + const pipe: AdvancedPipeCallStruct[] = []; + + // 0: approve from.well.lpToken to use from.well.lpToken + pipe.push(PipelineConvert.snippets.erc20Approve(from.well.lpToken, from.well.lpToken.address)); + + // 1: remove liquidity from from.well + pipe.push( + PipelineConvert.snippets.removeLiquidity( + from.well, + from.amountIn, + [TokenValue.ZERO, TokenValue.ZERO], + from.well.lpToken.address + ) + ); + + // 2: Approve swap contract to spend sellToken + pipe.push(PipelineConvert.snippets.erc20Approve(swap.sellToken, swap.quote.allowanceTarget)); + + // 3: Swap non-bean token of well 1 for non-bean token of well 2 + pipe.push({ + target: swap.quote.to, + callData: swap.quote.data, + clipboard: Clipboard.encode([]) + }); + + // 4: transfer BuyToken to to.well + pipe.push( + PipelineConvert.snippets.erc20Transfer( + swap.buyToken, + to.well.address, + to.amountOut, + Clipboard.encodeSlot(3, 0, 1) + ) + ); + + // 5: transfer from.well.tokens[0] to to.well + pipe.push( + PipelineConvert.snippets.erc20Transfer( + from.well.tokens[sellTokenIndex === 1 ? 0 : 1], + to.well.address, + TokenValue.MAX_UINT256, // set to max uint256 to ensure transfer succeeds + Clipboard.encodeSlot(1, 2, 1) + ) + ); + + // 6. Call Sync on to.well + pipe.push( + PipelineConvert.snippets.wellSync( + to.well, + PipelineConvert.sdk.contracts.pipeline.address, // set recipient to pipeline + to.amountOut + ) + ); + } + + private static snippets = { + erc20Approve: function ( + token: ERC20Token, + spender: string, + amount: TokenValue = TokenValue.MAX_UINT256, + clipboard: string = Clipboard.encode([]) + ): AdvancedPipeCallStruct { + return { + target: token.address, + callData: token + .getContract() + .interface.encodeFunctionData("approve", [spender, amount.toBigNumber()]), + clipboard + }; + }, + erc20Transfer: function ( + token: ERC20Token, + recipient: string, + amount: TokenValue, + clipboard: string = Clipboard.encode([]) + ): AdvancedPipeCallStruct { + return { + target: token.address, + callData: token + .getContract() + .interface.encodeFunctionData("transfer", [recipient, amount.toBigNumber()]), + clipboard + }; + }, + removeLiquidity: function ( + well: BasinWell, + amountIn: TokenValue, + minAmountsOut: TokenValue[], + recipient: string, + clipboard: string = Clipboard.encode([]) + ): AdvancedPipeCallStruct { + return { + target: well.address, + callData: well + .getContract() + .interface.encodeFunctionData("removeLiquidity", [ + amountIn.toBigNumber(), + minAmountsOut.map((a) => a.toBigNumber()), + recipient, + ethers.constants.MaxUint256 + ]), + clipboard: clipboard + }; + }, + wellSync: function ( + well: BasinWell, + recipient: string, + amount: TokenValue, + clipboard: string = Clipboard.encode([]) + ): AdvancedPipeCallStruct { + return { + target: well.address, + callData: well + .getContract() + .interface.encodeFunctionData("sync", [recipient, amount.toBigNumber()]), + clipboard + }; + }, + gte: function ( + value: TokenValue, + compareTo: TokenValue, + clipboard: string = Clipboard.encode([]) + ): AdvancedPipeCallStruct { + return { + target: PipelineConvert.sdk.contracts.junction.address, + // value >= compare + callData: PipelineConvert.sdk.contracts.junction.interface.encodeFunctionData("gte", [ + value.toBigNumber(), + compareTo.toBigNumber() + ]), + clipboard + }; + }, + check: function ( + // index of the math or logic operation in the pipe + index: number + ): AdvancedPipeCallStruct { + return { + target: PipelineConvert.sdk.contracts.junction.address, + callData: PipelineConvert.sdk.contracts.junction.interface.encodeFunctionData("check", [ + false + ]), + clipboard: Clipboard.encodeSlot(index, 0, 0) + }; + } + }; +} From 6eb415fcb82603bcf9a0c7b4092eba856d14c609 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 00:58:41 -0600 Subject: [PATCH 08/16] feat: Update price contract address --- projects/sdk/src/constants/addresses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/sdk/src/constants/addresses.ts b/projects/sdk/src/constants/addresses.ts index 22006df63..bca62a091 100644 --- a/projects/sdk/src/constants/addresses.ts +++ b/projects/sdk/src/constants/addresses.ts @@ -21,7 +21,7 @@ export const addresses = { // ---------------------------------------- BEANSTALK_PRICE: Address.make({ [ChainId.ETH_MAINNET]: "0x4BEd6cb142b7d474242d87F4796387DEB9E1E1B4", - [ChainId.ARBITRUM_MAINNET]: "0xC218F5a782b0913931DCF502FA2aA959b36Ac9E7" + [ChainId.ARBITRUM_MAINNET]: "0xA560c3aFcEb9a046573bf6F401134a6837f6D321" }), JUNCTION: Address.make({ [ChainId.ETH_MAINNET]: "0x16a903b66403d3de69db50e6d1ad0b07490b740a", From 2ff384f9bc45cf839a6c952dad00976b43665d8b Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 01:19:15 -0600 Subject: [PATCH 09/16] feat: update pipeline convert calls --- projects/sdk/src/lib/silo/PipelineConvert.ts | 83 ++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/projects/sdk/src/lib/silo/PipelineConvert.ts b/projects/sdk/src/lib/silo/PipelineConvert.ts index 724c601a3..48947ff93 100644 --- a/projects/sdk/src/lib/silo/PipelineConvert.ts +++ b/projects/sdk/src/lib/silo/PipelineConvert.ts @@ -13,6 +13,74 @@ export class PipelineConvert { PipelineConvert.sdk = sdk; } + public async estimateEqual2Equal( + fromWell: BasinWell, + toWell: BasinWell, + amountIn: TokenValue, + slippage: number + ) { + PipelineConvert.sdk.debug("[PipelineConvert] estimateEqual2Equal", { + fromWell: fromWell.address, + toWell: toWell.address, + amountIn: amountIn.toHuman(), + slippage + }); + + const BEAN = PipelineConvert.sdk.tokens.BEAN; + const fromWellBeanIndex = fromWell.tokens.findIndex((t) => t.equals(BEAN)); + const toWellBeanIndex = toWell.tokens.findIndex((t) => t.equals(BEAN)); + + const [outIndex0, outIndex1] = await fromWell.getRemoveLiquidityOutEqual(amountIn); + + const sellToken = fromWell.tokens[fromWellBeanIndex === 0 ? 1 : 0]; + const buyToken = toWell.tokens[toWellBeanIndex === 0 ? 1 : 0]; + + const sellAmount = fromWellBeanIndex === 0 ? outIndex1 : outIndex0; + const beanAmount = fromWellBeanIndex === 0 ? outIndex0 : outIndex1; + + const quote = await PipelineConvert.sdk.zeroX.fetchSwapQuote({ + sellToken: sellToken.address, + buyToken: buyToken.address, + sellAmount: sellAmount.blockchainString, + takerAddress: PipelineConvert.sdk.contracts.pipeline.address, + shouldSellEntireBalance: true, + skipValidation: true, + slippagePercentage: slippage.toString() + }); + + const buyAmount = buyToken.fromBlockchain(quote.buyAmount); + + const toWellAmountsIn = [beanAmount, buyAmount]; + if (fromWellBeanIndex === 0) { + toWellAmountsIn.reverse(); + } + + const amountOut = await toWell.getAddLiquidityOut(toWellAmountsIn); + + const advPipeCalls = this.buildEq2EqAdvancedPipeCalls({ + from: { + well: fromWell, + amountIn: amountIn + }, + swap: { + buyToken: buyToken, + sellToken: sellToken, + quote: quote + }, + to: { + well: toWell, + amountOut + } + }); + + return { + fromWellAmountsOut: [beanAmount, sellAmount], + quote, + amountOut, + advPipeCalls + }; + } + /** * Equal2Equal * - remove in equal parts from Well 1 @@ -21,22 +89,25 @@ export class PipelineConvert { * Builds the advanced pipe calls for the pipeline convert * @param quote */ - public buildEq2EqAdvancedPipeCalls( + public buildEq2EqAdvancedPipeCalls({ + from, + swap, + to + }: { from: { well: BasinWell; amountIn: TokenValue; - amountsOut: TokenValue[]; - }, + }; swap: { buyToken: ERC20Token; sellToken: ERC20Token; quote: MinimumViableSwapQuote; - }, + }; to: { well: BasinWell; amountOut: TokenValue; - } - ) { + }; + }) { const sellTokenIndex = from.well.tokens.findIndex( (t) => t.address.toLowerCase() === swap.sellToken.address.toLowerCase() ); From 2edebc5c6ada455e1c564c98bba3979578dcaeec Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 03:30:05 -0600 Subject: [PATCH 10/16] improve pipeline convert --- projects/sdk/src/lib/silo/PipelineConvert.ts | 156 +++++++++++++------ 1 file changed, 108 insertions(+), 48 deletions(-) diff --git a/projects/sdk/src/lib/silo/PipelineConvert.ts b/projects/sdk/src/lib/silo/PipelineConvert.ts index 48947ff93..9002f0349 100644 --- a/projects/sdk/src/lib/silo/PipelineConvert.ts +++ b/projects/sdk/src/lib/silo/PipelineConvert.ts @@ -13,30 +13,83 @@ export class PipelineConvert { PipelineConvert.sdk = sdk; } - public async estimateEqual2Equal( - fromWell: BasinWell, - toWell: BasinWell, + ///// Remove Equal 2 Add Equal + // 1. Remove in equal parts from Well 1 + // 2. Swap non-bean token of well 1 for non-bean token of well 2 + // 3. Add in equal parts to well 2 + + /** + * Preforms a Remove Equal 2 Add Equal pipeline convert + * @param tokenIn + * @param stems + * @param amounts + * @param tokenOut + * @param advPipeCalls + * @param overrides + * @returns + */ + async removeEqualAddEqual( + tokenIn: ERC20Token, + stems: ethers.BigNumber[], + amounts: ethers.BigNumber[], + tokenOut: ERC20Token, + advPipeCalls: AdvancedPipeCallStruct[], + overrides?: ethers.PayableOverrides + ) { + return PipelineConvert.sdk.contracts.beanstalk.pipelineConvert( + tokenIn.address, + stems, + amounts, + tokenOut.address, + advPipeCalls, + overrides + ); + } + + /** + * Estimates the result of a Remove Equal 2 Add Equal pipeline convert + * @param sourceWell Well to remove liquidity from + * @param targetWell Well to add liquidity to + * @param amountIn Amount of sourceWell.lpToken to remove + * @param slippage Slippage tolerance for swap + */ + async removeEqual2AddEqualQuote( + sourceWell: BasinWell, + targetWell: BasinWell, amountIn: TokenValue, slippage: number ) { - PipelineConvert.sdk.debug("[PipelineConvert] estimateEqual2Equal", { - fromWell: fromWell.address, - toWell: toWell.address, + if (!PipelineConvert.sdk.pools.whitelistedPools.has(sourceWell.address)) { + throw new Error(`${sourceWell.name} is not a whitelisted well`); + } + if (!PipelineConvert.sdk.pools.whitelistedPools.has(targetWell.address)) { + throw new Error(`${targetWell.name} is not a whitelisted well`); + } + if (amountIn.lte(0)) { + throw new Error("Cannot convert 0 or less tokens"); + } + if (slippage < 0) { + throw new Error("Invalid slippage"); + } + + PipelineConvert.sdk.debug("[PipelineConvert] estimateRemoveEqual2AddEqual", { + sourceWell: sourceWell.address, + targetWell: targetWell.address, amountIn: amountIn.toHuman(), slippage }); const BEAN = PipelineConvert.sdk.tokens.BEAN; - const fromWellBeanIndex = fromWell.tokens.findIndex((t) => t.equals(BEAN)); - const toWellBeanIndex = toWell.tokens.findIndex((t) => t.equals(BEAN)); + const sourceWellBeanIndex = sourceWell.tokens.findIndex((t) => t.equals(BEAN)); + const targetWellBeanIndex = targetWell.tokens.findIndex((t) => t.equals(BEAN)); - const [outIndex0, outIndex1] = await fromWell.getRemoveLiquidityOutEqual(amountIn); + const [outIndex0, outIndex1] = await sourceWell.getRemoveLiquidityOutEqual(amountIn); - const sellToken = fromWell.tokens[fromWellBeanIndex === 0 ? 1 : 0]; - const buyToken = toWell.tokens[toWellBeanIndex === 0 ? 1 : 0]; + const sellToken = sourceWell.tokens[sourceWellBeanIndex === 0 ? 1 : 0]; + const buyToken = targetWell.tokens[targetWellBeanIndex === 0 ? 1 : 0]; - const sellAmount = fromWellBeanIndex === 0 ? outIndex1 : outIndex0; - const beanAmount = fromWellBeanIndex === 0 ? outIndex0 : outIndex1; + const sellAmount = sourceWellBeanIndex === 0 ? outIndex1 : outIndex0; + const beanAmount = sourceWellBeanIndex === 0 ? outIndex0 : outIndex1; const quote = await PipelineConvert.sdk.zeroX.fetchSwapQuote({ sellToken: sellToken.address, @@ -51,15 +104,15 @@ export class PipelineConvert { const buyAmount = buyToken.fromBlockchain(quote.buyAmount); const toWellAmountsIn = [beanAmount, buyAmount]; - if (fromWellBeanIndex === 0) { + if (sourceWellBeanIndex === 0) { toWellAmountsIn.reverse(); } - const amountOut = await toWell.getAddLiquidityOut(toWellAmountsIn); + const amountOut = await targetWell.getAddLiquidityOut(toWellAmountsIn); - const advPipeCalls = this.buildEq2EqAdvancedPipeCalls({ - from: { - well: fromWell, + const advPipeCalls = this.buildRemoveEqual2AddEqualAdvancedPipe({ + source: { + well: sourceWell, amountIn: amountIn }, swap: { @@ -67,34 +120,30 @@ export class PipelineConvert { sellToken: sellToken, quote: quote }, - to: { - well: toWell, + target: { + well: targetWell, amountOut } }); - return { + const result = { fromWellAmountsOut: [beanAmount, sellAmount], + toWellAmountsIn: toWellAmountsIn, quote, amountOut, advPipeCalls }; + + PipelineConvert.sdk.debug("[PipelineConvert] Result: ", result); + + return result; } /** - * Equal2Equal - * - remove in equal parts from Well 1 - * - swap non-bean token of well 1 for non-bean token of well 2 - * - add in equal parts to well 2 - * Builds the advanced pipe calls for the pipeline convert - * @param quote + * Builds the advanced pipe calls for a Remove Equal 2 Add Equal pipeline convert */ - public buildEq2EqAdvancedPipeCalls({ - from, - swap, - to - }: { - from: { + public buildRemoveEqual2AddEqualAdvancedPipe(params: { + source: { well: BasinWell; amountIn: TokenValue; }; @@ -103,27 +152,31 @@ export class PipelineConvert { sellToken: ERC20Token; quote: MinimumViableSwapQuote; }; - to: { + target: { well: BasinWell; amountOut: TokenValue; }; }) { - const sellTokenIndex = from.well.tokens.findIndex( + const { source, swap, target } = params; + + const sellTokenIndex = source.well.tokens.findIndex( (t) => t.address.toLowerCase() === swap.sellToken.address.toLowerCase() ); const pipe: AdvancedPipeCallStruct[] = []; // 0: approve from.well.lpToken to use from.well.lpToken - pipe.push(PipelineConvert.snippets.erc20Approve(from.well.lpToken, from.well.lpToken.address)); + pipe.push( + PipelineConvert.snippets.erc20Approve(source.well.lpToken, source.well.lpToken.address) + ); // 1: remove liquidity from from.well pipe.push( PipelineConvert.snippets.removeLiquidity( - from.well, - from.amountIn, + source.well, + source.amountIn, [TokenValue.ZERO, TokenValue.ZERO], - from.well.lpToken.address + source.well.lpToken.address ) ); @@ -137,37 +190,42 @@ export class PipelineConvert { clipboard: Clipboard.encode([]) }); - // 4: transfer BuyToken to to.well + // 4: transfer swap result to target well pipe.push( PipelineConvert.snippets.erc20Transfer( swap.buyToken, - to.well.address, - to.amountOut, + target.well.address, + target.amountOut, Clipboard.encodeSlot(3, 0, 1) ) ); - // 5: transfer from.well.tokens[0] to to.well + // 5: transfer from from.well.tokens[non-bean index] to target well pipe.push( PipelineConvert.snippets.erc20Transfer( - from.well.tokens[sellTokenIndex === 1 ? 0 : 1], - to.well.address, + source.well.tokens[sellTokenIndex === 1 ? 0 : 1], + target.well.address, TokenValue.MAX_UINT256, // set to max uint256 to ensure transfer succeeds Clipboard.encodeSlot(1, 2, 1) ) ); - // 6. Call Sync on to.well + // 6. Call Sync on target well pipe.push( PipelineConvert.snippets.wellSync( - to.well, + target.well, PipelineConvert.sdk.contracts.pipeline.address, // set recipient to pipeline - to.amountOut + target.amountOut ) ); } + // ---------- static methods ---------- + /** + * building blocks for the advanced pipe calls + */ private static snippets = { + // ERC20 Token Methods erc20Approve: function ( token: ERC20Token, spender: string, @@ -196,6 +254,7 @@ export class PipelineConvert { clipboard }; }, + // Well Methods removeLiquidity: function ( well: BasinWell, amountIn: TokenValue, @@ -230,6 +289,7 @@ export class PipelineConvert { clipboard }; }, + // Junction methods gte: function ( value: TokenValue, compareTo: TokenValue, From 712c367ac4b2e821f165fde679525f5183ea4cb8 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 03:43:17 -0600 Subject: [PATCH 11/16] feat: return pipe calls from quote --- projects/sdk/src/lib/silo/PipelineConvert.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/projects/sdk/src/lib/silo/PipelineConvert.ts b/projects/sdk/src/lib/silo/PipelineConvert.ts index 9002f0349..71cc8e436 100644 --- a/projects/sdk/src/lib/silo/PipelineConvert.ts +++ b/projects/sdk/src/lib/silo/PipelineConvert.ts @@ -3,7 +3,7 @@ import { BeanstalkSDK } from "src/lib/BeanstalkSDK"; import { MinimumViableSwapQuote } from "src/lib/matcha"; import { AdvancedPipeCallStruct, Clipboard } from "src/lib/depot"; import { ERC20Token } from "src/classes/Token"; -import { ethers } from "ethers"; +import { BigNumber, ethers } from "ethers"; import { BasinWell } from "src/classes/Pool/BasinWell"; export class PipelineConvert { @@ -30,8 +30,8 @@ export class PipelineConvert { */ async removeEqualAddEqual( tokenIn: ERC20Token, - stems: ethers.BigNumber[], - amounts: ethers.BigNumber[], + stems: BigNumber[], + amounts: BigNumber[], tokenOut: ERC20Token, advPipeCalls: AdvancedPipeCallStruct[], overrides?: ethers.PayableOverrides @@ -58,7 +58,13 @@ export class PipelineConvert { targetWell: BasinWell, amountIn: TokenValue, slippage: number - ) { + ): Promise<{ + fromWellAmountsOut: TokenValue[]; + toWellAmountsIn: TokenValue[]; + quote: MinimumViableSwapQuote; + amountOut: TokenValue; + advPipeCalls: AdvancedPipeCallStruct[]; + }> { if (!PipelineConvert.sdk.pools.whitelistedPools.has(sourceWell.address)) { throw new Error(`${sourceWell.name} is not a whitelisted well`); } @@ -218,6 +224,8 @@ export class PipelineConvert { target.amountOut ) ); + + return pipe; } // ---------- static methods ---------- From cbd65716c4f0a0d65602684dfe5e3c007b1a08f1 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 12:22:08 -0600 Subject: [PATCH 12/16] feat: update pipelineconvert + basinWell sdk --- projects/sdk/src/classes/Pool/BasinWell.ts | 29 +++++++ projects/sdk/src/lib/silo/PipelineConvert.ts | 84 +++++++++++--------- 2 files changed, 75 insertions(+), 38 deletions(-) diff --git a/projects/sdk/src/classes/Pool/BasinWell.ts b/projects/sdk/src/classes/Pool/BasinWell.ts index d1dc6d4e1..f1c08c176 100644 --- a/projects/sdk/src/classes/Pool/BasinWell.ts +++ b/projects/sdk/src/classes/Pool/BasinWell.ts @@ -24,6 +24,21 @@ export class BasinWell extends Pool { ); } + // Ensure tokens are in the correct order + async updateTokenIndexes() { + const data = await this.getContract().tokens(); + if (!data || data.length !== 2) { + throw new Error(`could not validate well tokens for ${this.name}`); + } + + const first = data[0].toLowerCase(); + const thisFirst = this.tokens[0].address.toLowerCase(); + + if (first !== thisFirst) { + this.tokens.reverse(); + } + } + async getAddLiquidityOut(amounts: TokenValue[]): Promise { return this.getContract() .getAddLiquidityOut(amounts.map((a) => a.toBigNumber())) @@ -49,4 +64,18 @@ export class BasinWell extends Pool { .getRemoveLiquidityOneTokenOut(lpAmountIn.toBigNumber(), tokenOut.address) .then((result) => tokenOut.fromBlockchain(result)); } + + getBeanWellTokenIndexes() { + // assumes tokens are in correct order + const beanIndex = this.tokens.findIndex((token) => token.equals(BasinWell.sdk.tokens.BEAN)); + + if (beanIndex < 0) { + throw new Error(`Bean token not found in well ${this.name}`); + } + + return { + bean: beanIndex, + nonBean: beanIndex === 0 ? 1 : 0 + }; + } } diff --git a/projects/sdk/src/lib/silo/PipelineConvert.ts b/projects/sdk/src/lib/silo/PipelineConvert.ts index 71cc8e436..ace816b15 100644 --- a/projects/sdk/src/lib/silo/PipelineConvert.ts +++ b/projects/sdk/src/lib/silo/PipelineConvert.ts @@ -57,7 +57,7 @@ export class PipelineConvert { sourceWell: BasinWell, targetWell: BasinWell, amountIn: TokenValue, - slippage: number + swapSlippage: number ): Promise<{ fromWellAmountsOut: TokenValue[]; toWellAmountsIn: TokenValue[]; @@ -74,28 +74,37 @@ export class PipelineConvert { if (amountIn.lte(0)) { throw new Error("Cannot convert 0 or less tokens"); } - if (slippage < 0) { + if (swapSlippage < 0) { throw new Error("Invalid slippage"); } PipelineConvert.sdk.debug("[PipelineConvert] estimateRemoveEqual2AddEqual", { - sourceWell: sourceWell.address, - targetWell: targetWell.address, + sourceWell: sourceWell, + targetWell: targetWell, amountIn: amountIn.toHuman(), - slippage + swapSlippage }); - const BEAN = PipelineConvert.sdk.tokens.BEAN; - const sourceWellBeanIndex = sourceWell.tokens.findIndex((t) => t.equals(BEAN)); - const targetWellBeanIndex = targetWell.tokens.findIndex((t) => t.equals(BEAN)); + const sourceIndexes = sourceWell.getBeanWellTokenIndexes(); + const targetIndexes = targetWell.getBeanWellTokenIndexes(); - const [outIndex0, outIndex1] = await sourceWell.getRemoveLiquidityOutEqual(amountIn); + const removeAmountsOut = await sourceWell.getRemoveLiquidityOutEqual(amountIn); - const sellToken = sourceWell.tokens[sourceWellBeanIndex === 0 ? 1 : 0]; - const buyToken = targetWell.tokens[targetWellBeanIndex === 0 ? 1 : 0]; + const sellToken = sourceWell.tokens[sourceIndexes.nonBean]; + const buyToken = targetWell.tokens[targetIndexes.nonBean]; - const sellAmount = sourceWellBeanIndex === 0 ? outIndex1 : outIndex0; - const beanAmount = sourceWellBeanIndex === 0 ? outIndex0 : outIndex1; + const sellAmount = removeAmountsOut[sourceIndexes.nonBean]; + const beanAmount = removeAmountsOut[sourceIndexes.bean]; + + PipelineConvert.sdk.debug("[PipelineConvert] removeLiquidityQuote + Prepare swap quote", { + removeAmountsOut, + swap: { + buyToken, + sellToken, + sellAmount, + beanAmount + } + }); const quote = await PipelineConvert.sdk.zeroX.fetchSwapQuote({ sellToken: sellToken.address, @@ -104,13 +113,13 @@ export class PipelineConvert { takerAddress: PipelineConvert.sdk.contracts.pipeline.address, shouldSellEntireBalance: true, skipValidation: true, - slippagePercentage: slippage.toString() + slippagePercentage: swapSlippage.toString() }); const buyAmount = buyToken.fromBlockchain(quote.buyAmount); const toWellAmountsIn = [beanAmount, buyAmount]; - if (sourceWellBeanIndex === 0) { + if (targetIndexes.bean !== 0) { toWellAmountsIn.reverse(); } @@ -119,7 +128,8 @@ export class PipelineConvert { const advPipeCalls = this.buildRemoveEqual2AddEqualAdvancedPipe({ source: { well: sourceWell, - amountIn: amountIn + amountIn: amountIn, + minAmountsOut: removeAmountsOut }, swap: { buyToken: buyToken, @@ -152,6 +162,7 @@ export class PipelineConvert { source: { well: BasinWell; amountIn: TokenValue; + minAmountsOut: TokenValue[]; }; swap: { buyToken: ERC20Token; @@ -181,8 +192,8 @@ export class PipelineConvert { PipelineConvert.snippets.removeLiquidity( source.well, source.amountIn, - [TokenValue.ZERO, TokenValue.ZERO], - source.well.lpToken.address + source.minAmountsOut, + PipelineConvert.sdk.contracts.pipeline.address ) ); @@ -201,7 +212,7 @@ export class PipelineConvert { PipelineConvert.snippets.erc20Transfer( swap.buyToken, target.well.address, - target.amountOut, + TokenValue.ZERO, // overriden w/ clipboard Clipboard.encodeSlot(3, 0, 1) ) ); @@ -211,7 +222,7 @@ export class PipelineConvert { PipelineConvert.snippets.erc20Transfer( source.well.tokens[sellTokenIndex === 1 ? 0 : 1], target.well.address, - TokenValue.MAX_UINT256, // set to max uint256 to ensure transfer succeeds + TokenValue.MAX_UINT256, // overriden w/ clipboard Clipboard.encodeSlot(1, 2, 1) ) ); @@ -221,7 +232,7 @@ export class PipelineConvert { PipelineConvert.snippets.wellSync( target.well, PipelineConvert.sdk.contracts.pipeline.address, // set recipient to pipeline - target.amountOut + target.amountOut // min LP Out ) ); @@ -229,17 +240,14 @@ export class PipelineConvert { } // ---------- static methods ---------- - /** - * building blocks for the advanced pipe calls - */ private static snippets = { // ERC20 Token Methods - erc20Approve: function ( + erc20Approve: ( token: ERC20Token, spender: string, amount: TokenValue = TokenValue.MAX_UINT256, clipboard: string = Clipboard.encode([]) - ): AdvancedPipeCallStruct { + ): AdvancedPipeCallStruct => { return { target: token.address, callData: token @@ -248,12 +256,12 @@ export class PipelineConvert { clipboard }; }, - erc20Transfer: function ( + erc20Transfer: ( token: ERC20Token, recipient: string, amount: TokenValue, clipboard: string = Clipboard.encode([]) - ): AdvancedPipeCallStruct { + ): AdvancedPipeCallStruct => { return { target: token.address, callData: token @@ -262,33 +270,33 @@ export class PipelineConvert { clipboard }; }, - // Well Methods - removeLiquidity: function ( + // // Well Methods + removeLiquidity: ( well: BasinWell, amountIn: TokenValue, minAmountsOut: TokenValue[], recipient: string, clipboard: string = Clipboard.encode([]) - ): AdvancedPipeCallStruct { + ): AdvancedPipeCallStruct => { return { target: well.address, callData: well .getContract() .interface.encodeFunctionData("removeLiquidity", [ amountIn.toBigNumber(), - minAmountsOut.map((a) => a.toBigNumber()), + minAmountsOut.map((v) => v.toBigNumber()), recipient, ethers.constants.MaxUint256 ]), clipboard: clipboard }; }, - wellSync: function ( + wellSync: ( well: BasinWell, recipient: string, amount: TokenValue, clipboard: string = Clipboard.encode([]) - ): AdvancedPipeCallStruct { + ): AdvancedPipeCallStruct => { return { target: well.address, callData: well @@ -298,11 +306,11 @@ export class PipelineConvert { }; }, // Junction methods - gte: function ( + gte: ( value: TokenValue, compareTo: TokenValue, clipboard: string = Clipboard.encode([]) - ): AdvancedPipeCallStruct { + ): AdvancedPipeCallStruct => { return { target: PipelineConvert.sdk.contracts.junction.address, // value >= compare @@ -313,10 +321,10 @@ export class PipelineConvert { clipboard }; }, - check: function ( + check: ( // index of the math or logic operation in the pipe index: number - ): AdvancedPipeCallStruct { + ): AdvancedPipeCallStruct => { return { target: PipelineConvert.sdk.contracts.junction.address, callData: PipelineConvert.sdk.contracts.junction.interface.encodeFunctionData("check", [ From 5dc7859edbb00722429a2d10c261ea0a14c346b4 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 12:28:37 -0600 Subject: [PATCH 13/16] feat: update sdk pools --- .../components/Silo/Actions/Convert/index.tsx | 25 ++++++++++--------- projects/ui/src/hooks/sdk/index.ts | 19 ++++++++++++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/projects/ui/src/components/Silo/Actions/Convert/index.tsx b/projects/ui/src/components/Silo/Actions/Convert/index.tsx index ad7dcbf67..6c2f81f0e 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/index.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/index.tsx @@ -28,7 +28,8 @@ import { FormTxn, ConvertFarmStep } from '~/lib/Txn'; import { useWhitelistedTokens } from '~/hooks/beanstalk/useTokens'; import { useFetchFarmerSilo } from '~/state/farmer/silo/updater'; import { DefaultConvertForm } from './DefaultConvertForm'; -import { PipelineConvertForm } from './PipelineConvertForm'; +import { PipelineConvertFormV2 } from './PipelineconvertFormV2'; +// import { PipelineConvertForm } from './PipelineConvertForm'; import { BaseConvertFormProps, ConvertFormSubmitHandler, @@ -110,7 +111,7 @@ const PipelineConvertFormWrapper = (props: Props) => { stalk: TokenValue.ZERO, }); - return ; + return ; }; // ---------- Convert Form Router ---------- @@ -321,16 +322,16 @@ function usePipelineConvertSubmitHandler({ throw new Error('Could not find any crates to convert'); } - farm.add( - new sdk.farm.actions.PipelineConvert( - tokenIn as ERC20Token, - crates.map((c) => c.stem), - crates.map((c) => c.amount.toBigNumber()), - tokenOut as ERC20Token, - amountOut, - advPipeStructs - ) - ); + // farm.add( + // new sdk.farm.actions.PipelineConvert( + // tokenIn as ERC20Token, + // crates.map((c) => c.stem), + // crates.map((c) => c.amount.toBigNumber()), + // tokenOut as ERC20Token, + // amountOut, + // advPipeStructs + // ) + // ); const gasEstimate = await farm.estimateGas(amountIn, { slippage }); const adjustedGas = Math.round(gasEstimate.toNumber() * 1.2).toString(); diff --git a/projects/ui/src/hooks/sdk/index.ts b/projects/ui/src/hooks/sdk/index.ts index 66aa077d6..9d674dac2 100644 --- a/projects/ui/src/hooks/sdk/index.ts +++ b/projects/ui/src/hooks/sdk/index.ts @@ -42,23 +42,38 @@ export const useRefreshSeeds = () => { ); }; +export const useUpdateSdkPoolTokenIndicies = () => { + const updatePoolIndicies = useCallback(async (sdk: BeanstalkSDK) => { + await Promise.all( + sdk.pools.getWells().map((well) => well.updateTokenIndexes()) + ); + }, []); + + return updatePoolIndicies; +}; + export const useDynamicSeeds = ( sdk: BeanstalkSDK, allowRun: boolean = true ) => { const [ready, setReady] = useState(false); const refreshSeeds = useRefreshSeeds(); + const updatePoolIndicies = useUpdateSdkPoolTokenIndicies(); const { isArbitrum } = useChainState(); useEffect(() => { if (!allowRun) return; const load = async () => { - await refreshSeeds(sdk); + await Promise.all([ + refreshSeeds(sdk), + // fix me - put me somewhere else? + updatePoolIndicies(sdk), + ]); setReady(true); }; load(); - }, [refreshSeeds, sdk, allowRun, isArbitrum]); + }, [refreshSeeds, updatePoolIndicies, sdk, allowRun, isArbitrum]); return ready; }; From 33c34542b48afc90f276eff88da3ec0ed168bd77 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 13:45:15 -0600 Subject: [PATCH 14/16] 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 ; From 9862d19e30118c2c2b13cb686e041a67badd8240 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 13:45:50 -0600 Subject: [PATCH 15/16] feat: remove pipeline convert class from ui --- .../lib/PipelineConvert/PipelineConvert.ts | 240 ------------------ 1 file changed, 240 deletions(-) delete mode 100644 projects/ui/src/lib/PipelineConvert/PipelineConvert.ts diff --git a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts deleted file mode 100644 index 35c22d770..000000000 --- a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { ethers } from 'ethers'; -import { - BeanstalkSDK, - BasinWell, - TokenValue, - AdvancedPipeStruct, - ERC20Token, - Clipboard, - ZeroExQuoteResponse, -} from '@beanstalk/sdk'; - -/** - * Parameters needed for PipelineConvert for the equal<->equal - * equal<>equal refering to - * - remove liquidity in equal proportions from source Well - * - add liquidity in equal proportions to target Well - */ -export interface BuildPipeCallArgsEqual { - sdk: BeanstalkSDK; - source: { - well: BasinWell; - lpAmountIn: TokenValue; - beanAmountOut: TokenValue; - nonBeanAmountOut: TokenValue; - }; - swap: { - buyToken: ERC20Token; - sellToken: ERC20Token; - // amount from 0xQuote.buyAmount - buyAmount: TokenValue; - // 0x quote.allowanceTarget - quote: ZeroExQuoteResponse; - }; - target: { - well: BasinWell; - amountOut: TokenValue; - }; - slippage: number; -} - -export class PipelineConvertUtil { - private static erc20Approve( - token: ERC20Token, - spender: string, - amount: ethers.BigNumberish = ethers.constants.MaxUint256, - clipboard: string = Clipboard.encode([]) - ): AdvancedPipeStruct { - return { - target: token.address, - callData: token - .getContract() - .interface.encodeFunctionData('approve', [spender, amount]), - clipboard: clipboard, - }; - } - - private static getRemoveLiquidityEqual( - sourceWell: BasinWell, - amountIn: TokenValue, - minAmountsOut: TokenValue[], - recipient: string, - clipboard: string = Clipboard.encode([]) - ): AdvancedPipeStruct { - return { - target: sourceWell.address, - callData: sourceWell - .getContract() - .interface.encodeFunctionData('removeLiquidity', [ - amountIn.toBigNumber(), - minAmountsOut.map((a) => a.toBigNumber()), - recipient, - ethers.constants.MaxUint256, - ]), - clipboard: clipboard, - }; - } - - private static wellSync( - well: BasinWell, - recipient: string, - amount: ethers.BigNumberish, - clipboard: string = Clipboard.encode([]) - ): AdvancedPipeStruct { - return { - target: well.address, - callData: well - .getContract() - .interface.encodeFunctionData('sync', [recipient, amount]), - clipboard, - }; - } - - private static transferToken( - token: ERC20Token, - recipient: string, - amount: ethers.BigNumberish, - clipboard: string = Clipboard.encode([]) - ): AdvancedPipeStruct { - return { - target: token.address, - callData: token - .getContract() - .interface.encodeFunctionData('transfer', [recipient, amount]), - clipboard: clipboard, - }; - } - - private static junctionGte( - junction: BeanstalkSDK['contracts']['junction'], - value: ethers.BigNumberish, - expectedGteValue: ethers.BigNumberish, - clipboard: string = Clipboard.encode([]) - ): AdvancedPipeStruct { - return { - target: junction.address, - callData: junction.interface.encodeFunctionData('gte', [ - value, - expectedGteValue, - ]), - clipboard: clipboard, - }; - } - - private static junctionCheck( - junction: BeanstalkSDK['contracts']['junction'], - value: boolean, - clipboard: string = Clipboard.encode([]) - ): AdvancedPipeStruct { - return { - target: junction.address, - callData: junction.interface.encodeFunctionData('check', [value]), - clipboard: clipboard, - }; - } - - static buildEqual2Equal({ - sdk, - source, - swap, - target, - slippage, - }: BuildPipeCallArgsEqual): AdvancedPipeStruct[] { - const pipe: AdvancedPipeStruct[] = []; - - const sourceWellAmountsOut = [ - source.beanAmountOut, - source.nonBeanAmountOut, - ]; - - const sourceWellBeanIndex = source.well.tokens.findIndex((t) => - t.equals(sdk.tokens.BEAN) - ); - - if (sourceWellBeanIndex !== 0) { - sourceWellAmountsOut.reverse(); - } - - // 0. Approve source well to spend LP tokens - pipe.push( - PipelineConvertUtil.erc20Approve(source.well.lpToken, source.well.address) - ); - - // 1. Remove liquidity from source well & set recipient to pipeline - pipe.push( - PipelineConvertUtil.getRemoveLiquidityEqual( - source.well, - source.lpAmountIn, - sourceWellAmountsOut, - sdk.contracts.pipeline.address - ) - ); - - // 2. Approve 0x - pipe.push( - PipelineConvertUtil.erc20Approve( - swap.sellToken, - swap.quote.allowanceTarget - ) - ); - - // 3. Swap nonBeanToken1 for nonBeanToken2. recipient MUST be Pipeline or this will fail. - pipe.push({ - target: swap.quote.to, - callData: swap.quote.data, - clipboard: Clipboard.encode([]), - }); - - // 4. transfer BuyToken to target well - pipe.push( - PipelineConvertUtil.transferToken( - swap.buyToken, - target.well.address, - ethers.constants.Zero, - Clipboard.encodeSlot(3, 0, 1) - ) - ); - - // 5. Transfer well.tokens[0] to target well - pipe.push( - PipelineConvertUtil.transferToken( - source.well.tokens[sourceWellBeanIndex], - target.well.address, - ethers.constants.Zero, - Clipboard.encodeSlot(1, 2, 1) - ) - ); - - const minLPOut = target.amountOut.subSlippage(slippage).toBigNumber(); - - // 6. Call Sync on target well - pipe.push( - PipelineConvertUtil.wellSync( - target.well, - sdk.contracts.pipeline.address, - minLPOut - ) - ); - - // 7. Check if amount receieved from sync >= minLPOut - pipe.push( - PipelineConvertUtil.junctionGte( - sdk.contracts.junction, - minLPOut.add(1), // add 1 to ensure copy clip gte is true - minLPOut, - Clipboard.encodeSlot(6, 0, 0) - ) - ); - - // 8. Check 7 is true - pipe.push( - PipelineConvertUtil.junctionCheck( - sdk.contracts.junction, - false, // default false - Clipboard.encodeSlot(7, 0, 0) - ) - ); - - return pipe; - } -} From 50692ad5616b3b1823d6ae7d56512565f41f7fdb Mon Sep 17 00:00:00 2001 From: Spacebean Date: Mon, 23 Sep 2024 13:55:33 -0600 Subject: [PATCH 16/16] feat: pipeconvert integration finish --- .../Actions/Convert/PipelineConvertForm.tsx | 6 ++-- .../components/Silo/Actions/Convert/index.tsx | 36 +++++++++++++------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index 60ff40e0d..b52646a3d 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -34,7 +34,7 @@ import WarningAlert from '~/components/Common/Alert/WarningAlert'; import TxnAccordion from '~/components/Common/TxnAccordion'; import StatHorizontal from '~/components/Common/StatHorizontal'; -import { ActionType, displayFullBN, transform } from '~/util'; +import { ActionType, displayFullBN } from '~/util'; import { useAppSelector } from '~/state'; import { BaseConvertFormProps } from './types'; @@ -145,7 +145,7 @@ const PipelineConvertFormInner = ({ ); setFieldValue('pipe.structs', advPipeCalls); - + setFieldValue('pipe.amountOut', new BigNumber(amountOut.toHuman())); return { amountOut, advPipeCalls @@ -178,8 +178,6 @@ const PipelineConvertFormInner = ({ targetToken.address, data.advPipeCalls ); - const toAmount = transform(result.toAmount, 'bnjs', targetToken); - setFieldValue('pipe.amountOut', toAmount); setConvertResults({ toAmount: result.toAmount, diff --git a/projects/ui/src/components/Silo/Actions/Convert/index.tsx b/projects/ui/src/components/Silo/Actions/Convert/index.tsx index c4f84589e..6ae0e675a 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/index.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/index.tsx @@ -178,6 +178,7 @@ const ConvertFormsWrapper = ({ fromToken }: ConvertProps) => { const pipelineConvertSubmitHandler = usePipelineConvertSubmitHandler({ sdk, + fromToken, farmerSilo, middleware, initialValues, @@ -243,9 +244,11 @@ function usePipelineConvertSubmitHandler({ sdk, farmerSilo, middleware, + fromToken, initialValues, }: { sdk: BeanstalkSDK; + fromToken: ERC20Token; farmerSilo: FarmerSilo; middleware: ReturnType; initialValues: ConvertFormValues; @@ -276,7 +279,7 @@ function usePipelineConvertSubmitHandler({ const advPipeStructs = values.pipe?.structs || []; const slippage = values?.settings?.slippage; - const farmerBalances = farmerSilo.balancesSdk.get(tokenIn); + const farmerBalances = farmerSilo.balancesSdk.get(fromToken); const amountIn = tokenIn?.amount(_amountIn?.toString() || '0'); // amount of from token const amountOut = tokenOut?.amount(_amountOut?.toString() || '0'); // amount of to token @@ -308,16 +311,26 @@ function usePipelineConvertSubmitHandler({ throw new Error('Could not find any crates to convert'); } - // farm.add( - // new sdk.farm.actions.PipelineConvert( - // tokenIn as ERC20Token, - // crates.map((c) => c.stem), - // crates.map((c) => c.amount.toBigNumber()), - // tokenOut as ERC20Token, - // amountOut, - // advPipeStructs - // ) - // ); + farm.add(() => ({ + name: 'pipe-convert', + amountOut: amountOut.toBigNumber(), + prepare: () => ({ + target: sdk.contracts.beanstalk.address, + callData: sdk.contracts.beanstalk.interface.encodeFunctionData( + 'pipelineConvert', + [ + tokenIn.address, + crates.map((c) => c.stem), + crates.map((c) => c.amount.toBigNumber()), + tokenOut.address, + advPipeStructs, + ] + ), + clipboard: undefined, + }), + decode: () => undefined, + decodeResult: () => undefined, + })); const gasEstimate = await farm.estimateGas(amountIn, { slippage }); const adjustedGas = Math.round(gasEstimate.toNumber() * 1.2).toString(); @@ -351,6 +364,7 @@ function usePipelineConvertSubmitHandler({ [ sdk, account, + fromToken, farmerSilo.balancesSdk, initialValues, middleware,