diff --git a/lib/modules/pool/PoolList/PoolListSearch.tsx b/lib/modules/pool/PoolList/PoolListSearch.tsx index a202b217e..1720e0391 100644 --- a/lib/modules/pool/PoolList/PoolListSearch.tsx +++ b/lib/modules/pool/PoolList/PoolListSearch.tsx @@ -6,6 +6,7 @@ import { useEffect } from 'react' import { useDebounce } from '@/lib/shared/hooks/useDebounce' import { usePoolList } from './usePoolList' import { useBreakpoints } from '@/lib/shared/hooks/useBreakpoints' +import { defaultDebounceMs } from '@/lib/shared/utils/queries' const SEARCH = 'search' @@ -20,7 +21,7 @@ export function PoolListSearch() { setSearch(event.target.value) } - const debouncedChangeHandler = useDebounce(changeHandler, 300) + const debouncedChangeHandler = useDebounce(changeHandler, defaultDebounceMs) useEffect(() => { reset({ diff --git a/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts b/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts new file mode 100644 index 000000000..c4934baac --- /dev/null +++ b/lib/modules/pool/actions/LiquidityActionHelpers.spec.ts @@ -0,0 +1,23 @@ +import { hasValidHumanAmounts } from './LiquidityActionHelpers' +import { HumanAmountIn } from './liquidity-types' + +describe('hasValidHumanAmounts', () => { + test('when all humanAmounts are empty', () => { + const humanAmountsIn: HumanAmountIn[] = [ + { tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', humanAmount: '' }, + { tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', humanAmount: '' }, + ] + expect(hasValidHumanAmounts(humanAmountsIn)).toBeFalsy() + }) + test('when all humanAmounts are zero', () => { + const humanAmountsIn: HumanAmountIn[] = [ + { tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', humanAmount: '0' }, + { tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', humanAmount: '0' }, + ] + expect(hasValidHumanAmounts(humanAmountsIn)).toBeFalsy() + }) + test('when humanAmounts is an empty array', () => { + const humanAmountsIn: HumanAmountIn[] = [] + expect(hasValidHumanAmounts(humanAmountsIn)).toBeFalsy() + }) +}) diff --git a/lib/modules/pool/actions/add-liquidity/AddLiquidityHelpers.ts b/lib/modules/pool/actions/LiquidityActionHelpers.ts similarity index 63% rename from lib/modules/pool/actions/add-liquidity/AddLiquidityHelpers.ts rename to lib/modules/pool/actions/LiquidityActionHelpers.ts index 99c374662..31bd0649a 100644 --- a/lib/modules/pool/actions/add-liquidity/AddLiquidityHelpers.ts +++ b/lib/modules/pool/actions/LiquidityActionHelpers.ts @@ -1,15 +1,15 @@ import { getChainId, getNetworkConfig } from '@/lib/config/app.config' import { TokenAmountToApprove } from '@/lib/modules/tokens/approvals/approval-rules' import { nullAddress } from '@/lib/modules/web3/contracts/wagmi-helpers' -import { PoolStateInput } from '@balancer/sdk' -import { keyBy } from 'lodash' +import { isSameAddress } from '@/lib/shared/utils/addresses' +import { HumanAmount, PoolStateInput } from '@balancer/sdk' +import { Dictionary, keyBy } from 'lodash' import { parseUnits } from 'viem' import { Address } from 'wagmi' -import { toPoolStateInput } from '../../pool.helpers' -import { Pool } from '../../usePool' -import { HumanAmountInWithTokenInfo } from './AddLiquidityFlowButton' -import { HumanAmountIn } from './add-liquidity.types' -import { isSameAddress } from '@/lib/shared/utils/addresses' +import { toPoolStateInput } from '../pool.helpers' +import { Pool } from '../usePool' +import { HumanAmountIn } from './liquidity-types' +import { GqlToken } from '@/lib/shared/services/api/generated/graphql' // TODO: this should be imported from the SDK export type InputAmount = { @@ -27,10 +27,10 @@ const NullPool: Pool = { } as unknown as Pool /* - AddLiquidityHelpers provides helper methods to traverse the pool state and prepare data structures needed by add liquidity handlers - to implement the AddLiquidityHandler interface + This class provides helper methods to traverse the pool state and prepare data structures needed by add/remove liquidity handlers + to implement the Add/RemoveLiquidityHandler interface */ -export class AddLiquidityHelpers { +export class LiquidityActionHelpers { constructor(public pool: Pool = NullPool) {} public get poolStateInput(): PoolStateInput { @@ -50,15 +50,16 @@ export class AddLiquidityHelpers { } public getAmountsToApprove( - humanAmountsInWithTokenInfo: HumanAmountInWithTokenInfo[] + humanAmountsIn: HumanAmountIn[], + tokensByAddress: Dictionary ): TokenAmountToApprove[] { - return this.toInputAmounts(humanAmountsInWithTokenInfo).map(({ address, rawAmount }, index) => { - const humanAmountWithInfo = humanAmountsInWithTokenInfo[index] + return this.toInputAmounts(humanAmountsIn).map(({ address, rawAmount }, index) => { + const humanAmountIn = humanAmountsIn[index] return { tokenAddress: address, - humanAmount: humanAmountWithInfo.humanAmount || '0', + humanAmount: humanAmountIn.humanAmount || '0', rawAmount, - tokenSymbol: humanAmountWithInfo.symbol, + tokenSymbol: tokensByAddress[humanAmountIn.tokenAddress].symbol, } }) } @@ -97,3 +98,14 @@ export class AddLiquidityHelpers { return humanAmountsIn.some(amountIn => isSameAddress(amountIn.tokenAddress, nativeAssetAddress)) } } + +export const isEmptyAmount = (amountIn: HumanAmountIn) => isEmptyHumanAmount(amountIn.humanAmount) + +export const isEmptyHumanAmount = (humanAmount: HumanAmount | '') => + !humanAmount || humanAmount === '0' + +export const areEmptyAmounts = (humanAmountsIn: HumanAmountIn[]) => + !humanAmountsIn || humanAmountsIn.length === 0 || humanAmountsIn.every(isEmptyAmount) + +export const hasValidHumanAmounts = (humanAmountsIn: HumanAmountIn[]) => + humanAmountsIn.some(a => a.humanAmount && a.humanAmount !== '0') diff --git a/lib/modules/pool/actions/add-liquidity/AddLiquidityFlowButton.tsx b/lib/modules/pool/actions/add-liquidity/AddLiquidityFlowButton.tsx index 6eed866ff..0cbfa554f 100644 --- a/lib/modules/pool/actions/add-liquidity/AddLiquidityFlowButton.tsx +++ b/lib/modules/pool/actions/add-liquidity/AddLiquidityFlowButton.tsx @@ -2,26 +2,30 @@ import { useNextTokenApprovalStep } from '@/lib/modules/tokens/approvals/useNext import TransactionFlow from '@/lib/shared/components/btns/transaction-steps/TransactionFlow' import { GqlToken } from '@/lib/shared/services/api/generated/graphql' import { Text, VStack } from '@chakra-ui/react' -import { HumanAmountIn } from './add-liquidity.types' import { useConstructAddLiquidityStep } from './useConstructAddLiquidityStep' import { useAddLiquidity } from './useAddLiquidity' - -export type HumanAmountInWithTokenInfo = HumanAmountIn & GqlToken +import { HumanAmountIn } from '../liquidity-types' +import { useTokens } from '@/lib/modules/tokens/useTokens' +import { zipObject } from 'lodash' +import { Pool } from '../../usePool' type Props = { - humanAmountsInWithTokenInfo: HumanAmountInWithTokenInfo[] - poolId: string + humanAmountsIn: HumanAmountIn[] + pool: Pool } -export function AddLiquidityFlowButton({ humanAmountsInWithTokenInfo, poolId }: Props) { +export function AddLiquidityFlowButton({ humanAmountsIn, pool }: Props) { const { helpers } = useAddLiquidity() + const { getToken } = useTokens() + + const tokenAddresses = humanAmountsIn.map(h => h.tokenAddress) + const tokens = humanAmountsIn.map(h => getToken(h.tokenAddress, pool.chain)) + const tokensByAddress = zipObject(tokenAddresses, tokens as GqlToken[]) + const { tokenApprovalStep, initialAmountsToApprove } = useNextTokenApprovalStep( - helpers.getAmountsToApprove(humanAmountsInWithTokenInfo) + helpers.getAmountsToApprove(humanAmountsIn, tokensByAddress) ) - const { step: addLiquidityStep } = useConstructAddLiquidityStep( - humanAmountsInWithTokenInfo, - poolId - ) + const { step: addLiquidityStep } = useConstructAddLiquidityStep(pool.id) const steps = [tokenApprovalStep, addLiquidityStep] function handleJoinCompleted() { diff --git a/lib/modules/pool/actions/add-liquidity/AddLiquidityForm.tsx b/lib/modules/pool/actions/add-liquidity/AddLiquidityForm.tsx index 5a318950c..e7f948509 100644 --- a/lib/modules/pool/actions/add-liquidity/AddLiquidityForm.tsx +++ b/lib/modules/pool/actions/add-liquidity/AddLiquidityForm.tsx @@ -23,18 +23,20 @@ import { useRef } from 'react' import { Address } from 'wagmi' import { AddLiquidityModal } from './AddLiquidityModal' import { useAddLiquidity } from './useAddLiquidity' +import { fNum, safeTokenFormat } from '@/lib/shared/utils/numbers' +import { BPT_DECIMALS } from '../../pool.constants' export function AddLiquidityForm() { const { - amountsIn, + humanAmountsIn: amountsIn, totalUSDValue, - setAmountIn, + setHumanAmountIn: setAmountIn, tokens, validTokens, - formattedPriceImpact, + priceImpact, isPriceImpactLoading, - bptOutUnits, - isBptOutQueryLoading, + bptOut, + isPreviewQueryLoading, isDisabled, disabledReason, } = useAddLiquidity() @@ -48,6 +50,9 @@ export function AddLiquidityForm() { return amountIn ? amountIn.humanAmount : '' } + const bptOutLabel = safeTokenFormat(bptOut?.amount, BPT_DECIMALS) + const formattedPriceImpact = priceImpact ? fNum('priceImpact', priceImpact) : '-' + return (
@@ -100,7 +105,7 @@ export function AddLiquidityForm() { Bpt out - {isBptOutQueryLoading ? : bptOutUnits} + {isPreviewQueryLoading ? : bptOutLabel} diff --git a/lib/modules/pool/actions/add-liquidity/AddLiquidityModal.tsx b/lib/modules/pool/actions/add-liquidity/AddLiquidityModal.tsx index 0d1be98c9..9aaf7dc50 100644 --- a/lib/modules/pool/actions/add-liquidity/AddLiquidityModal.tsx +++ b/lib/modules/pool/actions/add-liquidity/AddLiquidityModal.tsx @@ -7,6 +7,7 @@ import { TokenAllowancesProvider } from '@/lib/modules/web3/useTokenAllowances' import { useUserAccount } from '@/lib/modules/web3/useUserAccount' import { NumberText } from '@/lib/shared/components/typography/NumberText' import { useCurrency } from '@/lib/shared/hooks/useCurrency' +import { isSameAddress } from '@/lib/shared/utils/addresses' import { fNum } from '@/lib/shared/utils/numbers' import { HumanAmount } from '@balancer/sdk' import { InfoOutlineIcon } from '@chakra-ui/icons' @@ -27,9 +28,13 @@ import { VStack, } from '@chakra-ui/react' import { RefObject, useRef } from 'react' +import { formatUnits } from 'viem' import { Address } from 'wagmi' +import { BPT_DECIMALS } from '../../pool.constants' +import { bptUsdValue } from '../../pool.helpers' import { usePool } from '../../usePool' -import { AddLiquidityFlowButton, HumanAmountInWithTokenInfo } from './AddLiquidityFlowButton' +import { HumanAmountIn } from '../liquidity-types' +import { AddLiquidityFlowButton } from './AddLiquidityFlowButton' import { useAddLiquidity } from './useAddLiquidity' type Props = { @@ -43,17 +48,24 @@ function TokenAmountRow({ tokenAddress, humanAmount, symbol, + isBpt, }: { tokenAddress: Address humanAmount: HumanAmount | '' symbol?: string + isBpt?: boolean }) { const { pool } = usePool() const { getToken, usdValueForToken } = useTokens() const { toCurrency } = useCurrency() const token = getToken(tokenAddress, pool.chain) - const usdValue = token ? usdValueForToken(token, humanAmount) : undefined + let usdValue: string | undefined + if (isBpt) { + usdValue = bptUsdValue(pool, humanAmount) + } else { + usdValue = token ? usdValueForToken(token, humanAmount) : undefined + } return ( @@ -79,19 +91,15 @@ export function AddLiquidityModal({ ...rest }: Props & Omit) { const initialFocusRef = useRef(null) - const { amountsIn, totalUSDValue, helpers, formattedPriceImpact, bptOutUnits } = useAddLiquidity() + const { humanAmountsIn, totalUSDValue, helpers, bptOut, priceImpact, tokens } = useAddLiquidity() const { toCurrency } = useCurrency() const { pool } = usePool() // TODO: move userAddress up const spenderAddress = useContractAddress('balancer.vaultV2') const { userAddress } = useUserAccount() - const { getToken } = useTokens() - const humanAmountsInWithTokenInfo: HumanAmountInWithTokenInfo[] = amountsIn.map(humanAmountIn => { - return { - ...humanAmountIn, - ...getToken(humanAmountIn.tokenAddress, pool.chain), - } as HumanAmountInWithTokenInfo - }) + + const bptOutLabel = bptOut ? formatUnits(bptOut.amount, BPT_DECIMALS) : '0' + const formattedPriceImpact = priceImpact ? fNum('priceImpact', priceImpact) : '-' return ( {"You're adding"} {toCurrency(totalUSDValue)} - {amountsIn.map(amountIn => ( - - ))} + {tokens.map(token => { + if (!token) return
Missing token
+ + const amountIn = humanAmountsIn.find(amountIn => + isSameAddress(amountIn.tokenAddress, token?.address) + ) as HumanAmountIn + + return + })} @@ -128,12 +142,12 @@ export function AddLiquidityModal({ {"You'll get (if no slippage)"} - {pool.symbol} @@ -160,8 +174,8 @@ export function AddLiquidityModal({ tokenAddresses={helpers.poolTokenAddresses} > diff --git a/lib/modules/pool/actions/add-liquidity/add-liquidity.helpers.ts b/lib/modules/pool/actions/add-liquidity/add-liquidity.helpers.ts deleted file mode 100644 index 4c6abc76f..000000000 --- a/lib/modules/pool/actions/add-liquidity/add-liquidity.helpers.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { HumanAmountIn } from './add-liquidity.types' - -export const isEmptyAmount = (amountIn: HumanAmountIn) => - !amountIn.humanAmount || amountIn.humanAmount === '0' - -export const areEmptyAmounts = (humanAmountsIn: HumanAmountIn[]) => - humanAmountsIn.every(isEmptyAmount) diff --git a/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts b/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts index 3901fcb12..696e65156 100644 --- a/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts +++ b/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts @@ -1,14 +1,10 @@ -import { AddLiquidityQueryOutput, HumanAmount, PriceImpact, TokenAmount } from '@balancer/sdk' +import { AddLiquidityQueryOutput, PriceImpact, TokenAmount } from '@balancer/sdk' import { Address } from 'wagmi' +import { HumanAmountIn } from '../liquidity-types' // TODO: this type should be exposed by the SDK export type PriceImpactAmount = Awaited> -export type HumanAmountIn = { - humanAmount: HumanAmount | '' - tokenAddress: Address -} - export type AddLiquidityInputs = { humanAmountsIn: HumanAmountIn[] account?: Address @@ -22,9 +18,6 @@ export type AddLiquidityOutputs = { sdkQueryOutput?: AddLiquidityQueryOutput } -// sdkQueryOutput is optional because it will be only used in cases where we use the SDK to query/build the transaction -// We will probably need a more abstract interface to be used by edge cases export type BuildLiquidityInputs = { inputs: AddLiquidityInputs - sdkQueryOutput?: AddLiquidityQueryOutput } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts index 0f39af125..6f239c3c7 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts @@ -7,8 +7,8 @@ import { /** * AddLiquidityHandler is an interface that defines the methods that must be implemented by a handler. - * They take standard inputs from the UI and return frontend standardised - * outputs. The outputs should not be return types from the SDK. This is to + * They take standard inputs from the UI and return frontend standardised outputs. + * The outputs should not be return types from the SDK. This is to * allow handlers to be developed in the future that may not use the SDK. */ export interface AddLiquidityHandler { diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts index 25aa21c3f..e5e8a4b4a 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts @@ -6,8 +6,8 @@ import { HumanAmount } from '@balancer/sdk' import { Address } from 'viem' import { aPhantomStablePoolStateInputMock } from '../../../__mocks__/pool.builders' import { Pool } from '../../../usePool' -import { HumanAmountIn } from '../add-liquidity.types' -import { selectAddLiquidityHandler } from '../selectAddLiquidityHandler' +import { selectAddLiquidityHandler } from './selectAddLiquidityHandler' +import { HumanAmountIn } from '../../liquidity-types' function selectUnbalancedHandler() { //TODO: refactor mock builders to build poolStateInput and pool at the same time @@ -63,7 +63,7 @@ describe('When adding unbalanced liquidity for a weighted pool', () => { const handler = selectUnbalancedHandler() - const { sdkQueryOutput } = await handler.queryAddLiquidity({ + await handler.queryAddLiquidity({ humanAmountsIn, }) @@ -72,14 +72,14 @@ describe('When adding unbalanced liquidity for a weighted pool', () => { account: defaultTestUserAccount, slippagePercent: '0.2', } - const result = await handler.buildAddLiquidityTx({ inputs, sdkQueryOutput }) + const result = await handler.buildAddLiquidityTx({ inputs }) expect(result.to).toBe(networkConfig.contracts.balancer.vaultV2) expect(result.data).toBeDefined() }) }) -describe('When adding unbalanced liquidity for an stable pool', () => { +describe('When adding unbalanced liquidity for a stable pool', () => { test('calculates price impact', async () => { const pool = aPhantomStablePoolStateInputMock() as Pool // wstETH-rETH-sfrxETH @@ -95,7 +95,7 @@ describe('When adding unbalanced liquidity for an stable pool', () => { }) const priceImpact = await handler.calculatePriceImpact({ humanAmountsIn }) - expect(priceImpact).toMatchInlineSnapshot(`0.006104055180098694`) + expect(priceImpact).toBeGreaterThan(0.001) }) }) diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index a94447919..b2df50391 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -3,21 +3,21 @@ import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' import { AddLiquidity, AddLiquidityKind, + AddLiquidityQueryOutput, AddLiquidityUnbalancedInput, PriceImpact, Slippage, } from '@balancer/sdk' -import { AddLiquidityHelpers } from '../AddLiquidityHelpers' -import { areEmptyAmounts } from '../add-liquidity.helpers' import { AddLiquidityInputs, AddLiquidityOutputs, BuildLiquidityInputs, - HumanAmountIn, PriceImpactAmount, } from '../add-liquidity.types' import { AddLiquidityHandler } from './AddLiquidity.handler' import { Pool } from '../../../usePool' +import { LiquidityActionHelpers, areEmptyAmounts } from '../../LiquidityActionHelpers' +import { HumanAmountIn } from '../../liquidity-types' /** * UnbalancedAddLiquidityHandler is a handler that implements the @@ -27,9 +27,11 @@ import { Pool } from '../../../usePool' * asset instead of the wrapped native asset. */ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { - addLiquidityHelpers: AddLiquidityHelpers + helpers: LiquidityActionHelpers + sdkQueryOutput?: AddLiquidityQueryOutput + constructor(pool: Pool) { - this.addLiquidityHelpers = new AddLiquidityHelpers(pool) + this.helpers = new LiquidityActionHelpers(pool) } public async queryAddLiquidity({ @@ -38,11 +40,9 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { const addLiquidity = new AddLiquidity() const addLiquidityInput = this.constructSdkInput(humanAmountsIn) - const sdkQueryOutput = await addLiquidity.query( - addLiquidityInput, - this.addLiquidityHelpers.poolStateInput - ) - return { bptOut: sdkQueryOutput.bptOut, sdkQueryOutput } + this.sdkQueryOutput = await addLiquidity.query(addLiquidityInput, this.helpers.poolStateInput) + + return { bptOut: this.sdkQueryOutput.bptOut } } public async calculatePriceImpact({ humanAmountsIn }: AddLiquidityInputs): Promise { @@ -55,7 +55,7 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { const priceImpactABA: PriceImpactAmount = await PriceImpact.addLiquidityUnbalanced( addLiquidityInput, - this.addLiquidityHelpers.poolStateInput + this.helpers.poolStateInput ) return priceImpactABA.decimal @@ -66,14 +66,19 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { */ public async buildAddLiquidityTx(buildInputs: BuildLiquidityInputs): Promise { const { account, slippagePercent } = buildInputs.inputs - const sdkQueryOutput = buildInputs.sdkQueryOutput if (!account || !slippagePercent) throw new Error('Missing account or slippage') - if (!sdkQueryOutput) throw new Error('Missing sdkQueryOutput') + if (!this.sdkQueryOutput) { + console.error('Missing sdkQueryOutput.') + throw new Error( + `Missing sdkQueryOutput. +It looks that you did not call useAddLiquidityBtpOutQuery before trying to build the tx config` + ) + } const addLiquidity = new AddLiquidity() const { call, to, value } = addLiquidity.buildCall({ - ...sdkQueryOutput, + ...this.sdkQueryOutput, slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), sender: account, recipient: account, @@ -81,7 +86,7 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { return { account, - chainId: this.addLiquidityHelpers.chainId, + chainId: this.helpers.chainId, data: call, to, value, @@ -92,14 +97,14 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { * PRIVATE METHODS */ private constructSdkInput(humanAmountsIn: HumanAmountIn[]): AddLiquidityUnbalancedInput { - const amountsIn = this.addLiquidityHelpers.toInputAmounts(humanAmountsIn) + const amountsIn = this.helpers.toInputAmounts(humanAmountsIn) return { - chainId: this.addLiquidityHelpers.chainId, - rpcUrl: getDefaultRpcUrl(this.addLiquidityHelpers.chainId), + chainId: this.helpers.chainId, + rpcUrl: getDefaultRpcUrl(this.helpers.chainId), amountsIn, kind: AddLiquidityKind.Unbalanced, - useNativeAssetAsWrappedAmountIn: this.addLiquidityHelpers.isNativeAssetIn(humanAmountsIn), + useNativeAssetAsWrappedAmountIn: this.helpers.isNativeAssetIn(humanAmountsIn), } } } diff --git a/lib/modules/pool/actions/add-liquidity/selectAddLiquidityHandler.ts b/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts similarity index 66% rename from lib/modules/pool/actions/add-liquidity/selectAddLiquidityHandler.ts rename to lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts index 59ab746cb..2cf741ecc 100644 --- a/lib/modules/pool/actions/add-liquidity/selectAddLiquidityHandler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts @@ -1,8 +1,8 @@ import { getChainId } from '@/lib/config/app.config' -import { Pool } from '../../usePool' -import { AddLiquidityHandler } from './handlers/AddLiquidity.handler' -import { TwammAddLiquidityHandler } from './handlers/TwammAddLiquidity.handler' -import { UnbalancedAddLiquidityHandler } from './handlers/UnbalancedAddLiquidity.handler' +import { Pool } from '../../../usePool' +import { AddLiquidityHandler } from './AddLiquidity.handler' +import { TwammAddLiquidityHandler } from './TwammAddLiquidity.handler' +import { UnbalancedAddLiquidityHandler } from './UnbalancedAddLiquidity.handler' export function selectAddLiquidityHandler(pool: Pool) { // TODO: Depending on the pool attributes we will return a different handler diff --git a/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.spec.ts b/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.spec.ts new file mode 100644 index 000000000..c65abbd82 --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.spec.ts @@ -0,0 +1,36 @@ +/* eslint-disable max-len */ +import { defaultTestUserAccount } from '@/test/utils/wagmi' +import { poolId } from '@/lib/debug-helpers' +import { HumanAmountIn } from '../../liquidity-types' +import { addLiquidityKeys } from './add-liquidity-keys' + +test('Generates expected query keys', () => { + const humanAmountsIn: HumanAmountIn[] = [ + { tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', humanAmount: '0' }, + { tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', humanAmount: '0' }, + ] + const result = addLiquidityKeys.priceImpact({ + userAddress: defaultTestUserAccount, + poolId, + slippage: '0.2', + humanAmountsIn, + }) + expect(result).toMatchInlineSnapshot( + ` + [ + "add-liquidity", + "price-impact", + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:0x68e3266c9c8bbd44ad9dca5afbfe629022aee9fe000200000000000000000512:0.2:[{"tokenAddress":"0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f","humanAmount":"0"},{"tokenAddress":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","humanAmount":"0"}]", + ] + ` + ) + + const result2 = addLiquidityKeys.priceImpact({ + userAddress: defaultTestUserAccount, + poolId, + slippage: '0.3', + humanAmountsIn, + }) + + expect(result).not.toEqual(result2) +}) diff --git a/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts b/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts new file mode 100644 index 000000000..01aeb14e3 --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts @@ -0,0 +1,19 @@ +import { HumanAmountIn } from '../../liquidity-types' + +const addLiquidity = 'add-liquidity' + +type LiquidityParams = { + userAddress: string + poolId: string + slippage: string + humanAmountsIn: HumanAmountIn[] +} +function liquidityParams({ userAddress, poolId, slippage, humanAmountsIn }: LiquidityParams) { + return `${userAddress}:${poolId}:${slippage}:${JSON.stringify(humanAmountsIn)}` +} +export const addLiquidityKeys = { + priceImpact: (params: LiquidityParams) => + [addLiquidity, 'price-impact', liquidityParams(params)] as const, + preview: (params: LiquidityParams) => [addLiquidity, 'preview', liquidityParams(params)] as const, + buildTx: (params: LiquidityParams) => [addLiquidity, 'buildTx', liquidityParams(params)] as const, +} diff --git a/lib/modules/pool/actions/add-liquidity/queries/generateAddLiquidityQueryKey.ts b/lib/modules/pool/actions/add-liquidity/queries/generateAddLiquidityQueryKey.ts deleted file mode 100644 index 76e7026ec..000000000 --- a/lib/modules/pool/actions/add-liquidity/queries/generateAddLiquidityQueryKey.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HumanAmountIn } from '../add-liquidity.types' - -type Props = { - userAddress: string - poolId: string - slippage: string - humanAmountsIn: HumanAmountIn[] -} - -export function generateAddLiquidityQueryKey({ - userAddress, - poolId, - slippage, - humanAmountsIn, -}: Props): string { - return `${userAddress}:${poolId}:${slippage}:${JSON.stringify(humanAmountsIn)}` -} diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBtpOutQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBtpOutQuery.ts deleted file mode 100644 index 3d109a98e..000000000 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBtpOutQuery.ts +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' -import { useUserAccount } from '@/lib/modules/web3/useUserAccount' -import { AddLiquidityQueryOutput, TokenAmount } from '@balancer/sdk' -import { useState } from 'react' -import { useDebounce } from 'use-debounce' -import { formatUnits } from 'viem' -import { useQuery } from 'wagmi' -import { areEmptyAmounts } from '../add-liquidity.helpers' -import { HumanAmountIn } from '../add-liquidity.types' -import { AddLiquidityHandler } from '../handlers/AddLiquidity.handler' -import { generateAddLiquidityQueryKey } from './generateAddLiquidityQueryKey' -import { fNum } from '@/lib/shared/utils/numbers' - -const debounceMillis = 300 - -export function useAddLiquidityBtpOutQuery( - handler: AddLiquidityHandler, - humanAmountsIn: HumanAmountIn[], - poolId: string -) { - const { userAddress } = useUserAccount() - const { slippage } = useUserSettings() - const [bptOut, setBptOut] = useState(null) - const [lastSdkQueryOutput, setLastSdkQueryOutput] = useState( - undefined - ) - const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, debounceMillis) - - function queryKey(): string { - return generateAddLiquidityQueryKey({ - userAddress, - poolId, - slippage, - humanAmountsIn: debouncedHumanAmountsIn as unknown as HumanAmountIn[], - }) - } - - async function queryBptOut() { - const queryResult = await handler.queryAddLiquidity({ humanAmountsIn }) - - const { bptOut } = queryResult - - setBptOut(bptOut) - - // Only SDK handlers will return this output - if (queryResult.sdkQueryOutput) { - setLastSdkQueryOutput(queryResult.sdkQueryOutput) - } - return bptOut - } - - const query = useQuery( - [queryKey()], - async () => { - return await queryBptOut() - }, - { - enabled: !!userAddress && !areEmptyAmounts(humanAmountsIn), - } - ) - - const bptOutUnits = bptOut ? fNum('token', formatUnits(bptOut.amount, 18)) : '-' - - return { bptOut, bptOutUnits, isBptOutQueryLoading: query.isLoading, lastSdkQueryOutput } -} diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBtpOutQuery.integration.spec.tsx b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPreviewQuery.integration.spec.tsx similarity index 65% rename from lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBtpOutQuery.integration.spec.tsx rename to lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPreviewQuery.integration.spec.tsx index 55129c07c..490ff0d2b 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBtpOutQuery.integration.spec.tsx +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPreviewQuery.integration.spec.tsx @@ -4,14 +4,14 @@ import { waitFor } from '@testing-library/react' import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' import { defaultTestUserAccount } from '@/test/utils/wagmi' -import { HumanAmountIn } from '../add-liquidity.types' -import { selectAddLiquidityHandler } from '../selectAddLiquidityHandler' -import { useAddLiquidityBtpOutQuery } from './useAddLiquidityBtpOutQuery' +import { selectAddLiquidityHandler } from '../handlers/selectAddLiquidityHandler' +import { useAddLiquidityPreviewQuery } from './useAddLiquidityPreviewQuery' +import { HumanAmountIn } from '../../liquidity-types' async function testQuery(humanAmountsIn: HumanAmountIn[]) { const handler = selectAddLiquidityHandler(aWjAuraWethPoolElementMock()) const { result } = testHook(() => - useAddLiquidityBtpOutQuery(handler, humanAmountsIn, defaultTestUserAccount) + useAddLiquidityPreviewQuery(handler, humanAmountsIn, defaultTestUserAccount) ) return result } @@ -26,7 +26,6 @@ test('queries btp out for add liquidity', async () => { await waitFor(() => expect(result.current.bptOut).not.toBeNull()) - expect(result.current.bptOut).toBeDefined() - expect(result.current.bptOutUnits).toBe('33.4271k') - expect(result.current.isBptOutQueryLoading).toBeFalsy() + expect(result.current.bptOut?.amount).toBeDefined() + expect(result.current.isPreviewQueryLoading).toBeFalsy() }) diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPreviewQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPreviewQuery.ts new file mode 100644 index 000000000..bdb07ed8f --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPreviewQuery.ts @@ -0,0 +1,53 @@ +'use client' + +import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' +import { useUserAccount } from '@/lib/modules/web3/useUserAccount' +import { defaultDebounceMs } from '@/lib/shared/utils/queries' +import { TokenAmount } from '@balancer/sdk' +import { useState } from 'react' +import { useDebounce } from 'use-debounce' +import { useQuery } from 'wagmi' +import { hasValidHumanAmounts } from '../../LiquidityActionHelpers' +import { HumanAmountIn } from '../../liquidity-types' +import { AddLiquidityHandler } from '../handlers/AddLiquidity.handler' +import { addLiquidityKeys } from './add-liquidity-keys' + +export function useAddLiquidityPreviewQuery( + handler: AddLiquidityHandler, + humanAmountsIn: HumanAmountIn[], + poolId: string +) { + const { userAddress, isConnected } = useUserAccount() + const { slippage } = useUserSettings() + const [bptOut, setBptOut] = useState(null) + const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, defaultDebounceMs)[0] + + async function queryBptOut() { + const queryResult = await handler.queryAddLiquidity({ humanAmountsIn }) + + const { bptOut } = queryResult + + setBptOut(bptOut) + + return bptOut + } + + const query = useQuery( + addLiquidityKeys.priceImpact({ + userAddress, + slippage, + poolId, + humanAmountsIn: debouncedHumanAmountsIn, + }), + async () => { + return await queryBptOut() + }, + { + enabled: isConnected && hasValidHumanAmounts(debouncedHumanAmountsIn), + // TODO: remove when finishing debugging + onError: error => console.log('Error in queryBptOut', error), + } + ) + + return { bptOut, isPreviewQueryLoading: query.isLoading } +} diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.integration.spec.tsx b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.integration.spec.tsx index c31efb8f0..8a1cd57f1 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.integration.spec.tsx +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.integration.spec.tsx @@ -4,9 +4,9 @@ import { waitFor } from '@testing-library/react' import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' import { defaultTestUserAccount } from '@/test/utils/wagmi' -import { HumanAmountIn } from '../add-liquidity.types' -import { selectAddLiquidityHandler } from '../selectAddLiquidityHandler' +import { selectAddLiquidityHandler } from '../handlers/selectAddLiquidityHandler' import { useAddLiquidityPriceImpactQuery } from './useAddLiquidityPriceImpactQuery' +import { HumanAmountIn } from '../../liquidity-types' async function testQuery(humanAmountsIn: HumanAmountIn[]) { const handler = selectAddLiquidityHandler(aWjAuraWethPoolElementMock()) @@ -24,9 +24,8 @@ test('queries price impact for add liquidity', async () => { const result = await testQuery(humanAmountsIn) - await waitFor(() => expect(result.current.formattedPriceImpact).not.toBe('-')) + await waitFor(() => expect(result.current.priceImpact).not.toBeNull()) - expect(result.current.priceImpact).toBeCloseTo(0.002368782867485742) - expect(result.current.formattedPriceImpact).toBe('0.24%') + expect(result.current.priceImpact).toBeCloseTo(0.005) expect(result.current.isPriceImpactLoading).toBeFalsy() }) diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.ts index b619d8c7b..23354546b 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityPriceImpactQuery.ts @@ -2,35 +2,24 @@ import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' import { useUserAccount } from '@/lib/modules/web3/useUserAccount' +import { defaultDebounceMs } from '@/lib/shared/utils/queries' import { useState } from 'react' import { useDebounce } from 'use-debounce' import { useQuery } from 'wagmi' -import { areEmptyAmounts } from '../add-liquidity.helpers' -import { HumanAmountIn } from '../add-liquidity.types' +import { areEmptyAmounts } from '../../LiquidityActionHelpers' +import { HumanAmountIn } from '../../liquidity-types' import { AddLiquidityHandler } from '../handlers/AddLiquidity.handler' -import { generateAddLiquidityQueryKey } from './generateAddLiquidityQueryKey' -import { fNum } from '@/lib/shared/utils/numbers' - -const debounceMillis = 250 +import { addLiquidityKeys } from './add-liquidity-keys' export function useAddLiquidityPriceImpactQuery( handler: AddLiquidityHandler, humanAmountsIn: HumanAmountIn[], poolId: string ) { - const { userAddress } = useUserAccount() + const { userAddress, isConnected } = useUserAccount() const { slippage } = useUserSettings() const [priceImpact, setPriceImpact] = useState(null) - const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, debounceMillis) - - function queryKey(): string { - return generateAddLiquidityQueryKey({ - userAddress, - poolId, - slippage, - humanAmountsIn: debouncedHumanAmountsIn as unknown as HumanAmountIn[], - }) - } + const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, defaultDebounceMs)[0] async function queryPriceImpact() { const _priceImpact = await handler.calculatePriceImpact({ @@ -42,15 +31,19 @@ export function useAddLiquidityPriceImpactQuery( } const query = useQuery( - [queryKey()], + addLiquidityKeys.preview({ + userAddress, + slippage, + poolId, + humanAmountsIn: debouncedHumanAmountsIn, + }), async () => { return await queryPriceImpact() }, { - enabled: !!userAddress && !areEmptyAmounts(humanAmountsIn), + enabled: isConnected && !areEmptyAmounts(humanAmountsIn), } ) - const formattedPriceImpact = priceImpact ? fNum('priceImpact', priceImpact) : '-' - return { priceImpact, formattedPriceImpact, isPriceImpactLoading: query.isLoading } + return { priceImpact, isPriceImpactLoading: query.isLoading } } diff --git a/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.integration.spec.tsx b/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.integration.spec.tsx deleted file mode 100644 index 4db761b6a..000000000 --- a/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.integration.spec.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { poolId, wETHAddress, wjAuraAddress } from '@/lib/debug-helpers' -import { - DefaultTokenAllowancesTestProvider, - buildDefaultPoolTestProvider, - testHook, -} from '@/test/utils/custom-renderers' -import { defaultTestUserAccount } from '@/test/utils/wagmi' -import { waitFor } from '@testing-library/react' - -import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' -import { PropsWithChildren } from 'react' -import { HumanAmountIn } from '../add-liquidity.types' -import { useBuildAddLiquidityQuery } from './useBuildAddLiquidityTxQuery' - -const PoolProvider = buildDefaultPoolTestProvider(aWjAuraWethPoolElementMock()) - -export const Providers = ({ children }: PropsWithChildren) => ( - - {children} - -) - -async function testQuery(humanAmountsIn: HumanAmountIn[]) { - const enabled = true - const { result } = testHook(() => useBuildAddLiquidityQuery(humanAmountsIn, enabled, poolId), { - wrapper: Providers, - }) - return result -} - -test('does not build add liquidity config when user is not connected', async () => { - const result = await testQuery([]) - - await waitFor(() => expect(result.current.isLoading).toBeFalsy()) - - expect(result.current.data).toBeUndefined() -}) - -test.skip('builds add liquidity config when user is connected', async () => { - const humanAmountsIn: HumanAmountIn[] = [ - { tokenAddress: wETHAddress, humanAmount: '1' }, - { tokenAddress: wjAuraAddress, humanAmount: '1' }, - ] - - const result = await testQuery(humanAmountsIn) - - await waitFor(() => expect(result.current.data?.to).toBeDefined()) - - expect(result.current.data?.account).toBe(defaultTestUserAccount) -}) diff --git a/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.ts index c1a936100..1619ce1ca 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/useBuildAddLiquidityTxQuery.ts @@ -1,46 +1,47 @@ 'use client' import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' +import { useTokenAllowances } from '@/lib/modules/web3/useTokenAllowances' import { useUserAccount } from '@/lib/modules/web3/useUserAccount' import { Dictionary } from 'lodash' import { useQuery } from 'wagmi' -import { HumanAmountIn } from '../add-liquidity.types' -import { useAddLiquidity } from '../useAddLiquidity' -import { generateAddLiquidityQueryKey } from './generateAddLiquidityQueryKey' +import { HumanAmountIn } from '../../liquidity-types' +import { AddLiquidityHandler } from '../handlers/AddLiquidity.handler' +import { addLiquidityKeys } from './add-liquidity-keys' // Uses the SDK to build a transaction config to be used by wagmi's useManagedSendTransaction export function useBuildAddLiquidityQuery( + handler: AddLiquidityHandler, humanAmountsIn: HumanAmountIn[], - enabled: boolean, + isActiveStep: boolean, poolId: string ) { const { userAddress, isConnected } = useUserAccount() - - const { buildAddLiquidityTx } = useAddLiquidity() const { slippage } = useUserSettings() - const allowances = {} - function queryKey(): string { - return generateAddLiquidityQueryKey({ + const { allowances } = useTokenAllowances() + + const addLiquidityQuery = useQuery( + addLiquidityKeys.buildTx({ userAddress, - poolId, slippage, + poolId, humanAmountsIn, - }) - } - - const addLiquidityQuery = useQuery( - [queryKey()], + }), async () => { const inputs = { humanAmountsIn, account: userAddress, slippagePercent: slippage, } - return await buildAddLiquidityTx(inputs) + return handler.buildAddLiquidityTx({ inputs }) }, { - enabled: enabled && isConnected && allowances && hasTokenAllowance(allowances), + enabled: + isActiveStep && // If the step is not active (the user did not click Next button) avoid running the build tx query to save RPC requests + isConnected && + allowances && + hasTokenAllowance(allowances), } ) @@ -48,6 +49,8 @@ export function useBuildAddLiquidityQuery( } function hasTokenAllowance(tokenAllowances: Dictionary) { + if (!tokenAllowances) return false + if (Object.values(tokenAllowances).length === 0) return false // TODO: depending on the user humanAmountsIn this rule will be different // Here we will check that the user has enough allowance for the current Join operation return Object.values(tokenAllowances).every(a => a > 0n) diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquidity.spec.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquidity.spec.tsx index 8e2b72220..3a3df756e 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquidity.spec.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquidity.spec.tsx @@ -3,8 +3,11 @@ import { aTokenExpandedMock } from '@/lib/modules/tokens/__mocks__/token.builder import { aGqlPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' import { buildDefaultPoolTestProvider, testHook } from '@/test/utils/custom-renderers' import { mainnet } from 'wagmi' -import { HumanAmountInWithTokenInfo } from './AddLiquidityFlowButton' import { _useAddLiquidity } from './useAddLiquidity' +import { HumanAmount } from '@balancer/sdk' +import { HumanAmountIn } from '../liquidity-types' +import { Dictionary } from 'lodash' +import { GqlToken } from '@/lib/shared/services/api/generated/graphql' async function testUseAddLiquidity() { const { result } = testHook(() => _useAddLiquidity(), { @@ -23,7 +26,7 @@ async function testUseAddLiquidity() { test('returns amountsIn with empty input amount by default', async () => { const result = await testUseAddLiquidity() - expect(result.current.amountsIn).toEqual([ + expect(result.current.humanAmountsIn).toEqual([ { tokenAddress: balAddress, humanAmount: '', @@ -41,16 +44,18 @@ test('returns add liquidity helpers', async () => { expect(result.current.helpers.chainId).toBe(mainnet.id) expect(result.current.helpers.poolTokenAddresses).toEqual([balAddress, wETHAddress]) - const humanAmountsIn = [ - { tokenAddress: balAddress, humanAmount: '1', symbol: 'BAL' }, - { tokenAddress: wETHAddress, humanAmount: '2', symbol: 'WETH' }, + const humanAmountsIn: HumanAmountIn[] = [ + { tokenAddress: balAddress, humanAmount: '1' }, + { tokenAddress: wETHAddress, humanAmount: '2' }, ] - expect( - result.current.helpers.getAmountsToApprove( - humanAmountsIn as unknown as HumanAmountInWithTokenInfo[] - ) - ).toMatchInlineSnapshot(` + const tokensByAddress = { + [balAddress]: { symbol: 'BAL' }, + [wETHAddress]: { symbol: 'WETH' }, + } as Dictionary + + expect(result.current.helpers.getAmountsToApprove(humanAmountsIn, tokensByAddress)) + .toMatchInlineSnapshot(` [ { "humanAmount": "1", diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquidity.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquidity.tsx index 1a5b4f3cd..9d1d2050e 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquidity.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquidity.tsx @@ -11,31 +11,31 @@ import { HumanAmount } from '@balancer/sdk' import { PropsWithChildren, createContext, useEffect, useMemo } from 'react' import { Address } from 'viem' import { usePool } from '../../usePool' -import { AddLiquidityHelpers } from './AddLiquidityHelpers' -import { AddLiquidityInputs, HumanAmountIn } from './add-liquidity.types' -import { useAddLiquidityBtpOutQuery } from './queries/useAddLiquidityBtpOutQuery' +import { useAddLiquidityPreviewQuery } from './queries/useAddLiquidityPreviewQuery' import { useAddLiquidityPriceImpactQuery } from './queries/useAddLiquidityPriceImpactQuery' -import { selectAddLiquidityHandler } from './selectAddLiquidityHandler' +import { HumanAmountIn } from '../liquidity-types' +import { LiquidityActionHelpers, areEmptyAmounts } from '../LiquidityActionHelpers' +import { useBuildAddLiquidityQuery } from './queries/useBuildAddLiquidityTxQuery' import { isDisabledWithReason } from '@/lib/shared/utils/functions/isDisabledWithReason' import { useUserAccount } from '@/lib/modules/web3/useUserAccount' import { LABELS } from '@/lib/shared/labels' -import { areEmptyAmounts } from './add-liquidity.helpers' +import { selectAddLiquidityHandler } from './handlers/selectAddLiquidityHandler' export type UseAddLiquidityResponse = ReturnType export const AddLiquidityContext = createContext(null) -export const amountsInVar = makeVar([]) +export const humanAmountsInVar = makeVar([]) export function _useAddLiquidity() { - const amountsIn = useReactiveVar(amountsInVar) + const humanAmountsIn = useReactiveVar(humanAmountsInVar) const { pool, poolStateInput } = usePool() const { getToken, usdValueForToken } = useTokens() const { isConnected } = useUserAccount() - const handler = selectAddLiquidityHandler(pool) + const handler = useMemo(() => selectAddLiquidityHandler(pool), [pool.id]) - function setInitialAmountsIn() { + function setInitialHumanAmountsIn() { const amountsIn = pool.allTokens.map( token => ({ @@ -43,17 +43,17 @@ export function _useAddLiquidity() { humanAmount: '', } as HumanAmountIn) ) - amountsInVar(amountsIn) + humanAmountsInVar(amountsIn) } useEffect(() => { - setInitialAmountsIn() + setInitialHumanAmountsIn() }, []) - function setAmountIn(tokenAddress: Address, humanAmount: HumanAmount) { - const state = amountsInVar() + function setHumanAmountIn(tokenAddress: Address, humanAmount: HumanAmount) { + const state = humanAmountsInVar() - amountsInVar([ + humanAmountsInVar([ ...state.filter(amountIn => !isSameAddress(amountIn.tokenAddress, tokenAddress)), { tokenAddress, @@ -66,7 +66,7 @@ export function _useAddLiquidity() { const validTokens = tokens.filter((token): token is GqlToken => !!token) const usdAmountsIn = useMemo( () => - amountsIn.map(amountIn => { + humanAmountsIn.map(amountIn => { const token = validTokens.find(token => isSameAddress(token?.address, amountIn.tokenAddress) ) @@ -75,22 +75,25 @@ export function _useAddLiquidity() { return usdValueForToken(token, amountIn.humanAmount) }), - [amountsIn, usdValueForToken, validTokens] + [humanAmountsIn, usdValueForToken, validTokens] ) const totalUSDValue = safeSum(usdAmountsIn) - const { formattedPriceImpact, isPriceImpactLoading } = useAddLiquidityPriceImpactQuery( + const { isPriceImpactLoading, priceImpact } = useAddLiquidityPriceImpactQuery( handler, - amountsIn, + humanAmountsIn, pool.id ) - const { bptOut, bptOutUnits, isBptOutQueryLoading, lastSdkQueryOutput } = - useAddLiquidityBtpOutQuery(handler, amountsIn, pool.id) + const { bptOut, isPreviewQueryLoading } = useAddLiquidityPreviewQuery( + handler, + humanAmountsIn, + pool.id + ) const { isDisabled, disabledReason } = isDisabledWithReason( [!isConnected, LABELS.walletNotConnected], - [areEmptyAmounts(amountsIn), 'You must specify one or more token amounts'] + [areEmptyAmounts(humanAmountsIn), 'You must specify one or more token amounts'] ) /* We don't expose individual helper methods like getAmountsToApprove or poolTokenAddresses because @@ -98,28 +101,25 @@ export function _useAddLiquidity() { TypeError: Cannot read property getAmountsToApprove of undefined when trying to access the returned method */ - const helpers = new AddLiquidityHelpers(pool) + const helpers = new LiquidityActionHelpers(pool) - function buildAddLiquidityTx(inputs: AddLiquidityInputs) { - // There are edge cases where we will never call setLastSdkQueryOutput so that lastSdkQueryOutput will be undefined. - // That`s expected as sdkQueryOutput is an optional input - return handler.buildAddLiquidityTx({ inputs, sdkQueryOutput: lastSdkQueryOutput }) + function useBuildTx(isActiveStep: boolean) { + return useBuildAddLiquidityQuery(handler, humanAmountsIn, isActiveStep, pool.id) } return { - amountsIn, + humanAmountsIn, tokens, validTokens, totalUSDValue, - formattedPriceImpact, isPriceImpactLoading, + priceImpact, bptOut, - isBptOutQueryLoading, - bptOutUnits, + isPreviewQueryLoading, + setHumanAmountIn, + useBuildTx, isDisabled, disabledReason, - setAmountIn, - buildAddLiquidityTx, helpers, poolStateInput, } diff --git a/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.integration.spec.tsx b/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.integration.spec.tsx new file mode 100644 index 000000000..12b7fd4f3 --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.integration.spec.tsx @@ -0,0 +1,59 @@ +/* eslint-disable max-len */ +import { poolId, wETHAddress } from '@/lib/debug-helpers' +import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { + DefaultAddLiquidityTestProvider, + buildDefaultPoolTestProvider, + testHook, +} from '@/test/utils/custom-renderers' +import { act, waitFor } from '@testing-library/react' +import { PropsWithChildren } from 'react' +import { AddLiquidityProvider, useAddLiquidity } from './useAddLiquidity' +import { useConstructAddLiquidityStep } from './useConstructAddLiquidityStep' + +const PoolProvider = buildDefaultPoolTestProvider(aWjAuraWethPoolElementMock()) + +export const Providers = ({ children }: PropsWithChildren) => ( + + + {children} + + +) + +async function testConstructAddLiquidityStep() { + const { result } = testHook( + () => { + // return useConstructRemoveLiquidityStep(poolId) + // https://github.com/testing-library/react-hooks-testing-library/issues/615#issuecomment-835814029 + return { + providerResult: useAddLiquidity(), + constructStepResult: useConstructAddLiquidityStep(poolId), + } + }, + { + wrapper: Providers, + } + ) + return result +} + +test.skip('TBD', async () => { + const result = await testConstructAddLiquidityStep() + + // User fills token inputs + act(() => { + result.current.providerResult.setHumanAmountIn(wETHAddress, '1') + }) + + await waitFor(() => expect(result.current.providerResult.bptOut?.amount).toBeDefined()) + + // act(() => result.current.constructStepResult.step.activateStep()) + + // await waitFor(() => + // expect(result.current.constructStepResult.step.simulation.isError).toBeTruthy() + // ) + + // // expect(result.current.constructStepResult.step.simulation.error).not.toBeNull() + // console.log(result.current.constructStepResult.step.simulation.error) +}) diff --git a/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.ts b/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.ts index 70254bd6e..c0578fe3e 100644 --- a/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.ts +++ b/lib/modules/pool/actions/add-liquidity/useConstructAddLiquidityStep.ts @@ -3,14 +3,14 @@ import { useManagedSendTransaction } from '@/lib/modules/web3/contracts/useManag import { FlowStep } from '@/lib/shared/components/btns/transaction-steps/lib' import { Address } from 'wagmi' import { useActiveStep } from '../../../../shared/hooks/transaction-flows/useActiveStep' -import { HumanAmountIn } from './add-liquidity.types' -import { useBuildAddLiquidityQuery } from './queries/useBuildAddLiquidityTxQuery' +import { useAddLiquidity } from './useAddLiquidity' -export function useConstructAddLiquidityStep(humanAmountsIn: HumanAmountIn[], poolId: string) { +export function useConstructAddLiquidityStep(poolId: string) { const { isActiveStep, activateStep } = useActiveStep() - //TODO: add slippage - const addLiquidityQuery = useBuildAddLiquidityQuery(humanAmountsIn, isActiveStep, poolId) + const { useBuildTx } = useAddLiquidity() + + const addLiquidityQuery = useBuildTx(isActiveStep) const transactionLabels = buildAddLiquidityLabels(poolId) @@ -32,7 +32,6 @@ export function useConstructAddLiquidityStep(humanAmountsIn: HumanAmountIn[], po transaction?.execution.isLoading || addLiquidityQuery.isLoading, error: transaction?.simulation.error || transaction?.execution.error || addLiquidityQuery.error, - joinQuery: addLiquidityQuery, } } diff --git a/lib/modules/pool/actions/liquidity-types.ts b/lib/modules/pool/actions/liquidity-types.ts new file mode 100644 index 000000000..d3808765c --- /dev/null +++ b/lib/modules/pool/actions/liquidity-types.ts @@ -0,0 +1,7 @@ +import { HumanAmount } from '@balancer/sdk' +import { Address } from 'viem' + +export type HumanAmountIn = { + humanAmount: HumanAmount | '' + tokenAddress: Address +} diff --git a/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityFlowButton.tsx b/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityFlowButton.tsx new file mode 100644 index 000000000..119127b3e --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityFlowButton.tsx @@ -0,0 +1,26 @@ +import TransactionFlow from '@/lib/shared/components/btns/transaction-steps/TransactionFlow' +import { VStack } from '@chakra-ui/react' +import { useConstructRemoveLiquidityStep } from './useConstructRemoveLiquidityStep' + +type Props = { + poolId: string +} +export function RemoveLiquidityFlowButton({ poolId }: Props) { + const { step: removeLiquidityStep } = useConstructRemoveLiquidityStep(poolId) + const steps = [removeLiquidityStep] + + function handleRemoveCompleted() { + console.log('Remove completed') + } + + return ( + + + + ) +} diff --git a/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityForm.tsx b/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityForm.tsx index c3fe19f1c..4fcab6377 100644 --- a/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityForm.tsx +++ b/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityForm.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import { useDisclosure } from '@chakra-ui/hooks' @@ -128,18 +129,25 @@ export function RemoveLiquidityForm() { const { tokens, validTokens, - proportionalPercent, - setProportionalPercent, - singleToken, - setSingleToken, setProportional, + singleTokenAddress, + setSingleToken, + setSingleTokenAddress, + setProportionalAmounts, + setSliderPercent, + sliderPercent, + singleToken, + totalUsdValue, } = useRemoveLiquidity() const { toCurrency } = useCurrency() const previewDisclosure = useDisclosure() const nextBtn = useRef(null) const [activeTab, setActiveTab] = useState(TABS[0]) + const usdInputAmount = totalUsdValue + function submit() { + // TODO: implement isDisabledWithReason previewDisclosure.onOpen() } @@ -177,13 +185,14 @@ export function RemoveLiquidityForm() {
- {/* TODO: hook the slider up to the proportional amounts with more logic */} + {/* value={toCurrency(usdInputAmount)} */} Amount - {fNum('percentage', proportionalPercent / 100)} + {fNum('percentage', sliderPercent / 100)} {activeTab === TABS[0] && } {activeTab === TABS[1] && ( @@ -198,7 +207,7 @@ export function RemoveLiquidityForm() { Total - {toCurrency(0)} + {toCurrency(totalUsdValue)} diff --git a/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityModal.tsx b/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityModal.tsx index 98ba7237c..bf5e21ee8 100644 --- a/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityModal.tsx +++ b/lib/modules/pool/actions/remove-liquidity/RemoveLiquidityModal.tsx @@ -45,8 +45,9 @@ export function RemoveLiquidityModal({ const initialFocusRef = useRef(null) const { //executeRemoveLiquidity, - selectedRemoveLiquidityType, - singleToken, + isProportional, + isSingleToken, + singleTokenAddress, } = useRemoveLiquidity() const { pool, bptPrice } = usePool() const { slippage } = useUserSettings() @@ -91,7 +92,7 @@ export function RemoveLiquidityModal({ With max slippage: {fNum('slippage', slippage)} - {selectedRemoveLiquidityType === 'PROPORTIONAL' && + {isProportional && pool.displayTokens.map(token => ( ))} - {selectedRemoveLiquidityType === 'SINGLE_TOKEN' && singleToken && ( - + {isSingleToken && ( + )} diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts b/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts new file mode 100644 index 000000000..1d0c82ffb --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.integration.spec.ts @@ -0,0 +1,74 @@ +/* eslint-disable max-len */ +import networkConfig from '@/lib/config/networks/mainnet' +import { balAddress, wETHAddress } from '@/lib/debug-helpers' +import { aBalWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { defaultTestUserAccount } from '@/test/utils/wagmi' +import { aPhantomStablePoolStateInputMock } from '../../../__mocks__/pool.builders' +import { Pool } from '../../../usePool' +import { RemoveLiquidityInputs, RemoveLiquidityType } from '../remove-liquidity.types' +import { selectRemoveLiquidityHandler } from './selectRemoveLiquidityHandler' + +const poolMock = aBalWethPoolElementMock() // 80BAL-20WETH + +function selectProportionalHandler(pool: Pool) { + return selectRemoveLiquidityHandler(pool, RemoveLiquidityType.Proportional) +} + +const inputs: RemoveLiquidityInputs = { + humanBptIn: '1', + account: defaultTestUserAccount, + slippagePercent: '0.2', +} + +describe('When proportionally removing liquidity for a weighted pool', () => { + test('returns ZERO price impact', async () => { + const handler = selectProportionalHandler(poolMock) + + const result = await handler.queryRemoveLiquidity(inputs) + + const [balTokenAmountOut, wEthTokenAmountOut] = result.amountsOut + + expect(balTokenAmountOut.token.address).toBe(balAddress) + expect(balTokenAmountOut.amount).toBeGreaterThan(2000000000000000000n) + + expect(wEthTokenAmountOut.token.address).toBe(wETHAddress) + expect(wEthTokenAmountOut.amount).toBeGreaterThan(100000000000000n) + }) + test('queries amounts out', async () => { + const handler = selectProportionalHandler(poolMock) + + const result = await handler.queryRemoveLiquidity(inputs) + + const [balTokenAmountOut, wEthTokenAmountOut] = result.amountsOut + + expect(balTokenAmountOut.token.address).toBe(balAddress) + expect(balTokenAmountOut.amount).toBeGreaterThan(2000000000000000000n) + + expect(wEthTokenAmountOut.token.address).toBe(wETHAddress) + expect(wEthTokenAmountOut.amount).toBeGreaterThan(100000000000000n) + }) + + test('builds Tx Config', async () => { + const handler = selectProportionalHandler(poolMock) + + const { sdkQueryOutput } = await handler.queryRemoveLiquidity(inputs) + + const result = await handler.buildRemoveLiquidityTx({ inputs, sdkQueryOutput }) + + expect(result.to).toBe(networkConfig.contracts.balancer.vaultV2) + expect(result.data).toBeDefined() + }) +}) + +describe('When removing liquidity from a stable pool', () => { + test('queries remove liquidity', async () => { + const pool = aPhantomStablePoolStateInputMock() as Pool // wstETH-rETH-sfrxETH + + const handler = selectProportionalHandler(pool) + + const { sdkQueryOutput } = await handler.queryRemoveLiquidity(inputs) + + const result = await handler.buildRemoveLiquidityTx({ inputs, sdkQueryOutput }) + expect(result.account).toBe(defaultTestUserAccount) + }) +}) diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts b/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts new file mode 100644 index 000000000..38850511a --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts @@ -0,0 +1,103 @@ +import { getDefaultRpcUrl } from '@/lib/modules/web3/Web3Provider' +import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { + HumanAmount, + InputAmount, + RemoveLiquidity, + RemoveLiquidityKind, + RemoveLiquidityProportionalInput, + RemoveLiquidityQueryOutput, + Slippage, +} from '@balancer/sdk' +import { Address, parseEther } from 'viem' +import { Pool } from '../../../usePool' +import { LiquidityActionHelpers } from '../../LiquidityActionHelpers' +import { + BuildLiquidityInputs, + RemoveLiquidityInputs, + RemoveLiquidityOutputs, +} from '../remove-liquidity.types' +import { RemoveLiquidityHandler } from './RemoveLiquidity.handler' +import { BPT_DECIMALS } from '../../../pool.constants' + +export class ProportionalRemoveLiquidityHandler implements RemoveLiquidityHandler { + helpers: LiquidityActionHelpers + sdkQueryOutput?: RemoveLiquidityQueryOutput + + constructor(pool: Pool) { + this.helpers = new LiquidityActionHelpers(pool) + } + + public async queryRemoveLiquidity({ + humanBptIn, + }: RemoveLiquidityInputs): Promise { + const removeLiquidity = new RemoveLiquidity() + const removeLiquidityInput = this.constructSdkInput(humanBptIn) + + this.sdkQueryOutput = await removeLiquidity.query( + removeLiquidityInput, + this.helpers.poolStateInput + ) + + return { amountsOut: this.sdkQueryOutput.amountsOut } + } + + public async calculatePriceImpact(): Promise { + // proportional remove liquidity does not have price impact + return 0 + } + + /* + sdkQueryOutput is the result of the query that we run in the remove liquidity form + */ + public async buildRemoveLiquidityTx( + buildInputs: BuildLiquidityInputs + ): Promise { + const { account, slippagePercent } = buildInputs.inputs + if (!account || !slippagePercent) throw new Error('Missing account or slippage') + if (!this.sdkQueryOutput) { + console.error('Missing sdkQueryOutput in buildRemoveLiquidityTx') + throw new Error( + `Missing sdkQueryOutput. +It looks that you did not call useRemoveLiquidityBtpOutQuery before trying to build the tx config` + ) + } + + const removeLiquidity = new RemoveLiquidity() + + const { call, to, value } = removeLiquidity.buildCall({ + ...this.sdkQueryOutput, + slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), + sender: account, + recipient: account, + }) + + return { + account, + chainId: this.helpers.chainId, + data: call, + to, + value, + } + } + + /** + * PRIVATE METHODS + */ + private constructSdkInput(humanBptIn: HumanAmount | ''): RemoveLiquidityProportionalInput { + const bptIn: InputAmount = { + rawAmount: parseEther(`${humanBptIn}`), + decimals: BPT_DECIMALS, + address: this.helpers.pool.address as Address, + } + + return { + chainId: this.helpers.chainId, + rpcUrl: getDefaultRpcUrl(this.helpers.chainId), + bptIn, + kind: RemoveLiquidityKind.Proportional, + //TODO: review this case + // toNativeAsset: this.helpers.isNativeAssetIn(humanAmountsIn), + } + } +} diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/RemoveLiquidity.handler.ts b/lib/modules/pool/actions/remove-liquidity/handlers/RemoveLiquidity.handler.ts new file mode 100644 index 000000000..af4c4830c --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/handlers/RemoveLiquidity.handler.ts @@ -0,0 +1,20 @@ +import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { + RemoveLiquidityInputs, + RemoveLiquidityOutputs, + BuildLiquidityInputs, +} from '../remove-liquidity.types' + +/** + * RemoveLiquidityHandler is an interface that defines the methods that must be implemented by a handler. + * They take standard inputs from the UI and return frontend standardised outputs. + * This is to allow handlers to be developed in the future that may not use the SDK. + */ +export interface RemoveLiquidityHandler { + // Query the SDK for the expected output of removing liquidity + queryRemoveLiquidity(inputs: RemoveLiquidityInputs): Promise + // Calculate the price impact of removing liquidity + calculatePriceImpact(inputs: RemoveLiquidityInputs): Promise + // Build tx payload for removing liquidity + buildRemoveLiquidityTx(inputs: BuildLiquidityInputs): Promise +} diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.integration.spec.ts b/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.integration.spec.ts new file mode 100644 index 000000000..740e6c9e7 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.integration.spec.ts @@ -0,0 +1,72 @@ +/* eslint-disable max-len */ +import networkConfig from '@/lib/config/networks/mainnet' +import { balAddress, wETHAddress } from '@/lib/debug-helpers' +import { aBalWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { defaultTestUserAccount } from '@/test/utils/wagmi' +import { aPhantomStablePoolStateInputMock } from '../../../__mocks__/pool.builders' +import { Pool } from '../../../usePool' +import { RemoveLiquidityInputs, RemoveLiquidityType } from '../remove-liquidity.types' +import { selectRemoveLiquidityHandler } from './selectRemoveLiquidityHandler' + +const poolMock = aBalWethPoolElementMock() // 80BAL-20WETH + +function selectSingleTokenHandler(pool: Pool) { + return selectRemoveLiquidityHandler(pool, RemoveLiquidityType.SingleToken) +} + +describe('When removing unbalanced liquidity for a weighted pool', () => { + test('queries amounts out', async () => { + // TODO: why address and slippage are optional??? + const inputs: RemoveLiquidityInputs = { + humanBptIn: '1', + } + + const handler = selectSingleTokenHandler(poolMock) + + const result = await handler.queryRemoveLiquidity(inputs) + + const [balTokenAmountOut, wEthTokenAmountOut] = result.amountsOut + + expect(balTokenAmountOut.token.address).toBe(balAddress) + expect(balTokenAmountOut.amount).toBeGreaterThan(2000000000000000000n) + + expect(wEthTokenAmountOut.token.address).toBe(wETHAddress) + expect(wEthTokenAmountOut.amount).toBeGreaterThan(100000000000000n) + }) + + test('builds Tx Config', async () => { + const handler = selectSingleTokenHandler(poolMock) + + const inputs: RemoveLiquidityInputs = { + humanBptIn: '1', + account: defaultTestUserAccount, + slippagePercent: '0.2', + } + + const { sdkQueryOutput } = await handler.queryRemoveLiquidity(inputs) + + const result = await handler.buildRemoveLiquidityTx({ inputs, sdkQueryOutput }) + + expect(result.to).toBe(networkConfig.contracts.balancer.vaultV2) + expect(result.data).toBeDefined() + }) +}) + +describe('When removing liquidity from a stable pool', () => { + test('queries remove liquidity', async () => { + const pool = aPhantomStablePoolStateInputMock() as Pool // wstETH-rETH-sfrxETH + + const handler = selectSingleTokenHandler(pool) + + const inputs: RemoveLiquidityInputs = { + humanBptIn: '1', + account: defaultTestUserAccount, + slippagePercent: '0.2', + } + const { sdkQueryOutput } = await handler.queryRemoveLiquidity(inputs) + + const result = await handler.buildRemoveLiquidityTx({ inputs, sdkQueryOutput }) + expect(result.account).toBe(defaultTestUserAccount) + expect(result.data.startsWith('0x')).toBeTruthy() + }) +}) diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts b/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts new file mode 100644 index 000000000..b5ad49a86 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts @@ -0,0 +1,127 @@ +import { getDefaultRpcUrl } from '@/lib/modules/web3/Web3Provider' +import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { + HumanAmount, + InputAmount, + PriceImpact, + RemoveLiquidity, + RemoveLiquidityKind, + RemoveLiquidityQueryOutput, + RemoveLiquiditySingleTokenInput, + Slippage, +} from '@balancer/sdk' +import { Address, parseEther } from 'viem' +import { BPT_DECIMALS } from '../../../pool.constants' +import { Pool } from '../../../usePool' +import { LiquidityActionHelpers, isEmptyHumanAmount } from '../../LiquidityActionHelpers' +import { PriceImpactAmount } from '../../add-liquidity/add-liquidity.types' +import { + BuildLiquidityInputs, + RemoveLiquidityOutputs, + SingleTokenRemoveLiquidityInputs, +} from '../remove-liquidity.types' +import { RemoveLiquidityHandler } from './RemoveLiquidity.handler' + +export class SingleTokenRemoveLiquidityHandler implements RemoveLiquidityHandler { + helpers: LiquidityActionHelpers + sdkQueryOutput?: RemoveLiquidityQueryOutput + + constructor(pool: Pool) { + this.helpers = new LiquidityActionHelpers(pool) + } + + public async queryRemoveLiquidity({ + humanBptIn, + tokenOut, + }: SingleTokenRemoveLiquidityInputs): Promise { + const removeLiquidity = new RemoveLiquidity() + const removeLiquidityInput = this.constructSdkInput(humanBptIn, tokenOut) + + this.sdkQueryOutput = await removeLiquidity.query( + removeLiquidityInput, + this.helpers.poolStateInput + ) + + return { amountsOut: this.sdkQueryOutput.amountsOut } + } + + public async calculatePriceImpact({ + humanBptIn, + tokenOut, + }: SingleTokenRemoveLiquidityInputs): Promise { + if (isEmptyHumanAmount(humanBptIn)) { + // Avoid price impact calculation when there are no amounts in + return 0 + } + + const removeLiquidityInput = this.constructSdkInput(humanBptIn, tokenOut) + + const priceImpactABA: PriceImpactAmount = await PriceImpact.removeLiquidity( + removeLiquidityInput, + this.helpers.poolStateInput + ) + + return priceImpactABA.decimal + } + + /* + sdkQueryOutput is the result of the query that we run in the remove liquidity form + */ + public async buildRemoveLiquidityTx( + buildInputs: BuildLiquidityInputs + ): Promise { + const { account, slippagePercent } = buildInputs.inputs + if (!account || !slippagePercent) throw new Error('Missing account or slippage') + if (!this.sdkQueryOutput) { + console.error('Missing sdkQueryOutput in buildRemoveLiquidityTx') + throw new Error( + `Missing sdkQueryOutput. +It looks that you did not call useRemoveLiquidityBtpOutQuery before trying to build the tx config` + ) + } + + const removeLiquidity = new RemoveLiquidity() + + const { call, to, value } = removeLiquidity.buildCall({ + ...this.sdkQueryOutput, + slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), + sender: account, + recipient: account, + }) + + return { + account, + chainId: this.helpers.chainId, + data: call, + to, + value, + } + } + + /** + * PRIVATE METHODS + */ + private constructSdkInput( + humanBptIn: HumanAmount | '', + tokenOut: Address + ): RemoveLiquiditySingleTokenInput { + // const bptToken = new Token(ChainId.MAINNET, poolMock.address as Address, 18) + // const bptIn = TokenAmount.fromRawAmount(bptToken, parseEther(humanBptInAmount)), + + const bptIn: InputAmount = { + rawAmount: parseEther(`${humanBptIn}`), + decimals: BPT_DECIMALS, + address: this.helpers.pool.address as Address, + } + + return { + chainId: this.helpers.chainId, + rpcUrl: getDefaultRpcUrl(this.helpers.chainId), + bptIn, + kind: RemoveLiquidityKind.SingleToken, + tokenOut, + //TODO: review this case + // toNativeAsset: this.helpers.isNativeAssetIn(humanAmountsIn), + } + } +} diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts b/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts new file mode 100644 index 000000000..a88b25a68 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/handlers/selectRemoveLiquidityHandler.ts @@ -0,0 +1,21 @@ +import { Pool } from '../../../usePool' +import { RemoveLiquidityType } from '../remove-liquidity.types' +import { ProportionalRemoveLiquidityHandler } from './ProportionalRemoveLiquidity.handler' +import { RemoveLiquidityHandler } from './RemoveLiquidity.handler' + +export function selectRemoveLiquidityHandler( + pool: Pool, + kind: RemoveLiquidityType +): RemoveLiquidityHandler { + // TODO: Depending on the pool attributes we will return a different handler + // if (pool.id === 'TWAMM-example') { + // // This is just an example to illustrate how edge-case handlers would receive different inputs but return a common contract + // return new TwammRemoveLiquidityHandler(getChainId(pool.chain)) + // } + if (kind === RemoveLiquidityType.Proportional) { + return new ProportionalRemoveLiquidityHandler(pool) + } + + // Default type + return new ProportionalRemoveLiquidityHandler(pool) +} diff --git a/lib/modules/pool/actions/remove-liquidity/queries/generateRemoveLiquidityQueryKey.ts b/lib/modules/pool/actions/remove-liquidity/queries/generateRemoveLiquidityQueryKey.ts new file mode 100644 index 000000000..93f3e29cb --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/queries/generateRemoveLiquidityQueryKey.ts @@ -0,0 +1,20 @@ +import { HumanAmount } from '@balancer/sdk' + +type Props = { + queryId: string + userAddress: string + poolId: string + slippage: string + humanBptIn: HumanAmount | '' +} + +// Should we share the same function for add and remove liquidity? +export function generateRemoveLiquidityQueryKey({ + queryId, + userAddress, + poolId, + slippage, + humanBptIn, +}: Props): string { + return `'Remove_Liquidity:${queryId}:${userAddress}:${poolId}:${slippage}:${humanBptIn}` +} diff --git a/lib/modules/pool/actions/remove-liquidity/queries/useBuildRemoveLiquidityTxQuery.ts b/lib/modules/pool/actions/remove-liquidity/queries/useBuildRemoveLiquidityTxQuery.ts new file mode 100644 index 000000000..f4a0238bc --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/queries/useBuildRemoveLiquidityTxQuery.ts @@ -0,0 +1,49 @@ +'use client' + +import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' +import { useUserAccount } from '@/lib/modules/web3/useUserAccount' +import { HumanAmount } from '@balancer/sdk' +import { useQuery } from 'wagmi' +import { RemoveLiquidityHandler } from '../handlers/RemoveLiquidity.handler' +import { generateRemoveLiquidityQueryKey } from './generateRemoveLiquidityQueryKey' + +// Queries the SDK to create a transaction config to be used by wagmi's useManagedSendTransaction +export function useBuildRemoveLiquidityQuery( + handler: RemoveLiquidityHandler, + humanBptIn: HumanAmount | '', + isActiveStep: boolean, + poolId: string +) { + const { userAddress, isConnected } = useUserAccount() + + const { slippage } = useUserSettings() + + function queryKey(): string { + return generateRemoveLiquidityQueryKey({ + queryId: 'BuildTxConfig', + userAddress, + poolId, + slippage, + humanBptIn, + }) + } + + const removeLiquidityQuery = useQuery( + [queryKey()], + async () => { + const inputs = { + humanBptIn, + account: userAddress, + slippagePercent: slippage, + } + return handler.buildRemoveLiquidityTx({ inputs }) + }, + { + enabled: + isActiveStep && // If the step is not active (the user did not click Next button) avoid running the build tx query to save RPC requests + isConnected, + } + ) + + return removeLiquidityQuery +} diff --git a/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPreviewQuery.integration.spec.tsx b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPreviewQuery.integration.spec.tsx new file mode 100644 index 000000000..02cf5f448 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPreviewQuery.integration.spec.tsx @@ -0,0 +1,29 @@ +import { poolId } from '@/lib/debug-helpers' +import { testHook } from '@/test/utils/custom-renderers' +import { waitFor } from '@testing-library/react' + +import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { HumanAmount } from '@balancer/sdk' +import { selectRemoveLiquidityHandler } from '../handlers/selectRemoveLiquidityHandler' +import { RemoveLiquidityType } from '../remove-liquidity.types' +import { useRemoveLiquidityPreviewQuery } from './useRemoveLiquidityPreviewQuery' + +async function testQuery(humanBptIn: HumanAmount) { + const handler = selectRemoveLiquidityHandler( + aWjAuraWethPoolElementMock(), + RemoveLiquidityType.Proportional + ) + const { result } = testHook(() => useRemoveLiquidityPreviewQuery(handler, poolId, humanBptIn)) + return result +} + +test.skip('queries btp in for remove liquidity', async () => { + const humanBptIn: HumanAmount = '1' + + const result = await testQuery(humanBptIn) + + await waitFor(() => expect(result.current.amountsOut).toBeDefined()) + + expect(result.current.amountsOut).toBeDefined() + expect(result.current.isPreviewQueryLoading).toBeFalsy() +}) diff --git a/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPreviewQuery.ts b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPreviewQuery.ts new file mode 100644 index 000000000..fc21cc3e3 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPreviewQuery.ts @@ -0,0 +1,54 @@ +'use client' + +import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' +import { useUserAccount } from '@/lib/modules/web3/useUserAccount' +import { HumanAmount, TokenAmount } from '@balancer/sdk' +import { useState } from 'react' +import { useDebounce } from 'use-debounce' +import { useQuery } from 'wagmi' +import { isEmptyHumanAmount } from '../../LiquidityActionHelpers' +import { RemoveLiquidityHandler } from '../handlers/RemoveLiquidity.handler' +import { generateRemoveLiquidityQueryKey } from './generateRemoveLiquidityQueryKey' +import { defaultDebounceMs } from '@/lib/shared/utils/queries' + +export function useRemoveLiquidityPreviewQuery( + handler: RemoveLiquidityHandler, + poolId: string, + humanBptIn: HumanAmount | '' +) { + const { userAddress, isConnected } = useUserAccount() + const { slippage } = useUserSettings() + const [amountsOut, setAmountsOut] = useState(undefined) + const debouncedHumanBptIn = useDebounce(humanBptIn, defaultDebounceMs)[0] + + function queryKey(): string { + return generateRemoveLiquidityQueryKey({ + queryId: 'BptIn', + userAddress, + poolId, + slippage, + humanBptIn: debouncedHumanBptIn, + }) + } + + async function queryBptIn() { + const { amountsOut } = await handler.queryRemoveLiquidity({ humanBptIn: debouncedHumanBptIn }) + + setAmountsOut(amountsOut) + + return amountsOut + } + + const query = useQuery( + [queryKey()], + async () => { + return await queryBptIn() + }, + { + enabled: isConnected && !isEmptyHumanAmount(debouncedHumanBptIn), + onError: (error: Error) => console.log('Error in queryRemoveLiquidity', error.name), + } + ) + + return { amountsOut, isPreviewQueryLoading: query.isLoading } +} diff --git a/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPriceImpactQuery.integration.spec.tsx b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPriceImpactQuery.integration.spec.tsx new file mode 100644 index 000000000..92583709d --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPriceImpactQuery.integration.spec.tsx @@ -0,0 +1,32 @@ +import { testHook } from '@/test/utils/custom-renderers' +import { waitFor } from '@testing-library/react' + +import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { HumanAmount } from '@balancer/sdk' +import { selectRemoveLiquidityHandler } from '../handlers/selectRemoveLiquidityHandler' +import { RemoveLiquidityType } from '../remove-liquidity.types' +import { useRemoveLiquidityPriceImpactQuery } from './useRemoveLiquidityPriceImpactQuery' + +const poolMock = aWjAuraWethPoolElementMock() + +async function testQuery(humanBptIn: HumanAmount) { + const handler = selectRemoveLiquidityHandler( + aWjAuraWethPoolElementMock(), + RemoveLiquidityType.Proportional + ) + const { result } = testHook(() => + useRemoveLiquidityPriceImpactQuery(handler, poolMock.id, humanBptIn) + ) + return result +} + +test('queries price impact for add liquidity', async () => { + const humanBptIn = '1' + + const result = await testQuery(humanBptIn) + + await waitFor(() => expect(result.current.priceImpact).toBeDefined()) + + expect(result.current.priceImpact).toBeCloseTo(0.002368782867485742) + expect(result.current.isPriceImpactLoading).toBeFalsy() +}) diff --git a/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPriceImpactQuery.ts b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPriceImpactQuery.ts new file mode 100644 index 000000000..f884edb77 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/queries/useRemoveLiquidityPriceImpactQuery.ts @@ -0,0 +1,54 @@ +'use client' + +import { useUserSettings } from '@/lib/modules/user/settings/useUserSettings' +import { useUserAccount } from '@/lib/modules/web3/useUserAccount' +import { useState } from 'react' +import { useDebounce } from 'use-debounce' +import { useQuery } from 'wagmi' +import { RemoveLiquidityHandler } from '../handlers/RemoveLiquidity.handler' +import { generateRemoveLiquidityQueryKey } from './generateRemoveLiquidityQueryKey' +import { HumanAmount } from '@balancer/sdk' +import { isEmpty } from 'lodash' +import { defaultDebounceMs } from '@/lib/shared/utils/queries' + +export function useRemoveLiquidityPriceImpactQuery( + handler: RemoveLiquidityHandler, + poolId: string, + humanBptIn: HumanAmount | '' +) { + const { userAddress, isConnected } = useUserAccount() + const { slippage } = useUserSettings() + const [priceImpact, setPriceImpact] = useState(null) + const debouncedHumanBptIn = useDebounce(humanBptIn, defaultDebounceMs)[0] + + function queryKey(): string { + return generateRemoveLiquidityQueryKey({ + queryId: 'PriceImpact', + userAddress, + poolId, + slippage, + humanBptIn: debouncedHumanBptIn, + }) + } + + async function queryPriceImpact() { + const _priceImpact = await handler.calculatePriceImpact({ + humanBptIn: debouncedHumanBptIn, + }) + + setPriceImpact(_priceImpact) + return _priceImpact + } + + const query = useQuery( + [queryKey()], + async () => { + return await queryPriceImpact() + }, + { + enabled: isConnected && !isEmpty(debouncedHumanBptIn), + } + ) + + return { priceImpact, isPriceImpactLoading: query.isLoading } +} diff --git a/lib/modules/pool/actions/remove-liquidity/remove-liquidity.types.ts b/lib/modules/pool/actions/remove-liquidity/remove-liquidity.types.ts new file mode 100644 index 000000000..ead7f0aa5 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/remove-liquidity.types.ts @@ -0,0 +1,42 @@ +import { + RemoveLiquidityKind as SdkRemoveLiquidityKind, + RemoveLiquidityQueryOutput, + TokenAmount, + HumanAmount, +} from '@balancer/sdk' +import { Address } from 'wagmi' + +type CommonRemoveLiquidityInputs = { account?: Address; slippagePercent?: string } + +export type ProportionalRemoveLiquidityInputs = { + humanBptIn: HumanAmount | '' +} & CommonRemoveLiquidityInputs + +export type SingleTokenRemoveLiquidityInputs = { + humanBptIn: HumanAmount | '' + tokenOut: Address +} & CommonRemoveLiquidityInputs + +export type RemoveLiquidityInputs = + | ProportionalRemoveLiquidityInputs + | SingleTokenRemoveLiquidityInputs + +// sdkQueryOutput is optional because it will be only used in cases where we use the SDK to query/build the transaction +// We will probably need a more abstract interface to be used by edge cases +export type RemoveLiquidityOutputs = { + amountsOut: TokenAmount[] + sdkQueryOutput?: RemoveLiquidityQueryOutput +} + +// sdkQueryOutput is optional because it will be only used in cases where we use the SDK to query/build the transaction +// We will probably need a more abstract interface to be used by edge cases +export type BuildLiquidityInputs = { + inputs: RemoveLiquidityInputs + sdkQueryOutput?: RemoveLiquidityQueryOutput +} + +// There are other kinds but we only support two of them +export enum RemoveLiquidityType { + Proportional = SdkRemoveLiquidityKind.Proportional, + SingleToken = SdkRemoveLiquidityKind.SingleToken, +} diff --git a/lib/modules/pool/actions/remove-liquidity/useConstructRemoveLiquidityStep.integration.spec.tsx b/lib/modules/pool/actions/remove-liquidity/useConstructRemoveLiquidityStep.integration.spec.tsx new file mode 100644 index 000000000..189b103af --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/useConstructRemoveLiquidityStep.integration.spec.tsx @@ -0,0 +1,66 @@ +/* eslint-disable max-len */ +import { poolId } from '@/lib/debug-helpers' +import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { + DefaultRemoveLiquidityTestProvider, + buildDefaultPoolTestProvider, + testHook, +} from '@/test/utils/custom-renderers' +import { act } from '@testing-library/react' +import { PropsWithChildren } from 'react' +import { useConstructRemoveLiquidityStep } from './useConstructRemoveLiquidityStep' +import { RemoveLiquidityProvider, useRemoveLiquidity } from './useRemoveLiquidity' + +const PoolProvider = buildDefaultPoolTestProvider(aWjAuraWethPoolElementMock()) + +export const Providers = ({ children }: PropsWithChildren) => ( + + + {children} + + +) + +async function testConstructRemoveLiquidityStep() { + const { result } = testHook( + () => { + // return useConstructRemoveLiquidityStep(poolId) + // https://github.com/testing-library/react-hooks-testing-library/issues/615#issuecomment-835814029 + return { + providerResult: useRemoveLiquidity(), + constructStepResult: useConstructRemoveLiquidityStep(poolId), + } + }, + { + wrapper: Providers, + } + ) + return result +} + +test.skip('Throws error when user tries to remove liquidity in a pool where they does not have balance', async () => { + const result = await testConstructRemoveLiquidityStep() + + // User fills token inputs + act(() => { + result.current.providerResult.setProportional() + }) + + // await waitFor(() => expect(result.current.providerResult.amountsOut).toBeDefined()) + + // act(() => result.current.constructStepResult.step.activateStep()) + + // await waitFor(() => + // expect(result.current.constructStepResult.step.simulation.isFetched).toBeTruthy() + // ) + // await waitFor(() => + // expect(result.current.constructStepResult.step.simulation.error).toBeDefined() + // ) + + // expect(result.current.constructStepResult.step.simulation.error?.cause).toMatchInlineSnapshot(` + // [ExecutionRevertedError: Execution reverted with reason: BAL#434. + + // Details: execution reverted: BAL#434 + // Version: viem@1.18.1] + // `) +}) diff --git a/lib/modules/pool/actions/remove-liquidity/useConstructRemoveLiquidityStep.ts b/lib/modules/pool/actions/remove-liquidity/useConstructRemoveLiquidityStep.ts new file mode 100644 index 000000000..6794419a7 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/useConstructRemoveLiquidityStep.ts @@ -0,0 +1,46 @@ +import { BuildTransactionLabels } from '@/lib/modules/web3/contracts/transactionLabels' +import { useManagedSendTransaction } from '@/lib/modules/web3/contracts/useManagedSendTransaction' +import { FlowStep } from '@/lib/shared/components/btns/transaction-steps/lib' +import { Address } from 'wagmi' +import { useActiveStep } from '../../../../shared/hooks/transaction-flows/useActiveStep' +import { useRemoveLiquidity } from './useRemoveLiquidity' + +export function useConstructRemoveLiquidityStep(poolId: string) { + const { isActiveStep, activateStep } = useActiveStep() + + const { useBuildTx } = useRemoveLiquidity() + + const removeLiquidityQuery = useBuildTx(isActiveStep) + + const transactionLabels = buildRemoveLiquidityLabels(poolId) + + const transaction = useManagedSendTransaction(transactionLabels, removeLiquidityQuery.data) + + const step: FlowStep = { + ...transaction, + transactionLabels, + id: `removeLiquidityPool${poolId}`, + stepType: 'removeLiquidity', + isComplete: () => false, + activateStep, + } + + return { + step, + isLoading: + transaction?.simulation.isLoading || + transaction?.execution.isLoading || + removeLiquidityQuery.isLoading, + error: + transaction?.simulation.error || transaction?.execution.error || removeLiquidityQuery.error, + } +} + +export const buildRemoveLiquidityLabels: BuildTransactionLabels = (poolId: Address) => { + return { + init: 'Remove liquidity', + confirming: 'Confirm remove liquidity', + tooltip: 'TODO', + description: `🎉 Liquidity removed from pool ${poolId}`, + } +} diff --git a/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.spec.tsx b/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.spec.tsx new file mode 100644 index 000000000..43646bf90 --- /dev/null +++ b/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.spec.tsx @@ -0,0 +1,50 @@ +import { balAddress, wETHAddress } from '@/lib/debug-helpers' +import { aBalWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders' +import { buildDefaultPoolTestProvider, testHook } from '@/test/utils/custom-renderers' +import { act } from 'react-dom/test-utils' +import { mainnet } from 'wagmi' +import { _useRemoveLiquidity } from './useRemoveLiquidity' + +const poolMock = aBalWethPoolElementMock() // 80BAL-20WETH +poolMock.dynamicData.totalLiquidity = '1000' +poolMock.dynamicData.totalShares = '100' +// bptPrice will be 1000/10 = 100 + +async function testUseRemoveLiquidity() { + const { result } = testHook(() => _useRemoveLiquidity(), { + wrapper: buildDefaultPoolTestProvider(poolMock), + }) + return result +} + +describe('When the user choses proportional remove liquidity', () => { + test('uses Proportional liquidity type with 100% percentage by default', async () => { + const result = await testUseRemoveLiquidity() + + expect(result.current.isProportional).toBeTruthy() + expect(result.current.sliderPercent).toBe(100) + expect(result.current.totalUsdValue).toBe('10000') + }) + + test('recalculates totalUsdValue when changing the slider', async () => { + const result = await testUseRemoveLiquidity() + + expect(result.current.totalUsdValue).toBe('10000') + + act(() => result.current.setSliderPercent(50)) + expect(result.current.sliderPercent).toBe(50) + + expect(result.current.totalUsdValue).toBe('5000') + }) +}) + +describe('When the user choses single token remove liquidity', () => { + test('returns selected token address with empty amount by default', async () => { + const result = await testUseRemoveLiquidity() + + act(() => result.current.setSingleTokenAddress(wETHAddress)) + + expect(result.current.singleTokenAddress).toEqual(wETHAddress) + //TODO: check empty amount + }) +}) diff --git a/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.tsx b/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.tsx index 209112f09..65569624e 100644 --- a/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.tsx +++ b/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity.tsx @@ -1,126 +1,112 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' -import { createContext, PropsWithChildren } from 'react' -import { useMandatoryContext } from '@/lib/shared/utils/contexts' -import { makeVar, useReactiveVar } from '@apollo/client' -import { usePool } from '../../usePool' import { useTokens } from '@/lib/modules/tokens/useTokens' +import { useUserAccount } from '@/lib/modules/web3/useUserAccount' +import { LABELS } from '@/lib/shared/labels' import { GqlToken, GqlTokenAmountHumanReadable } from '@/lib/shared/services/api/generated/graphql' +import { useMandatoryContext } from '@/lib/shared/utils/contexts' +import { isDisabledWithReason } from '@/lib/shared/utils/functions/isDisabledWithReason' +import { bn } from '@/lib/shared/utils/numbers' +import { HumanAmount } from '@balancer/sdk' +import { PropsWithChildren, createContext, useMemo, useState } from 'react' +import { usePool } from '../../usePool' +import { isEmptyHumanAmount } from '../LiquidityActionHelpers' +import { selectRemoveLiquidityHandler } from './handlers/selectRemoveLiquidityHandler' +import { useBuildRemoveLiquidityQuery } from './queries/useBuildRemoveLiquidityTxQuery' +import { useRemoveLiquidityPreviewQuery } from './queries/useRemoveLiquidityPreviewQuery' +import { useRemoveLiquidityPriceImpactQuery } from './queries/useRemoveLiquidityPriceImpactQuery' +import { RemoveLiquidityType } from './remove-liquidity.types' export type UseRemoveLiquidityResponse = ReturnType export const RemoveLiquidityContext = createContext(null) -type RemoveLiquidityType = 'PROPORTIONAL' | 'SINGLE_TOKEN' - -interface RemoveLiquidityState { - type: RemoveLiquidityType - singleToken: GqlTokenAmountHumanReadable | null - proportionalPercent: number - selectedOptions: { [poolTokenIndex: string]: string } - proportionalAmounts: GqlTokenAmountHumanReadable[] | null -} - -export const removeliquidityStateVar = makeVar({ - type: 'PROPORTIONAL', - proportionalPercent: 100, - singleToken: null, - selectedOptions: {}, - proportionalAmounts: null, -}) - export function _useRemoveLiquidity() { - const { pool } = usePool() + const { pool, bptPrice } = usePool() const { getToken, usdValueForToken } = useTokens() + const { isConnected } = useUserAccount() - async function setProportionalPercent(value: number) { - removeliquidityStateVar({ ...removeliquidityStateVar(), proportionalPercent: value }) - } + const [removalType, setRemovalType] = useState( + RemoveLiquidityType.Proportional + ) + const [singleTokenAddress, setSingleTokenAddress] = useState(null) + const [singleTokenHumanAmount, setSingleTokenHumanAmount] = useState('') - function setProportional() { - removeliquidityStateVar({ - ...removeliquidityStateVar(), - type: 'PROPORTIONAL', - singleToken: null, - }) - } + const [sliderPercent, setSliderPercent] = useState(100) - function setProportionalAmounts(proportionalAmounts: GqlTokenAmountHumanReadable[]) { - removeliquidityStateVar({ ...removeliquidityStateVar(), proportionalAmounts }) - } + const handler = useMemo(() => selectRemoveLiquidityHandler(pool, removalType), [pool.id]) - function setSingleToken(address: string) { - removeliquidityStateVar({ - ...removeliquidityStateVar(), - type: 'SINGLE_TOKEN', - singleToken: { address, amount: '' }, - }) - } + // const maxBptIn = pool.userBalance.totalBalance + // TODO: Hardcoded until it is ready in the API + const maxBptIn = 1000 + const bptIn = bn(maxBptIn).times(sliderPercent / 100) - function setSingleTokenAmount(tokenAmount: GqlTokenAmountHumanReadable) { - removeliquidityStateVar({ - ...removeliquidityStateVar(), - singleToken: tokenAmount, - }) - } + // TODO: Do we want to deal with human format + const humanBptIn: HumanAmount = bptIn.toFormat() as HumanAmount - function setSelectedOption(poolTokenIndex: number, tokenAddress: string) { - const state = removeliquidityStateVar() + const totalUsdValue = bn(bptIn).times(bptPrice).toString() - removeliquidityStateVar({ - ...state, - selectedOptions: { - ...state.selectedOptions, - [`${poolTokenIndex}`]: tokenAddress, - }, - }) + function setProportionalAmounts(proportionalAmounts: GqlTokenAmountHumanReadable[]) { + console.log({ proportionalAmounts }) } - function clearRemoveLiquidityState() { - removeliquidityStateVar({ - type: 'PROPORTIONAL', - proportionalPercent: 100, - singleToken: null, - selectedOptions: {}, - proportionalAmounts: null, - }) - } + const setProportional = () => setRemovalType(RemoveLiquidityType.Proportional) + const setSingleToken = () => setRemovalType(RemoveLiquidityType.SingleToken) + const isSingleToken = removalType === RemoveLiquidityType.SingleToken + const isProportional = removalType === RemoveLiquidityType.Proportional - const removeliquidityState = useReactiveVar(removeliquidityStateVar) + const singleToken: GqlTokenAmountHumanReadable = { + address: singleTokenAddress || '', //TODO remove null + amount: singleTokenHumanAmount, + } const tokens = pool.allTokens.map(token => getToken(token.address, pool.chain)) const validTokens = tokens.filter((token): token is GqlToken => !!token) - // // When the amounts in change we should fetch the expected output. - // useEffect(() => { - // queryRemoveLiquidity() - // }, [amountsOut]) + const { isPriceImpactLoading, priceImpact } = useRemoveLiquidityPriceImpactQuery( + handler, + pool.id, + humanBptIn + ) + + const { amountsOut, isPreviewQueryLoading } = useRemoveLiquidityPreviewQuery( + handler, + pool.id, + humanBptIn + ) - // // TODO: Call underlying SDK query function - // function queryRemoveLiquidity() { - // console.log('amountsOut', amountsOut) - // } + function useBuildTx(isActiveStep: boolean) { + return useBuildRemoveLiquidityQuery(handler, humanBptIn, isActiveStep, pool.id) + } - // // TODO: Call underlying SDK execution function - // function executeRemoveLiquidity() { - // console.log('amountsOut', amountsOut) - // } + const { isDisabled, disabledReason } = isDisabledWithReason( + [!isConnected, LABELS.walletNotConnected], + [isEmptyHumanAmount(humanBptIn), 'You must specify a valid bpt in'] + ) return { tokens, validTokens, - selectedRemoveLiquidityType: removeliquidityState.type, - singleToken: removeliquidityState.singleToken, - proportionalPercent: removeliquidityState.proportionalPercent, - selectedOptions: removeliquidityState.selectedOptions, - proportionalAmounts: removeliquidityState.proportionalAmounts, - setProportionalPercent, setProportional, - setSingleToken, - setSingleTokenAmount, - setSelectedOption, - clearRemoveLiquidityState, setProportionalAmounts, - //executeRemoveLiquidity, + setSingleToken, + setSingleTokenAddress, + setSingleTokenHumanAmount, + singleToken, + singleTokenAddress, + sliderPercent, + setSliderPercent, + isSingleToken, + isProportional, + setRemovalType, + totalUsdValue, + useBuildTx, + isPreviewQueryLoading, + isPriceImpactLoading, + priceImpact, + amountsOut, + isDisabled, + disabledReason, } } diff --git a/lib/modules/pool/pool.constants.ts b/lib/modules/pool/pool.constants.ts new file mode 100644 index 000000000..665e77f5f --- /dev/null +++ b/lib/modules/pool/pool.constants.ts @@ -0,0 +1,3 @@ +// all BPT tokens are represented with 18 decimals +// We use this constant to be specific about it in the context of BPTs code +export const BPT_DECIMALS = 18 diff --git a/lib/modules/pool/pool.helpers.ts b/lib/modules/pool/pool.helpers.ts index 0dda46ab8..4228016a1 100644 --- a/lib/modules/pool/pool.helpers.ts +++ b/lib/modules/pool/pool.helpers.ts @@ -7,7 +7,7 @@ import { GqlPoolType, } from '@/lib/shared/services/api/generated/graphql' import { getAddressBlockExplorerLink, isSameAddress } from '@/lib/shared/utils/addresses' -import { bn } from '@/lib/shared/utils/numbers' +import { Numberish, bn } from '@/lib/shared/utils/numbers' import { MinimalToken, PoolStateInput } from '@balancer/sdk' import BigNumber from 'bignumber.js' import { Address, Hex, getAddress } from 'viem' @@ -110,6 +110,10 @@ export function calcBptPrice(pool: GetPoolQuery['pool']): string { return bn(pool.dynamicData.totalLiquidity).div(pool.dynamicData.totalShares).toString() } +export function bptUsdValue(pool: GetPoolQuery['pool'], bptAmount: Numberish): string { + return bn(bptAmount).times(calcBptPrice(pool)).toString() +} + export function createdAfterTimestamp(pool: GqlPoolBase): boolean { // Pools should always have valid createTime so, for safety, we block the pool in case we don't get it // (createTime should probably not be treated as optional in the SDK types) @@ -156,6 +160,7 @@ export function usePoolHelpers(pool: Pool, chain: GqlChain) { export function toPoolStateInput(pool: Pool): PoolStateInput { // TODO: double check if we need an extra request to get PoolStateInput to get index token field + // Add index in GQL query instead of this const tokens = pool.tokens.map((t, index) => { return { ...t, index } }) diff --git a/lib/modules/tokens/approvals/useNextTokenApprovalStep.integration.spec.tsx b/lib/modules/tokens/approvals/useNextTokenApprovalStep.integration.spec.tsx index b25974236..84a394e10 100644 --- a/lib/modules/tokens/approvals/useNextTokenApprovalStep.integration.spec.tsx +++ b/lib/modules/tokens/approvals/useNextTokenApprovalStep.integration.spec.tsx @@ -1,5 +1,5 @@ import { wETHAddress, wjAuraAddress } from '@/lib/debug-helpers' -import { DefaultTokenAllowancesTestProvider, testHook } from '@/test/utils/custom-renderers' +import { DefaultAddLiquidityTestProvider, testHook } from '@/test/utils/custom-renderers' import { testPublicClient } from '@/test/utils/wagmi' import { waitFor } from '@testing-library/react' import { act } from 'react-dom/test-utils' @@ -19,7 +19,7 @@ async function resetForkState() { function testUseTokenApprovals(amountsToApprove: TokenAmountToApprove[]) { const { result } = testHook(() => useNextTokenApprovalStep(amountsToApprove), { - wrapper: DefaultTokenAllowancesTestProvider, + wrapper: DefaultAddLiquidityTestProvider, }) return result } diff --git a/lib/modules/web3/contracts/useManagedSendTransaction.integration.spec.ts b/lib/modules/web3/contracts/useManagedSendTransaction.integration.spec.ts index 42744fd64..26494d9e4 100644 --- a/lib/modules/web3/contracts/useManagedSendTransaction.integration.spec.ts +++ b/lib/modules/web3/contracts/useManagedSendTransaction.integration.spec.ts @@ -10,9 +10,9 @@ import { defaultTestUserAccount, testPublicClient as testClient } from '@/test/u import { ChainId, HumanAmount } from '@balancer/sdk' import { act, waitFor } from '@testing-library/react' import { SendTransactionResult } from 'wagmi/actions' -import { HumanAmountIn } from '../../pool/actions/add-liquidity/add-liquidity.types' -import { selectAddLiquidityHandler } from '../../pool/actions/add-liquidity/selectAddLiquidityHandler' +import { selectAddLiquidityHandler } from '../../pool/actions/add-liquidity/handlers/selectAddLiquidityHandler' import { buildAddLiquidityLabels } from '../../pool/actions/add-liquidity/useConstructAddLiquidityStep' +import { HumanAmountIn } from '../../pool/actions/liquidity-types' const chainId = ChainId.MAINNET const account = defaultTestUserAccount @@ -49,7 +49,7 @@ describe('weighted join test', () => { } const { sdkQueryOutput } = await handler.queryAddLiquidity(inputs) - const txConfig = await handler.buildAddLiquidityTx({ inputs, sdkQueryOutput }) + const txConfig = await handler.buildAddLiquidityTx({ inputs }) const { result } = testHook(() => { return useManagedSendTransaction(buildAddLiquidityLabels(), txConfig) diff --git a/lib/shared/components/btns/transaction-steps/lib.tsx b/lib/shared/components/btns/transaction-steps/lib.tsx index 529ba1aab..e52e60343 100644 --- a/lib/shared/components/btns/transaction-steps/lib.tsx +++ b/lib/shared/components/btns/transaction-steps/lib.tsx @@ -21,7 +21,7 @@ export type TransactionLabels = { preparing?: string } -type StepType = 'batchRelayerApproval' | 'tokenApproval' | 'addLiquidity' +type StepType = 'batchRelayerApproval' | 'tokenApproval' | 'addLiquidity' | 'removeLiquidity' export type ManagedResult = TransactionBundle & Executable diff --git a/lib/shared/components/inputs/InputWithSlider/InputWithSlider.tsx b/lib/shared/components/inputs/InputWithSlider/InputWithSlider.tsx index 8ae3fbf7c..90b18343d 100644 --- a/lib/shared/components/inputs/InputWithSlider/InputWithSlider.tsx +++ b/lib/shared/components/inputs/InputWithSlider/InputWithSlider.tsx @@ -20,10 +20,21 @@ type Props = { value?: string boxProps?: BoxProps setValue?: any + isNumberInputDisabled?: boolean } export const InputWithSlider = forwardRef( - ({ value, boxProps, setValue, children, ...numberInputProps }: NumberInputProps & Props, ref) => { + ( + { + value, + boxProps, + setValue, + children, + isNumberInputDisabled, + ...numberInputProps + }: NumberInputProps & Props, + ref + ) => { const { formatCurrency, parseCurrency } = useCurrency() function handleChange(value: string | number) { @@ -68,6 +79,7 @@ export const InputWithSlider = forwardRef( onKeyDown={blockInvalidNumberInput} onChange={handleChange} w="50%" + isDisabled={isNumberInputDisabled} {...numberInputProps} > { expect(JSON.stringify(12345n)).toBe('"12345"') @@ -50,3 +50,10 @@ describe('tokenFormat', () => { expect(fNum('token', '123456789.12345678', { abbreviated: false })).toBe('123,456,789.1235') }) }) + +describe('safeTokenFormat', () => { + test('for a bigint amount', () => { + expect(safeTokenFormat(251359380787607529n, 18)).toBe('0.2514') + expect(safeTokenFormat(null, 18)).toBe('-') + }) +}) diff --git a/lib/shared/utils/numbers.ts b/lib/shared/utils/numbers.ts index ba2bd1553..c6d5f7079 100644 --- a/lib/shared/utils/numbers.ts +++ b/lib/shared/utils/numbers.ts @@ -4,6 +4,7 @@ import { MAX_UINT256 } from '@balancer/sdk' import BigNumber from 'bignumber.js' import numeral from 'numeral' import { KeyboardEvent } from 'react' +import { formatUnits } from 'viem' // Allows calling JSON.stringify with bigints // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json @@ -148,3 +149,10 @@ export function fNum(format: NumberFormat, val: Numberish, opts?: FormatOpts): s throw new Error(`Number format not implemented: ${format}`) } } + +// Returns dash if token amount is null, otherwise returns humanized token amount in token display format. +export function safeTokenFormat(amount: bigint | null | undefined, decimals: number): string { + if (!amount) return '-' + + return fNum('token', formatUnits(amount, decimals)) +} diff --git a/lib/shared/utils/queries.ts b/lib/shared/utils/queries.ts index a66f492e6..b6b99144d 100644 --- a/lib/shared/utils/queries.ts +++ b/lib/shared/utils/queries.ts @@ -11,3 +11,5 @@ export function isRefetchingQueries(...queries: Pick[]) { return Promise.all(queries.map(query => query.refetch())) } + +export const defaultDebounceMs = 300 // milliseconds diff --git a/package.json b/package.json index 36daa882c..43d411f6e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@apollo/client": "3.8.0-rc.1", - "@balancer/sdk": "github:balancer/b-sdk#price-impact-with-dist", + "@balancer/sdk": "^0.5.0", "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/hooks": "^2.2.1", "@chakra-ui/icons": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd53335b0..0ec03c85e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ dependencies: specifier: 3.8.0-rc.1 version: 3.8.0-rc.1(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0) '@balancer/sdk': - specifier: github:balancer/b-sdk#price-impact-with-dist - version: github.com/balancer/b-sdk/b3c89775e2ddc86b2aa5dfed8af2cd37479a29bf(typescript@5.1.6)(zod@3.22.4) + specifier: ^0.5.0 + version: 0.5.0(typescript@5.1.6)(zod@3.22.4) '@chakra-ui/anatomy': specifier: ^2.2.2 version: 2.2.2 @@ -1062,6 +1062,22 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@balancer/sdk@0.5.0(typescript@5.1.6)(zod@3.22.4): + resolution: {integrity: sha512-2ys18xZAfGvwJpQmStmX2O6b/QkIQQWHpm09/qk2oHDg/lhnYYY5otfs14QuEQubdlA3Kth0U0uAxMlc4NxN/w==} + engines: {node: '>=18.x'} + dependencies: + async-retry: 1.3.3 + decimal.js-light: 2.5.1 + lodash: 4.17.21 + pino: 8.16.2 + viem: 1.18.1(typescript@5.1.6)(zod@3.22.4) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + dev: false + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -12644,21 +12660,3 @@ packages: '@types/react': 18.2.34 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) - - github.com/balancer/b-sdk/b3c89775e2ddc86b2aa5dfed8af2cd37479a29bf(typescript@5.1.6)(zod@3.22.4): - resolution: {tarball: https://codeload.github.com/balancer/b-sdk/tar.gz/b3c89775e2ddc86b2aa5dfed8af2cd37479a29bf} - id: github.com/balancer/b-sdk/b3c89775e2ddc86b2aa5dfed8af2cd37479a29bf - name: '@balancer/sdk' - version: 0.3.1 - engines: {node: '>=18.x'} - dependencies: - async-retry: 1.3.3 - decimal.js-light: 2.5.1 - pino: 8.16.2 - viem: 1.18.1(typescript@5.1.6)(zod@3.22.4) - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - - zod - dev: false diff --git a/test/anvil/anvil-global-setup.ts b/test/anvil/anvil-global-setup.ts index eeb715f03..fbb09110b 100644 --- a/test/anvil/anvil-global-setup.ts +++ b/test/anvil/anvil-global-setup.ts @@ -14,7 +14,9 @@ const port = 8555 const anvilOptions: CreateAnvilOptions = { forkUrl, port, - forkBlockNumber: 17878719, + // From time to time this block gets outdated having this kind of error in integration tests: + // ContractFunctionExecutionError: The contract function "queryJoin" returned no data ("0x"). + // forkBlockNumber: 18814198, } // https://www.npmjs.com/package/@viem/anvil diff --git a/test/msw/builders/gqlPoolElement.builders.ts b/test/msw/builders/gqlPoolElement.builders.ts index 0cf1b84e7..bd34afab4 100644 --- a/test/msw/builders/gqlPoolElement.builders.ts +++ b/test/msw/builders/gqlPoolElement.builders.ts @@ -14,6 +14,24 @@ import { aGqlStakingMock } from './gqlStaking.builders' import { balAddress, poolId, wETHAddress, wjAuraAddress } from '@/lib/debug-helpers' import { getPoolAddress } from '@balancer/sdk' +export function aBalWethPoolElementMock(...options: Partial[]): GqlPoolElement { + const poolId = '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014' // 80BAL-20WETH + const tokens = [ + aTokenExpandedMock({ address: balAddress }), + aTokenExpandedMock({ address: wETHAddress }), + ] + + const options2 = { + id: poolId, + address: getPoolAddress(poolId), + allTokens: tokens, + tokens: tokens as unknown as GqlPoolToken[], + ...options, + } + + return aGqlPoolElementMock(options2) +} + export function aWjAuraWethPoolElementMock(...options: Partial[]): GqlPoolElement { const tokens = [ aTokenExpandedMock({ address: wjAuraAddress }), @@ -61,6 +79,7 @@ export function aGqlPoolElementMock(...options: Partial[]): GqlP ], dynamicData: { totalLiquidity: '176725796.079429', + totalShares: '13131700.67391808961378162', lifetimeVolume: '1221246014.434743', lifetimeSwapFees: '5171589.170118799', volume24h: '545061.9941007149', diff --git a/test/utils/custom-renderers.tsx b/test/utils/custom-renderers.tsx index 3206c6ada..f6dad18d4 100644 --- a/test/utils/custom-renderers.tsx +++ b/test/utils/custom-renderers.tsx @@ -1,4 +1,5 @@ import { poolId, vaultV2Address, wETHAddress, wjAuraAddress } from '@/lib/debug-helpers' +import { AddLiquidityProvider } from '@/lib/modules/pool/actions/add-liquidity/useAddLiquidity' import { PoolVariant } from '@/lib/modules/pool/pool.types' import { PoolProvider } from '@/lib/modules/pool/usePool' import { @@ -8,6 +9,7 @@ import { } from '@/lib/modules/tokens/__mocks__/token.builders' import { TokensProvider } from '@/lib/modules/tokens/useTokens' import { RecentTransactionsProvider } from '@/lib/modules/transactions/RecentTransactionsProvider' +import { UserSettingsProvider } from '@/lib/modules/user/settings/useUserSettings' import { createWagmiConfig } from '@/lib/modules/web3/Web3Provider' import { AbiMap } from '@/lib/modules/web3/contracts/AbiMap' import { WriteAbiMutability } from '@/lib/modules/web3/contracts/contract.types' @@ -32,8 +34,7 @@ import { aGqlPoolElementMock } from '../msw/builders/gqlPoolElement.builders' import { apolloTestClient } from './apollo-test-client' import { AppRouterContextProviderMock } from './app-router-context-provider-mock' import { createWagmiTestConfig, defaultTestUserAccount, mainnetMockConnector } from './wagmi' -import { AddLiquidityProvider } from '@/lib/modules/pool/actions/add-liquidity/useAddLiquidity' -import { UserSettingsProvider } from '@/lib/modules/user/settings/useUserSettings' +import { RemoveLiquidityProvider } from '@/lib/modules/pool/actions/remove-liquidity/useRemoveLiquidity' export type WrapperProps = { children: ReactNode } export type Wrapper = ({ children }: WrapperProps) => ReactNode @@ -158,7 +159,7 @@ export async function useConnectTestAccount() { } } -export const DefaultTokenAllowancesTestProvider = ({ children }: PropsWithChildren) => ( +export const DefaultAddLiquidityTestProvider = ({ children }: PropsWithChildren) => ( ) +export const DefaultRemoveLiquidityTestProvider = ({ children }: PropsWithChildren) => ( + + + {children} + + +) + /* Builds a PoolProvider that injects the provided pool data*/ export const buildDefaultPoolTestProvider = (pool: GqlPoolElement = aGqlPoolElementMock()) => diff --git a/vitest.config.integration.ts b/vitest.config.integration.ts index 3649ef03e..f1a6461da 100644 --- a/vitest.config.integration.ts +++ b/vitest.config.integration.ts @@ -15,7 +15,7 @@ const integrationTestOptions: Partial = { testTimeout: 20_000, // Consider disabling threads if we detect problems with anvil // threads: false, - retry: 3, + retry: 1, // Uncomment the next line to exclude test for debug reasons // exclude: ['lib/modules/tokens/useTokenBalances.integration.spec.ts', 'node_modules', 'dist'], }