Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ff6c07d
enable create reclamm preview
MattPereira May 27, 2025
6c793cf
clean up alerts
MattPereira May 27, 2025
3734854
merge main updates
MattPereira Jun 12, 2025
13b7fed
Merge branch 'main' into reclamm
MattPereira Jul 10, 2025
8ad8152
update to new reclamm chart
MattPereira Jul 13, 2025
bd27b33
fix chart for dynamic centeredness margin
MattPereira Jul 13, 2025
29c2fc3
fetch computed reclamm init amounts
MattPereira Jul 14, 2025
7f82607
fix proportional init amount updates
MattPereira Jul 14, 2025
a7104e5
round suggested price params to 5 decimals
MattPereira Jul 14, 2025
cd33891
temp fix reclamm v2 factory addy
MattPereira Jul 17, 2025
6ad5780
update suggested reclamm price params on per usd input change
MattPereira Jul 17, 2025
1a2549b
UX to price in terms of underlying
MattPereira Jul 17, 2025
c9739e9
merge main
MattPereira Jul 18, 2025
2cda394
Merge branch 'main' into re-clamm
MattPereira Jul 21, 2025
b31489e
update to latest SDK w/ reclamm factory v2 addresses
MattPereira Jul 21, 2025
b6d9640
toggle if token price includes rate
MattPereira Jul 29, 2025
36ddaa0
force tokenPriceIncludesRate to be false
MattPereira Jul 30, 2025
2d21920
clean up comments
MattPereira Jul 30, 2025
1499a9e
Merge branch 'main' into re-clamm
MattPereira Jul 30, 2025
e2384dc
Merge branch 'main' into re-clamm
MattPereira Aug 28, 2025
868b9f5
Merge branch 'main' into re-clamm
MattPereira Sep 5, 2025
93805dc
Merge branch 'main' into re-clamm
MattPereira Sep 8, 2025
74d717b
Merge branch 'main' into re-clamm
MattPereira Sep 8, 2025
492162d
Merge branch 'main' into re-clamm
MattPereira Sep 9, 2025
7f134f8
remove eclp init amount updates
MattPereira Sep 9, 2025
9999891
hide deprecated eclp init on re-clamm branch
MattPereira Sep 12, 2025
d43e3a2
add support for plasma chain
MattPereira Sep 18, 2025
dc18a80
add pool type description
MattPereira Oct 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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, "");
Expand All @@ -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 (
<div className="bg-base-100 p-5 rounded-xl">
<div className="text-lg font-bold mb-3 inline-flex">
Expand All @@ -42,75 +60,140 @@ export const ReClammParams = () => {
</a>
</div>

<div className="bg-base-200 w-full h-96 rounded-xl mb-4">
<ReClammChart />
</div>
<ReClammChart />

<div className="flex flex-col gap-4">
<Alert type="info">
Initial prices represent the value of {sortedTokenConfigs[0].tokenInfo?.symbol} denominated in{" "}
{sortedTokenConfigs[1].tokenInfo?.symbol}
</Alert>

<div className="grid grid-cols-2 gap-4">
<TextField
label={`${sortedTokenConfigs[0].tokenInfo?.symbol} / USD`}
value={usdPerTokenInputA}
isDollarValue={true}
onChange={e => {
updateReClammParam({ usdPerTokenInputA: sanitizeNumberInput(e.target.value) });
}}
/>
<TextField
label={`${sortedTokenConfigs[1].tokenInfo?.symbol} / USD`}
value={usdPerTokenInputB}
isDollarValue={true}
onChange={e => {
updateReClammParam({ usdPerTokenInputB: sanitizeNumberInput(e.target.value) });
}}
/>
<div className="relative">
<TextField
label={`${tokenConfigs[0].tokenInfo?.symbol} price ${boostedLabelA}`}
tooltip={
isBoostedA && (
<div
className="tooltip hover:cursor-pointer tooltip-primary"
data-tip={`${tokenConfigs[0].tokenInfo?.symbol} rate is ${humanRateA}`}
>
<InformationCircleIcon className="w-5 h-5" />
</div>
)
}
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 && (
<TogglePriceIncludesRate
tokenPriceIncludesRate={tokenAPriceIncludesRate}
onChange={() => {
const updatedTokenAPriceIncludesRate = !tokenAPriceIncludesRate;
const updatedUsdPerTokenA = usdPerTokenA
? updatedTokenAPriceIncludesRate
? (usdPerTokenA / Number(formatUnits(currentRateTokenA, 18))).toString()
: usdPerTokenA.toString()
: usdPerTokenInputA;

updateReClammParam({
tokenAPriceIncludesRate: updatedTokenAPriceIncludesRate,
usdPerTokenInputA: updatedUsdPerTokenA,
});
}}
/>
)} */}
</div>
<div className="relative">
<TextField
label={`${tokenConfigs[1].tokenInfo?.symbol} price ${boostedLabelB}`}
value={usdPerTokenInputB}
isDollarValue={true}
tooltip={
isBoostedB && (
<div
className="tooltip hover:cursor-pointer tooltip-primary"
data-tip={`${tokenConfigs[1].tokenInfo?.symbol} rate is ${humanRateB}`}
>
<InformationCircleIcon className="w-5 h-5" />
</div>
)
}
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 && (
<TogglePriceIncludesRate
tokenPriceIncludesRate={tokenBPriceIncludesRate}
onChange={() => {
const updatedTokenBPriceIncludesRate = !tokenBPriceIncludesRate;
const updatedUsdPerTokenB = usdPerTokenB
? updatedTokenBPriceIncludesRate
? (usdPerTokenB / Number(formatUnits(currentRateTokenB, 18))).toString()
: usdPerTokenB.toString()
: usdPerTokenInputB;

updateReClammParam({
tokenBPriceIncludesRate: updatedTokenBPriceIncludesRate,
usdPerTokenInputB: updatedUsdPerTokenB,
});
}}
/>
)} */}
</div>
</div>

<div className="grid grid-cols-3 gap-4">
<TextField
label="Initial Min Price"
value={initialMinPrice}
onChange={e => updateReClammParam({ initialMinPrice: sanitizeNumberInput(e.target.value) })}
onChange={e => {
updateReClammParam({ initialMinPrice: sanitizeNumberInput(e.target.value) });
updateUserData({ hasEditedReclammParams: true });
}}
/>
<TextField
label="Initial Target Price"
value={initialTargetPrice}
onChange={e => updateReClammParam({ initialTargetPrice: sanitizeNumberInput(e.target.value) })}
onChange={e => {
updateReClammParam({ initialTargetPrice: sanitizeNumberInput(e.target.value) });
updateUserData({ hasEditedReclammParams: true });
}}
/>
<TextField
label="Initial Max Price"
value={initialMaxPrice}
onChange={e => updateReClammParam({ initialMaxPrice: sanitizeNumberInput(e.target.value) })}
onChange={e => {
updateReClammParam({ initialMaxPrice: sanitizeNumberInput(e.target.value) });
updateUserData({ hasEditedReclammParams: true });
}}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-2 gap-4">
<NumberInput
label="Centeredness Margin"
min={0}
max={100}
max={90}
isPercentage={true}
value={centerednessMargin}
placeholder="0 - 100"
onChange={e => updateReClammParam({ centerednessMargin: sanitizeNumberInput(e.target.value) })}
placeholder="0 - 90"
onChange={e => {
updateReClammParam({ centerednessMargin: sanitizeNumberInput(e.target.value) });
}}
/>
<NumberInput
label="Price Shift Daily Rate"
label="Daily Price Shift Exponent"
min={0}
max={300}
max={100}
isPercentage={true}
value={priceShiftDailyRate}
placeholder="0 - 300"
onChange={e => updateReClammParam({ priceShiftDailyRate: sanitizeNumberInput(e.target.value) })}
/>
<TextField
label={`Initial Balance of ${sortedTokenConfigs[0].tokenInfo?.symbol}`}
value={initialBalanceA}
onChange={e => updateReClammParam({ initialBalanceA: sanitizeNumberInput(e.target.value) })}
value={dailyPriceShiftExponent}
placeholder="0 - 100"
onChange={e => {
updateReClammParam({ dailyPriceShiftExponent: sanitizeNumberInput(e.target.value) });
}}
/>
</div>
</div>
Expand All @@ -119,7 +202,71 @@ export const ReClammParams = () => {
};

function ReClammChart() {
const { option } = useReclAmmChart();
const { options } = useReclAmmChart();

return <ReactECharts option={option} style={{ height: "100%", width: "100%" }} />;
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 (
<div className="bg-base-300 rounded-xl relative">
<div className="bg-base-200 w-full h-72 rounded-xl mb-4">
<ReactECharts option={options} style={{ height: "100%", width: "100%" }} />
<div
className="btn btn-sm rounded-lg absolute top-2 right-3 btn-primary px-2 py-0.5 text-neutral-700 bg-gradient-to-r from-violet-300 via-violet-200 to-orange-300 [box-shadow:0_0_10px_5px_rgba(139,92,246,0.5)] border-none"
onClick={handleInvertReClammParams}
>
<ArrowsRightLeftIcon className="w-[15px] h-[15px]" />
</div>
</div>
</div>
);
}

// function TogglePriceIncludesRate({
// tokenPriceIncludesRate,
// onChange,
// }: {
// tokenPriceIncludesRate: boolean;
// onChange: () => void;
// }) {
// return (
// <div className="absolute -top-2 right-0">
// <fieldset className="fieldset ">
// <label className="label cursor-pointer gap-2">
// <span className={`label-text ${tokenPriceIncludesRate ? "text-success" : "text-stone-400"}`}>
// {tokenPriceIncludesRate ? "price includes rate" : "price without rate"}
// </span>
// <input
// type="checkbox"
// checked={tokenPriceIncludesRate}
// onChange={onChange}
// className={`toggle toggle-sm ${tokenPriceIncludesRate ? "toggle-success" : "toggle-error"}`}
// />
// </label>
// </fieldset>
// </div>
// );
// }
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="bg-base-100 p-5 rounded-xl">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type HandleNumberInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
field: "swapFeePercentage" | "amplificationParameter",
field: "swapFeePercentage" | "amplificationParameter" | "centerednessMargin" | "dailyPriceShiftExponent",
min: number,
max: number,
) => void;
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<div className="bg-base-100 p-5 rounded-xl flex flex-col gap-3 relative">
{boostedVariant && poolType !== PoolType.GyroE && (
{boostedVariant && !shouldHideBoostUnderlying && (
<div
className={`flex justify-end items-center gap-1 cursor-pointer absolute top-4 right-5 text-lg ${
useBoostedVariant ? "text-success" : "text-info"
Expand Down Expand Up @@ -310,7 +313,7 @@ export function ChooseToken({ index }: { index: number }) {
{showRateProviderModal && tokenInfo && (
<RateProviderModal setShowRateProviderModal={setShowRateProviderModal} tokenIndex={index} />
)}
{showBoostOpportunityModal && tokenInfo && boostedVariant && poolType !== PoolType.GyroE && (
{showBoostOpportunityModal && tokenInfo && boostedVariant && !shouldHideBoostUnderlying && (
<BoostOpportunityModal
tokenIndex={index}
standardVariant={tokenInfo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function PoolTypeButton({ selectedPoolType }: { selectedPoolType: Support
<button
className={`${
selectedPoolType === poolType ? `${selectedPoolStyles}` : `bg-base-100 ${hoverPoolStyles} shadow-lg`
} p-2 w-full rounded-xl h-[58px]`}
} p-2 w-full rounded-xl h-20`}
onClick={handlePoolTypeSelection}
>
<div className="flex flex-col text-center">
Expand Down
Loading