@nradko/metric-omm-sdk-v1 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +111 -0
  2. package/dist/abis/MetricOmmPool.d.ts +784 -0
  3. package/dist/abis/MetricOmmPool.d.ts.map +1 -0
  4. package/dist/abis/MetricOmmPool.js +1008 -0
  5. package/dist/abis/MetricOmmPool.js.map +1 -0
  6. package/dist/abis/MetricOmmPoolFactory.d.ts +1365 -0
  7. package/dist/abis/MetricOmmPoolFactory.d.ts.map +1 -0
  8. package/dist/abis/MetricOmmPoolFactory.js +1754 -0
  9. package/dist/abis/MetricOmmPoolFactory.js.map +1 -0
  10. package/dist/abis/MetricOmmPoolLiquidityAdder.d.ts +308 -0
  11. package/dist/abis/MetricOmmPoolLiquidityAdder.d.ts.map +1 -0
  12. package/dist/abis/MetricOmmPoolLiquidityAdder.js +401 -0
  13. package/dist/abis/MetricOmmPoolLiquidityAdder.js.map +1 -0
  14. package/dist/abis/MetricOmmPoolStateView.d.ts +538 -0
  15. package/dist/abis/MetricOmmPoolStateView.d.ts.map +1 -0
  16. package/dist/abis/MetricOmmPoolStateView.js +717 -0
  17. package/dist/abis/MetricOmmPoolStateView.js.map +1 -0
  18. package/dist/abis/MetricOmmPoolSwapper.d.ts +557 -0
  19. package/dist/abis/MetricOmmPoolSwapper.d.ts.map +1 -0
  20. package/dist/abis/MetricOmmPoolSwapper.js +723 -0
  21. package/dist/abis/MetricOmmPoolSwapper.js.map +1 -0
  22. package/dist/abis/Multicall3.d.ts +333 -0
  23. package/dist/abis/Multicall3.d.ts.map +1 -0
  24. package/dist/abis/Multicall3.js +441 -0
  25. package/dist/abis/Multicall3.js.map +1 -0
  26. package/dist/abis/PriceProvider.d.ts +142 -0
  27. package/dist/abis/PriceProvider.d.ts.map +1 -0
  28. package/dist/abis/PriceProvider.js +184 -0
  29. package/dist/abis/PriceProvider.js.map +1 -0
  30. package/dist/abis/PriceProviderUi.d.ts +433 -0
  31. package/dist/abis/PriceProviderUi.d.ts.map +1 -0
  32. package/dist/abis/PriceProviderUi.js +318 -0
  33. package/dist/abis/PriceProviderUi.js.map +1 -0
  34. package/dist/abis/index.d.ts +11 -0
  35. package/dist/abis/index.d.ts.map +1 -0
  36. package/dist/abis/index.js +11 -0
  37. package/dist/abis/index.js.map +1 -0
  38. package/dist/addresses.d.ts +52 -0
  39. package/dist/addresses.d.ts.map +1 -0
  40. package/dist/addresses.js +58 -0
  41. package/dist/addresses.js.map +1 -0
  42. package/dist/constants.d.ts +20 -0
  43. package/dist/constants.d.ts.map +1 -0
  44. package/dist/constants.js +26 -0
  45. package/dist/constants.js.map +1 -0
  46. package/dist/factory/collectFees.d.ts +41 -0
  47. package/dist/factory/collectFees.d.ts.map +1 -0
  48. package/dist/factory/collectFees.js +120 -0
  49. package/dist/factory/collectFees.js.map +1 -0
  50. package/dist/factory/index.d.ts +5 -0
  51. package/dist/factory/index.d.ts.map +1 -0
  52. package/dist/factory/index.js +5 -0
  53. package/dist/factory/index.js.map +1 -0
  54. package/dist/index.d.ts +16 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +24 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/pool/create.d.ts +80 -0
  59. package/dist/pool/create.d.ts.map +1 -0
  60. package/dist/pool/create.js +80 -0
  61. package/dist/pool/create.js.map +1 -0
  62. package/dist/pool/index.d.ts +7 -0
  63. package/dist/pool/index.d.ts.map +1 -0
  64. package/dist/pool/index.js +10 -0
  65. package/dist/pool/index.js.map +1 -0
  66. package/dist/pool/liquidity.d.ts +165 -0
  67. package/dist/pool/liquidity.d.ts.map +1 -0
  68. package/dist/pool/liquidity.js +508 -0
  69. package/dist/pool/liquidity.js.map +1 -0
  70. package/dist/pool/read.d.ts +22 -0
  71. package/dist/pool/read.d.ts.map +1 -0
  72. package/dist/pool/read.js +88 -0
  73. package/dist/pool/read.js.map +1 -0
  74. package/dist/router/index.d.ts +5 -0
  75. package/dist/router/index.d.ts.map +1 -0
  76. package/dist/router/index.js +5 -0
  77. package/dist/router/index.js.map +1 -0
  78. package/dist/router/swap.d.ts +117 -0
  79. package/dist/router/swap.d.ts.map +1 -0
  80. package/dist/router/swap.js +298 -0
  81. package/dist/router/swap.js.map +1 -0
  82. package/dist/stateView/index.d.ts +5 -0
  83. package/dist/stateView/index.d.ts.map +1 -0
  84. package/dist/stateView/index.js +5 -0
  85. package/dist/stateView/index.js.map +1 -0
  86. package/dist/stateView/read.d.ts +80 -0
  87. package/dist/stateView/read.d.ts.map +1 -0
  88. package/dist/stateView/read.js +249 -0
  89. package/dist/stateView/read.js.map +1 -0
  90. package/dist/types.d.ts +134 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +6 -0
  93. package/dist/types.js.map +1 -0
  94. package/dist/utils/binData.d.ts +65 -0
  95. package/dist/utils/binData.d.ts.map +1 -0
  96. package/dist/utils/binData.js +109 -0
  97. package/dist/utils/binData.js.map +1 -0
  98. package/dist/utils/index.d.ts +7 -0
  99. package/dist/utils/index.d.ts.map +1 -0
  100. package/dist/utils/index.js +10 -0
  101. package/dist/utils/index.js.map +1 -0
  102. package/dist/utils/liquidityMath.d.ts +10 -0
  103. package/dist/utils/liquidityMath.d.ts.map +1 -0
  104. package/dist/utils/liquidityMath.js +31 -0
  105. package/dist/utils/liquidityMath.js.map +1 -0
  106. package/dist/utils/price.d.ts +13 -0
  107. package/dist/utils/price.d.ts.map +1 -0
  108. package/dist/utils/price.js +21 -0
  109. package/dist/utils/price.js.map +1 -0
  110. package/package.json +74 -0
  111. package/src/abis/MetricOmmPool.ts +1007 -0
  112. package/src/abis/MetricOmmPoolFactory.ts +1753 -0
  113. package/src/abis/MetricOmmPoolLiquidityAdder.ts +400 -0
  114. package/src/abis/MetricOmmPoolStateView.ts +716 -0
  115. package/src/abis/MetricOmmPoolSwapper.ts +722 -0
  116. package/src/abis/Multicall3.ts +440 -0
  117. package/src/abis/PriceProvider.ts +183 -0
  118. package/src/abis/PriceProviderUi.ts +317 -0
  119. package/src/abis/index.ts +11 -0
  120. package/src/addresses.ts +100 -0
  121. package/src/constants.ts +35 -0
  122. package/src/factory/collectFees.ts +197 -0
  123. package/src/factory/index.ts +12 -0
  124. package/src/index.ts +157 -0
  125. package/src/pool/create.ts +158 -0
  126. package/src/pool/index.ts +47 -0
  127. package/src/pool/liquidity.ts +839 -0
  128. package/src/pool/read.ts +131 -0
  129. package/src/router/index.ts +27 -0
  130. package/src/router/swap.ts +507 -0
  131. package/src/stateView/index.ts +18 -0
  132. package/src/stateView/read.ts +355 -0
  133. package/src/types.ts +162 -0
  134. package/src/utils/binData.ts +127 -0
  135. package/src/utils/index.ts +26 -0
  136. package/src/utils/liquidityMath.ts +47 -0
  137. package/src/utils/price.ts +23 -0
@@ -0,0 +1,839 @@
1
+ /**
2
+ * MetricAMM SDK - Liquidity
3
+ * Unified liquidity calculations and position operations.
4
+ */
5
+
6
+ import type { Address, PublicClient, Hex } from "viem";
7
+ import { encodeFunctionData } from "viem";
8
+ import { Q64, MAX_UINT104, MAX_INT104, MAX_INT128, LIQUIDITY_ADDER_ABI } from "../constants.js";
9
+ import { MetricOmmPoolAbi } from "../abis/MetricOmmPool.js";
10
+ import type { LiquidityDelta, LiquidityBinValueInput, PoolLiquidityDelta } from "../types.js";
11
+ import { getPoolImmutables } from "./read.js";
12
+ import {
13
+ getSlot0,
14
+ getBinStatesScaled,
15
+ getPositionBinSharesRange,
16
+ getPositionBinSharesForBins,
17
+ } from "../stateView/read.js";
18
+ import {
19
+ ONE_E18,
20
+ ceilDiv,
21
+ toPriceE18,
22
+ convertToken0ToToken1,
23
+ convertToken1ToToken0,
24
+ scaledPositiveDeltaToExternal,
25
+ } from "../utils/liquidityMath.js";
26
+ export { convertToken0ToToken1, convertToken1ToToken0 };
27
+ export const getPositionShares = getPositionBinSharesRange;
28
+ export const getPositionSharesForBins = getPositionBinSharesForBins;
29
+
30
+ export interface RemoveLiquidityParams {
31
+ percentageToRemove: number;
32
+ bins?: number[];
33
+ lowerBin?: number;
34
+ upperBin?: number;
35
+ }
36
+
37
+ export interface BinRemovalSpec {
38
+ bin: number;
39
+ percentageToRemove?: number;
40
+ sharesToRemove?: bigint;
41
+ }
42
+
43
+ export const NO_SLIPPAGE_LIMIT = MAX_INT128;
44
+
45
+ type ModifyLiquidityParams = {
46
+ salt: bigint;
47
+ deltas: LiquidityDelta[];
48
+ specAmount0: bigint;
49
+ specAmount1: bigint;
50
+ };
51
+
52
+ export type ModifyLiquidityArgs = readonly [bigint, LiquidityDelta[], bigint, bigint];
53
+
54
+ export interface BuildModifyLiquidityArgsBase {
55
+ publicClient: PublicClient;
56
+ /** Factory that deployed the pool (used to read `poolImmutables`). */
57
+ factoryAddress: Address;
58
+ stateViewAddress: Address;
59
+ poolAddress: Address;
60
+ specAmountBufferPercent?: number;
61
+ }
62
+
63
+ export interface BuildModifyLiquidityArgsForAdditionParams extends BuildModifyLiquidityArgsBase {
64
+ bins: LiquidityBinValueInput[];
65
+ currentPrice: number;
66
+ salt?: bigint;
67
+ }
68
+
69
+ export interface BuildModifyLiquidityArgsForUniformAdditionParams extends BuildModifyLiquidityArgsBase {
70
+ amountInTokensPerBin: bigint;
71
+ amountIsInToken0: boolean;
72
+ currentPrice: number;
73
+ lowerBin: number;
74
+ upperBin: number;
75
+ salt?: bigint;
76
+ }
77
+
78
+ export interface BuildModifyLiquidityArgsForUniformAdditionWithTotalTokenAmountParams extends BuildModifyLiquidityArgsBase {
79
+ bins: number[];
80
+ weights?: number[];
81
+ totalValueToAddInToken: bigint;
82
+ totalValueInToken0: boolean;
83
+ currentPriceX64: bigint;
84
+ salt?: bigint;
85
+ }
86
+
87
+ export interface BuildModifyLiquidityArgsForPercentageRemovalParams extends BuildModifyLiquidityArgsBase {
88
+ owner: Address;
89
+ salt: bigint;
90
+ percentageToRemove: number;
91
+ bins?: number[];
92
+ lowerBin?: number;
93
+ upperBin?: number;
94
+ }
95
+
96
+ export interface BuildModifyLiquidityArgsForRemovalParams extends BuildModifyLiquidityArgsBase {
97
+ owner: Address;
98
+ salt: bigint;
99
+ specs: BinRemovalSpec[];
100
+ }
101
+
102
+ /**
103
+ * Prepare uniform liquidity distribution across a bin range.
104
+ */
105
+ export async function buildModifyLiquidityArgsForUniformAddition({
106
+ publicClient,
107
+ factoryAddress,
108
+ stateViewAddress,
109
+ poolAddress,
110
+ amountInTokensPerBin,
111
+ amountIsInToken0,
112
+ currentPrice,
113
+ lowerBin,
114
+ upperBin,
115
+ salt = 0n,
116
+ specAmountBufferPercent = 0,
117
+ }: BuildModifyLiquidityArgsForUniformAdditionParams): Promise<ModifyLiquidityArgs> {
118
+ if (lowerBin > upperBin) {
119
+ throw new Error("lowerBin must be <= upperBin");
120
+ }
121
+
122
+ const bins: LiquidityBinValueInput[] = [];
123
+ for (let bin = lowerBin; bin <= upperBin; bin++) {
124
+ bins.push({
125
+ bin,
126
+ targetValueInToken: amountInTokensPerBin,
127
+ targetValueInToken0: amountIsInToken0,
128
+ });
129
+ }
130
+
131
+ return buildModifyLiquidityArgsForAddition({
132
+ publicClient,
133
+ factoryAddress,
134
+ stateViewAddress,
135
+ poolAddress,
136
+ bins,
137
+ currentPrice,
138
+ salt,
139
+ specAmountBufferPercent,
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Prepare uniform liquidity across arbitrary bins from a TOTAL value budget.
145
+ *
146
+ * Algorithm:
147
+ * 1. Calibrate by assigning weighted 1e18 targets per bin with zero buffer.
148
+ * 2. Compute linear scale so selected specAmount token matches totalValueToAddInToken.
149
+ * 3. Rebuild with scaled per-bin value and caller-provided specAmountBufferPercent.
150
+ */
151
+ export async function buildModifyLiquidityArgsForUniformAdditionWithTotalTokenAmount({
152
+ publicClient,
153
+ factoryAddress,
154
+ stateViewAddress,
155
+ poolAddress,
156
+ bins,
157
+ weights,
158
+ totalValueToAddInToken,
159
+ totalValueInToken0,
160
+ currentPriceX64,
161
+ salt = 0n,
162
+ specAmountBufferPercent = 0,
163
+ }: BuildModifyLiquidityArgsForUniformAdditionWithTotalTokenAmountParams): Promise<ModifyLiquidityArgs> {
164
+ validateSpecAmountBufferPercent(specAmountBufferPercent);
165
+
166
+ if (!Array.isArray(bins) || bins.length === 0) {
167
+ throw new Error("bins must be a non-empty array");
168
+ }
169
+ if (weights != null) {
170
+ if (!Array.isArray(weights) || weights.length !== bins.length) {
171
+ throw new Error("weights must be an array with the same length as bins");
172
+ }
173
+ }
174
+ const uniqueBins = new Set(bins);
175
+ if (uniqueBins.size !== bins.length) {
176
+ throw new Error("bins must not contain duplicates");
177
+ }
178
+ if (totalValueToAddInToken <= 0n) {
179
+ throw new Error("totalValueToAddInToken must be > 0");
180
+ }
181
+ if (currentPriceX64 <= 0n) {
182
+ throw new Error("currentPriceX64 must be > 0");
183
+ }
184
+
185
+ const currentPrice = Number(currentPriceX64.toString()) / Number(Q64.toString());
186
+ if (!Number.isFinite(currentPrice) || currentPrice <= 0) {
187
+ throw new Error("currentPriceX64 is out of supported numeric range");
188
+ }
189
+
190
+ const calibrationValue = totalValueToAddInToken / BigInt(bins.length) + 1n;
191
+ const calibrationTargets: bigint[] = weights
192
+ ? weights.map((w) => (calibrationValue * BigInt(Math.ceil(w * 1e6) + 1)) / 1_000_000n)
193
+ : Array(bins.length).fill(calibrationValue);
194
+
195
+ const calibrationBins: LiquidityBinValueInput[] = bins.map((bin, i) => ({
196
+ bin,
197
+ targetValueInToken: calibrationTargets[i],
198
+ targetValueInToken0: totalValueInToken0,
199
+ }));
200
+
201
+ const [, , calibrationSpecAmount0, calibrationSpecAmount1] =
202
+ await buildModifyLiquidityArgsForAddition({
203
+ publicClient,
204
+ factoryAddress,
205
+ stateViewAddress,
206
+ poolAddress,
207
+ bins: calibrationBins,
208
+ currentPrice,
209
+ salt,
210
+ specAmountBufferPercent: 0,
211
+ });
212
+
213
+ const calibrationSpecAmount = totalValueInToken0
214
+ ? calibrationSpecAmount0
215
+ : calibrationSpecAmount1;
216
+
217
+ if (calibrationSpecAmount <= 0n) {
218
+ throw new Error("failed to calibrate uniform total amount scaling: specAmount is zero");
219
+ }
220
+
221
+ const scaledBins: LiquidityBinValueInput[] = bins.map((bin, i) => ({
222
+ bin,
223
+ targetValueInToken: (calibrationTargets[i] * totalValueToAddInToken) / calibrationSpecAmount,
224
+ targetValueInToken0: totalValueInToken0,
225
+ }));
226
+
227
+ return buildModifyLiquidityArgsForAddition({
228
+ publicClient,
229
+ factoryAddress,
230
+ stateViewAddress,
231
+ poolAddress,
232
+ bins: scaledBins,
233
+ currentPrice,
234
+ salt,
235
+ specAmountBufferPercent,
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Prepare per-bin liquidity distribution from arbitrary per-bin amounts.
241
+ *
242
+ * Each input entry defines a single bin and a value expressed in token0 or token1.
243
+ * The function computes `deltaShares` for every bin and totals `specAmount0/specAmount1`
244
+ * needed to execute `modifyLiquidity` safely.
245
+ *
246
+ * ## Algorithm Overview
247
+ *
248
+ * This is the core distribution engine used by both:
249
+ * - direct per-bin plans (computeModifyLiquidityForAddition)
250
+ * - uniform plans via adapter (computeModifyLiquidityForUniformAddition)
251
+ *
252
+ * ## Steps
253
+ *
254
+ * 1. Validate inputs, price, bin range, and reject duplicate bins.
255
+ * 2. Convert each bin target into both token0/token1 amounts using current price.
256
+ * 3. Fetch slot0 and bin states, then classify each bin as below/current/above.
257
+ * 4. Compute shares to add using ceiling division to avoid under-provisioning:
258
+ * - Existing bins: proportional to current scaled balances and total shares.
259
+ * - Empty bins: derived from initial scaled per-share constants.
260
+ * 5. Convert scaled amounts back to external token amounts and accumulate
261
+ * `specAmount0/specAmount1` with overflow checks.
262
+ */
263
+ export async function buildModifyLiquidityArgsForAddition({
264
+ publicClient,
265
+ factoryAddress,
266
+ stateViewAddress,
267
+ poolAddress,
268
+ bins,
269
+ currentPrice,
270
+ salt = 0n,
271
+ specAmountBufferPercent = 0,
272
+ }: BuildModifyLiquidityArgsForAdditionParams): Promise<ModifyLiquidityArgs> {
273
+ validateSpecAmountBufferPercent(specAmountBufferPercent);
274
+
275
+ if (!Array.isArray(bins)) {
276
+ throw new Error("bins must be an array");
277
+ }
278
+ if (bins.length === 0) {
279
+ return [salt, [], 0n, 0n];
280
+ }
281
+
282
+ const immutables = await getPoolImmutables(publicClient, factoryAddress, poolAddress);
283
+ const priceE18 = toPriceE18(currentPrice);
284
+
285
+ const targetsByBin = new Map<
286
+ number,
287
+ {
288
+ targetAmount0: bigint;
289
+ targetAmount1: bigint;
290
+ targetValueInToken1: bigint;
291
+ }
292
+ >();
293
+
294
+ for (const entry of bins) {
295
+ if (!Number.isInteger(entry.bin)) {
296
+ throw new Error(`bin must be an integer, got ${entry.bin}`);
297
+ }
298
+ if (entry.bin < immutables.lowestBin || entry.bin > immutables.highestBin) {
299
+ throw new Error(
300
+ `bin ${entry.bin} is out of range [${immutables.lowestBin}, ${immutables.highestBin}]`,
301
+ );
302
+ }
303
+ if (entry.targetValueInToken <= 0n) {
304
+ throw new Error(`amountInToken for bin ${entry.bin} must be > 0`);
305
+ }
306
+
307
+ const amount0 = entry.targetValueInToken0
308
+ ? entry.targetValueInToken
309
+ : convertToken1ToToken0(
310
+ entry.targetValueInToken,
311
+ immutables.token0ScaleMultiplier,
312
+ immutables.token1ScaleMultiplier,
313
+ priceE18,
314
+ );
315
+ const amount1 = entry.targetValueInToken0
316
+ ? convertToken0ToToken1(
317
+ entry.targetValueInToken,
318
+ immutables.token0ScaleMultiplier,
319
+ immutables.token1ScaleMultiplier,
320
+ priceE18,
321
+ )
322
+ : entry.targetValueInToken;
323
+ const targetValueInToken1 = entry.targetValueInToken0
324
+ ? convertToken0ToToken1(
325
+ entry.targetValueInToken,
326
+ immutables.token0ScaleMultiplier,
327
+ immutables.token1ScaleMultiplier,
328
+ priceE18,
329
+ )
330
+ : entry.targetValueInToken;
331
+
332
+ if (targetsByBin.has(entry.bin)) {
333
+ throw new Error(`duplicate bin ${entry.bin} is not supported`);
334
+ }
335
+
336
+ targetsByBin.set(entry.bin, {
337
+ targetAmount0: amount0,
338
+ targetAmount1: amount1,
339
+ targetValueInToken1,
340
+ });
341
+ }
342
+
343
+ const binIndices = Array.from(targetsByBin.keys()).sort((a, b) => a - b);
344
+ const [slot0, binStates] = await Promise.all([
345
+ getSlot0(publicClient, stateViewAddress, poolAddress),
346
+ getBinStatesScaled(publicClient, stateViewAddress, poolAddress, binIndices),
347
+ ]);
348
+
349
+ const deltas: LiquidityDelta[] = [];
350
+ let specAmount0 = 0n;
351
+ let specAmount1 = 0n;
352
+
353
+ const curBinIdx = slot0.curBinIdx;
354
+ const curPosInBin = slot0.curPosInBin;
355
+
356
+ for (let i = 0; i < binIndices.length; i++) {
357
+ const binIdx = binIndices[i];
358
+ const target = targetsByBin.get(binIdx);
359
+ if (!target) {
360
+ throw new Error(`internal error: missing target for bin ${binIdx}`);
361
+ }
362
+
363
+ const targetAmount0Scaled = target.targetAmount0 * immutables.token0ScaleMultiplier;
364
+ const targetAmount1Scaled = target.targetAmount1 * immutables.token1ScaleMultiplier;
365
+
366
+ const targetValueInToken1Scaled = target.targetValueInToken1 * immutables.token1ScaleMultiplier;
367
+
368
+ const token0BalanceScaled = binStates.token0BalancesScaled[i];
369
+ const token1BalanceScaled = binStates.token1BalancesScaled[i];
370
+ const totalShares = binStates.totalShares[i];
371
+
372
+ const isBelowBin = binIdx < curBinIdx || (binIdx === curBinIdx && curPosInBin === MAX_UINT104);
373
+ const isAboveBin = binIdx > curBinIdx || (binIdx === curBinIdx && curPosInBin === 0n);
374
+
375
+ let sharesToAdd: bigint;
376
+
377
+ if (isBelowBin) {
378
+ if (targetAmount1Scaled <= 0n) {
379
+ throw new Error(
380
+ `amount for bin ${binIdx} is too small after conversion and results in 0 token1 target`,
381
+ );
382
+ }
383
+ if (totalShares > 0n) {
384
+ if (token1BalanceScaled <= 0n) {
385
+ throw new Error(`bin ${binIdx} has zero token1 balance with non-zero shares`);
386
+ }
387
+ sharesToAdd = ceilDiv(targetAmount1Scaled * totalShares, token1BalanceScaled);
388
+ } else {
389
+ sharesToAdd = ceilDiv(
390
+ targetAmount1Scaled * ONE_E18,
391
+ immutables.initialScaledToken1PerShareE18,
392
+ );
393
+ }
394
+ } else if (isAboveBin) {
395
+ if (targetAmount0Scaled <= 0n) {
396
+ throw new Error(
397
+ `amount for bin ${binIdx} is too small after conversion and results in 0 token0 target`,
398
+ );
399
+ }
400
+ if (totalShares > 0n) {
401
+ if (token0BalanceScaled <= 0n) {
402
+ throw new Error(`bin ${binIdx} has zero token0 balance with non-zero shares`);
403
+ }
404
+ sharesToAdd = ceilDiv(targetAmount0Scaled * totalShares, token0BalanceScaled);
405
+ } else {
406
+ sharesToAdd = ceilDiv(
407
+ targetAmount0Scaled * ONE_E18,
408
+ immutables.initialScaledToken0PerShareE18,
409
+ );
410
+ }
411
+ } else {
412
+ if (totalShares > 0n) {
413
+ const totalValueInToken1Scaled =
414
+ token1BalanceScaled + convertToken0ToToken1(token0BalanceScaled, 1n, 1n, priceE18);
415
+
416
+ sharesToAdd = (targetValueInToken1Scaled * totalShares) / totalValueInToken1Scaled;
417
+ } else {
418
+ const token0Portion = MAX_UINT104 - curPosInBin;
419
+ const token1Portion = curPosInBin;
420
+
421
+ sharesToAdd =
422
+ (targetValueInToken1Scaled * ONE_E18) /
423
+ ((immutables.initialScaledToken1PerShareE18 * token1Portion +
424
+ (immutables.initialScaledToken0PerShareE18 * token0Portion * priceE18) / ONE_E18) /
425
+ MAX_UINT104);
426
+ }
427
+ }
428
+
429
+ if (sharesToAdd === 0n) {
430
+ sharesToAdd = 1n;
431
+ }
432
+
433
+ if (sharesToAdd > MAX_INT104) {
434
+ throw new Error(`sharesToAdd (${sharesToAdd}) for bin ${binIdx} exceeds int104 max`);
435
+ }
436
+
437
+ const [amount0ScaledToAdd, amount1ScaledToAdd] =
438
+ totalShares > 0n
439
+ ? [
440
+ ceilDiv(sharesToAdd * token0BalanceScaled, totalShares),
441
+ ceilDiv(sharesToAdd * token1BalanceScaled, totalShares),
442
+ ]
443
+ : isBelowBin
444
+ ? [0n, ceilDiv(sharesToAdd * immutables.initialScaledToken1PerShareE18, ONE_E18)]
445
+ : isAboveBin
446
+ ? [ceilDiv(sharesToAdd * immutables.initialScaledToken0PerShareE18, ONE_E18), 0n]
447
+ : (() => {
448
+ const token0Portion = MAX_UINT104 - curPosInBin;
449
+ const token1Portion = curPosInBin;
450
+ return [
451
+ ceilDiv(
452
+ sharesToAdd * immutables.initialScaledToken0PerShareE18 * token0Portion,
453
+ ONE_E18 * MAX_UINT104,
454
+ ),
455
+ ceilDiv(
456
+ sharesToAdd * immutables.initialScaledToken1PerShareE18 * token1Portion,
457
+ ONE_E18 * MAX_UINT104,
458
+ ),
459
+ ] as const;
460
+ })();
461
+
462
+ const amount0External = scaledPositiveDeltaToExternal(
463
+ amount0ScaledToAdd,
464
+ immutables.token0ScaleMultiplier,
465
+ );
466
+ const amount1External = scaledPositiveDeltaToExternal(
467
+ amount1ScaledToAdd,
468
+ immutables.token1ScaleMultiplier,
469
+ );
470
+
471
+ specAmount0 += amount0External;
472
+ specAmount1 += amount1External;
473
+
474
+ if (specAmount0 > MAX_INT128 || specAmount1 > MAX_INT128) {
475
+ throw new Error("specAmount exceeds int128 max");
476
+ }
477
+
478
+ deltas.push({
479
+ bin: binIdx,
480
+ deltaShares: sharesToAdd,
481
+ });
482
+ }
483
+
484
+ const computed: ModifyLiquidityParams = {
485
+ salt,
486
+ deltas,
487
+ specAmount0: applySpecAmountBuffer(specAmount0, specAmountBufferPercent),
488
+ specAmount1: applySpecAmountBuffer(specAmount1, specAmountBufferPercent),
489
+ };
490
+
491
+ return toModifyLiquidityArgs(computed);
492
+ }
493
+
494
+ export async function buildModifyLiquidityArgsForRemoval(
495
+ params: BuildModifyLiquidityArgsForRemovalParams,
496
+ ): Promise<ModifyLiquidityArgs> {
497
+ const {
498
+ publicClient,
499
+ stateViewAddress,
500
+ poolAddress,
501
+ owner,
502
+ salt,
503
+ specs,
504
+ specAmountBufferPercent = 0,
505
+ } = params;
506
+ validateSpecAmountBufferPercent(specAmountBufferPercent);
507
+
508
+ if (specs.length === 0) {
509
+ return [salt, [], NO_SLIPPAGE_LIMIT, NO_SLIPPAGE_LIMIT];
510
+ }
511
+
512
+ for (const spec of specs) {
513
+ if (spec.percentageToRemove != null && spec.sharesToRemove != null) {
514
+ throw new Error(
515
+ `Bin ${spec.bin}: specify either percentageToRemove or sharesToRemove, not both`,
516
+ );
517
+ }
518
+ if (spec.percentageToRemove == null && spec.sharesToRemove == null) {
519
+ throw new Error(`Bin ${spec.bin}: must specify either percentageToRemove or sharesToRemove`);
520
+ }
521
+ if (
522
+ spec.percentageToRemove != null &&
523
+ (spec.percentageToRemove <= 0 || spec.percentageToRemove > 100)
524
+ ) {
525
+ throw new Error(`Bin ${spec.bin}: percentageToRemove must be between 0 and 100`);
526
+ }
527
+ }
528
+
529
+ const binIndices = specs.map((s) => s.bin);
530
+ const positionShares = await getPositionBinSharesForBins(
531
+ publicClient,
532
+ stateViewAddress,
533
+ poolAddress,
534
+ owner,
535
+ salt,
536
+ binIndices,
537
+ );
538
+
539
+ const sharesMap = new Map(positionShares.map((p) => [p.binIdx, p.shares]));
540
+ const deltas: LiquidityDelta[] = [];
541
+
542
+ for (const spec of specs) {
543
+ const currentShares = sharesMap.get(spec.bin) ?? 0n;
544
+ if (currentShares === 0n) continue;
545
+
546
+ let sharesToRemove: bigint;
547
+
548
+ if (spec.sharesToRemove != null) {
549
+ sharesToRemove = spec.sharesToRemove > currentShares ? currentShares : spec.sharesToRemove;
550
+ } else {
551
+ sharesToRemove =
552
+ spec.percentageToRemove === 100
553
+ ? currentShares
554
+ : (currentShares * BigInt(Math.floor(spec.percentageToRemove! * 100))) / 10000n;
555
+ }
556
+
557
+ if (sharesToRemove > 0n) {
558
+ deltas.push({
559
+ bin: spec.bin,
560
+ deltaShares: -sharesToRemove,
561
+ });
562
+ }
563
+ }
564
+
565
+ const computed: ModifyLiquidityParams = {
566
+ salt,
567
+ deltas,
568
+ specAmount0: applySpecAmountBuffer(NO_SLIPPAGE_LIMIT, specAmountBufferPercent),
569
+ specAmount1: applySpecAmountBuffer(NO_SLIPPAGE_LIMIT, specAmountBufferPercent),
570
+ };
571
+
572
+ return toModifyLiquidityArgs(computed);
573
+ }
574
+
575
+ export async function buildModifyLiquidityArgsForPercentageRemoval({
576
+ publicClient,
577
+ factoryAddress,
578
+ stateViewAddress,
579
+ poolAddress,
580
+ owner,
581
+ salt,
582
+ percentageToRemove,
583
+ bins,
584
+ lowerBin = -10,
585
+ upperBin = 10,
586
+ specAmountBufferPercent = 0,
587
+ }: BuildModifyLiquidityArgsForPercentageRemovalParams): Promise<ModifyLiquidityArgs> {
588
+ validateSpecAmountBufferPercent(specAmountBufferPercent);
589
+
590
+ if (percentageToRemove <= 0 || percentageToRemove > 100) {
591
+ throw new Error("percentageToRemove must be between 0 and 100");
592
+ }
593
+
594
+ const binsWithShares = bins
595
+ ? bins
596
+ : (
597
+ await getPositionBinSharesRange(
598
+ publicClient,
599
+ stateViewAddress,
600
+ poolAddress,
601
+ owner,
602
+ salt,
603
+ lowerBin,
604
+ upperBin,
605
+ )
606
+ ).map((p) => p.binIdx);
607
+
608
+ if (binsWithShares.length === 0) {
609
+ return [salt, [], NO_SLIPPAGE_LIMIT, NO_SLIPPAGE_LIMIT];
610
+ }
611
+
612
+ const specs: BinRemovalSpec[] = binsWithShares.map((bin) => ({
613
+ bin,
614
+ percentageToRemove,
615
+ }));
616
+
617
+ return buildModifyLiquidityArgsForRemoval({
618
+ publicClient,
619
+ factoryAddress,
620
+ stateViewAddress,
621
+ poolAddress,
622
+ owner,
623
+ salt,
624
+ specs,
625
+ specAmountBufferPercent,
626
+ });
627
+ }
628
+
629
+ export function additionPlanToPoolLiquidityDelta(
630
+ deltas: readonly LiquidityDelta[],
631
+ ): PoolLiquidityDelta {
632
+ const pos = deltas.filter((d) => d.deltaShares > 0n).sort((a, b) => a.bin - b.bin);
633
+ return {
634
+ binIdxs: pos.map((d) => BigInt(d.bin)),
635
+ shares: pos.map((d) => d.deltaShares),
636
+ };
637
+ }
638
+
639
+ export function removalPlanToPoolLiquidityDelta(
640
+ deltas: readonly LiquidityDelta[],
641
+ ): PoolLiquidityDelta {
642
+ const burn = deltas.filter((d) => d.deltaShares < 0n).sort((a, b) => a.bin - b.bin);
643
+ return {
644
+ binIdxs: burn.map((d) => BigInt(d.bin)),
645
+ shares: burn.map((d) => -d.deltaShares),
646
+ };
647
+ }
648
+
649
+ const WAD = 10n ** 18n;
650
+
651
+ /**
652
+ * Matches `MetricOmmPoolLiquidityAdder._scaleWeightsToShares`: given probe token needs from a weighted
653
+ * probe with the same weight vector, scale integer shares so neither token exceeds the caller caps.
654
+ */
655
+ export function scaleWeightedPoolDeltasToFitCaps(params: {
656
+ weights: PoolLiquidityDelta;
657
+ probeNeed0: bigint;
658
+ probeNeed1: bigint;
659
+ maxAmountToken0: bigint;
660
+ maxAmountToken1: bigint;
661
+ }): PoolLiquidityDelta {
662
+ const { weights, probeNeed0, probeNeed1, maxAmountToken0, maxAmountToken1 } = params;
663
+ const maxU = 2n ** 256n - 2n;
664
+ const scaleWad0 = probeNeed0 === 0n ? maxU : (maxAmountToken0 * WAD) / probeNeed0;
665
+ const scaleWad1 = probeNeed1 === 0n ? maxU : (maxAmountToken1 * WAD) / probeNeed1;
666
+ const scaleWad = scaleWad0 < scaleWad1 ? scaleWad0 : scaleWad1;
667
+ const shares = weights.shares.map((w, i) => {
668
+ const scaled = (w * scaleWad) / WAD;
669
+ if (w !== 0n && scaled === 0n) {
670
+ throw new Error(`shares rounded to zero at weight index ${i}`);
671
+ }
672
+ return scaled;
673
+ });
674
+ return { binIdxs: weights.binIdxs, shares };
675
+ }
676
+
677
+ export async function buildAddLiquidityExactSharesParamsFromBinTargets(
678
+ params: BuildModifyLiquidityArgsForAdditionParams,
679
+ ): Promise<{
680
+ salt: bigint;
681
+ poolLiquidityDelta: PoolLiquidityDelta;
682
+ maxAmountToken0: bigint;
683
+ maxAmountToken1: bigint;
684
+ }> {
685
+ const [salt, deltas, max0, max1] = await buildModifyLiquidityArgsForAddition(params);
686
+ return {
687
+ salt,
688
+ poolLiquidityDelta: additionPlanToPoolLiquidityDelta(deltas),
689
+ maxAmountToken0: max0,
690
+ maxAmountToken1: max1,
691
+ };
692
+ }
693
+
694
+ export function encodeAddLiquidityExactSharesSelfCalldata(params: {
695
+ pool: Address;
696
+ salt: bigint;
697
+ deltas: PoolLiquidityDelta;
698
+ maxAmountToken0: bigint;
699
+ maxAmountToken1: bigint;
700
+ }): Hex {
701
+ return encodeFunctionData({
702
+ abi: LIQUIDITY_ADDER_ABI,
703
+ functionName: "addLiquidityExactShares",
704
+ args: [
705
+ params.pool,
706
+ params.salt,
707
+ { binIdxs: [...params.deltas.binIdxs], shares: [...params.deltas.shares] },
708
+ params.maxAmountToken0,
709
+ params.maxAmountToken1,
710
+ ],
711
+ });
712
+ }
713
+
714
+ export function encodeAddLiquidityExactSharesWithOwnerCalldata(params: {
715
+ pool: Address;
716
+ owner: Address;
717
+ salt: bigint;
718
+ deltas: PoolLiquidityDelta;
719
+ maxAmountToken0: bigint;
720
+ maxAmountToken1: bigint;
721
+ }): Hex {
722
+ return encodeFunctionData({
723
+ abi: LIQUIDITY_ADDER_ABI,
724
+ functionName: "addLiquidityExactShares",
725
+ args: [
726
+ params.pool,
727
+ params.owner,
728
+ params.salt,
729
+ { binIdxs: [...params.deltas.binIdxs], shares: [...params.deltas.shares] },
730
+ params.maxAmountToken0,
731
+ params.maxAmountToken1,
732
+ ],
733
+ });
734
+ }
735
+
736
+ export function encodeAddLiquidityWeightedSelfCalldata(params: {
737
+ pool: Address;
738
+ salt: bigint;
739
+ weightDeltas: PoolLiquidityDelta;
740
+ maxAmountToken0: bigint;
741
+ maxAmountToken1: bigint;
742
+ }): Hex {
743
+ return encodeFunctionData({
744
+ abi: LIQUIDITY_ADDER_ABI,
745
+ functionName: "addLiquidityWeighted",
746
+ args: [
747
+ params.pool,
748
+ params.salt,
749
+ { binIdxs: [...params.weightDeltas.binIdxs], shares: [...params.weightDeltas.shares] },
750
+ params.maxAmountToken0,
751
+ params.maxAmountToken1,
752
+ ],
753
+ });
754
+ }
755
+
756
+ export function encodeAddLiquidityWeightedWithOwnerCalldata(params: {
757
+ pool: Address;
758
+ owner: Address;
759
+ salt: bigint;
760
+ weightDeltas: PoolLiquidityDelta;
761
+ maxAmountToken0: bigint;
762
+ maxAmountToken1: bigint;
763
+ }): Hex {
764
+ return encodeFunctionData({
765
+ abi: LIQUIDITY_ADDER_ABI,
766
+ functionName: "addLiquidityWeighted",
767
+ args: [
768
+ params.pool,
769
+ params.owner,
770
+ params.salt,
771
+ { binIdxs: [...params.weightDeltas.binIdxs], shares: [...params.weightDeltas.shares] },
772
+ params.maxAmountToken0,
773
+ params.maxAmountToken1,
774
+ ],
775
+ });
776
+ }
777
+
778
+ export function encodeRemoveLiquidityCalldata(params: {
779
+ owner: Address;
780
+ salt: bigint;
781
+ deltas: PoolLiquidityDelta;
782
+ }): Hex {
783
+ return encodeFunctionData({
784
+ abi: MetricOmmPoolAbi,
785
+ functionName: "removeLiquidity",
786
+ args: [
787
+ params.owner,
788
+ params.salt,
789
+ { binIdxs: [...params.deltas.binIdxs], shares: [...params.deltas.shares] },
790
+ ],
791
+ });
792
+ }
793
+
794
+ export async function buildRemoveLiquidityPoolDelta(
795
+ params: BuildModifyLiquidityArgsForRemovalParams,
796
+ ): Promise<{ salt: bigint; poolLiquidityDelta: PoolLiquidityDelta }> {
797
+ const [salt, deltas] = await buildModifyLiquidityArgsForRemoval(params);
798
+ return { salt, poolLiquidityDelta: removalPlanToPoolLiquidityDelta(deltas) };
799
+ }
800
+
801
+ function validateSpecAmountBufferPercent(specAmountBufferPercent: number): void {
802
+ if (!Number.isFinite(specAmountBufferPercent) || specAmountBufferPercent < 0) {
803
+ throw new Error("specAmountBufferPercent must be a non-negative percent number");
804
+ }
805
+ }
806
+
807
+ function applySpecAmountBuffer(specAmount: bigint, specAmountBufferPercent: number): bigint {
808
+ if (specAmountBufferPercent === 0 || specAmount === NO_SLIPPAGE_LIMIT) {
809
+ return specAmount;
810
+ }
811
+
812
+ const bufferRatio = specAmountBufferPercent / 100;
813
+
814
+ if (specAmount > 0n) {
815
+ const multiplier = BigInt(Math.floor((1 + bufferRatio) * 1e18));
816
+ return (specAmount * multiplier) / BigInt(1e18);
817
+ }
818
+
819
+ if (specAmount < 0n) {
820
+ const multiplier = BigInt(Math.floor((1 - bufferRatio) * 1e18));
821
+ if (multiplier <= 0n) {
822
+ return 0n;
823
+ }
824
+ return (specAmount * multiplier) / BigInt(1e18);
825
+ }
826
+
827
+ return specAmount;
828
+ }
829
+
830
+ function toModifyLiquidityArgs(
831
+ preparedModifyLiquidity: ModifyLiquidityParams,
832
+ ): ModifyLiquidityArgs {
833
+ return [
834
+ preparedModifyLiquidity.salt,
835
+ preparedModifyLiquidity.deltas,
836
+ preparedModifyLiquidity.specAmount0,
837
+ preparedModifyLiquidity.specAmount1,
838
+ ];
839
+ }