diff --git a/.env.template b/.env.template index 97d153ef1..6e2fc4c37 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 # Allow skip transaction (optional) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 163aaaac3..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: @@ -53,5 +52,7 @@ jobs: uses: ./.github/actions/setup - name: Set up foundry (includes anvil) uses: foundry-rs/foundry-toolchain@v1 + with: + cache: false - name: Run integration tests run: pnpm test:integration diff --git a/app/(app)/pools/page.tsx b/app/(app)/pools/page.tsx index 04726d6e3..d9182cd9a 100644 --- a/app/(app)/pools/page.tsx +++ b/app/(app)/pools/page.tsx @@ -9,7 +9,7 @@ import { Suspense } from 'react' // import { getProjectConfig } from '@/lib/config/getProjectConfig' // import { GetFeaturedPoolsDocument } from '@/lib/shared/services/api/generated/graphql' // import { FeaturedPools } from '@/lib/modules/featured-pools/FeaturedPools' -import { CowGalxeQuestPromoBanner } from '@/lib/shared/components/promos/CowGalxeQuestPromoBanner' +import { CowPromoBanner } from '@/lib/shared/components/promos/CowPromoBanner' export default async function PoolsPage() { // Featured pools set up @@ -33,7 +33,7 @@ export default async function PoolsPage() { - + {/* 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 dd1d9ff0f..d91a14f70 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 29aabfb31..9ad6022a8 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' @@ -19,6 +18,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 @@ -26,12 +26,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 59d33018e..4eaae19c4 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -19,6 +19,7 @@ export function useAddLiquiditySteps({ handler, humanAmountsIn, simulationQuery, + slippage, }: AddLiquidityStepsParams) { const { pool, chainId, chain } = usePool() const relayerMode = useRelayerMode(pool) @@ -46,6 +47,7 @@ export function useAddLiquiditySteps({ handler, humanAmountsIn, simulationQuery, + slippage, }) const steps = useMemo(() => { 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 a2e5aa9aa..c26f9ad62 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/receipts/receipt.hooks.integration.spec.ts b/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts index 71181dfa0..191d841be 100644 --- a/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts +++ b/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts @@ -54,11 +54,11 @@ test('queries add liquidity transaction', async () => { expect(result.current.sentTokens).toEqual([ { - tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + tokenAddress: '0x198d7387Fa97A73F05b8578CdEFf8F2A1f34Cd1F', humanAmount: '12', }, { - tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', humanAmount: '0.04', }, ]) @@ -99,11 +99,11 @@ test('queries remove liquidity transaction', async () => { expect(result.current.receivedTokens).toEqual([ { humanAmount: '16597.845312687911573359', - tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + tokenAddress: '0x198d7387Fa97A73F05b8578CdEFf8F2A1f34Cd1F', }, { humanAmount: '4.553531492712836774', - tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, ]) @@ -112,8 +112,8 @@ test('queries remove liquidity transaction', async () => { describe('queries swap transaction', () => { const polAddress = '0x0000000000000000000000000000000000001010' - const wPolAddress = '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270' - const daiAddress = '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063' + const wPolAddress = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' + const daiAddress = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063' test('when the native asset is not included (from DAI to WPOL)', async () => { const userAddress = '0xf76142b79Db34E57852d68F9c52C0E24f7349647' 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/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 1b1707a4f..37c8ed55c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@apollo/client": "^3.11.8", - "@balancer/sdk": "^0.26.0", + "@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 bc5d80c09..d54e3160a 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.0 - version: 0.26.0(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 @@ -1314,8 +1314,8 @@ packages: resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} engines: {node: '>=6.9.0'} - '@balancer/sdk@0.26.0': - resolution: {integrity: sha512-FgYUiCCL1vL1r7qI6rQFZLc+3o0uvBAUYNo2sYtyFLfEQEJLI6WRlx0KfdNR361rcW9Ra4emDw8wEHY5GJ+I6w==} + '@balancer/sdk@0.27.0': + resolution: {integrity: sha512-vYg1qLsmCCUqYMrLZLoCMzDnfI/1cKnYm4ve1KWVcKPNX/xjU5U69GhXK2Kst1SD0I8vIEPvbowBeSz+AeWtJw==} engines: {node: '>=18.x'} '@bcoe/v8-coverage@0.2.3': @@ -10702,7 +10702,7 @@ snapshots: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - '@balancer/sdk@0.26.0(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') } }