diff --git a/.env.template b/.env.template index a2a29bb54..f025be690 100644 --- a/.env.template +++ b/.env.template @@ -10,7 +10,6 @@ PRIVATE_HYPERNATIVE_API_SECRET=xxx PRIVATE_CURRENCYAPI_KEY=xxx # For integration tests and rpc proxy routes (optional) -NEXT_PRIVATE_ALCHEMY_KEY=xxx NEXT_PRIVATE_DRPC_KEY=xxx diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4717f4114..98be0ac47 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -6,7 +6,6 @@ on: env: NEXT_PUBLIC_BALANCER_API_URL: https://api-v3.balancer.fi/graphql NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.NEXT_PUBLIC_WALLET_CONNECT_ID }} - NEXT_PRIVATE_ALCHEMY_KEY: ${{ secrets.PRIVATE_ALCHEMY_KEY }} NEXT_PRIVATE_DRPC_KEY: ${{ secrets.PRIVATE_DRPC_KEY }} jobs: diff --git a/app/api/rpc/[chain]/route.ts b/app/api/rpc/[chain]/route.ts index 176a30a1d..8dbbb1164 100644 --- a/app/api/rpc/[chain]/route.ts +++ b/app/api/rpc/[chain]/route.ts @@ -6,22 +6,23 @@ type Params = { } } -const ALCHEMY_KEY = process.env.NEXT_PRIVATE_ALCHEMY_KEY || '' const DRPC_KEY = process.env.NEXT_PRIVATE_DRPC_KEY || '' +const dRpcUrl = (chainName: string) => + `https://lb.drpc.org/ogrpc?network=${chainName}&dkey=${DRPC_KEY}` const chainToRpcMap: Record = { - [GqlChain.Mainnet]: `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Arbitrum]: `https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Optimism]: `https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Base]: `https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Polygon]: `https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Avalanche]: `https://avax-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Fantom]: `https://fantom-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Sepolia]: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Fraxtal]: `https://frax-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, - [GqlChain.Gnosis]: `https://lb.drpc.org/ogrpc?network=gnosis&dkey=${DRPC_KEY}`, - [GqlChain.Mode]: undefined, - [GqlChain.Zkevm]: `https://polygonzkevm-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, + [GqlChain.Mainnet]: dRpcUrl('ethereum'), + [GqlChain.Arbitrum]: dRpcUrl('arbitrum'), + [GqlChain.Optimism]: dRpcUrl('optimism'), + [GqlChain.Base]: dRpcUrl('base'), + [GqlChain.Polygon]: dRpcUrl('polygon'), + [GqlChain.Avalanche]: dRpcUrl('avalanche'), + [GqlChain.Fantom]: dRpcUrl('fantom'), + [GqlChain.Sepolia]: dRpcUrl('sepolia'), + [GqlChain.Fraxtal]: dRpcUrl('fraxtal'), + [GqlChain.Gnosis]: dRpcUrl('gnosis'), + [GqlChain.Mode]: dRpcUrl('mode'), + [GqlChain.Zkevm]: dRpcUrl('polygon-zkevm'), } function getRpcUrl(chain: string) { @@ -35,11 +36,6 @@ function getRpcUrl(chain: string) { } export async function POST(request: Request, { params: { chain } }: Params) { - if (!ALCHEMY_KEY) { - return new Response(JSON.stringify({ error: 'NEXT_PRIVATE_ALCHEMY_KEY is missing' }), { - status: 500, - }) - } if (!DRPC_KEY) { return new Response(JSON.stringify({ error: 'NEXT_PRIVATE_DRPC_KEY is missing' }), { status: 500, diff --git a/lib/modules/pool/actions/PoolActionsPriceImpactDetails.tsx b/lib/modules/pool/actions/PoolActionsPriceImpactDetails.tsx index 44da270e7..736a71fa6 100644 --- a/lib/modules/pool/actions/PoolActionsPriceImpactDetails.tsx +++ b/lib/modules/pool/actions/PoolActionsPriceImpactDetails.tsx @@ -2,7 +2,6 @@ import { NumberText } from '@/lib/shared/components/typography/NumberText' import { fNum, bn } from '@/lib/shared/utils/numbers' import { HStack, VStack, Text, Tooltip, Icon, Box, Skeleton } from '@chakra-ui/react' import { usePriceImpact } from '@/lib/modules/price-impact/PriceImpactProvider' -import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' import { useCurrency } from '@/lib/shared/hooks/useCurrency' import { usePool } from '../PoolProvider' import { ArrowRight } from 'react-feather' @@ -13,6 +12,7 @@ import { InfoIcon } from '@/lib/shared/components/icons/InfoIcon' interface PoolActionsPriceImpactDetailsProps { bptAmount: bigint | undefined totalUSDValue: string + slippage: string isAddLiquidity?: boolean isLoading?: boolean } @@ -20,10 +20,10 @@ interface PoolActionsPriceImpactDetailsProps { export function PoolActionsPriceImpactDetails({ bptAmount, totalUSDValue, + slippage, isAddLiquidity = false, isLoading = false, }: PoolActionsPriceImpactDetailsProps) { - const { slippage } = useUserSettings() const { toCurrency } = useCurrency() const { pool } = usePool() diff --git a/lib/modules/pool/actions/add-liquidity/AddLiquidityProvider.tsx b/lib/modules/pool/actions/add-liquidity/AddLiquidityProvider.tsx index 82740c144..8b289b5ac 100644 --- a/lib/modules/pool/actions/add-liquidity/AddLiquidityProvider.tsx +++ b/lib/modules/pool/actions/add-liquidity/AddLiquidityProvider.tsx @@ -29,6 +29,7 @@ import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { isUnhandledAddPriceImpactError } from '@/lib/modules/price-impact/price-impact.utils' import { useModalWithPoolRedirect } from '../../useModalWithPoolRedirect' import { getPoolTokens } from '../../pool.helpers' +import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' export type UseAddLiquidityResponse = ReturnType export const AddLiquidityContext = createContext(null) @@ -39,12 +40,14 @@ export function _useAddLiquidity(urlTxHash?: Hash) { const [acceptPoolRisks, setAcceptPoolRisks] = useState(false) const [wethIsEth, setWethIsEth] = useState(false) const [totalUSDValue, setTotalUSDValue] = useState('0') + const [proportionalSlippage, setProportionalSlippage] = useState('0') const { pool, refetch: refetchPool, isLoading } = usePool() const { getToken, getNativeAssetToken, getWrappedNativeAssetToken, isLoadingTokenPrices } = useTokens() const { isConnected } = useUserAccount() const { hasValidationErrors } = useTokenInputsValidation() + const { slippage: userSlippage } = useUserSettings() const handler = useMemo(() => selectAddLiquidityHandler(pool), [pool.id, isLoading]) @@ -56,7 +59,8 @@ export function _useAddLiquidity(urlTxHash?: Hash) { const chain = pool.chain const nativeAsset = getNativeAssetToken(chain) const wNativeAsset = getWrappedNativeAssetToken(chain) - + const isForcedProportionalAdd = requiresProportionalInput(pool.type) + const slippage = isForcedProportionalAdd ? proportionalSlippage : userSlippage const tokens = getPoolTokens(pool, getToken) function setInitialHumanAmountsIn() { @@ -115,6 +119,7 @@ export function _useAddLiquidity(urlTxHash?: Hash) { handler, humanAmountsIn, simulationQuery, + slippage, }) const transactionSteps = useTransactionSteps(steps, isLoadingSteps) @@ -126,7 +131,7 @@ export function _useAddLiquidity(urlTxHash?: Hash) { const hasQuoteContext = !!simulationQuery.data async function refetchQuote() { - if (requiresProportionalInput(pool.type)) { + if (isForcedProportionalAdd) { /* This is the only edge-case where the SDK needs pool onchain data from the frontend (calculateProportionalAmounts uses pool.dynamicData.totalShares in its parameters) @@ -186,6 +191,10 @@ export function _useAddLiquidity(urlTxHash?: Hash) { addLiquidityTxHash, hasQuoteContext, addLiquidityTxSuccess, + slippage, + proportionalSlippage, + isForcedProportionalAdd, + setProportionalSlippage, refetchQuote, setHumanAmountIn, setHumanAmountsIn, diff --git a/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx b/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx index 8ace18282..100c3171e 100644 --- a/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx +++ b/lib/modules/pool/actions/add-liquidity/form/AddLiquidityForm.tsx @@ -21,7 +21,10 @@ import { Address } from 'viem' import { AddLiquidityModal } from '../modal/AddLiquidityModal' import { useAddLiquidity } from '../AddLiquidityProvider' import { bn, fNum } from '@/lib/shared/utils/numbers' -import { TransactionSettings } from '@/lib/modules/user/settings/TransactionSettings' +import { + ProportionalTransactionSettings, + TransactionSettings, +} from '@/lib/modules/user/settings/TransactionSettings' import { TokenInputs } from './TokenInputs' import { TokenInputsWithAddable } from './TokenInputsWithAddable' import { usePool } from '../../../PoolProvider' @@ -52,10 +55,10 @@ import { useTokens } from '@/lib/modules/tokens/TokensProvider' // small wrapper to prevent out of context error export function AddLiquidityForm() { - const { validTokens } = useAddLiquidity() + const { validTokens, proportionalSlippage } = useAddLiquidity() return ( - + ) @@ -78,6 +81,9 @@ function AddLiquidityMainForm() { nativeAsset, wNativeAsset, previewModalDisclosure, + proportionalSlippage, + slippage, + setProportionalSlippage, } = useAddLiquidity() const nextBtn = useRef(null) @@ -144,12 +150,6 @@ function AddLiquidityMainForm() { }) } - useEffect(() => { - if (addLiquidityTxHash) { - previewModalDisclosure.onOpen() - } - }, [addLiquidityTxHash]) - function onModalClose() { // restart polling for token prices when modal is closed again startTokenPricePolling() @@ -157,13 +157,27 @@ function AddLiquidityMainForm() { previewModalDisclosure.onClose() } + useEffect(() => { + if (addLiquidityTxHash) { + previewModalDisclosure.onOpen() + } + }, [addLiquidityTxHash]) + return ( Add liquidity - + {requiresProportionalInput(pool.type) ? ( + + ) : ( + + )} @@ -204,6 +218,7 @@ function AddLiquidityMainForm() { diff --git a/lib/modules/pool/actions/add-liquidity/form/TokenInputsWithAddable.tsx b/lib/modules/pool/actions/add-liquidity/form/TokenInputsWithAddable.tsx index b6c7cad73..86d4001b2 100644 --- a/lib/modules/pool/actions/add-liquidity/form/TokenInputsWithAddable.tsx +++ b/lib/modules/pool/actions/add-liquidity/form/TokenInputsWithAddable.tsx @@ -159,7 +159,7 @@ export function TokenInputsWithAddable({ + /> ) } diff --git a/lib/modules/pool/actions/add-liquidity/form/useMaximumInputs.tsx b/lib/modules/pool/actions/add-liquidity/form/useMaximumInputs.tsx index 3db011d25..fbf0ea7a8 100644 --- a/lib/modules/pool/actions/add-liquidity/form/useMaximumInputs.tsx +++ b/lib/modules/pool/actions/add-liquidity/form/useMaximumInputs.tsx @@ -10,6 +10,7 @@ import { useMemo, useState } from 'react' import { usePool } from '../../../PoolProvider' import { useAddLiquidity } from '../AddLiquidityProvider' import { useTotalUsdValue } from '@/lib/modules/tokens/useTotalUsdValue' +import { TokenAmount } from '@/lib/modules/tokens/token.types' export function useMaximumInputs() { const { isConnected } = useUserAccount() @@ -20,12 +21,15 @@ export function useMaximumInputs() { const { isLoadingTokenPrices } = useTokens() const [isMaximized, setIsMaximized] = useState(false) + // Depending on if the user is using WETH or ETH, we need to filter out the + // native asset or wrapped native asset. + const nativeAssetFilter = (balance: TokenAmount) => + wethIsEth + ? wNativeAsset && balance.address !== wNativeAsset.address + : nativeAsset && balance.address !== nativeAsset.address + const filteredBalances = useMemo(() => { - return balances.filter(balance => - wethIsEth - ? wNativeAsset && balance.address !== wNativeAsset.address - : nativeAsset && balance.address !== nativeAsset.address - ) + return balances.filter(nativeAssetFilter) }, [wethIsEth, isBalancesLoading]) function handleMaximizeUserAmounts() { diff --git a/lib/modules/pool/actions/add-liquidity/form/useProportionalInputs.tsx b/lib/modules/pool/actions/add-liquidity/form/useProportionalInputs.tsx index 4dbfb76ec..7734e54cf 100644 --- a/lib/modules/pool/actions/add-liquidity/form/useProportionalInputs.tsx +++ b/lib/modules/pool/actions/add-liquidity/form/useProportionalInputs.tsx @@ -17,7 +17,7 @@ import { } from '../../LiquidityActionHelpers' import { useAddLiquidity } from '../AddLiquidityProvider' import { useTotalUsdValue } from '@/lib/modules/tokens/useTotalUsdValue' -import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' +import { HumanTokenAmountWithAddress, TokenAmount } from '@/lib/modules/tokens/token.types' import { swapWrappedWithNative } from '@/lib/modules/tokens/token.helpers' type OptimalToken = { @@ -42,13 +42,16 @@ export function useProportionalInputs() { const [isMaximized, setIsMaximized] = useState(false) const { isLoadingTokenPrices } = useTokens() + // Depending on if the user is using WETH or ETH, we need to filter out the + // native asset or wrapped native asset. + const nativeAssetFilter = (balance: TokenAmount) => + wethIsEth + ? wNativeAsset && balance.address !== wNativeAsset.address + : nativeAsset && balance.address !== nativeAsset.address + const filteredBalances = useMemo(() => { - return balances.filter(balance => - wethIsEth - ? wNativeAsset && balance.address !== wNativeAsset.address - : nativeAsset && balance.address !== nativeAsset.address - ) - }, [wethIsEth, isBalancesLoading]) + return balances.filter(nativeAssetFilter) + }, [wethIsEth, isBalancesLoading, balances]) function clearAmountsIn(changedAmount?: HumanTokenAmountWithAddress) { setHumanAmountsIn( diff --git a/lib/modules/pool/actions/add-liquidity/handlers/ProportionalAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/ProportionalAddLiquidity.handler.ts index f9cad592b..646235f87 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/ProportionalAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/ProportionalAddLiquidity.handler.ts @@ -48,17 +48,13 @@ export class ProportionalAddLiquidityHandler implements AddLiquidityHandler { account, queryOutput, humanAmountsIn, + slippagePercent, }: SdkBuildAddLiquidityInput): Promise { const addLiquidity = new AddLiquidity() const { callData, to, value } = addLiquidity.buildCall({ ...queryOutput.sdkQueryOutput, - // Setting slippage to zero ensures the build call can't fail if the user - // maxes out their balance. It can result in a tx failure if the pool - // state changes significantly in the background. The assumption is that - // this should be rare. If not, we will have to re-introduce slippage here - // and limit the user input amounts to their balance - slippage. - slippage: Slippage.fromPercentage('0' as HumanAmount), + slippage: Slippage.fromPercentage(slippagePercent as HumanAmount), sender: account, recipient: account, wethIsEth: this.helpers.isNativeAssetIn(humanAmountsIn), diff --git a/lib/modules/pool/actions/add-liquidity/modal/AddLiquiditySummary.tsx b/lib/modules/pool/actions/add-liquidity/modal/AddLiquiditySummary.tsx index 8e739c3e1..e7cbb9086 100644 --- a/lib/modules/pool/actions/add-liquidity/modal/AddLiquiditySummary.tsx +++ b/lib/modules/pool/actions/add-liquidity/modal/AddLiquiditySummary.tsx @@ -34,6 +34,7 @@ export function AddLiquiditySummary({ tokens, addLiquidityTxHash, addLiquidityTxSuccess, + slippage, } = useAddLiquidity() const { pool } = usePool() const { isMobile } = useBreakpoints() @@ -123,6 +124,7 @@ export function AddLiquiditySummary({ diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts index 8bb745f67..fd3c6c02c 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts @@ -1,4 +1,3 @@ -import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { defaultDebounceMs, onlyExplicitRefetch } from '@/lib/shared/utils/queries' import { useQuery } from '@tanstack/react-query' @@ -21,6 +20,7 @@ export type AddLiquidityBuildQueryParams = { handler: AddLiquidityHandler humanAmountsIn: HumanTokenAmountWithAddress[] simulationQuery: AddLiquiditySimulationQueryResult + slippage: string } // Uses the SDK to build a transaction config to be used by wagmi's useManagedSendTransaction @@ -28,12 +28,12 @@ export function useAddLiquidityBuildCallDataQuery({ handler, humanAmountsIn, simulationQuery, + slippage, enabled, }: AddLiquidityBuildQueryParams & { enabled: boolean }) { const { userAddress, isConnected } = useUserAccount() - const { slippage } = useUserSettings() const { pool, chainId } = usePool() const { data: blockNumber } = useBlockNumber({ chainId }) const { relayerApprovalSignature } = useRelayerSignature() diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index 5dda3318b..fdc784fed 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -62,6 +62,7 @@ export function useAddLiquiditySteps({ handler, humanAmountsIn, simulationQuery, + slippage, }) const addSteps = isPermit2 ? [signPermit2Step, addLiquidityStep] : [addLiquidityStep] diff --git a/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx b/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx index a542f2d1c..0a6deb38c 100644 --- a/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx +++ b/lib/modules/pool/actions/remove-liquidity/form/RemoveLiquidityForm.tsx @@ -35,6 +35,7 @@ import { SimulationError } from '@/lib/shared/components/errors/SimulationError' import { InfoIcon } from '@/lib/shared/components/icons/InfoIcon' import { SafeAppAlert } from '@/lib/shared/components/alerts/SafeAppAlert' import { useTokens } from '@/lib/modules/tokens/TokensProvider' +import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' const TABS: ButtonGroupOption[] = [ { value: 'proportional', @@ -71,6 +72,7 @@ export function RemoveLiquidityForm() { const nextBtn = useRef(null) const [activeTab, setActiveTab] = useState(TABS[0]) const { startTokenPricePolling } = useTokens() + const { slippage } = useUserSettings() useEffect(() => { setPriceImpact(priceImpactQuery.data) @@ -177,6 +179,7 @@ export function RemoveLiquidityForm() { } diff --git a/lib/modules/pool/actions/remove-liquidity/modal/RemoveLiquiditySummary.tsx b/lib/modules/pool/actions/remove-liquidity/modal/RemoveLiquiditySummary.tsx index a535730d9..0e1758af2 100644 --- a/lib/modules/pool/actions/remove-liquidity/modal/RemoveLiquiditySummary.tsx +++ b/lib/modules/pool/actions/remove-liquidity/modal/RemoveLiquiditySummary.tsx @@ -14,6 +14,7 @@ import { RemoveLiquidityReceiptResult } from '@/lib/modules/transactions/transac import { BalAlert } from '@/lib/shared/components/alerts/BalAlert' import { useTokens } from '@/lib/modules/tokens/TokensProvider' import { CardPopAnim } from '@/lib/shared/components/animations/CardPopAnim' +import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' export function RemoveLiquiditySummary({ isLoading: isLoadingReceipt, @@ -34,6 +35,7 @@ export function RemoveLiquiditySummary({ const { getTokensByChain } = useTokens() const { pool } = usePool() const { userAddress, isLoading: isUserAddressLoading } = useUserAccount() + const { slippage } = useUserSettings() const _amountsOut = amountsOut.filter(amount => bn(amount.humanAmount).gt(0)) @@ -87,6 +89,7 @@ export function RemoveLiquiditySummary({ diff --git a/lib/modules/tokens/TokenBalancesProvider.tsx b/lib/modules/tokens/TokenBalancesProvider.tsx index 77b0f206b..a46bc9107 100644 --- a/lib/modules/tokens/TokenBalancesProvider.tsx +++ b/lib/modules/tokens/TokenBalancesProvider.tsx @@ -12,6 +12,7 @@ import { useMandatoryContext } from '@/lib/shared/utils/contexts' import { getNetworkConfig } from '@/lib/config/app.config' import { GqlToken } from '@/lib/shared/services/api/generated/graphql' import { exclNativeAssetFilter, nativeAssetFilter } from './token.helpers' +import { HumanAmount, Slippage } from '@balancer/sdk' const BALANCE_CACHE_TIME_MS = 30_000 @@ -19,10 +20,18 @@ export type UseTokenBalancesResponse = ReturnType export const TokenBalancesContext = createContext(null) /** - * If initTokens are provided the tokens state will be managed internally. - * If extTokens are provided the tokens state will be managed externally. + * @param initTokens If initTokens are provided the tokens state will be managed internally. + * @param extTokens If extTokens are provided the tokens state will be managed externally. + * @param bufferPercentage An amount used to reduce the balances of the user's tokens. This is + * primarily used for forced proportional adds where we need to "reserve" + * an amount of the user's tokens to ensure the add is successful. In this + * case the buffer is set to the slippage percentage set by the user. */ -export function _useTokenBalances(initTokens?: GqlToken[], extTokens?: GqlToken[]) { +export function _useTokenBalances( + initTokens?: GqlToken[], + extTokens?: GqlToken[], + bufferPercentage: HumanAmount | string = '0' +) { if (!initTokens && !extTokens) throw new Error('initTokens or tokens must be provided') if (initTokens && extTokens) throw new Error('initTokens and tokens cannot be provided together') @@ -79,7 +88,10 @@ export function _useTokenBalances(initTokens?: GqlToken[], extTokens?: GqlToken[ .map((balance, index) => { const token = tokensExclNativeAsset[index] if (!token) return - const amount = balance.status === 'success' ? (balance.result as bigint) : 0n + + let amount = balance.status === 'success' ? (balance.result as bigint) : 0n + const slippage = Slippage.fromPercentage(bufferPercentage as HumanAmount) + amount = slippage.applyTo(amount, -1) return { chainId, @@ -129,10 +141,16 @@ export function _useTokenBalances(initTokens?: GqlToken[], extTokens?: GqlToken[ type ProviderProps = PropsWithChildren<{ initTokens?: GqlToken[] extTokens?: GqlToken[] + bufferPercentage?: HumanAmount | string }> -export function TokenBalancesProvider({ initTokens, extTokens, children }: ProviderProps) { - const hook = _useTokenBalances(initTokens, extTokens) +export function TokenBalancesProvider({ + initTokens, + extTokens, + bufferPercentage, + children, +}: ProviderProps) { + const hook = _useTokenBalances(initTokens, extTokens, bufferPercentage) return {children} } diff --git a/lib/modules/transactions/transaction-steps/lib.tsx b/lib/modules/transactions/transaction-steps/lib.tsx index c5a5841c9..b3916f17b 100644 --- a/lib/modules/transactions/transaction-steps/lib.tsx +++ b/lib/modules/transactions/transaction-steps/lib.tsx @@ -69,9 +69,15 @@ type Executable = { setTxConfig?: any } +export type SubSteps = { + tokens: string[] + gas: number +} + export type TransactionStep = { id: string stepType: StepType + subSteps?: SubSteps labels: TransactionLabels isComplete: () => boolean renderAction: () => ReactNode diff --git a/lib/modules/transactions/transaction-steps/step-tracker/DesktopStepTracker.tsx b/lib/modules/transactions/transaction-steps/step-tracker/DesktopStepTracker.tsx index bbc63c7e6..b8d40af41 100644 --- a/lib/modules/transactions/transaction-steps/step-tracker/DesktopStepTracker.tsx +++ b/lib/modules/transactions/transaction-steps/step-tracker/DesktopStepTracker.tsx @@ -13,7 +13,7 @@ type Props = { export function DesktopStepTracker({ chain, transactionSteps }: Props) { return ( - + diff --git a/lib/modules/transactions/transaction-steps/step-tracker/Step.tsx b/lib/modules/transactions/transaction-steps/step-tracker/Step.tsx index 8859c94fd..3581a4b3e 100644 --- a/lib/modules/transactions/transaction-steps/step-tracker/Step.tsx +++ b/lib/modules/transactions/transaction-steps/step-tracker/Step.tsx @@ -1,8 +1,17 @@ -import { CircularProgress, CircularProgressLabel, HStack, Text, VStack } from '@chakra-ui/react' +import { + Box, + CircularProgress, + CircularProgressLabel, + HStack, + Text, + VStack, +} from '@chakra-ui/react' import { StepProps, getStepSettings } from './getStepSettings' import { Check } from 'react-feather' import { ManagedResult } from '../lib' +import type { SubSteps } from '../lib' import { useTransactionState } from '../TransactionStateProvider' +import { indexToLetter } from '@/lib/shared/labels' export function Step(props: StepProps) { const { getTransaction } = useTransactionState() @@ -10,12 +19,13 @@ export function Step(props: StepProps) { const { color, isActive, title } = getStepSettings(props, transaction) return ( - + {title} + {props.step?.subSteps && } ) @@ -58,3 +68,34 @@ export function StepIndicator({ ) } + +function SubSteps({ color, subSteps }: { color: string; subSteps: SubSteps }) { + return ( + + {subSteps.gas === 0 && ( + + Signature: Free + + )} + {subSteps.tokens.length > 1 && + subSteps.tokens.map((subStep, index) => ( + + + + {subStep} + + + ))} + + ) +} + +function SubStepIndicator({ color, label }: { color: string; label: string }) { + return ( + + + {label} + + + ) +} diff --git a/lib/modules/transactions/transaction-steps/step-tracker/Steps.tsx b/lib/modules/transactions/transaction-steps/step-tracker/Steps.tsx index 87dfe176e..1659423ca 100644 --- a/lib/modules/transactions/transaction-steps/step-tracker/Steps.tsx +++ b/lib/modules/transactions/transaction-steps/step-tracker/Steps.tsx @@ -1,4 +1,4 @@ -import { Box, VStack } from '@chakra-ui/react' +import { VStack } from '@chakra-ui/react' import { Step } from './Step' import { useThemeColorMode } from '@/lib/shared/services/chakra/useThemeColorMode' import { TransactionStepsResponse } from '../useTransactionSteps' @@ -23,9 +23,9 @@ export function Steps({ transactionSteps }: Props) { colorMode={colorMode} isLastStep={isLastStep(index)} /> - {!isLastStep(index) && ( + {/* {!isLastStep(index) && ( - )} + )} */} ))} diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index a559a74e7..00a4a822a 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -10,17 +10,20 @@ import { useSignPermit2Transfer, } from '../../tokens/approvals/permit2/useSignPermit2Transfer' import { useChainSwitch } from '../../web3/useChainSwitch' -import { TransactionStep } from './lib' +import { SubSteps, TransactionStep } from './lib' import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonces' import { getChainId } from '@/lib/config/app.config' import { SignatureState } from '../../web3/signatures/signature.helpers' +import { getTokenAddresses, getTokenSymbols } from '../../pool/actions/LiquidityActionHelpers' +import { useTokens } from '../../tokens/TokensProvider' export function useSignPermit2Step(params: AddLiquidityPermit2Params): TransactionStep { const { isConnected, userAddress } = useUserAccount() + const { getToken } = useTokens() const { isLoadingNonces, nonces } = usePermit2Nonces({ chainId: getChainId(params.pool.chain), - tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), + tokenAddresses: getTokenAddresses(params.queryOutput), owner: userAddress, enabled: params.isPermit2, }) @@ -37,7 +40,8 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti getChainId(params.pool.chain) ) - const isLoading = isLoadingTransfer || isLoadingNonces + const isLoading = + isLoadingTransfer || isLoadingNonces || signPermit2State === SignatureState.Confirming const SignPermitButton = () => ( @@ -63,13 +67,18 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti const isComplete = () => signPermit2State === SignatureState.Completed + const subSteps: SubSteps = { + gas: 0, + tokens: getTokenSymbols(getToken, params.pool.chain, params.queryOutput), + } + return useMemo( () => ({ id: 'sign-permit2', stepType: 'signPermit2', + subSteps, labels: { - // TODO: display nested permit tokens in Step Tracker - title: `Permit on balancer`, + title: getTitle(subSteps), init: `Permit transfer`, tooltip: 'Sign permit2 transfer', }, @@ -80,3 +89,9 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti [signPermit2State, isLoading, isConnected] ) } + +function getTitle(subSteps?: SubSteps): string { + if (!subSteps) return `Permit on balancer` + if (subSteps.tokens.length === 1) return `${subSteps.tokens[0]}: Permit on balancer` + return 'Permit tokens on balancer' +} diff --git a/lib/modules/user/settings/TransactionSettings.tsx b/lib/modules/user/settings/TransactionSettings.tsx index 4497f46ca..9100ee42b 100644 --- a/lib/modules/user/settings/TransactionSettings.tsx +++ b/lib/modules/user/settings/TransactionSettings.tsx @@ -1,5 +1,4 @@ 'use client' - import { Button, HStack, @@ -14,15 +13,16 @@ import { VStack, Text, ButtonProps, + useDisclosure, } from '@chakra-ui/react' import { useUserSettings } from './UserSettingsProvider' import { fNum } from '@/lib/shared/utils/numbers' -import { Settings } from 'react-feather' +import { AlertTriangle, Settings } from 'react-feather' import { CurrencySelect } from './CurrencySelect' import { SlippageInput } from './UserSettings' export function TransactionSettings(props: ButtonProps) { - const { slippage } = useUserSettings() + const { slippage, setSlippage } = useUserSettings() return ( @@ -46,7 +46,75 @@ export function TransactionSettings(props: ButtonProps) { Slippage - + + + + Currency + + + + + + + ) +} + +interface ProportionalTransactionSettingsProps extends ButtonProps { + slippage: string + setSlippage: (value: string) => void +} + +export function ProportionalTransactionSettings({ + slippage, + setSlippage, + ...props +}: ProportionalTransactionSettingsProps) { + const { isOpen, onOpen, onClose } = useDisclosure() + + return ( + + + + + + + + + Transaction settings + + + + + + Slippage + + + + + + + + + + Slippage is set to 0 by default for forced proportional actions to reduce + dust left over. If you need to set slippage higher than 0 it will + effectively lower the amount of tokens you can add in the form below. Then, + if slippage occurs, the transaction can take the amount of tokens you + specified + slippage from your token balance. + + + + + + Currency diff --git a/lib/modules/user/settings/UserSettings.tsx b/lib/modules/user/settings/UserSettings.tsx index 84f13b7aa..e8948dd32 100644 --- a/lib/modules/user/settings/UserSettings.tsx +++ b/lib/modules/user/settings/UserSettings.tsx @@ -23,8 +23,12 @@ import { blockInvalidNumberInput } from '@/lib/shared/utils/numbers' import { Percent, Settings } from 'react-feather' import { CurrencySelect } from './CurrencySelect' -export function SlippageInput() { - const { slippage, setSlippage } = useUserSettings() +interface SlippageInputProps { + slippage: string + setSlippage: (value: string) => void +} + +export function SlippageInput({ slippage, setSlippage }: SlippageInputProps) { const presetOpts = ['0.5', '1', '2'] return ( @@ -81,6 +85,8 @@ function ToggleAllowSounds() { } export function UserSettings() { + const { slippage, setSlippage } = useUserSettings() + return ( @@ -109,7 +115,7 @@ export function UserSettings() { Slippage - + diff --git a/lib/shared/labels.ts b/lib/shared/labels.ts index 5da845fc8..af3369868 100644 --- a/lib/shared/labels.ts +++ b/lib/shared/labels.ts @@ -1,4 +1,10 @@ -// Common labels used in app, helps keep common text consitent and easy to change. +// Common labels used in app, helps keep common text consistent and easy to change. export const LABELS = { walletNotConnected: 'Wallet not connected', } + +// Converts an index to a letter. 0 is A, 1 is B, etc. +export function indexToLetter(index: number): string { + if (index >= 0 && index <= 25) return String.fromCharCode(index + 65) + throw new Error('index but be between 0 and 25') +} diff --git a/lib/shared/utils/query-errors.ts b/lib/shared/utils/query-errors.ts index 73cf5c3ff..82a163f1f 100644 --- a/lib/shared/utils/query-errors.ts +++ b/lib/shared/utils/query-errors.ts @@ -308,6 +308,18 @@ export function shouldIgnore(message: string, stackTrace = ''): boolean { return true } + /* + Extension related error which does not crash. + Examples: https://balancer-labs.sentry.io/issues/5622743248/ + */ + if ( + message === + "Cannot destructure property 'address' of '(intermediate value)' as it is undefined." && + stackTrace.includes('extensionPageScript.js') + ) { + return true + } + /* Waller Connect bug More info: https://github.com/WalletConnect/walletconnect-monorepo/issues/4318 diff --git a/package.json b/package.json index bb324beda..51d83b721 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@apollo/client": "^3.11.8", - "@balancer/sdk": "^0.26.1", + "@balancer/sdk": "^0.27.0", "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/hooks": "^2.2.1", "@chakra-ui/icons": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b9558c0d..7b91dc342 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^3.11.8 version: 3.11.8(@types/react@18.2.34)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@balancer/sdk': - specifier: ^0.26.1 - version: 0.26.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4) + specifier: ^0.27.0 + version: 0.27.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4) '@chakra-ui/anatomy': specifier: ^2.2.2 version: 2.2.2 @@ -1311,8 +1311,8 @@ packages: resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} engines: {node: '>=6.9.0'} - '@balancer/sdk@0.26.1': - resolution: {integrity: sha512-RH4XKjtX2itE1SwDz5SZUOuO67XRcFVwCGysef6nh/iPFEf0qYdDAFAu5ms+V6ZXyVq3OmjDUrkarfDKrKkGhw==} + '@balancer/sdk@0.27.0': + resolution: {integrity: sha512-vYg1qLsmCCUqYMrLZLoCMzDnfI/1cKnYm4ve1KWVcKPNX/xjU5U69GhXK2Kst1SD0I8vIEPvbowBeSz+AeWtJw==} engines: {node: '>=18.x'} '@bcoe/v8-coverage@0.2.3': @@ -10667,7 +10667,7 @@ snapshots: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - '@balancer/sdk@0.26.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@balancer/sdk@0.27.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: decimal.js-light: 2.5.1 lodash.clonedeep: 4.5.0 diff --git a/test/anvil/anvil-setup.ts b/test/anvil/anvil-setup.ts index 7113a1918..520eb9450 100644 --- a/test/anvil/anvil-setup.ts +++ b/test/anvil/anvil-setup.ts @@ -84,16 +84,19 @@ export function getTestRpcSetup(networkName: NetworksWithFork) { } export function getForkUrl(network: NetworkSetup, verbose = false): string { - const privateAlchemyKey = process.env['NEXT_PRIVATE_ALCHEMY_KEY'] - if (privateAlchemyKey) { + const privateKey = process.env['NEXT_PRIVATE_DRPC_KEY'] + const dRpcUrl = (chainName: string) => + `https://lb.drpc.org/ogrpc?network=${chainName}&dkey=${privateKey}` + + if (privateKey) { if (network.networkName === 'Ethereum') { - return `https://eth-mainnet.g.alchemy.com/v2/${privateAlchemyKey}` + return dRpcUrl('ethereum') } if (network.networkName === 'Polygon') { - return `https://polygon-mainnet.g.alchemy.com/v2/${privateAlchemyKey}` + return dRpcUrl('polygon') } if (network.networkName === 'Sepolia') { - return `https://eth-sepolia.g.alchemy.com/v2/${privateAlchemyKey}` + return dRpcUrl('sepolia') } }