diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/ReClammParams.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/ReClammParams.tsx index 6cec508..75c0dce 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/ReClammParams.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/ReClammParams.tsx @@ -1,25 +1,34 @@ import ReactECharts from "echarts-for-react"; -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid"; -import { Alert, NumberInput, TextField } from "~~/components/common"; -import { useSortedTokenConfigs } from "~~/hooks/balancer"; +import { formatUnits } from "viem"; +import { ArrowTopRightOnSquareIcon, ArrowsRightLeftIcon } from "@heroicons/react/20/solid"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { NumberInput, TextField } from "~~/components/common"; import { useReclAmmChart } from "~~/hooks/reclamm/useReclammChart"; -import { usePoolCreationStore } from "~~/hooks/v3"; +// import { useTokenUsdValue } from "~~/hooks/token"; +import { useFetchTokenRate, usePoolCreationStore, useUserDataStore } from "~~/hooks/v3"; export const ReClammParams = () => { - const { reClammParams, updateReClammParam } = usePoolCreationStore(); - const sortedTokenConfigs = useSortedTokenConfigs(); + const { reClammParams, updateReClammParam, tokenConfigs } = usePoolCreationStore(); + const { updateUserData } = useUserDataStore(); const { initialTargetPrice, initialMinPrice, initialMaxPrice, - priceShiftDailyRate, + dailyPriceShiftExponent, centerednessMargin, - initialBalanceA, + // initialBalanceA, usdPerTokenInputA, usdPerTokenInputB, + // tokenAPriceIncludesRate, + // tokenBPriceIncludesRate, } = reClammParams; + const { data: currentRateTokenA } = useFetchTokenRate(tokenConfigs[0].rateProvider); + const { data: currentRateTokenB } = useFetchTokenRate(tokenConfigs[1].rateProvider); + // const { tokenUsdValue: usdPerTokenA } = useTokenUsdValue(tokenConfigs[0].address, "1"); + // const { tokenUsdValue: usdPerTokenB } = useTokenUsdValue(tokenConfigs[1].address, "1"); + const sanitizeNumberInput = (input: string) => { // Remove non-numeric characters except decimal point const sanitized = input.replace(/[^0-9.]/g, ""); @@ -28,6 +37,15 @@ export const ReClammParams = () => { return parts.length > 2 ? parts[0] + "." + parts.slice(1).join("") : sanitized; }; + const humanRateA = currentRateTokenA && Number(formatUnits(currentRateTokenA, 18)); + const humanRateB = currentRateTokenB && Number(formatUnits(currentRateTokenB, 18)); + + const isBoostedA = !!currentRateTokenA; + const isBoostedB = !!currentRateTokenB; + + const boostedLabelA = isBoostedA ? `without rate` : ""; + const boostedLabelB = currentRateTokenB ? `without rate` : ""; + return (
@@ -42,75 +60,140 @@ export const ReClammParams = () => {
-
- -
+
- - Initial prices represent the value of {sortedTokenConfigs[0].tokenInfo?.symbol} denominated in{" "} - {sortedTokenConfigs[1].tokenInfo?.symbol} - -
- { - updateReClammParam({ usdPerTokenInputA: sanitizeNumberInput(e.target.value) }); - }} - /> - { - updateReClammParam({ usdPerTokenInputB: sanitizeNumberInput(e.target.value) }); - }} - /> +
+ + +
+ ) + } + value={usdPerTokenInputA} + isDollarValue={true} + onChange={e => { + updateReClammParam({ usdPerTokenInputA: sanitizeNumberInput(e.target.value) }); + // if user changes usd price per token, this triggers useInitialPricingParams hook to move price params to surround new "current price" of pool + if (usdPerTokenInputA !== e.target.value) updateUserData({ hasEditedReclammParams: false }); + }} + /> + {/* {!!currentRateTokenA && ( + { + const updatedTokenAPriceIncludesRate = !tokenAPriceIncludesRate; + const updatedUsdPerTokenA = usdPerTokenA + ? updatedTokenAPriceIncludesRate + ? (usdPerTokenA / Number(formatUnits(currentRateTokenA, 18))).toString() + : usdPerTokenA.toString() + : usdPerTokenInputA; + + updateReClammParam({ + tokenAPriceIncludesRate: updatedTokenAPriceIncludesRate, + usdPerTokenInputA: updatedUsdPerTokenA, + }); + }} + /> + )} */} +
+
+ + +
+ ) + } + onChange={e => { + updateReClammParam({ usdPerTokenInputB: sanitizeNumberInput(e.target.value) }); + // if user changes usd price per token, this triggers useInitialPricingParams hook to move price params to surround new "current price" of pool + if (usdPerTokenInputB !== e.target.value) updateUserData({ hasEditedReclammParams: false }); + }} + /> + {/* {!!currentRateTokenB && ( + { + const updatedTokenBPriceIncludesRate = !tokenBPriceIncludesRate; + const updatedUsdPerTokenB = usdPerTokenB + ? updatedTokenBPriceIncludesRate + ? (usdPerTokenB / Number(formatUnits(currentRateTokenB, 18))).toString() + : usdPerTokenB.toString() + : usdPerTokenInputB; + + updateReClammParam({ + tokenBPriceIncludesRate: updatedTokenBPriceIncludesRate, + usdPerTokenInputB: updatedUsdPerTokenB, + }); + }} + /> + )} */} +
updateReClammParam({ initialMinPrice: sanitizeNumberInput(e.target.value) })} + onChange={e => { + updateReClammParam({ initialMinPrice: sanitizeNumberInput(e.target.value) }); + updateUserData({ hasEditedReclammParams: true }); + }} /> updateReClammParam({ initialTargetPrice: sanitizeNumberInput(e.target.value) })} + onChange={e => { + updateReClammParam({ initialTargetPrice: sanitizeNumberInput(e.target.value) }); + updateUserData({ hasEditedReclammParams: true }); + }} /> updateReClammParam({ initialMaxPrice: sanitizeNumberInput(e.target.value) })} + onChange={e => { + updateReClammParam({ initialMaxPrice: sanitizeNumberInput(e.target.value) }); + updateUserData({ hasEditedReclammParams: true }); + }} />
-
+
updateReClammParam({ centerednessMargin: sanitizeNumberInput(e.target.value) })} + placeholder="0 - 90" + onChange={e => { + updateReClammParam({ centerednessMargin: sanitizeNumberInput(e.target.value) }); + }} /> updateReClammParam({ priceShiftDailyRate: sanitizeNumberInput(e.target.value) })} - /> - updateReClammParam({ initialBalanceA: sanitizeNumberInput(e.target.value) })} + value={dailyPriceShiftExponent} + placeholder="0 - 100" + onChange={e => { + updateReClammParam({ dailyPriceShiftExponent: sanitizeNumberInput(e.target.value) }); + }} />
@@ -119,7 +202,71 @@ export const ReClammParams = () => { }; function ReClammChart() { - const { option } = useReclAmmChart(); + const { options } = useReclAmmChart(); - return ; + const { tokenConfigs, updatePool, updateReClammParam, reClammParams } = usePoolCreationStore(); + + // TODO: make re-usable invert function to share with usePoolTypeSpecificParams + const handleInvertReClammParams = () => { + const { + initialTargetPrice, + initialMinPrice, + initialMaxPrice, + usdPerTokenInputA, + usdPerTokenInputB, + tokenAPriceIncludesRate, + tokenBPriceIncludesRate, + } = reClammParams; + + updateReClammParam({ + initialTargetPrice: (1 / Number(initialTargetPrice)).toString(), + initialMinPrice: (1 / Number(initialMaxPrice)).toString(), + initialMaxPrice: (1 / Number(initialMinPrice)).toString(), + usdPerTokenInputA: usdPerTokenInputB, + usdPerTokenInputB: usdPerTokenInputA, + tokenAPriceIncludesRate: tokenBPriceIncludesRate, + tokenBPriceIncludesRate: tokenAPriceIncludesRate, + }); + updatePool({ tokenConfigs: [...tokenConfigs].reverse() }); + }; + + return ( +
+
+ +
+ +
+
+
+ ); } + +// function TogglePriceIncludesRate({ +// tokenPriceIncludesRate, +// onChange, +// }: { +// tokenPriceIncludesRate: boolean; +// onChange: () => void; +// }) { +// return ( +//
+//
+// +//
+//
+// ); +// } diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/SwapFeePercentage.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/SwapFeePercentage.tsx index 5d827f2..ef5db8e 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/SwapFeePercentage.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/SwapFeePercentage.tsx @@ -12,7 +12,6 @@ export function SwapFeePercentage({ handleNumberInputChange }: { handleNumberInp let minSwapFeePercentage = 0.001; // Weighted & GyroECLP if (poolType === PoolType.Stable || poolType === PoolType.StableSurge) minSwapFeePercentage = 0.0001; - if (poolType === PoolType.ReClamm) minSwapFeePercentage = 0.1; return (
diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/index.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/index.tsx index 9ad5558..4ed38f8 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/index.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/index.tsx @@ -12,7 +12,7 @@ import { PoolType } from "@balancer/sdk"; import { usePoolCreationStore } from "~~/hooks/v3"; export const ChooseParameters = () => { - const { poolType, updatePool } = usePoolCreationStore(); + const { poolType, updatePool, reClammParams } = usePoolCreationStore(); const handleNumberInputChange: HandleNumberInputChange = (e, field, min, max) => { const value = e.target.value; @@ -31,6 +31,10 @@ export const ChooseParameters = () => { } else { if (field === "amplificationParameter") { updatePool({ [field]: Math.round(numberValue).toString() }); + } else if (field === "dailyPriceShiftExponent" || field === "centerednessMargin") { + updatePool({ + reClammParams: { ...reClammParams, [field]: Math.round(numberValue).toString() }, + }); } else { updatePool({ [field]: value.toString() }); } diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/types.ts b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/types.ts index 8b471fb..05b85d2 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/types.ts +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseParameters/types.ts @@ -1,6 +1,6 @@ export type HandleNumberInputChange = ( e: React.ChangeEvent, - field: "swapFeePercentage" | "amplificationParameter", + field: "swapFeePercentage" | "amplificationParameter" | "centerednessMargin" | "dailyPriceShiftExponent", min: number, max: number, ) => void; diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseTokens/ChooseToken.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseTokens/ChooseToken.tsx index ff188a4..4d6eab0 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseTokens/ChooseToken.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseTokens/ChooseToken.tsx @@ -169,10 +169,13 @@ export function ChooseToken({ index }: { index: number }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [token.tokenInfo?.priceRateProviderData, token.useBoostedVariant, token.address, showBoostOpportunityModal]); + // Prevent user from boosting underlying for gyro and reclamm cus logic too prone to bugs + const shouldHideBoostUnderlying = poolType === PoolType.GyroE || poolType === PoolType.ReClamm; + return ( <>
- {boostedVariant && poolType !== PoolType.GyroE && ( + {boostedVariant && !shouldHideBoostUnderlying && (
)} - {showBoostOpportunityModal && tokenInfo && boostedVariant && poolType !== PoolType.GyroE && ( + {showBoostOpportunityModal && tokenInfo && boostedVariant && !shouldHideBoostUnderlying && (
diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseType/index.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseType/index.tsx index b174ee0..1437ca0 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseType/index.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/ChooseType/index.tsx @@ -1,21 +1,28 @@ import React from "react"; import { PoolTypeButton } from "./PoolTypeButton"; +import { usePoolCreationStore } from "~~/hooks/v3"; import { type SupportedPoolTypes, poolTypeMap } from "~~/utils/constants"; export function ChooseType() { - const poolTypes = Object.keys(poolTypeMap).slice(0, 4) as SupportedPoolTypes[]; + const poolTypes = Object.keys(poolTypeMap) as SupportedPoolTypes[]; + + const { poolType } = usePoolCreationStore(); return ( <>
-
Choose a pool type:
-
- {poolTypes.map((type: SupportedPoolTypes) => ( +
Choose a pool type:
+
+ {poolTypes.slice(3, 4).map((type: SupportedPoolTypes) => ( ))}
+ + {poolType ? ( +
{poolTypeMap[poolType].description}
+ ) : null}
); diff --git a/packages/nextjs/app/v3/_components/PoolConfiguration/index.tsx b/packages/nextjs/app/v3/_components/PoolConfiguration/index.tsx index 403992c..346ef08 100644 --- a/packages/nextjs/app/v3/_components/PoolConfiguration/index.tsx +++ b/packages/nextjs/app/v3/_components/PoolConfiguration/index.tsx @@ -7,6 +7,8 @@ import { ChooseParameters } from "./ChooseParameters"; import { ChooseTokens } from "./ChooseTokens"; import { ChooseType } from "./ChooseType"; import { ExistingPoolsWarning } from "./ExistingPoolsWarning"; +import { TokenType } from "@balancer/sdk"; +import { useQueryClient } from "@tanstack/react-query"; import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; import { TransactionButton } from "~~/components/common"; import { useTargetNetwork } from "~~/hooks/scaffold-eth"; @@ -24,6 +26,7 @@ export function PoolConfiguration() { const { prev, next } = getAdjacentTabs(selectedTab); const { isParametersValid, isTypeValid, isTokensValid, isPoolCreationInputValid } = useValidateCreationInputs(); + const queryClient = useQueryClient(); const { existingPools } = useCheckIfV3PoolExists( poolType, tokenConfigs.map(token => token.address), @@ -36,9 +39,18 @@ export function PoolConfiguration() { Information: , }; + const isRateProvidersValid = tokenConfigs.every(token => { + // Check tanstack query cache for rate provider validity + if (token.tokenType === TokenType.TOKEN_WITH_RATE) { + const cachedRate = queryClient.getQueryData(["fetchTokenRate", token.rateProvider]); + if (!cachedRate) return false; + } + return true; + }); + function isNextDisabled() { if (selectedTab === "Type") return !isTypeValid; - if (selectedTab === "Tokens") return !isTokensValid; + if (selectedTab === "Tokens") return !isTokensValid || !isRateProvidersValid; if (selectedTab === "Parameters") return !isParametersValid; return false; } diff --git a/packages/nextjs/app/v3/_components/PoolCreation/ChooseTokenAmounts/ChooseTokenAmount.tsx b/packages/nextjs/app/v3/_components/PoolCreation/ChooseTokenAmounts/ChooseTokenAmount.tsx index 94857e6..2b818d8 100644 --- a/packages/nextjs/app/v3/_components/PoolCreation/ChooseTokenAmounts/ChooseTokenAmount.tsx +++ b/packages/nextjs/app/v3/_components/PoolCreation/ChooseTokenAmounts/ChooseTokenAmount.tsx @@ -1,15 +1,16 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { TokenAmountField } from "./TokenAmountField"; import { PoolType } from "@balancer/sdk"; import { useQueryClient } from "@tanstack/react-query"; import { erc20Abi, formatUnits } from "viem"; import { useAccount, useReadContract } from "wagmi"; +import { useComputeInitialBalances } from "~~/hooks/reclamm"; import { useTokenUsdValue } from "~~/hooks/token"; import { type TokenConfig, usePoolCreationStore, useUserDataStore } from "~~/hooks/v3"; export function ChooseTokenAmount({ index, tokenConfig }: { index: number; tokenConfig: TokenConfig }) { const { updateUserData, userTokenBalances } = useUserDataStore(); - const { poolType, updateTokenConfig, eclpParams, tokenConfigs } = usePoolCreationStore(); + const { poolType, updateTokenConfig, eclpParams, tokenConfigs, poolAddress } = usePoolCreationStore(); const { tokenInfo, amount, address, weight } = tokenConfig; const { usdPerTokenInput0, usdPerTokenInput1 } = eclpParams; @@ -29,11 +30,45 @@ export function ChooseTokenAmount({ index, tokenConfig }: { index: number; token args: connectedAddress ? [connectedAddress] : undefined, }); + const { data: initAmountsRaw, isLoading: isLoadingReclammInitAmounts } = useComputeInitialBalances( + poolAddress, + address, + amount, + tokenInfo?.decimals, + ); + const lastUpdatedAmountByIndex = useRef(null); + useEffect(() => { updateUserData({ userTokenBalances: { ...userTokenBalances, [address]: userTokenBalance?.toString() ?? "0" } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [userTokenBalance, address]); + useEffect(() => { + if (poolType === PoolType.ReClamm && initAmountsRaw && !isLoadingReclammInitAmounts) { + const otherTokenIndex = index === 0 ? 1 : 0; + + // create array of ordered token addresses + const tokenAddresses = tokenConfigs.map(tokenConfig => tokenConfig.address.toLowerCase()); + const sortedTokenAddresses = tokenAddresses.sort((a, b) => a.localeCompare(b)); + + // find index of other token within sorted token configs + const addressOfOtherToken = tokenConfigs[otherTokenIndex].address.toLowerCase(); + const indexOfOtherTokenAmount = sortedTokenAddresses.indexOf(addressOfOtherToken); + + // use indexOfOtherTokenAmount to access the initAmounts array (which comes sorted from on chain call) + const otherTokenAmount = formatUnits( + initAmountsRaw[indexOfOtherTokenAmount], + tokenConfigs[otherTokenIndex]?.tokenInfo?.decimals || 0, + ); + + if (lastUpdatedAmountByIndex.current === index) { + updateTokenConfig(otherTokenIndex, { amount: otherTokenAmount }); + lastUpdatedAmountByIndex.current = null; + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initAmountsRaw, isLoadingReclammInitAmounts, poolType, index, lastUpdatedAmountByIndex]); + // Helper function to get rate-adjusted USD price for a token const getRateAdjustedUsdPrice = (tokenIndex: number) => { const rateProvider = tokenConfigs[tokenIndex].rateProvider; @@ -47,6 +82,8 @@ export function ChooseTokenAmount({ index, tokenConfig }: { index: number; token const handleAmountChange = (e: React.ChangeEvent) => { const inputValue = e.target.value.trim(); if (Number(inputValue) >= 0) { + if (poolType === PoolType.ReClamm) lastUpdatedAmountByIndex.current = index; + updateTokenConfig(index, { amount: inputValue }); } else { updateTokenConfig(index, { amount: "" }); diff --git a/packages/nextjs/app/v3/_components/PoolDetails.tsx b/packages/nextjs/app/v3/_components/PoolDetails.tsx index 06eac9a..47281bf 100644 --- a/packages/nextjs/app/v3/_components/PoolDetails.tsx +++ b/packages/nextjs/app/v3/_components/PoolDetails.tsx @@ -51,7 +51,7 @@ export function PoolDetails({ isPreview }: { isPreview?: boolean }) { const { isOnlyInitializingPool } = useUserDataStore(); const { isParametersValid, isTypeValid, isInfoValid, isTokensValid } = useValidateCreationInputs(); - const { initialTargetPrice, initialMinPrice, initialMaxPrice, priceShiftDailyRate, centerednessMargin } = + const { initialTargetPrice, initialMinPrice, initialMaxPrice, dailyPriceShiftExponent, centerednessMargin } = reClammParams; const poolDeploymentUrl = poolAddress ? getBlockExplorerAddressLink(targetNetwork, poolAddress) : undefined; @@ -151,7 +151,7 @@ export function PoolDetails({ isPreview }: { isPreview?: boolean }) {
Price Shift Daily Rate
-
{priceShiftDailyRate ? priceShiftDailyRate : "-"}
+
{dailyPriceShiftExponent ? dailyPriceShiftExponent : "-"}
Centeredness Margin
diff --git a/packages/nextjs/components/common/TextField.tsx b/packages/nextjs/components/common/TextField.tsx index e7d885d..e62f666 100644 --- a/packages/nextjs/components/common/TextField.tsx +++ b/packages/nextjs/components/common/TextField.tsx @@ -5,6 +5,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { useValidateHooksContract } from "~~/hooks/v3"; interface TextFieldProps { + tooltip?: React.ReactNode; label?: string; placeholder?: string; value: string | undefined; @@ -22,6 +23,7 @@ interface TextFieldProps { export const TextField: React.FC = ({ label, + tooltip, placeholder, value, onChange, @@ -55,7 +57,10 @@ export const TextField: React.FC = ({ return (
-
{label && }
+
+ {label && } + {tooltip} +
{isDollarValue &&
$
} {isPercentage &&
%
} diff --git a/packages/nextjs/hooks/balancer/index.ts b/packages/nextjs/hooks/balancer/index.ts index 015f27a..f8faf09 100644 --- a/packages/nextjs/hooks/balancer/index.ts +++ b/packages/nextjs/hooks/balancer/index.ts @@ -1,4 +1,3 @@ export * from "./useApiConfig"; export * from "./useUninitializedPool"; export * from "./useReadPool"; -export * from "./useSortedTokenConfigs"; diff --git a/packages/nextjs/hooks/balancer/useSortedTokenConfigs.ts b/packages/nextjs/hooks/balancer/useSortedTokenConfigs.ts index 5f51ed9..260d29c 100644 --- a/packages/nextjs/hooks/balancer/useSortedTokenConfigs.ts +++ b/packages/nextjs/hooks/balancer/useSortedTokenConfigs.ts @@ -1,6 +1,7 @@ import { usePoolCreationStore } from "~~/hooks/v3"; import { useBoostableWhitelist } from "~~/hooks/v3/"; +// TODO: figure out this logic for boosted variant for reclamm pool creation export function useSortedTokenConfigs() { const { data: boostableWhitelist } = useBoostableWhitelist(); diff --git a/packages/nextjs/hooks/hyperliquid/useIsHyperEvm.ts b/packages/nextjs/hooks/hyperliquid/useIsHyperEvm.ts index c428379..f7eacbb 100644 --- a/packages/nextjs/hooks/hyperliquid/useIsHyperEvm.ts +++ b/packages/nextjs/hooks/hyperliquid/useIsHyperEvm.ts @@ -4,5 +4,5 @@ import { usePublicClient } from "wagmi"; export const useIsHyperEvm = () => { const publicClient = usePublicClient(); - return publicClient?.chain.id === ChainId.HYPER_EVM; + return publicClient?.chain.id === ChainId.HYPEREVM; }; diff --git a/packages/nextjs/hooks/hyperliquid/useIsUsingBigBlocks.ts b/packages/nextjs/hooks/hyperliquid/useIsUsingBigBlocks.ts index 416a9ea..6cc28d7 100644 --- a/packages/nextjs/hooks/hyperliquid/useIsUsingBigBlocks.ts +++ b/packages/nextjs/hooks/hyperliquid/useIsUsingBigBlocks.ts @@ -13,7 +13,7 @@ export const useIsUsingBigBlocks = () => { queryKey: ["isUsingBigBlocks", publicClient?.chain.id, address], queryFn: async () => { if (!publicClient || !address) return false; - if (publicClient.chain.id !== ChainId.HYPER_EVM) return false; + if (publicClient.chain.id !== ChainId.HYPEREVM) return false; const isUsingBigBlocks: boolean = await publicClient.transport.request({ method: "eth_usingBigBlocks", diff --git a/packages/nextjs/hooks/reclamm/index.ts b/packages/nextjs/hooks/reclamm/index.ts index 5da94f4..43d2b63 100644 --- a/packages/nextjs/hooks/reclamm/index.ts +++ b/packages/nextjs/hooks/reclamm/index.ts @@ -1 +1,2 @@ export * from "./useReclammChart"; +export * from "./useComputeInitialBalances"; diff --git a/packages/nextjs/hooks/reclamm/reClammMath.ts b/packages/nextjs/hooks/reclamm/reClammMath.ts index a802b95..d585081 100644 --- a/packages/nextjs/hooks/reclamm/reClammMath.ts +++ b/packages/nextjs/hooks/reclamm/reClammMath.ts @@ -2,27 +2,25 @@ import * as WeightedMath from "./weightedMath"; const timeFix = 12464900; // Using the same constant as the Contract. The full value is 12464935.015039. -export function calculatePoolCenteredness(params: { +export function computeCenteredness(params: { balanceA: number; balanceB: number; virtualBalanceA: number; virtualBalanceB: number; -}) { - if (params.balanceA === 0 || params.balanceB === 0) return 0; - if (isAboveCenter(params)) { - return (params.balanceB * params.virtualBalanceA) / (params.balanceA * params.virtualBalanceB); - } - return (params.balanceA * params.virtualBalanceB) / (params.balanceB * params.virtualBalanceA); -} +}): { poolCenteredness: number; isPoolAboveCenter: boolean } { + if (params.balanceA === 0) return { poolCenteredness: 0, isPoolAboveCenter: false }; + if (params.balanceB === 0) return { poolCenteredness: 0, isPoolAboveCenter: true }; -export function isAboveCenter(params: { - balanceA: number; - balanceB: number; - virtualBalanceA: number; - virtualBalanceB: number; -}) { - if (params.balanceB === 0) return true; - return params.balanceA / params.balanceB > params.virtualBalanceA / params.virtualBalanceB; + const numerator = params.balanceA * params.virtualBalanceB; + const denominator = params.balanceB * params.virtualBalanceA; + + if (numerator < denominator) { + return { + poolCenteredness: numerator / denominator, + isPoolAboveCenter: false, + }; + } + return { poolCenteredness: denominator / numerator, isPoolAboveCenter: true }; } export function calculateLowerMargin(params: { @@ -184,7 +182,7 @@ export const recalculateVirtualBalances = (params: { }; } - const poolCenteredness = calculatePoolCenteredness({ + const { poolCenteredness, isPoolAboveCenter } = computeCenteredness({ balanceA: params.balanceA, balanceB: params.balanceB, virtualBalanceA: params.oldVirtualBalanceA, @@ -195,13 +193,6 @@ export const recalculateVirtualBalances = (params: { let newVirtualBalanceB = params.oldVirtualBalanceB; let newPriceRatio = params.currentPriceRatio; - const isPoolAboveCenter = isAboveCenter({ - balanceA: params.balanceA, - balanceB: params.balanceB, - virtualBalanceA: params.oldVirtualBalanceA, - virtualBalanceB: params.oldVirtualBalanceB, - }); - const isPriceRatioUpdating = params.simulationParams.simulationSeconds >= params.updateQ0Params.startTime && (params.simulationParams.simulationSeconds <= params.updateQ0Params.endTime || diff --git a/packages/nextjs/hooks/reclamm/useComputeInitialBalances.ts b/packages/nextjs/hooks/reclamm/useComputeInitialBalances.ts new file mode 100644 index 0000000..bcb5c7e --- /dev/null +++ b/packages/nextjs/hooks/reclamm/useComputeInitialBalances.ts @@ -0,0 +1,35 @@ +import { usePoolCreationStore } from "../v3"; +import { PoolType } from "@balancer/sdk"; +import { useQuery } from "@tanstack/react-query"; +import { Address, parseAbi, parseUnits } from "viem"; +import { usePublicClient } from "wagmi"; + +export const useComputeInitialBalances = ( + poolAddress: Address | undefined, + tokenAddress: Address, + humanAmount: string, + tokenDecimals: number | undefined, +) => { + const client = usePublicClient(); + const { poolType } = usePoolCreationStore(); + const isReClamm = poolType === PoolType.ReClamm; + + return useQuery({ + queryKey: ["computeInitialBalancesRaw", humanAmount, poolAddress, tokenAddress, tokenDecimals], + queryFn: async () => { + if (!client) throw new Error("client not found for fetching reclamm initial balances"); + if (!poolAddress) throw new Error("pool address required to fetch reclamm initial balances"); + if (!tokenDecimals) throw new Error("token decimals required to fetch reclamm initial balances"); + + const rawAmount = parseUnits(humanAmount, tokenDecimals); + + return await client.readContract({ + address: poolAddress, + abi: parseAbi(["function computeInitialBalancesRaw(address, uint256) view returns (uint256[])"]), + functionName: "computeInitialBalancesRaw", + args: [tokenAddress, rawAmount], + }); + }, + enabled: !!poolAddress && !!tokenAddress && !!humanAmount && !!tokenDecimals && isReClamm, + }); +}; diff --git a/packages/nextjs/hooks/reclamm/useInitialPricingParams.ts b/packages/nextjs/hooks/reclamm/useInitialPricingParams.ts index 6a77c38..f26ce4d 100644 --- a/packages/nextjs/hooks/reclamm/useInitialPricingParams.ts +++ b/packages/nextjs/hooks/reclamm/useInitialPricingParams.ts @@ -1,7 +1,9 @@ import { useEffect } from "react"; -import { useSortedTokenConfigs } from "~~/hooks/balancer"; +import { formatUnits } from "viem"; import { useTokenUsdValue } from "~~/hooks/token"; -import { usePoolCreationStore } from "~~/hooks/v3"; +import { useFetchTokenRate, usePoolCreationStore } from "~~/hooks/v3"; +import { useUserDataStore } from "~~/hooks/v3"; +import { fNumCustom } from "~~/utils/numbers"; /** * 1. fetch usd per token A and B from API @@ -9,34 +11,70 @@ import { usePoolCreationStore } from "~~/hooks/v3"; * 3. Figure out how much higher and lower to set min and max price relative to target price */ export function useInitialPricingParams() { - const sortedTokenConfigs = useSortedTokenConfigs(); - const { updateReClammParam, reClammParams } = usePoolCreationStore(); + const { updateReClammParam, reClammParams, tokenConfigs } = usePoolCreationStore(); + const { hasEditedReclammParams } = useUserDataStore(); + const { initialTargetPrice, usdPerTokenInputA, usdPerTokenInputB, tokenAPriceIncludesRate, tokenBPriceIncludesRate } = + reClammParams; - const { initialTargetPrice, usdPerTokenInputA, usdPerTokenInputB } = reClammParams; + const { tokenUsdValue: usdPerTokenA } = useTokenUsdValue(tokenConfigs[0].address, "1"); + const { tokenUsdValue: usdPerTokenB } = useTokenUsdValue(tokenConfigs[1].address, "1"); - const { tokenUsdValue: usdPerTokenA } = useTokenUsdValue(sortedTokenConfigs[0].address, "1"); - const { tokenUsdValue: usdPerTokenB } = useTokenUsdValue(sortedTokenConfigs[1].address, "1"); + const { data: currentRateTokenA } = useFetchTokenRate(tokenConfigs[0].rateProvider); + const { data: currentRateTokenB } = useFetchTokenRate(tokenConfigs[1].rateProvider); + + useEffect(() => { + if (currentRateTokenA) { + updateReClammParam({ tokenAPriceIncludesRate: true }); + } + if (currentRateTokenB) { + updateReClammParam({ tokenBPriceIncludesRate: true }); + } + }, [currentRateTokenA, currentRateTokenB, updateReClammParam]); + + const valueTokenA = usdPerTokenInputA ? Number(usdPerTokenInputA) : usdPerTokenA ? usdPerTokenA : null; + const valueTokenB = usdPerTokenInputB ? Number(usdPerTokenInputB) : usdPerTokenB ? usdPerTokenB : null; + + const initialUsdPerTokenA = + currentRateTokenA && valueTokenA ? valueTokenA / Number(formatUnits(currentRateTokenA, 18)) : valueTokenA; + const initialUsdPerTokenB = + currentRateTokenB && valueTokenB ? valueTokenB / Number(formatUnits(currentRateTokenB, 18)) : valueTokenB; // update usd per token inputs if API data available and user has not already set them useEffect(() => { - if (usdPerTokenA && !usdPerTokenInputA) { - updateReClammParam({ usdPerTokenInputA: usdPerTokenA.toString() }); + if (!usdPerTokenInputA && initialUsdPerTokenA) { + updateReClammParam({ usdPerTokenInputA: initialUsdPerTokenA.toString() }); } - if (usdPerTokenB && !usdPerTokenInputB) { - updateReClammParam({ usdPerTokenInputB: usdPerTokenB.toString() }); + + if (!usdPerTokenInputB && initialUsdPerTokenB) { + updateReClammParam({ usdPerTokenInputB: initialUsdPerTokenB.toString() }); } - }, [usdPerTokenA, usdPerTokenB, usdPerTokenInputA, usdPerTokenInputB, updateReClammParam]); + }, [ + initialUsdPerTokenA, + initialUsdPerTokenB, + usdPerTokenInputA, + usdPerTokenInputB, + updateReClammParam, + valueTokenA, + valueTokenB, + tokenAPriceIncludesRate, + tokenBPriceIncludesRate, + ]); - // update initial target price if user has not already set it + // update initial price params if user has not dirtied them useEffect(() => { - if (!initialTargetPrice && Number(usdPerTokenInputA) && Number(usdPerTokenInputB)) { + if (!hasEditedReclammParams && Number(usdPerTokenInputA) && Number(usdPerTokenInputB)) { const newInitialTargetPrice = Number(usdPerTokenInputA) / Number(usdPerTokenInputB); updateReClammParam({ - initialTargetPrice: newInitialTargetPrice.toString(), - initialMinPrice: (newInitialTargetPrice * 0.9).toString(), - initialMaxPrice: (newInitialTargetPrice * 1.1).toString(), + initialTargetPrice: formatPriceParam(newInitialTargetPrice), + initialMinPrice: formatPriceParam(newInitialTargetPrice * 0.9), + initialMaxPrice: formatPriceParam(newInitialTargetPrice * 1.1), }); } - }, [updateReClammParam, usdPerTokenInputA, usdPerTokenInputB, initialTargetPrice]); + }, [updateReClammParam, usdPerTokenInputA, usdPerTokenInputB, initialTargetPrice, hasEditedReclammParams]); +} + +function formatPriceParam(price: number) { + const decimals = price > 1 ? "0.[00000]" : "0.[0000000000]"; + return fNumCustom(price, decimals); } diff --git a/packages/nextjs/hooks/reclamm/useReclammChart.ts b/packages/nextjs/hooks/reclamm/useReclammChart.ts index 8a163cc..e1e31aa 100644 --- a/packages/nextjs/hooks/reclamm/useReclammChart.ts +++ b/packages/nextjs/hooks/reclamm/useReclammChart.ts @@ -1,10 +1,28 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { useMemo } from "react"; -import { useSortedTokenConfigs } from "../balancer"; import { usePoolCreationStore } from "../v3"; -import { calculateInitialBalances, calculateLowerMargin, calculateUpperMargin } from "./reClammMath"; +import { + calculateInitialBalances, + calculateLowerMargin, + calculateUpperMargin, + computeCenteredness, +} from "./reClammMath"; import { useInitialPricingParams } from "./useInitialPricingParams"; -import { bn } from "~~/utils/numbers"; +import { bn, fNum } from "~~/utils/numbers"; + +function getGradientColor(colorStops: string[]) { + return { + type: "linear", + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: colorStops.map((color, index) => ({ offset: index, color })), + }; +} + +const isMobile = false; +const GREEN = "#93F6D2"; +const ORANGE = "rgb(253, 186, 116)"; /** * Using math from reclamm simulator and chart from frontend monorepo @@ -12,10 +30,21 @@ import { bn } from "~~/utils/numbers"; export function useReclAmmChart() { useInitialPricingParams(); - const sortedTokenConfigs = useSortedTokenConfigs(); + const { tokenConfigs, reClammParams } = usePoolCreationStore(); + const { + centerednessMargin, + initialBalanceA, + initialMinPrice, + initialMaxPrice, + initialTargetPrice, + usdPerTokenInputA, + usdPerTokenInputB, + } = reClammParams; - const { reClammParams } = usePoolCreationStore(); - const { centerednessMargin, initialBalanceA, initialMinPrice, initialMaxPrice, initialTargetPrice } = reClammParams; + const tokens = useMemo(() => { + const tokenSymbols = tokenConfigs.map(token => token.tokenInfo?.symbol); + return tokenSymbols.join(" / "); + }, [tokenConfigs]); const currentChartData = useMemo(() => { // TODO: review validation logic for reclamm params @@ -29,7 +58,7 @@ export function useReclAmmChart() { Number(initialMinPrice) > Number(initialTargetPrice) || Number(initialTargetPrice) > Number(initialMaxPrice) ) - return null; + return {}; const { balanceA, balanceB, virtualBalanceA, virtualBalanceB } = calculateInitialBalances({ minPrice: Number(initialMinPrice), @@ -37,362 +66,373 @@ export function useReclAmmChart() { targetPrice: Number(initialTargetPrice), initialBalanceA: Number(initialBalanceA), }); - const margin = centerednessMargin; const invariant = bn(bn(balanceA).plus(virtualBalanceA)).times(bn(balanceB).plus(virtualBalanceB)); - // Mathematical function for the curve: y = invariant / x - const curveFunction = (x: number): number => { - return invariant.div(bn(x)).toNumber(); - }; - - const xForPointB = bn(invariant).div(virtualBalanceB); - - const curvePoints = Array.from({ length: 100 }, (_, i) => { - const x = bn(0.7) - .times(virtualBalanceA) - .plus( - bn(i) - .times(bn(1.3).times(xForPointB).minus(bn(0.7).times(virtualBalanceA))) - .div(bn(100)), - ); - const y = curveFunction(x.toNumber()); - - return [x.toNumber(), y]; - }); - - const vBalanceA = Number(virtualBalanceA); - const vBalanceB = Number(virtualBalanceB); - const xForMinPrice = bn(invariant).div(virtualBalanceB).toNumber(); + const rBalanceA = balanceA; + const rBalanceB = balanceB; + const vBalanceA = virtualBalanceA; + const vBalanceB = virtualBalanceB; + const marginValue = Number(centerednessMargin); const lowerMargin = calculateLowerMargin({ - margin: Number(margin), + margin: marginValue, invariant: invariant.toNumber(), virtualBalanceA: vBalanceA, virtualBalanceB: vBalanceB, }); const upperMargin = calculateUpperMargin({ - margin: Number(margin), + margin: marginValue, invariant: invariant.toNumber(), virtualBalanceA: vBalanceA, virtualBalanceB: vBalanceB, }); - const currentBalance = bn(balanceA).plus(virtualBalanceA).toNumber(); - const minPriceValue = bn(virtualBalanceB).pow(2).div(invariant).toNumber(); const maxPriceValue = bn(invariant).div(bn(virtualBalanceA).pow(2)).toNumber(); const lowerMarginValue = bn(invariant).div(bn(lowerMargin).pow(2)).toNumber(); const upperMarginValue = bn(invariant).div(bn(upperMargin).pow(2)).toNumber(); - const currentPriceValue = bn(bn(balanceB).plus(virtualBalanceB)).div(bn(balanceA).plus(virtualBalanceA)).toNumber(); + // Using usd per token inputs populated by fetch (or user) to calc current price + const currentPriceValue = Number(usdPerTokenInputA) / Number(usdPerTokenInputB); - const markPoints = [ - { name: "upper limit", x: vBalanceA, color: "#FF4560", priceValue: maxPriceValue.toFixed(3) }, - { name: "lower limit", x: xForMinPrice, color: "#FF4560", priceValue: minPriceValue.toFixed(3) }, - { - name: "higher target", - x: lowerMargin, - color: "#E67E22", - priceValue: lowerMarginValue.toFixed(3), - }, - { - name: "lower target", - x: upperMargin, - color: "#E67E22", - priceValue: upperMarginValue.toFixed(3), - }, - { - name: "current", - x: currentBalance, - priceValue: currentPriceValue.toFixed(3), + const isPoolWithinRange = + (currentPriceValue > minPriceValue && currentPriceValue < lowerMarginValue) || + (currentPriceValue > upperMarginValue && currentPriceValue < maxPriceValue); - color: "#00E396", - }, - ].map(point => { - return { - name: point.name, - coord: [point.x, curveFunction(point.x)], - itemStyle: { - color: point.color, - }, - emphasis: { - disabled: true, - }, - silent: true, - priceValue: point.priceValue, - }; + const { poolCenteredness, isPoolAboveCenter } = computeCenteredness({ + balanceA: rBalanceA, + balanceB: rBalanceB, + virtualBalanceA: vBalanceA, + virtualBalanceB: vBalanceB, }); return { - series: curvePoints, - markPoints, - min: xForMinPrice, - max: vBalanceA, - lowerMargin, - upperMargin, + maxPriceValue, + minPriceValue, + lowerMarginValue, + upperMarginValue, + currentPriceValue, + isPoolWithinRange, + usdPerTokenInputA, + usdPerTokenInputB, + poolCenteredness, + isPoolAboveCenter, + marginValue, }; - }, [centerednessMargin, initialBalanceA, initialMinPrice, initialMaxPrice, initialTargetPrice]); - - const option = useMemo(() => { - if (!currentChartData?.series?.length || !currentChartData?.markPoints?.length) { - return { - grid: { - left: "3%", - right: "18%", - bottom: "5%", - top: "10%", - containLabel: true, - }, - xAxis: { - type: "value", - axisLabel: { show: true }, - }, - yAxis: { - type: "value", - axisLabel: { show: true }, - }, - series: [ - { - type: "line", - data: [], - }, - ], - }; + }, [ + centerednessMargin, + initialBalanceA, + initialMinPrice, + initialMaxPrice, + initialTargetPrice, + usdPerTokenInputA, + usdPerTokenInputB, + ]); + + const options = useMemo(() => { + const { + maxPriceValue, + minPriceValue, + lowerMarginValue, + upperMarginValue, + currentPriceValue, + marginValue, // is a true percentage + isPoolWithinRange, + } = currentChartData; + + let showTargetValues = true; + let showMinMaxValues = true; + const totalGreenAndOrangeBars = 52; + + // always have a minimum of 1 orange bar + const baseOrangeBarCount = + marginValue && marginValue < 4 ? 1 : Math.floor((totalGreenAndOrangeBars * (marginValue || 0)) / 100 / 2); + + // if the margin is very small or very big, show only the target values or min/max values depending on the pool state + if (marginValue && marginValue < 4) { + if (isPoolWithinRange) { + showTargetValues = true; + showMinMaxValues = false; + } else if (isPoolWithinRange) { + showTargetValues = false; + showMinMaxValues = true; + } + } else if (marginValue && marginValue > 92) { + showTargetValues = false; + showMinMaxValues = true; } - const series = currentChartData.series; - if (!series) return {}; + const baseGreenBarCount = totalGreenAndOrangeBars - 2 * baseOrangeBarCount; + const baseGreyBarCount = 9; + const totalBars = 2 * baseGreyBarCount + 2 * baseOrangeBarCount + baseGreenBarCount; + + // for some reason the number of orange (or green) bars matters to echarts in the grid + const orangeBarCountEven = baseOrangeBarCount % 2 === 0; + const gridTopDesktop = orangeBarCountEven ? "40%" : "35%"; + + const gridTopMobile = orangeBarCountEven && !(showMinMaxValues && !showTargetValues) ? "30%" : "22%"; + + const baseGreyBarConfig = { + count: baseGreyBarCount, + value: isMobile ? 1 : 3, + gradientColors: ["rgba(160, 174, 192, 0.5)", "rgba(160, 174, 192, 0.1)"], + borderRadius: 20, + }; + + const baseOrangeBarConfig = { + count: baseOrangeBarCount, + value: 100, + gradientColors: ["rgb(253, 186, 116)", "rgba(151, 111, 69, 0.5)"], + borderRadius: 20, + }; + + const greenBarConfig = { + name: "Green", + count: baseGreenBarCount, + value: 100, + gradientColors: ["rgb(99, 242, 190)", "rgba(57, 140, 110, 0.5)"], + borderRadius: 20, + }; + + const barSegmentsConfig = [ + { ...baseGreyBarConfig, name: "Left Grey" }, + { ...baseOrangeBarConfig, name: "Left Orange" }, + greenBarConfig, + { ...baseOrangeBarConfig, name: "Right Orange" }, + { ...baseGreyBarConfig, name: "Right Grey" }, + ]; + + const allCategories: string[] = []; + const seriesData: any[] = []; + let categoryNumber = 1; + + // Calculate which bar the current price corresponds to + const getCurrentPriceBarIndex = () => { + const { minPriceValue, maxPriceValue, currentPriceValue } = currentChartData || {}; + + if (!minPriceValue || !maxPriceValue || !currentPriceValue) { + return baseGreyBarCount; // Default to first green bar + } + + // Calculate position based on current price relative to min/max + const priceRange = maxPriceValue - minPriceValue; + const currentPricePosition = (currentPriceValue - minPriceValue) / priceRange; + + // Map to bar index + const totalGreenAndOrangeBars = 2 * baseOrangeBarCount + baseGreenBarCount; + const barIndex = Math.floor(currentPricePosition * totalGreenAndOrangeBars); - const xValues = series.map(point => point[0]); - const yValues = series.map(point => point[1]); + return Math.max(0, Math.min(barIndex + baseGreyBarCount, totalBars - 1)); + }; + + const currentPriceBarIndex = getCurrentPriceBarIndex(); + + barSegmentsConfig.forEach(segment => { + const segmentCategories: string[] = []; + const segmentStartIndex = allCategories.length; + + for (let i = 0; i < segment.count; i++) { + segmentCategories.push(String(categoryNumber++)); + } - const maxPricePoint = currentChartData.markPoints?.find(p => p.name === "upper limit"); - const minPricePoint = currentChartData.markPoints?.find(p => p.name === "lower limit"); + allCategories.push(...segmentCategories); - const xMin = maxPricePoint?.coord[0] || Math.min(...xValues); - const yMax = maxPricePoint?.coord[1] || Math.max(...yValues); - const xMax = minPricePoint?.coord[0] || Math.max(...xValues); - const yMin = minPricePoint?.coord[1] || Math.min(...yValues); + const segmentSeriesData = Array(segment.count) + .fill(null) + .map((_, i) => { + const isCurrentPriceBar = segmentStartIndex + i === currentPriceBarIndex; - const xPadding = (xMax - xMin) * 0.3; - const yPadding = (yMax - yMin) * 0.3; + return { + value: segment.value, + itemStyle: { + color: isCurrentPriceBar // Solid color for current price bar + ? isPoolWithinRange + ? GREEN + : ORANGE + : getGradientColor(segment.gradientColors), + borderRadius: segment.borderRadius, + }, + }; + }); + + seriesData.push(...segmentSeriesData); + }); + + const baseRichProps = { + fontSize: 12, + lineHeight: 13, + color: "#A0AEC0", + align: "center", + }; + + const paddingRight = isMobile ? 5 : 10; + + const richStyles = { + base: baseRichProps, + triangle: { + ...baseRichProps, + fontSize: 10, + lineHeight: 12, + color: "#718096", + }, + current: { + ...baseRichProps, + color: isPoolWithinRange ? GREEN : ORANGE, + }, + currentTriangle: { + ...baseRichProps, + fontSize: 10, + lineHeight: 12, + color: isPoolWithinRange ? GREEN : ORANGE, + }, + withRightPadding: { + ...baseRichProps, + padding: [0, paddingRight, 0, 0], + }, + withRightBottomPadding: { + ...baseRichProps, + padding: [0, paddingRight, 10, 0], + }, + withTopRightPadding: { + ...baseRichProps, + padding: [showMinMaxValues && !showTargetValues ? 0 : 100, paddingRight, 0, 0], + }, + }; return { + tooltip: { show: false }, + title: { + text: `${tokens}`, + textAlign: "left", + textVerticalAlign: "top", + textStyle: { + color: "#A0AEC0", + fontSize: 14, + fontWeight: "normal", + }, + right: "9%", + top: "5%", + }, grid: { - left: "3%", - right: "18%", - bottom: "5%", - top: "10%", + left: isMobile ? "-7%" : "-3%", + right: "1%", + top: isMobile ? gridTopMobile : gridTopDesktop, + bottom: orangeBarCountEven ? "20%" : "8%", containLabel: true, }, - visualMap: { - show: false, - dimension: 0, - min: currentChartData.max, - max: currentChartData.min, - inRange: { - color: ["#FF4560", "#FC7D02", "#93CE07", "#FC7D02", "#FF4560"], - }, - controller: { - inRange: { - color: ["#FF4560", "#FC7D02", "#93CE07", "#FC7D02", "#FF4560"], - }, - }, - pieces: [ - { - lte: currentChartData.max, - color: "#FF4560", - }, - { - gt: currentChartData.max, - lte: currentChartData.lowerMargin, - color: "#FC7D02", - }, - { - gt: currentChartData.lowerMargin, - lte: currentChartData.upperMargin, - color: "#93CE07", - }, - { - gt: currentChartData.upperMargin, - lte: currentChartData.min, - color: "#FC7D02", - }, - { - gt: currentChartData.min, - color: "#FF4560", - }, - ], - }, xAxis: { - type: "value", - min: xMin - xPadding, - max: xMax + xPadding, + show: true, + type: "category", + data: allCategories, + position: "bottom", + axisLine: { show: false }, + axisTick: { show: false }, axisLabel: { show: true, - showMinLabel: false, - showMaxLabel: false, - }, - axisLine: { - show: true, - lineStyle: { - color: "#666", + interval: 0, + formatter: (value: string, index: number) => { + if (showMinMaxValues && index === baseGreyBarCount) { + return `{${isMobile ? "triangleMobile" : "triangle"}|▲}\n{${ + isMobile ? "labelTextMobile" : "labelText" + }|Min price}\n{${isMobile ? "priceValueMobile" : "priceValue"}|${ + minPriceValue !== undefined ? fNum("clpPrice", minPriceValue) : "N/A" + }}`; + } + + if (showTargetValues && index === baseGreyBarCount + baseOrangeBarCount) { + return `{triangle|▲}\n{labelText|Low target}\n{priceValue|${ + upperMarginValue !== undefined ? fNum("clpPrice", upperMarginValue) : "N/A" + }}`; + } + + if (showTargetValues && index === totalBars - baseGreyBarCount - baseOrangeBarCount) { + return `{triangle|▲}\n{labelText|High target}\n{priceValue|${ + lowerMarginValue !== undefined ? fNum("clpPrice", lowerMarginValue) : "N/A" + }}`; + } + + if (showMinMaxValues && index === totalBars - baseGreyBarCount) { + return `{${isMobile ? "triangleMobile" : "triangle"}|▲}\n{${ + isMobile ? "labelTextMobile" : "labelText" + }|Max price}\n{${isMobile ? "priceValueMobile" : "priceValue"}|${ + maxPriceValue !== undefined ? fNum("clpPrice", maxPriceValue) : "N/A" + }}`; + } + + return ""; }, - }, - splitLine: { - show: true, - lineStyle: { - color: "#666", - opacity: 0.3, + rich: { + triangle: { + ...richStyles.triangle, + ...richStyles.withRightBottomPadding, + }, + labelText: { + ...richStyles.base, + ...richStyles.withRightBottomPadding, + }, + priceValue: { + ...richStyles.base, + ...richStyles.withRightPadding, + }, + triangleMobile: { + ...richStyles.triangle, + ...richStyles.withTopRightPadding, + }, + labelTextMobile: { + ...richStyles.base, + ...richStyles.withTopRightPadding, + }, + priceValueMobile: { + ...richStyles.base, + padding: [showMinMaxValues && !showTargetValues ? 0 : 110, 10, 0, 0], + }, }, }, - axisTick: { - show: true, - }, - name: sortedTokenConfigs[0].tokenInfo?.symbol, - nameLocation: "end", - nameTextStyle: { - align: "right", - verticalAlign: "bottom", - padding: [0, -30, -20, 0], - fontSize: 12, - color: "#999", - }, }, yAxis: { + show: false, type: "value", - min: yMin - yPadding, - max: yMax + yPadding, - axisLabel: { - show: true, - // remove min and max labels instead of hiding them - formatter: function (value: number) { - if (value === yMin - yPadding || value === yMax + yPadding) { - return ""; - } - return value; - }, - }, - axisLine: { - show: true, - lineStyle: { - color: "#666", - }, - }, - splitLine: { - show: true, - lineStyle: { - color: "#666", - opacity: 0.3, - }, - }, - axisTick: { - show: true, - }, - name: sortedTokenConfigs[1].tokenInfo?.symbol, - nameLocation: "end", - nameTextStyle: { - align: "left", - verticalAlign: "top", - padding: [-10, 0, 0, -40], - fontSize: 12, - color: "#999", - }, }, series: [ { - data: series, - type: "line", - smooth: true, - lineStyle: { - width: 3, - }, - symbol: "none", - silent: true, - tooltip: { - show: false, - }, - emphasis: { - disabled: true, - }, - markPoint: { - symbol: "circle", - symbolSize: 10, - label: { - show: false, - }, - data: currentChartData.markPoints, - }, - markLine: { - silent: true, - symbol: "none", - label: { - show: true, - position: "end", - distance: 10, - formatter: function (params: any) { - const pointName = params.name; - - const point = currentChartData.markPoints?.find(p => p.name === pointName); - if (point) { - return ["{value|" + point.priceValue + "}", "{name|" + `(${pointName})` + "}"].join("\n"); - } - - return ""; - }, - rich: { - value: { - color: "#333333", - fontWeight: "bold", - fontSize: 12, - padding: [2, 0, 1, 0], - align: "center", - width: 70, - textShadow: "none", - }, - name: { - color: "#333333", - fontSize: 12, - padding: [1, 0, 2, 0], - align: "center", - width: 70, - textShadow: "none", - }, - }, - backgroundColor: function (params: any) { - const point = currentChartData.markPoints?.find(p => p.name === params.name); - const color = point?.itemStyle?.color || "rgba(0, 0, 0, 0.75)"; - return color; - }, - borderColor: "rgba(255, 255, 255, 0.3)", - borderWidth: 1, - borderRadius: 4, - padding: [4, 8], - shadowBlur: 3, - shadowColor: "rgba(0, 0, 0, 0.3)", - shadowOffsetX: 1, - shadowOffsetY: 1, - }, - data: [ - ...(currentChartData.markPoints || []).map(point => ({ - name: point.name, - yAxis: point.coord[1], - - lineStyle: { - color: point.itemStyle.color, - }, + data: seriesData.map((value, index) => { + if (index === currentPriceBarIndex) { + return { + ...value, label: { - backgroundColor: point.itemStyle.color, + show: true, + position: "top", + formatter: `{labelText|Current price}\n{priceValue|${ + currentPriceValue !== undefined ? fNum("clpPrice", currentPriceValue) : "N/A" + }}\n{triangle|▼}`, + rich: { + triangle: { + ...richStyles.currentTriangle, + }, + labelText: { + ...richStyles.current, + padding: [0, 0, 5, 0], + }, + priceValue: { + ...richStyles.current, + }, + }, }, - })), - ], - }, + }; + } + + return value; + }), + type: "bar", + barWidth: "90%", + barCategoryGap: "25%", + silent: true, }, ], }; - }, [currentChartData]); + }, [currentChartData, tokens]); - return { option }; + return { options }; } diff --git a/packages/nextjs/hooks/v3/useCreatePool.ts b/packages/nextjs/hooks/v3/useCreatePool.ts index 35767a4..407a45c 100644 --- a/packages/nextjs/hooks/v3/useCreatePool.ts +++ b/packages/nextjs/hooks/v3/useCreatePool.ts @@ -108,26 +108,29 @@ export const useCreatePool = () => { const createPool = new CreatePool(); const input = createPoolInput(); + console.log("create pool input:", input); const call = createPool.buildCall(input); - // why is esimateGas reverting only on hyperliquid? const estimatedGas = await publicClient.estimateGas({ account: walletClient.account, to: call.to, data: call.callData, }); - const gas = isHyperEvm ? BigInt(estimatedGas) * 2n : undefined; // required to double the gas? - const gasPrice = isHyperEvm && bigBlockGasPrice ? bigBlockGasPrice : undefined; // big block gas price higher than smol block gas price + // hyperEVM big blocks require more gas? + // https://github.com/zekraken-bot/HyperEVM/blob/1a1a4468f0029ae9d80f846bf401ef1e219f1b63/stable_deploy_hyper.py#L132-L163 + const gas = isHyperEvm ? BigInt(estimatedGas) * 2n : undefined; + // big block gas price higher than smol block gas price + const gasPrice = isHyperEvm && bigBlockGasPrice ? bigBlockGasPrice : undefined; const hash = await writeTx( () => walletClient.sendTransaction({ account: walletClient.account, data: call.callData, - to: call.to, gas, gasPrice, + to: call.to, }), { // callbacks to save tx hash's to store @@ -149,7 +152,7 @@ export const useCreatePool = () => { // Returns pool type specific parameters necesary for the create pool input function usePoolTypeSpecificParams() { - const { poolType, amplificationParameter, eclpParams, reClammParams } = usePoolCreationStore(); + const { poolType, amplificationParameter, eclpParams, reClammParams, tokenConfigs } = usePoolCreationStore(); const isGyroEclp = poolType === PoolType.GyroE; const isStablePool = poolType === PoolType.Stable || poolType === PoolType.StableSurge; @@ -173,14 +176,37 @@ function usePoolTypeSpecificParams() { }; } - if (isReClamm) + if (isReClamm) { + // if tokenConfigs "out of order", invert the min max and target price + // TODO: account for if user is using boosted variant which means address will be underling so gotta look at other addy? + const isTokenConfigsInOrder = tokenConfigs[0].address.toLowerCase() < tokenConfigs[1].address.toLowerCase(); + + const { initialMinPrice, initialMaxPrice, initialTargetPrice } = reClammParams; + + // TODO: make re-usable invert function to share with handleInvertReClammParams + let minPrice = Number(initialMinPrice); + let maxPrice = Number(initialMaxPrice); + let targetPrice = Number(initialTargetPrice); + + if (!isTokenConfigsInOrder) { + minPrice = 1 / Number(initialMaxPrice); + maxPrice = 1 / Number(initialMinPrice); + targetPrice = 1 / Number(initialTargetPrice); + } + return { - initialTargetPrice: parseUnits(reClammParams.initialTargetPrice, 18), - initialMinPrice: parseUnits(reClammParams.initialMinPrice, 18), - initialMaxPrice: parseUnits(reClammParams.initialMaxPrice, 18), - priceShiftDailyRate: parseUnits(reClammParams.priceShiftDailyRate, 16), - centerednessMargin: parseUnits((Number(reClammParams.centerednessMargin) / 2).toString(), 16), // Charting UX based on pool math simulator setup allows 0 - 100% but on chain is 0 - 50% + priceParams: { + initialMinPrice: parseUnits(minPrice.toString(), 18), + initialMaxPrice: parseUnits(maxPrice.toString(), 18), + initialTargetPrice: parseUnits(targetPrice.toString(), 18), + // hardcoded price to not include rate until new reclamm deployments. without rate means boosted must be priced in terms of underlying + tokenAPriceIncludesRate: false, + tokenBPriceIncludesRate: false, + }, + priceShiftDailyRate: parseUnits(reClammParams.dailyPriceShiftExponent, 16), // SDK kept OG var name "priceShiftDailyRate" but on chain is same as creation ui + centerednessMargin: parseUnits(reClammParams.centerednessMargin, 16), }; + } return {}; } diff --git a/packages/nextjs/hooks/v3/useFetchTokenRate.ts b/packages/nextjs/hooks/v3/useFetchTokenRate.ts index fe30c21..9bddc03 100644 --- a/packages/nextjs/hooks/v3/useFetchTokenRate.ts +++ b/packages/nextjs/hooks/v3/useFetchTokenRate.ts @@ -24,5 +24,9 @@ export const useFetchTokenRate = (address: Address | undefined) => { } }, enabled: !!address && isValidAddress, + // staleTime: 0, // Always consider data stale + refetchOnMount: true, // Refetch when component mounts + refetchOnWindowFocus: true, // Refetch when window regains focus + refetchOnReconnect: true, // Refetch when network reconnects }); }; diff --git a/packages/nextjs/hooks/v3/usePoolCreationStore.ts b/packages/nextjs/hooks/v3/usePoolCreationStore.ts index 76ccd02..112d4d0 100644 --- a/packages/nextjs/hooks/v3/usePoolCreationStore.ts +++ b/packages/nextjs/hooks/v3/usePoolCreationStore.ts @@ -35,11 +35,13 @@ export type ReClammParams = { initialTargetPrice: string; initialMinPrice: string; initialMaxPrice: string; - priceShiftDailyRate: string; - centerednessMargin: string; - initialBalanceA: string; + tokenAPriceIncludesRate: boolean; + tokenBPriceIncludesRate: boolean; usdPerTokenInputA: string; usdPerTokenInputB: string; + dailyPriceShiftExponent: string; + centerednessMargin: string; + initialBalanceA: string; }; export interface TransactionDetails { @@ -107,9 +109,11 @@ export const initialReClammParams: ReClammParams = { initialTargetPrice: "", initialMinPrice: "", initialMaxPrice: "", - priceShiftDailyRate: "150", - centerednessMargin: "25", - initialBalanceA: "100", + tokenAPriceIncludesRate: false, + tokenBPriceIncludesRate: false, + dailyPriceShiftExponent: "25", + centerednessMargin: "50", + initialBalanceA: "100", // TODO: removed input. could this be hard coded? usdPerTokenInputA: "", usdPerTokenInputB: "", }; diff --git a/packages/nextjs/hooks/v3/useUserDataStore.ts b/packages/nextjs/hooks/v3/useUserDataStore.ts index 6397eb2..39d1637 100644 --- a/packages/nextjs/hooks/v3/useUserDataStore.ts +++ b/packages/nextjs/hooks/v3/useUserDataStore.ts @@ -8,6 +8,7 @@ export type UserDataStore = { hasEditedPoolSymbol: boolean; hasAgreedToWarning: boolean; hasEditedEclpParams: boolean; + hasEditedReclammParams: boolean; isOnlyInitializingPool: boolean; updateUserData: (updates: Partial) => void; clearUserData: () => void; @@ -19,6 +20,7 @@ export const initialUserDataStore = { hasEditedPoolName: false, hasEditedPoolSymbol: false, hasEditedEclpParams: false, + hasEditedReclammParams: false, isOnlyInitializingPool: false, }; diff --git a/packages/nextjs/hooks/v3/useValidateCreationInputs.ts b/packages/nextjs/hooks/v3/useValidateCreationInputs.ts index b8f314e..48d7973 100644 --- a/packages/nextjs/hooks/v3/useValidateCreationInputs.ts +++ b/packages/nextjs/hooks/v3/useValidateCreationInputs.ts @@ -1,13 +1,10 @@ import { PoolType, STABLE_POOL_CONSTRAINTS, TokenType } from "@balancer/sdk"; -import { useQueryClient } from "@tanstack/react-query"; import { isAddress } from "viem"; import { useEclpParamValidations } from "~~/hooks/gyro"; import { usePoolCreationStore, useValidateHooksContract } from "~~/hooks/v3"; import { MAX_POOL_NAME_LENGTH, MAX_POOL_SYMBOL_LENGTH } from "~~/utils/constants"; export function useValidateCreationInputs() { - const queryClient = useQueryClient(); - const { poolType, tokenConfigs, @@ -40,11 +37,6 @@ export function useValidateCreationInputs() { // Must have rate provider if token type is TOKEN_WITH_RATE if (token.tokenType === TokenType.TOKEN_WITH_RATE && !isAddress(token.rateProvider)) return false; - // Check tanstack query cache for rate provider validity - if (token.tokenType === TokenType.TOKEN_WITH_RATE) { - const cachedRate = queryClient.getQueryData(["fetchTokenRate", token.rateProvider]); - if (!cachedRate) return false; - } return true; }) && isValidTokenWeights; @@ -52,7 +44,15 @@ export function useValidateCreationInputs() { const { isValidPoolHooksContract } = useValidateHooksContract(poolHooksContract); const isGyroEclpParamsValid = !baseParamsError && !derivedParamsError; - const isReClammParamsValid = Object.values(reClammParams).every(value => value !== ""); + + const isDailyPriceShiftExponentValid = + Number(reClammParams.dailyPriceShiftExponent) >= 0 && Number(reClammParams.dailyPriceShiftExponent) <= 100; + const isCenterednessMarginValid = + Number(reClammParams.centerednessMargin) >= 0 && Number(reClammParams.centerednessMargin) <= 90; + const isReClammParamsValid = + Object.values(reClammParams).every(value => value !== "") && + isDailyPriceShiftExponentValid && + isCenterednessMarginValid; const isParametersValid = [ // Common param checks for all pool types diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 193fd5f..1db808f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@balancer-labs/balancer-maths": "^0.0.25", - "@balancer/sdk": "4.4.0", + "@balancer/sdk": "4.8.0", "@heroicons/react": "^2.0.11", "@nktkas/hyperliquid": "^0.23.1", "@rainbow-me/rainbowkit": "2.1.2", diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts index c18c0dc..2ba0a0f 100644 --- a/packages/nextjs/scaffold.config.ts +++ b/packages/nextjs/scaffold.config.ts @@ -1,4 +1,4 @@ -import { hyperEVM } from "./utils/constants"; +import { hyperEVM, plasma } from "./utils/constants"; import * as chains from "viem/chains"; export type ScaffoldConfig = { @@ -24,6 +24,7 @@ const scaffoldConfig = { chains.optimism, chains.sonic, hyperEVM, + plasma, ], // If using chains.foundry as your targetNetwork, you must specify a network to fork diff --git a/packages/nextjs/utils/constants/customChains.ts b/packages/nextjs/utils/constants/customChains.ts index 14d09da..b24a139 100644 --- a/packages/nextjs/utils/constants/customChains.ts +++ b/packages/nextjs/utils/constants/customChains.ts @@ -27,3 +27,32 @@ export const hyperEVM = /*#__PURE__*/ defineChain({ }, testnet: false, }); + +export const plasma = /*#__PURE__*/ defineChain({ + id: 9745, + name: "Plasma", + nativeCurrency: { + name: "XPL", + symbol: "XPL", + decimals: 18, + }, + rpcUrls: { + default: { + http: ["https://rpc.plasma.to/"], + webSocket: ["wss://rpc.plasma.to/"], + }, + }, + blockExplorers: { + default: { + name: "Plasma Explorer", + url: "https://plasmascan.to/", + apiUrl: "https://api.routescan.io/v2/network/mainnet/evm/9745/etherscan/api?", + }, + }, + contracts: { + multicall3: { + address: "0xcA11bde05977b3631167028862bE2a173976CA11", + blockCreated: 1, + }, + }, +}); diff --git a/packages/nextjs/utils/constants/poolTypes.ts b/packages/nextjs/utils/constants/poolTypes.ts index cb3acea..626195a 100644 --- a/packages/nextjs/utils/constants/poolTypes.ts +++ b/packages/nextjs/utils/constants/poolTypes.ts @@ -4,8 +4,8 @@ export type SupportedPoolTypes = | PoolType.Stable | PoolType.Weighted | PoolType.StableSurge - | PoolType.GyroE - | PoolType.ReClamm; + | PoolType.ReClamm + | PoolType.GyroE; export type PoolTypeDetails = { label: string; @@ -31,16 +31,17 @@ export const poolTypeMap: Record = { description: "Weighted pools support up to 8 tokens with customizable weightings, allowing for fine-tuned exposure to multiple assets", }, - [PoolType.GyroE]: { - label: "Gyro E-CLP", - maxTokens: 2, - description: - "Gyro's elliptic concentrated liquidity pools concentrate liquidity within price bounds with the flexibility to asymmetrically focus liquidity", - }, + [PoolType.ReClamm]: { label: "Readjusting CLAMM", maxTokens: 2, description: "A concentrated liquidity pool that automates adjustments to the range of liquidity provided as price moves", }, + [PoolType.GyroE]: { + label: "Gyro E-CLP", + maxTokens: 2, + description: + "Gyro's elliptic concentrated liquidity pools concentrate liquidity within price bounds with the flexibility to asymmetrically focus liquidity", + }, }; diff --git a/packages/nextjs/utils/numbers.ts b/packages/nextjs/utils/numbers.ts index ff1fcab..3a9b3aa 100644 --- a/packages/nextjs/utils/numbers.ts +++ b/packages/nextjs/utils/numbers.ts @@ -157,6 +157,13 @@ function gyroPriceFormat(val: Numberish): string { return numeral(val.toString()).format("0"); } +function clpPriceFormat(val: Numberish): string { + if (bn(val).lt(0.001)) return numeral(val.toString()).format("0.00000"); + if (bn(val).lt(10)) return numeral(val.toString()).format("0.0000"); + if (bn(val).lt(100)) return numeral(val.toString()).format("0.00"); + + return numeral(val.toString()).format("0"); +} // Sums an array of numbers safely using bignumber.js. export function safeSum(amounts: Numberish[]): string { return amounts.reduce((a, b) => bn(a).plus(b.toString()), bn(0)).toString(); @@ -180,7 +187,8 @@ type NumberFormat = | "sharePercent" | "stakedPercentage" | "boost" - | "gyroPrice"; + | "gyroPrice" + | "clpPrice"; // General number formatting function. export function fNum(format: NumberFormat, val: Numberish, opts?: FormatOpts): string { @@ -209,6 +217,8 @@ export function fNum(format: NumberFormat, val: Numberish, opts?: FormatOpts): s return boostFormat(val); case "gyroPrice": return gyroPriceFormat(val); + case "clpPrice": + return clpPriceFormat(val); default: throw new Error(`Number format not implemented: ${format}`); } diff --git a/packages/nextjs/utils/scaffold-eth/networks.ts b/packages/nextjs/utils/scaffold-eth/networks.ts index e0dfc14..3ce96b7 100644 --- a/packages/nextjs/utils/scaffold-eth/networks.ts +++ b/packages/nextjs/utils/scaffold-eth/networks.ts @@ -1,5 +1,5 @@ // import { hyperEVM } from "../constants"; -import { hyperEVM } from "../constants"; +import { hyperEVM, plasma } from "../constants"; import * as chains from "viem/chains"; import scaffoldConfig from "~~/scaffold.config"; @@ -145,6 +145,10 @@ export const NETWORKS_EXTRA_DATA: Record = { export function getBlockExplorerTxLink(chainId: number | undefined, txnHash: string | undefined) { if (!chainId || !txnHash) return undefined; + if (chainId === plasma.id) { + return `https://plasmascan.to/tx/${txnHash}`; + } + const chainNames = Object.keys(chains); const targetChainArr = chainNames.filter(chainName => { diff --git a/packages/nextjs/utils/supportedNetworks.ts b/packages/nextjs/utils/supportedNetworks.ts index cade37b..ac78a4f 100644 --- a/packages/nextjs/utils/supportedNetworks.ts +++ b/packages/nextjs/utils/supportedNetworks.ts @@ -1,8 +1,17 @@ -import { hyperEVM } from "./constants"; +import { hyperEVM, plasma } from "./constants"; import * as chains from "viem/chains"; export const supportedNetworks = { - balancerV3: [chains.mainnet, chains.arbitrum, chains.base, hyperEVM, chains.gnosis, chains.avalanche, chains.sepolia], + balancerV3: [ + chains.mainnet, + chains.arbitrum, + chains.base, + chains.gnosis, + chains.avalanche, + hyperEVM, + plasma, + chains.sepolia, + ], beets: [chains.sonic, chains.optimism], cowAmm: [chains.mainnet, chains.arbitrum, chains.base, chains.gnosis, chains.sepolia], }; diff --git a/yarn.lock b/yarn.lock index 1e2999a..b19f098 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,16 +193,16 @@ __metadata: languageName: node linkType: hard -"@balancer/sdk@npm:4.4.0": - version: 4.4.0 - resolution: "@balancer/sdk@npm:4.4.0" +"@balancer/sdk@npm:4.8.0": + version: 4.8.0 + resolution: "@balancer/sdk@npm:4.8.0" dependencies: "@balancer-labs/balancer-maths": 0.0.27 "@types/big.js": 6.2.2 big.js: 6.2.2 decimal.js-light: 2.5.1 viem: 2.27.0 - checksum: 0cc3d4cd67c225d5f1cbbe72398976d921d59e52462d27933a7eb76c02fbc11f62c8efd17321ce81c0571c880359c6178f4c14b23b6cbd0467ad36b0fd90e639 + checksum: fa4f4540af1a96f1dd7cb1bd495d3e8980e0f0f3427bcb45801d2700b66600681385fd3653107c6fab193b400d5941ed3b6315f0a478e644d62a217fad222c07 languageName: node linkType: hard @@ -1465,7 +1465,7 @@ __metadata: resolution: "@se-2/nextjs@workspace:packages/nextjs" dependencies: "@balancer-labs/balancer-maths": ^0.0.25 - "@balancer/sdk": 4.4.0 + "@balancer/sdk": 4.8.0 "@heroicons/react": ^2.0.11 "@nktkas/hyperliquid": ^0.23.1 "@rainbow-me/rainbowkit": 2.1.2