From fa440b0225d1781b50f852848e1409ff0e8b04c3 Mon Sep 17 00:00:00 2001 From: groninge Date: Wed, 29 Jul 2026 09:26:17 +0200 Subject: [PATCH 1/3] fix: use strict guards before BigNumber conversions --- packages/lib/modules/lbp/pool/usePriceInfo.ts | 4 ++-- .../LbpPoolCharts/LbpPoolChartsProvider.tsx | 4 ++-- .../pool/PoolDetail/PoolComposition.tsx | 22 +++++++++---------- .../add-liquidity/useIsMinimumDepositMet.ts | 6 ++--- .../lib/modules/pool/actions/stake.helpers.ts | 4 ++-- packages/lib/modules/pool/pool.utils.spec.ts | 17 ++++++++++++++ packages/lib/modules/pool/pool.utils.ts | 4 ++-- .../queries/useOnchainUserPoolBalances.ts | 6 ++--- .../modules/pool/useGetPoolRewards.spec.tsx | 2 +- .../lib/modules/pool/useGetPoolRewards.tsx | 6 ++--- .../modules/pool/user-balance.helpers.spec.ts | 2 +- .../lib/modules/pool/user-balance.helpers.ts | 16 +++++++------- .../recovered-funds/useRecoveredFunds.ts | 4 ++-- .../useGetHiddenHandRewards.tsx | 6 ++--- .../swap/handlers/AuraBalSwap.handler.ts | 6 ++--- .../tokens/TokenInput/useTokenInput.ts | 4 ++-- .../modules/tokens/TokenRow/TokenRowGroup.tsx | 6 ++--- .../lib/modules/tokens/TokensProvider.tsx | 10 ++++----- 18 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 packages/lib/modules/pool/pool.utils.spec.ts diff --git a/packages/lib/modules/lbp/pool/usePriceInfo.ts b/packages/lib/modules/lbp/pool/usePriceInfo.ts index 87e7398eac..83bcea3be1 100644 --- a/packages/lib/modules/lbp/pool/usePriceInfo.ts +++ b/packages/lib/modules/lbp/pool/usePriceInfo.ts @@ -12,7 +12,7 @@ import { secondsToMilliseconds, startOfHour, } from 'date-fns' -import { bn, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers' import { Address } from 'viem' import { now } from '@repo/lib/shared/utils/time' @@ -61,7 +61,7 @@ export function usePriceInfo(chain: GqlChain, poolId: Address, isFixedLbp = fals function toLbpSnapshots(apiPrices: LbpPriceChartDataFragment[]): LbpSnapshot[] { return apiPrices.map(price => ({ timestamp: new Date(secondsToMilliseconds(price.timestamp)), - projectTokenPrice: isValidNumber(price.projectTokenPrice) + projectTokenPrice: isBnParseable(price.projectTokenPrice) ? bn(price.projectTokenPrice) .times(price.reservePrice || 1) .toNumber() diff --git a/packages/lib/modules/pool/LbpDetail/LbpPoolCharts/LbpPoolChartsProvider.tsx b/packages/lib/modules/pool/LbpDetail/LbpPoolCharts/LbpPoolChartsProvider.tsx index ce648fff59..3f3e003fbf 100644 --- a/packages/lib/modules/pool/LbpDetail/LbpPoolCharts/LbpPoolChartsProvider.tsx +++ b/packages/lib/modules/pool/LbpDetail/LbpPoolCharts/LbpPoolChartsProvider.tsx @@ -16,7 +16,7 @@ import { } from 'date-fns' import { LbpV3 } from '@repo/lib/modules/pool/pool.types' import { isFixedLBP } from '@repo/lib/modules/pool/pool.helpers' -import { bn, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers' import { isSameAddress } from '@repo/lib/shared/utils/addresses' type LbpPoolChartsContextType = ReturnType @@ -62,7 +62,7 @@ export function useLbpPoolChartsLogic() { .toNumber() const fundsRaisedGoal = - projectToken?.balance && isValidNumber(projectToken.balance) && projectTokenRate + projectToken?.balance && isBnParseable(projectToken.balance) && projectTokenRate ? bn(projectToken.balance).times(projectTokenRate).toNumber() : null diff --git a/packages/lib/modules/pool/PoolDetail/PoolComposition.tsx b/packages/lib/modules/pool/PoolDetail/PoolComposition.tsx index 32a39c600f..253a1f6db4 100644 --- a/packages/lib/modules/pool/PoolDetail/PoolComposition.tsx +++ b/packages/lib/modules/pool/PoolDetail/PoolComposition.tsx @@ -18,7 +18,7 @@ import { useTokens } from '@repo/lib/modules/tokens/TokensProvider' import { useBreakpoints } from '@repo/lib/shared/hooks/useBreakpoints' import { useCurrency } from '@repo/lib/shared/hooks/useCurrency' import type { GqlChain } from '@repo/lib/shared/services/api/generated/graphql' -import { bn, fNum, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, fNum, isBnParseable } from '@repo/lib/shared/utils/numbers' import { useLayoutEffect, useRef, useState, Fragment } from 'react' import { Address } from 'viem' import { usePoolsMetadata } from '../metadata/PoolsMetadataProvider' @@ -51,7 +51,7 @@ function CardContent({ totalLiquidity, poolTokens, chain, pool }: CardContentPro const virtualToken = pool.poolTokens[pool.reserveTokenIndex].address const price = priceFor(virtualToken, chain) virtualAmount = - isValidNumber(pool.reserveTokenVirtualBalance) && isValidNumber(price) + isBnParseable(pool.reserveTokenVirtualBalance) && isBnParseable(price) ? bn(pool.reserveTokenVirtualBalance).times(price).toString() : '0' } @@ -86,7 +86,7 @@ function CardContent({ totalLiquidity, poolTokens, chain, pool }: CardContentPro const tokenValue = isSeedlessLBP && isVirtualPairedToken - ? isValidNumber(poolToken.balance) && isValidNumber(pool.reserveTokenVirtualBalance) + ? isBnParseable(poolToken.balance) && isBnParseable(pool.reserveTokenVirtualBalance) ? bn(poolToken.balance).plus(pool.reserveTokenVirtualBalance) : poolToken.balance : poolToken.balance @@ -109,8 +109,8 @@ function CardContent({ totalLiquidity, poolTokens, chain, pool }: CardContentPro {getNestedPoolTokens(poolToken).map(nestedPoolToken => { const calculatedWeight = - isValidNumber(nestedPoolToken.balanceUSD) && - isValidNumber(poolToken.balanceUSD) + isBnParseable(nestedPoolToken.balanceUSD) && + isBnParseable(poolToken.balanceUSD) ? bn(nestedPoolToken.balanceUSD).div(bn(poolToken.balanceUSD)) : bn(0) return ( @@ -124,8 +124,8 @@ function CardContent({ totalLiquidity, poolTokens, chain, pool }: CardContentPro targetWeight={ nestedPoolToken.weight && poolToken.weight && - isValidNumber(nestedPoolToken.weight) && - isValidNumber(poolToken.weight) + isBnParseable(nestedPoolToken.weight) && + isBnParseable(poolToken.weight) ? bn(nestedPoolToken.weight).times(poolToken.weight).toString() : undefined } @@ -140,8 +140,8 @@ function CardContent({ totalLiquidity, poolTokens, chain, pool }: CardContentPro stakedBalance.stakingType === GqlPoolStakingTypeValues.Gauge && stakedBalance.stakingId !== pool.staking?.gauge?.id && - isValidNumber(stakedBalance.balance) && + isBnParseable(stakedBalance.balance) && bn(stakedBalance.balance).gt(0) ) return found diff --git a/packages/lib/modules/pool/pool.utils.spec.ts b/packages/lib/modules/pool/pool.utils.spec.ts new file mode 100644 index 0000000000..b038d585e5 --- /dev/null +++ b/packages/lib/modules/pool/pool.utils.spec.ts @@ -0,0 +1,17 @@ +import type { GqlPoolAprItem } from '@repo/lib/shared/services/api/graphql-derived-types' +import { GqlPoolAprItemTypeValues } from '@repo/lib/shared/services/api/graphql-enums' +import { describe, expect, test } from 'vitest' +import { getTotalApr } from './pool.utils' + +describe('getTotalApr', () => { + test('skips API APR values that BigNumber cannot parse', () => { + const aprItems = [ + { type: GqlPoolAprItemTypeValues.SwapFee24h, apr: ' ' } as unknown as GqlPoolAprItem, + ] + + const [minTotal, maxTotal] = getTotalApr(aprItems) + + expect(minTotal.toString()).toBe('0') + expect(maxTotal.toString()).toBe('0') + }) +}) diff --git a/packages/lib/modules/pool/pool.utils.ts b/packages/lib/modules/pool/pool.utils.ts index 35011b36c8..03e696b28a 100644 --- a/packages/lib/modules/pool/pool.utils.ts +++ b/packages/lib/modules/pool/pool.utils.ts @@ -9,7 +9,7 @@ import { GqlPoolAprItemTypeValues, GqlPoolTypeValues, } from '@repo/lib/shared/services/api/graphql-enums' -import { Numberish, bn, fNum, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { Numberish, bn, fNum, isBnParseable } from '@repo/lib/shared/utils/numbers' import type BigNumber from 'bignumber.js' import { cloneDeep, invert } from 'lodash' import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime' @@ -138,7 +138,7 @@ export function getTotalApr(aprItems: GqlPoolAprItem[]): [BigNumber, BigNumber] // Filter known APR types to avoid including new unknown API types that are not yet displayed in the APR tooltip .filter(item => TOTAL_APR_TYPES.includes(item.type)) .forEach(item => { - if (!isValidNumber(item.apr)) return + if (!isBnParseable(item.apr)) return if (item.type === GqlPoolAprItemTypeValues.StakingBoost) { maxTotal = bn(item.apr).plus(maxTotal) diff --git a/packages/lib/modules/pool/queries/useOnchainUserPoolBalances.ts b/packages/lib/modules/pool/queries/useOnchainUserPoolBalances.ts index 99fb4b7aa9..75cc771918 100644 --- a/packages/lib/modules/pool/queries/useOnchainUserPoolBalances.ts +++ b/packages/lib/modules/pool/queries/useOnchainUserPoolBalances.ts @@ -4,7 +4,7 @@ import type { } from '@repo/lib/shared/services/api/graphql-derived-types' import type { GqlChain } from '@repo/lib/shared/services/api/generated/graphql' import { isSameAddress } from '@repo/lib/shared/utils/addresses' -import { bn, safeSum, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, safeSum, isBnParseable } from '@repo/lib/shared/utils/numbers' import { captureNonFatalError } from '@repo/lib/shared/utils/query-errors' import { HumanAmount } from '@balancer/sdk' import type BigNumber from 'bignumber.js' @@ -129,7 +129,7 @@ function overwriteOnchainPoolBalanceData( } const onchainUnstakedBalance = onchainUnstakedBalances.unstakedBalance as HumanAmount - const safeOnchainUnstakedBalance = isValidNumber(onchainUnstakedBalance) + const safeOnchainUnstakedBalance = isBnParseable(onchainUnstakedBalance) ? onchainUnstakedBalance : ('0' as HumanAmount) const onchainUnstakedBalanceUsd = bn(safeOnchainUnstakedBalance).times(bptPrice).toNumber() @@ -142,7 +142,7 @@ function overwriteOnchainPoolBalanceData( } const onchainTotalStakedBalance = safeSum( onchainStakedBalances.map(stakedBalance => - isValidNumber(stakedBalance.balance) ? stakedBalance.balance : '0' + isBnParseable(stakedBalance.balance) ? stakedBalance.balance : '0' ) ) diff --git a/packages/lib/modules/pool/useGetPoolRewards.spec.tsx b/packages/lib/modules/pool/useGetPoolRewards.spec.tsx index ad277d10c0..0bb34d2a7a 100644 --- a/packages/lib/modules/pool/useGetPoolRewards.spec.tsx +++ b/packages/lib/modules/pool/useGetPoolRewards.spec.tsx @@ -53,7 +53,7 @@ function getPoolWithInvalidStakingGaugeRewards() { pool.staking.gauge.rewards = [ { id: '0x5bbaed1fadc08c5fb3e4ae3c8848777e2da77103-0xba100000625a3754423978a60c9317c58a424e3d-balgauge', - rewardPerSecond: null as any, + rewardPerSecond: ' ' as any, tokenAddress: '0xba100000625a3754423978a60c9317c58a424e3d', } as GqlPoolStakingGaugeReward, { diff --git a/packages/lib/modules/pool/useGetPoolRewards.tsx b/packages/lib/modules/pool/useGetPoolRewards.tsx index 101b4e9724..b3af8e12e8 100644 --- a/packages/lib/modules/pool/useGetPoolRewards.tsx +++ b/packages/lib/modules/pool/useGetPoolRewards.tsx @@ -2,7 +2,7 @@ import { Pool } from './pool.types' import type { GqlToken } from '@repo/lib/shared/services/api/graphql-derived-types' import { sumBy } from 'lodash' import { useTokens } from '../tokens/TokensProvider' -import { bn, Numberish, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, Numberish, isBnParseable } from '@repo/lib/shared/utils/numbers' import { calcPotentialYieldFor } from './pool.utils' import { oneWeekInSecs } from '@repo/lib/shared/utils/time' @@ -14,7 +14,7 @@ export function useGetPoolRewards(pool: Pool) { const currentRewardsPerWeek = currentRewards.map(reward => { return { ...reward, - rewardPerWeek: isValidNumber(reward.rewardPerSecond) + rewardPerWeek: isBnParseable(reward.rewardPerSecond) ? bn(reward.rewardPerSecond).times(oneWeekInSecs) : bn(0), } @@ -33,7 +33,7 @@ export function useGetPoolRewards(pool: Pool) { const weeklyRewardsByToken = Object.fromEntries( currentRewards.map(reward => [ reward.tokenAddress, - isValidNumber(reward.rewardPerSecond) + isBnParseable(reward.rewardPerSecond) ? bn(reward.rewardPerSecond).times(oneWeekInSecs).toString() : '0', ]) diff --git a/packages/lib/modules/pool/user-balance.helpers.spec.ts b/packages/lib/modules/pool/user-balance.helpers.spec.ts index 19322dacd1..20d176bf65 100644 --- a/packages/lib/modules/pool/user-balance.helpers.spec.ts +++ b/packages/lib/modules/pool/user-balance.helpers.spec.ts @@ -119,7 +119,7 @@ describe('invalid balance fallbacks', () => { totalBalanceUsd: 300, stakedBalances: [ { - balance: '', + balance: ' ', balanceUsd: 0, stakingType: GqlPoolStakingTypeValues.Gauge, stakingId: '0x1', diff --git a/packages/lib/modules/pool/user-balance.helpers.ts b/packages/lib/modules/pool/user-balance.helpers.ts index 1b26d7a927..15ddcf5cab 100644 --- a/packages/lib/modules/pool/user-balance.helpers.ts +++ b/packages/lib/modules/pool/user-balance.helpers.ts @@ -1,4 +1,4 @@ -import { bn, safeSum, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, safeSum, isBnParseable } from '@repo/lib/shared/utils/numbers' import { Pool } from './pool.types' import { PoolListItem } from './pool.types' import { parseUnits } from 'viem' @@ -21,7 +21,7 @@ export function calcStakedBalance( return safeSum( filteredBalances.map(stakedBalance => - isValidNumber(stakedBalance.balance) ? bn(stakedBalance.balance) : bn(0) + isBnParseable(stakedBalance.balance) ? bn(stakedBalance.balance) : bn(0) ) ) as HumanAmount } @@ -41,7 +41,7 @@ export function calcTotalStakedBalanceUsd(pool: Pool): number { return Number( safeSum( userBalance.stakedBalances.map(stakedBalance => - isValidNumber(stakedBalance.balanceUsd) ? bn(stakedBalance.balanceUsd) : bn(0) + isBnParseable(stakedBalance.balanceUsd) ? bn(stakedBalance.balanceUsd) : bn(0) ) ) ) @@ -55,7 +55,7 @@ export function getUserTotalBalance(pool: Pool | PoolListItem): HumanAmount { const userBalance = pool.userBalance if (!userBalance) return '0' - return isValidNumber(userBalance.totalBalance) + return isBnParseable(userBalance.totalBalance) ? (bn(userBalance.totalBalance).toFixed(18) as HumanAmount) : '0' } @@ -91,7 +91,7 @@ export function getUserTotalBalanceInt(pool: Pool): bigint { // decimals and so it borked the whole pool page. toFixed(18) ensures the // value cannot be more than 18 decimals when passed into parseUnits. const totalBalanceStr = getUserTotalBalance(pool) - const totalBalance = isValidNumber(totalBalanceStr) + const totalBalance = isBnParseable(totalBalanceStr) ? bn(totalBalanceStr).toFixed(BPT_DECIMALS) : '0' return parseUnits(totalBalance, BPT_DECIMALS) @@ -150,7 +150,7 @@ export function calcStakedBalanceInt(pool: Pool, stakingType: GqlPoolStakingType export function calcStakedBalanceUsd(pool: Pool, stakingType: GqlPoolStakingType): number { const balanceUsd = getStakedBalance(pool, stakingType).balanceUsd - return isValidNumber(balanceUsd) ? Number(bn(balanceUsd)) : 0 + return isBnParseable(balanceUsd) ? Number(bn(balanceUsd)) : 0 } export function calcGaugeStakedBalanceUsd(pool: Pool): number { @@ -159,7 +159,7 @@ export function calcGaugeStakedBalanceUsd(pool: Pool): number { export function hasTotalBalance(pool: Pool) { const totalBalance = getUserTotalBalance(pool) - return isValidNumber(totalBalance) && bn(totalBalance).gt(0) + return isBnParseable(totalBalance) && bn(totalBalance).gt(0) } export function hasBalancerStakedBalance(pool: Pool | PoolListItem): boolean { @@ -180,7 +180,7 @@ export function hasStakedBalanceFor( return userBalance.stakedBalances.some( balance => balance.stakingType === stakingType && - isValidNumber(balance.balance) && + isBnParseable(balance.balance) && bn(balance.balance).gt(0) ) } diff --git a/packages/lib/modules/portfolio/PortfolioClaim/recovered-funds/useRecoveredFunds.ts b/packages/lib/modules/portfolio/PortfolioClaim/recovered-funds/useRecoveredFunds.ts index ebde6745c0..2003d53c11 100644 --- a/packages/lib/modules/portfolio/PortfolioClaim/recovered-funds/useRecoveredFunds.ts +++ b/packages/lib/modules/portfolio/PortfolioClaim/recovered-funds/useRecoveredFunds.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { MerklReward, MerklRewardsResponse } from '../../merkl/merkl.types' import { HumanTokenAmountWithSymbol } from '@repo/lib/modules/tokens/token.types' import { Address } from 'viem' -import { bn, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers' import { HumanAmount } from '@balancer/sdk' import { useState } from 'react' import { NORMALIZED_WRAPPER_TOKENS } from './wrapper-tokens' @@ -73,7 +73,7 @@ export function useRecoveredFunds() { function toRecoveredTokenClaim(item: MerklReward): RecoveredTokenClaim { return { amount: { - humanAmount: isValidNumber(item.amount) + humanAmount: isBnParseable(item.amount) ? (bn(item.amount).shiftedBy(-item.token.decimals).toString() as HumanAmount) : ('0' as HumanAmount), tokenAddress: item.token.address as Address, diff --git a/packages/lib/modules/portfolio/PortfolioClaim/useGetHiddenHandRewards.tsx b/packages/lib/modules/portfolio/PortfolioClaim/useGetHiddenHandRewards.tsx index 90db1627c8..0be842548b 100644 --- a/packages/lib/modules/portfolio/PortfolioClaim/useGetHiddenHandRewards.tsx +++ b/packages/lib/modules/portfolio/PortfolioClaim/useGetHiddenHandRewards.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { useUserAccount } from '../../web3/UserAccountProvider' -import { bn, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers' import { PROJECT_CONFIG } from '@repo/lib/config/getProjectConfig' import { useTokens } from '@repo/lib/modules/tokens/TokensProvider' @@ -79,13 +79,13 @@ export function useGetHiddenHandRewards() { refetchInterval: 300000, select: data => { const filteredRewards = data.data.filter( - reward => isValidNumber(reward.claimable) && bn(reward.claimable).gt(0) + reward => isBnParseable(reward.claimable) && bn(reward.claimable).gt(0) ) const aggregatedRewards = filteredRewards.reduce( (acc, reward) => { const tokenAddress = reward.token - if (!isValidNumber(reward.claimable)) return acc + if (!isBnParseable(reward.claimable)) return acc const rewardTokenUsdValue = usdValueForTokenAddress( tokenAddress, diff --git a/packages/lib/modules/swap/handlers/AuraBalSwap.handler.ts b/packages/lib/modules/swap/handlers/AuraBalSwap.handler.ts index 57e3e192d2..5149b0ec29 100644 --- a/packages/lib/modules/swap/handlers/AuraBalSwap.handler.ts +++ b/packages/lib/modules/swap/handlers/AuraBalSwap.handler.ts @@ -12,7 +12,7 @@ import { } from '../swap.types' import { getRpcUrl } from '../../web3/transports' import { isNativeAsset, isSameAddress } from '@repo/lib/shared/utils/addresses' -import { bn, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers' import { ApiToken } from '../../tokens/token.types' export class AuraBalSwapHandler implements SwapHandler { @@ -72,8 +72,8 @@ export class AuraBalSwapHandler implements SwapHandler { queryOutput.expectedAmountOut.token.decimals ) - const safeSwapAmount = isValidNumber(variables.swapAmount) ? variables.swapAmount : '0' - const safeReturnAmount = isValidNumber(returnAmount) ? returnAmount : '0' + const safeSwapAmount = isBnParseable(variables.swapAmount) ? variables.swapAmount : '0' + const safeReturnAmount = isBnParseable(returnAmount) ? returnAmount : '0' return { returnAmount, diff --git a/packages/lib/modules/tokens/TokenInput/useTokenInput.ts b/packages/lib/modules/tokens/TokenInput/useTokenInput.ts index 79a74048d3..2c29b7ebab 100644 --- a/packages/lib/modules/tokens/TokenInput/useTokenInput.ts +++ b/packages/lib/modules/tokens/TokenInput/useTokenInput.ts @@ -1,4 +1,4 @@ -import { Numberish, bn, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { Numberish, bn, isBnParseable } from '@repo/lib/shared/utils/numbers' import { ChangeEvent } from 'react' import { useTokenBalances } from '../TokenBalancesProvider' import { useTokenInputsValidation } from '../TokenInputsValidationProvider' @@ -55,7 +55,7 @@ export function useTokenInput({ const userBalance = balanceFor(tokenAddress) removeValidationErrors(tokenAddress, [EXCEEDS_BALANCE_ERROR]) - if (value && isValidNumber(value) && userBalance !== undefined && !disableBalanceValidation) { + if (value && isBnParseable(value) && userBalance !== undefined && !disableBalanceValidation) { if (bn(value).gt(bn(userBalance.formatted))) { return setValidationError(tokenAddress, EXCEEDS_BALANCE_ERROR) } diff --git a/packages/lib/modules/tokens/TokenRow/TokenRowGroup.tsx b/packages/lib/modules/tokens/TokenRow/TokenRowGroup.tsx index bdf6ffaee4..319e6631aa 100644 --- a/packages/lib/modules/tokens/TokenRow/TokenRowGroup.tsx +++ b/packages/lib/modules/tokens/TokenRow/TokenRowGroup.tsx @@ -8,7 +8,7 @@ import TokenRow from './TokenRow' import { ReactNode, useMemo } from 'react' import { HumanAmount } from '@balancer/sdk' import { Pool } from '@repo/lib/modules/pool/pool.types' -import { bn, formatFalsyValueAsDash, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, formatFalsyValueAsDash, isBnParseable } from '@repo/lib/shared/utils/numbers' type HumanTokenAmountWithSymbol = HumanTokenAmount & { symbol?: string } @@ -49,10 +49,10 @@ export function TokenRowGroup({ const key = amount.tokenAddress if (amountMap[key]) { - const existingAmount = isValidNumber(amountMap[key].humanAmount) + const existingAmount = isBnParseable(amountMap[key].humanAmount) ? bn(amountMap[key].humanAmount) : bn(0) - const newAmount = isValidNumber(amount.humanAmount) ? bn(amount.humanAmount) : bn(0) + const newAmount = isBnParseable(amount.humanAmount) ? bn(amount.humanAmount) : bn(0) amountMap[key] = { ...amountMap[key], humanAmount: existingAmount.plus(newAmount).toString() as HumanAmount, diff --git a/packages/lib/modules/tokens/TokensProvider.tsx b/packages/lib/modules/tokens/TokensProvider.tsx index 8f892231b2..7de714042d 100644 --- a/packages/lib/modules/tokens/TokensProvider.tsx +++ b/packages/lib/modules/tokens/TokensProvider.tsx @@ -8,7 +8,7 @@ import { import type { GqlChain } from '@repo/lib/shared/services/api/generated/graphql' import { isSameAddress } from '@repo/lib/shared/utils/addresses' import { useMandatoryContext } from '@repo/lib/shared/utils/contexts' -import { bn, Numberish, isValidNumber } from '@repo/lib/shared/utils/numbers' +import { bn, Numberish, isBnParseable } from '@repo/lib/shared/utils/numbers' import { useQuery } from '@apollo/client/react' import { createContext, PropsWithChildren, useCallback } from 'react' import { Address } from 'viem' @@ -87,7 +87,7 @@ export function useTokensLogic() { function usdValueForToken(token: ApiOrCustomToken | undefined, amount: Numberish) { if (!token) return '0' if (amount === '') return '0' - if (typeof amount === 'string' && !isValidNumber(amount)) return '0' + if (typeof amount === 'string' && !isBnParseable(amount)) return '0' return bn(amount) .times(priceFor(token.address as Address, token.chain)) .toFixed() @@ -100,7 +100,7 @@ export function useTokensLogic() { customUsdPrice?: string ) { if (amount === '') return '0' - if (typeof amount === 'string' && !isValidNumber(amount)) return '0' + if (typeof amount === 'string' && !isBnParseable(amount)) return '0' return bn(amount) .times(customUsdPrice || priceFor(address, chain)) .toFixed() @@ -123,7 +123,7 @@ export function useTokensLogic() { const tokenPrice = priceFor(tokenAddress, chain) const totalLiquidityBn = bn(totalLiquidity || 0) - if (totalLiquidityBn.isZero() || !isValidNumber(tokenBalance)) return '0' + if (totalLiquidityBn.isZero() || !isBnParseable(tokenBalance)) return '0' return bn(tokenPrice).times(tokenBalance).div(totalLiquidityBn).toString() } @@ -132,7 +132,7 @@ export function useTokensLogic() { (poolTokens: PoolToken[], chain: GqlChain) => { return poolTokens .reduce((total, token) => { - if (!isValidNumber(token.balance)) return total + if (!isBnParseable(token.balance)) return total return total.plus(bn(priceFor(token.address, chain)).times(token.balance)) }, bn(0)) .toString() From bb2e0a307fb659e412dcb558eca0286e64811b35 Mon Sep 17 00:00:00 2001 From: groninge Date: Wed, 29 Jul 2026 14:02:18 +0200 Subject: [PATCH 2/3] fix: use isBnParseable instead of isValidNumber in isGreaterThanZeroValidation Replace isValidNumber with isBnParseable in isGreaterThanZeroValidation to prevent throwing errors on whitespace input. Add test coverage for whitespace handling and other edge cases. --- packages/lib/shared/utils/numbers.spec.ts | 17 +++++++++++++++++ packages/lib/shared/utils/numbers.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/lib/shared/utils/numbers.spec.ts b/packages/lib/shared/utils/numbers.spec.ts index f7283c1bcd..ffe243ef7b 100644 --- a/packages/lib/shared/utils/numbers.spec.ts +++ b/packages/lib/shared/utils/numbers.spec.ts @@ -5,6 +5,7 @@ import { fNum, formatFalsyValueAsDash, isBnParseable, + isGreaterThanZeroValidation, isValidNumber, sum, ZERO_VALUE_DASH, @@ -267,6 +268,22 @@ describe('isValidNumber', () => { expect(isValidNumber('.5')).toBe(true) }) }) +describe('isGreaterThanZeroValidation', () => { + test('returns error for whitespace instead of throwing', () => { + expect(isGreaterThanZeroValidation(' ')).toBe('Amount must be greater than 0') + }) + + test('returns true for values greater than zero', () => { + expect(isGreaterThanZeroValidation('1')).toBe(true) + expect(isGreaterThanZeroValidation('0.0001')).toBe(true) + }) + + test('returns error for zero or negative values', () => { + expect(isGreaterThanZeroValidation('0')).toBe('Amount must be greater than 0') + expect(isGreaterThanZeroValidation('-1')).toBe('Amount must be greater than 0') + }) +}) + describe('isBnParseable', () => { test('returns false for nullish and values bn() cannot parse', () => { expect(isBnParseable(null)).toBe(false) diff --git a/packages/lib/shared/utils/numbers.ts b/packages/lib/shared/utils/numbers.ts index 0b68060c09..28c5d9dd17 100644 --- a/packages/lib/shared/utils/numbers.ts +++ b/packages/lib/shared/utils/numbers.ts @@ -328,7 +328,7 @@ export function safeParseFixedBigInt(value: string, decimals = 0): bigint { export const isGreaterThanZeroValidation = (value: string | undefined): string | true => { if (value == null) return 'Amount must be greater than 0' - return isValidNumber(value) && !isZero(value) ? true : 'Amount must be greater than 0' + return isBnParseable(value) && !isZero(value) ? true : 'Amount must be greater than 0' } export function sum(items: T[], extractFn: (item: T) => BigNumber): BigNumber { From fd08a8703e30a2d3abb96c662fd5ed1f8480e0ba Mon Sep 17 00:00:00 2001 From: groninge Date: Wed, 29 Jul 2026 15:22:37 +0200 Subject: [PATCH 3/3] fix: use bn().gt(0) instead of !isZero in isGreaterThanZeroValidation Replace !isZero(value) with bn(value).gt(0) to properly validate that amounts are strictly greater than zero, not just non-zero (which would allow negative values). --- packages/lib/shared/utils/numbers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/shared/utils/numbers.ts b/packages/lib/shared/utils/numbers.ts index 28c5d9dd17..802cde7f47 100644 --- a/packages/lib/shared/utils/numbers.ts +++ b/packages/lib/shared/utils/numbers.ts @@ -328,7 +328,7 @@ export function safeParseFixedBigInt(value: string, decimals = 0): bigint { export const isGreaterThanZeroValidation = (value: string | undefined): string | true => { if (value == null) return 'Amount must be greater than 0' - return isBnParseable(value) && !isZero(value) ? true : 'Amount must be greater than 0' + return isBnParseable(value) && bn(value).gt(0) ? true : 'Amount must be greater than 0' } export function sum(items: T[], extractFn: (item: T) => BigNumber): BigNumber {