@logswap/sdk 0.5.35

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 (53) hide show
  1. package/README.md +130 -0
  2. package/dist/actions.d.ts +136 -0
  3. package/dist/actions.js +140 -0
  4. package/dist/admin.d.ts +11 -0
  5. package/dist/admin.js +29 -0
  6. package/dist/claims.d.ts +53 -0
  7. package/dist/claims.js +98 -0
  8. package/dist/client.d.ts +68 -0
  9. package/dist/client.js +68 -0
  10. package/dist/fpool.d.ts +416 -0
  11. package/dist/fpool.js +653 -0
  12. package/dist/generated.d.ts +6522 -0
  13. package/dist/generated.js +3755 -0
  14. package/dist/ids.d.ts +42 -0
  15. package/dist/ids.js +72 -0
  16. package/dist/index.d.ts +23 -0
  17. package/dist/index.js +23 -0
  18. package/dist/indexer.d.ts +91 -0
  19. package/dist/indexer.js +197 -0
  20. package/dist/keys.d.ts +40 -0
  21. package/dist/keys.js +50 -0
  22. package/dist/liquidity.d.ts +223 -0
  23. package/dist/liquidity.js +305 -0
  24. package/dist/onboard.d.ts +56 -0
  25. package/dist/onboard.js +188 -0
  26. package/dist/pools.d.ts +182 -0
  27. package/dist/pools.js +322 -0
  28. package/dist/positions.d.ts +164 -0
  29. package/dist/positions.js +197 -0
  30. package/dist/swap.d.ts +68 -0
  31. package/dist/swap.js +91 -0
  32. package/dist/tokencache.d.ts +76 -0
  33. package/dist/tokencache.js +150 -0
  34. package/dist/write.d.ts +86 -0
  35. package/dist/write.js +106 -0
  36. package/package.json +48 -0
  37. package/src/actions.ts +207 -0
  38. package/src/admin.ts +33 -0
  39. package/src/claims.ts +132 -0
  40. package/src/client.ts +104 -0
  41. package/src/fpool.ts +935 -0
  42. package/src/generated.ts +3762 -0
  43. package/src/ids.ts +78 -0
  44. package/src/index.ts +23 -0
  45. package/src/indexer.ts +252 -0
  46. package/src/keys.ts +73 -0
  47. package/src/liquidity.ts +430 -0
  48. package/src/onboard.ts +228 -0
  49. package/src/pools.ts +396 -0
  50. package/src/positions.ts +276 -0
  51. package/src/swap.ts +140 -0
  52. package/src/tokencache.ts +169 -0
  53. package/src/write.ts +134 -0
package/src/pools.ts ADDED
@@ -0,0 +1,396 @@
1
+ /**
2
+ * pools.ts — market state and discovery.
3
+ *
4
+ * Two things worth knowing before reading further.
5
+ *
6
+ * **Markets are discovered from logs, not configuration.** `Initialize` carries the ENTIRE
7
+ * `PoolKey` in the clear, so a client can reconstruct any market's key — and therefore call any
8
+ * keyed function — having never been told the market exists. There is no registry to maintain.
9
+ *
10
+ * **`x` is the state, price is derived.** The contract stores $x = \log p$ and materialises the
11
+ * price only where unavoidable; `priceOf` below mirrors that, and is lossy in the way any float
12
+ * conversion is. Prefer comparing in log space when precision matters.
13
+ */
14
+
15
+ import { formatUnits, type Address, type Hex } from "viem";
16
+ import { logswapLensAbi, cPoolManagerAbi } from "./generated.js";
17
+ import type { LogswapClient } from "./client.js";
18
+ import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
19
+ import { poolId, type PoolKey } from "./keys.js";
20
+
21
+ /** The manager's `getPool` view, decoded. Field-for-field with `PoolView` in the contract. */
22
+ export interface PoolState {
23
+ initialized: boolean;
24
+ backstopSeeded: boolean;
25
+ /** $x = \log p$, WAD — the stored state. */
26
+ x: bigint;
27
+ /** Exposure currently earning. Steps only when a floor or cap is crossed. */
28
+ lActive: bigint;
29
+ reserveBase: bigint;
30
+ reserveQuote: bigint;
31
+ /** Fee-per-unit-L accumulator: $\phi \cdot \mathrm{TV}(\log p)$. */
32
+ F: bigint;
33
+ /** Quadratic variation of log-price, $\Sigma = \int (dx)^2$. */
34
+ bigSigma: bigint;
35
+ sigma2Ema: bigint;
36
+ currentK: bigint;
37
+ protocolOwed: bigint;
38
+ minBackstopL: bigint;
39
+ backstopFloor: bigint;
40
+ }
41
+
42
+ /** Read a market's full state by key. */
43
+ export async function getPool(c: LogswapClient, key: PoolKey): Promise<PoolState> {
44
+ const v = await c.public.readContract({
45
+ address: c.addresses.cPoolManager,
46
+ abi: cPoolManagerAbi,
47
+ functionName: "getPool",
48
+ args: [poolId(key)],
49
+ });
50
+ return v as unknown as PoolState;
51
+ }
52
+
53
+ /** The effective fee right now: $\max(\phi_{\min}, \kappa\sqrt{\sigma^2_{\text{ema}}})$, capped. */
54
+ export async function phiEff(c: LogswapClient, key: PoolKey): Promise<bigint> {
55
+ return c.public.readContract({
56
+ address: c.addresses.cPoolManager,
57
+ abi: cPoolManagerAbi,
58
+ functionName: "phiEff",
59
+ args: [key],
60
+ }) as Promise<bigint>;
61
+ }
62
+
63
+ /**
64
+ * Price from the stored log-price. **Lossy** — `x` is exact and this is a float, so use it for
65
+ * display and never for arithmetic you intend to send back on-chain.
66
+ */
67
+ export function priceOf(state: Pick<PoolState, "x">, scale = 1): number {
68
+ return Math.exp(Number(state.x) / 1e18) * scale;
69
+ }
70
+
71
+ /**
72
+ * The factor between the chain's price and the human one. The chain's log-price is of the WEI
73
+ * ratio, so with a 6-dec USDC quote and an 18-dec base the human price is `e^x · 10^12`. Pass
74
+ * this as `scale` to {@link priceOf}, {@link floorPrice} and `fPoolPriceOf` — with 18/18 tokens
75
+ * it is 1 and nothing changes.
76
+ */
77
+ export function priceScale(baseDecimals: number, quoteDecimals: number): number {
78
+ return 10 ** (baseDecimals - quoteDecimals);
79
+ }
80
+
81
+ /**
82
+ * The model-free LP edge, $F - \tfrac12\Sigma$ — the protocol's headline metric, in quote per unit
83
+ * of exposure.
84
+ *
85
+ * $F$ is fee income per unit active $L$; $\Sigma$ is the realised quadratic variation, whose half
86
+ * is the LVR a log pool pays. The difference is what an LP actually earns, with no model of the
87
+ * price process assumed. Both ride on every `Swap` event, so this is reconstructible from logs
88
+ * alone (docs/app.md) — no state read needed if you are already indexing.
89
+ *
90
+ * Signed: negative means variation has outrun fee income over the pool's life.
91
+ */
92
+ export function lpEdge(state: Pick<PoolState, "F" | "bigSigma">): bigint {
93
+ return state.F - state.bigSigma / 2n;
94
+ }
95
+
96
+ /** A market discovered from an `Initialize` log — the full key, plus where it was created. */
97
+ export interface DiscoveredMarket {
98
+ key: PoolKey;
99
+ poolId: Hex;
100
+ /** The market's opening log-price. */
101
+ x0: bigint;
102
+ blockNumber: bigint;
103
+ transactionHash: Hex;
104
+ }
105
+
106
+ /**
107
+ * Enumerate markets from `Initialize` logs.
108
+ *
109
+ * This is the whole market registry: the event carries every key field rather than just the id,
110
+ * deliberately, so nothing downstream needs a configured list. Pass `fromBlock` as the manager's
111
+ * deployment block — scanning from genesis is slow and pointless.
112
+ */
113
+ export async function discoverMarkets(
114
+ c: LogswapClient,
115
+ opts: { fromBlock?: bigint; toBlock?: bigint; base?: Address; quote?: Address } = {},
116
+ ): Promise<DiscoveredMarket[]> {
117
+ // Narrow the const ABI to the Initialize entry while KEEPING its literal type, so viem can infer
118
+ // the log shape. `as never` would compile but collapse every arg to `never` at the call site.
119
+ type Ev = Extract<(typeof cPoolManagerAbi)[number], { type: "event"; name: "Initialize" }>;
120
+ const ev = cPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Initialize");
121
+ if (!ev) throw new Error("logswap: Initialize event missing from the generated ABI");
122
+
123
+ const logs = await c.public.getLogs({
124
+ address: c.addresses.cPoolManager,
125
+ event: ev,
126
+ args: { base: opts.base, quote: opts.quote },
127
+ fromBlock: opts.fromBlock ?? 0n,
128
+ toBlock: opts.toBlock ?? "latest",
129
+ });
130
+
131
+ return logs.map((l) => {
132
+ const a = l.args;
133
+ const key: PoolKey = {
134
+ base: a.base!,
135
+ quote: a.quote!,
136
+ tickSpacing: a.tickSpacing!,
137
+ phiMin: a.phiMin!,
138
+ kappa: a.kappa!,
139
+ alpha: a.alpha!,
140
+ };
141
+ return {
142
+ key,
143
+ poolId: poolId(key),
144
+ x0: a.x0!,
145
+ blockNumber: l.blockNumber!,
146
+ transactionHash: l.transactionHash!,
147
+ };
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Ladder shape: the live FLOORS, in log-price WAD — **not** tick indices.
153
+ *
154
+ * Named `liveFloors` deliberately. The lens function it wraps is called `liveTicks`, which reads as
155
+ * if it returns `k`; it returns `k * tickSpacing`. Multiplying by the spacing again yields an
156
+ * off-grid value and an `OffGrid` revert at the next call, which is a confusing place to discover
157
+ * the mistake.
158
+ */
159
+ export async function liveFloors(c: LogswapClient, key: PoolKey): Promise<readonly bigint[]> {
160
+ return c.public.readContract({
161
+ address: c.addresses.lens,
162
+ abi: logswapLensAbi,
163
+ functionName: "liveTicks",
164
+ args: [key],
165
+ }) as Promise<readonly bigint[]>;
166
+ }
167
+
168
+ /** Is this floor on the market's grid, and inside `MAX_ABS_FLOOR`? Pure — no RPC on the manager. */
169
+ export async function isValidFloor(c: LogswapClient, key: PoolKey, floor: bigint): Promise<boolean> {
170
+ return c.public.readContract({
171
+ address: c.addresses.lens,
172
+ abi: logswapLensAbi,
173
+ functionName: "isValidFloor",
174
+ args: [key, floor],
175
+ }) as Promise<boolean>;
176
+ }
177
+
178
+ /**
179
+ * A market's token decimals, read from the tokens themselves.
180
+ *
181
+ * **Never assume these.** A market's two tokens routinely differ (USDC 6 against WETH 18), the
182
+ * manager itself probes `decimals()` to scale `minBackstopL`, and a token that does not implement
183
+ * it falls back to 18. Hardcoding a guess makes every parse and every display wrong by orders of
184
+ * magnitude while the contract behaves perfectly — which reads as "the app is broken".
185
+ */
186
+ export async function marketDecimals(
187
+ c: LogswapClient,
188
+ key: PoolKey,
189
+ ): Promise<{ base: number; quote: number }> {
190
+ const abi = [
191
+ { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
192
+ ] as const;
193
+ const read = async (token: Address): Promise<number> => {
194
+ try {
195
+ return Number(await c.public.readContract({ address: token, abi, functionName: "decimals" }));
196
+ } catch {
197
+ return 18; // same fallback the manager uses when the token does not implement it
198
+ }
199
+ };
200
+ const [base, quote] = await Promise.all([read(key.base), read(key.quote)]);
201
+ return { base, quote };
202
+ }
203
+
204
+ /** A token as a UI needs it: address, symbol and decimals, all read from the chain. */
205
+ export interface TokenInfo {
206
+ address: Address;
207
+ symbol: string;
208
+ decimals: number;
209
+ }
210
+
211
+ /**
212
+ * Both sides of a market, for labelling and scaling.
213
+ *
214
+ * **Decimals are read from the token and never guessed.** They used to fall back to 18 on any
215
+ * failure, which quietly reintroduced the worst bug this stack has had: an 18 assumed for a
216
+ * 6-decimal token is a 10^12 error in a number that goes on to `parseUnits` and becomes a transfer
217
+ * amount. A wrong scale is indistinguishable from a working app until somebody moves funds, so a
218
+ * read that will not resolve must fail loudly instead.
219
+ *
220
+ * `symbol` keeps a fallback because it is a label: a market shown as `0x5FbD…` is ugly, not wrong.
221
+ *
222
+ * Retries first, because a blip on the transport is not the same as a token that has no `decimals`,
223
+ * and killing a market over one dropped packet is its own kind of wrong.
224
+ *
225
+ * **Cached** per (chain, address) — see `tokencache.ts` for why only successes are stored and why
226
+ * the chain id is part of the key. Concurrent calls for the same token share one read, which
227
+ * matters because the quote asset is usually the same across every market in a deployment.
228
+ */
229
+ export async function marketTokens(
230
+ c: LogswapClient,
231
+ key: PoolKey,
232
+ ): Promise<{ base: TokenInfo; quote: TokenInfo }> {
233
+ const abi = [
234
+ { type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
235
+ { type: "function", name: "symbol", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
236
+ ] as const;
237
+
238
+ const chainId = c.public.chain?.id ?? (await c.public.getChainId());
239
+
240
+ const readDecimals = async (address: Address): Promise<number> => {
241
+ let last: unknown;
242
+ for (let attempt = 0; attempt < 3; attempt++) {
243
+ try {
244
+ const d = Number(await c.public.readContract({ address, abi, functionName: "decimals" }));
245
+ if (!Number.isInteger(d) || d < 0 || d > 36) {
246
+ throw new Error(`returned ${d}, which is not a plausible decimals value`);
247
+ }
248
+ return d;
249
+ } catch (e) {
250
+ last = e;
251
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 150 * (attempt + 1)));
252
+ }
253
+ }
254
+ throw new Error(
255
+ `logswap: could not read decimals() from ${address}. Refusing to assume — every amount for ` +
256
+ `this token would be scaled wrongly. Cause: ${last instanceof Error ? last.message.split("\n")[0] : String(last)}`,
257
+ );
258
+ };
259
+
260
+ const read = async (address: Address): Promise<TokenInfo> => {
261
+ const hit = readCachedToken(chainId, address);
262
+ if (hit) return { address, ...hit };
263
+
264
+ const inflight = pending.get(`${chainId}:${address.toLowerCase()}`);
265
+ if (inflight) return inflight;
266
+
267
+ const k = `${chainId}:${address.toLowerCase()}`;
268
+ // Deregistration happens INSIDE the job, so it completes before the promise settles. Cleaning
269
+ // up in a `.finally()` on the outside runs a microtask later — late enough that a caller
270
+ // arriving right after an `await` could still attach to a job that has already failed and
271
+ // inherit a failure a fresh read would not have hit. Sharing an in-flight read is an
272
+ // optimisation for concurrent callers; it must never make a later, separate call fail.
273
+ // `| undefined` because the assignment below is what tsc cannot see happening first; the
274
+ // finally only runs after an await, so by then it is set.
275
+ let self: Promise<TokenInfo> | undefined;
276
+ const job = (async (): Promise<TokenInfo> => {
277
+ try {
278
+ // `symbol` is allowed to fail; `decimals` is not. Caught separately so a missing symbol
279
+ // does not discard a decimals read that succeeded.
280
+ const [decimals, symbolResult] = await Promise.all([
281
+ readDecimals(address),
282
+ c.public
283
+ .readContract({ address, abi, functionName: "symbol" })
284
+ .then((v) => ({ ok: true as const, value: String(v) }))
285
+ .catch(() => ({ ok: false as const, value: address.slice(0, 6) })),
286
+ ]);
287
+ // Only a real symbol is cached. Persisting the 0x… fallback would let one blip name the
288
+ // token that for as long as the store lives.
289
+ if (symbolResult.ok) writeCachedToken(chainId, address, { symbol: symbolResult.value, decimals });
290
+ return { address, symbol: symbolResult.value, decimals };
291
+ } finally {
292
+ if (pending.get(k) === self) pending.delete(k);
293
+ }
294
+ })();
295
+ self = job;
296
+ pending.set(k, job);
297
+ return job;
298
+ };
299
+ const [base, quote] = await Promise.all([read(key.base), read(key.quote)]);
300
+ return { base, quote };
301
+ }
302
+
303
+ /** In-flight reads, so simultaneous callers for one token share a single round trip. */
304
+ const pending = new Map<string, Promise<TokenInfo>>();
305
+ registerCacheClear(() => pending.clear());
306
+
307
+ /** Format a quote-denominated amount for display, given the quote's decimals. */
308
+ export function formatQuote(amount: bigint, quoteDecimals: number): string {
309
+ return formatUnits(amount, quoteDecimals);
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------------------------
313
+ // Denominated readouts
314
+ //
315
+ // `F` and `lpEdge` are per unit of active L — dimensionless WAD fractions, not amounts. A
316
+ // dashboard wants amounts, and multiplying by L is the whole conversion. Keeping these here rather
317
+ // than in the app means one definition of "fees in quote" instead of one per screen.
318
+ // ---------------------------------------------------------------------------------------------
319
+
320
+ const WAD = 10n ** 18n;
321
+ const YEAR_SECONDS = 31_536_000n;
322
+
323
+ /** Fees the pool has accrued over its life, in QUOTE units: $F \cdot L$. */
324
+ export function feesQuote(state: Pick<PoolState, "F" | "lActive">): bigint {
325
+ return (state.F * state.lActive) / WAD;
326
+ }
327
+
328
+ /** The LP edge in QUOTE units: $(F - \tfrac12\Sigma) \cdot L$. Signed. */
329
+ export function edgeQuote(state: Pick<PoolState, "F" | "bigSigma" | "lActive">): bigint {
330
+ return (lpEdge(state) * state.lActive) / WAD;
331
+ }
332
+
333
+ /** The pool floor as a price. **Lossy** — display only, like {@link priceOf}. */
334
+ export function floorPrice(state: Pick<PoolState, "backstopFloor">, scale = 1): number {
335
+ return Math.exp(Number(state.backstopFloor) / 1e18) * scale;
336
+ }
337
+
338
+ /** $\log(p / p_{\text{floor}}) = x - \xi$ — how far spot sits above the floor, in log units (WAD). */
339
+ export function floorDistance(state: Pick<PoolState, "x" | "backstopFloor">): bigint {
340
+ return state.x - state.backstopFloor;
341
+ }
342
+
343
+ /**
344
+ * $-\log(p/p_{\text{floor}})$ as a fraction: the log-drawdown from spot down to the floor.
345
+ *
346
+ * **Negative** while price is above the floor — it is how far price has to fall, so a UI can print
347
+ * it with its sign and mean it. Zero at the floor.
348
+ *
349
+ * This is the LOG measure, not the arithmetic one: the actual price drawdown is
350
+ * $e^{-\Delta} - 1$, which differs materially once the distance is large (a log distance of 0.7
351
+ * is a 50% price fall, not 70%).
352
+ */
353
+ export function floorDrawdown(state: Pick<PoolState, "x" | "backstopFloor">): number {
354
+ return -Number(floorDistance(state)) / 1e18;
355
+ }
356
+
357
+ /** The arithmetic price drawdown to the floor, $p_{\text{floor}}/p - 1$. Negative above the floor. */
358
+ export function floorDrawdownPrice(state: Pick<PoolState, "x" | "backstopFloor">): number {
359
+ return Math.exp(-Number(floorDistance(state)) / 1e18) - 1;
360
+ }
361
+
362
+ /**
363
+ * Annualise a per-unit-L WAD rate over the window it accrued in.
364
+ *
365
+ * `F` is cumulative fee income per unit of exposure since the pool opened, so dividing by the
366
+ * elapsed time and scaling to a year gives a rate directly — no price process assumed. Returns a
367
+ * fraction: 0.12 is 12%.
368
+ *
369
+ * Treat it as what it is: a *realised* rate over one short devnet window, not a forecast.
370
+ */
371
+ export function annualise(perUnitL: bigint, elapsedSeconds: bigint): number {
372
+ if (elapsedSeconds <= 0n) return 0;
373
+ return (Number(perUnitL) / 1e18) * (Number(YEAR_SECONDS) / Number(elapsedSeconds));
374
+ }
375
+
376
+ /** When a market was created, as a unix timestamp — the denominator for {@link annualise}. */
377
+ export async function marketOpenedAt(c: LogswapClient, m: DiscoveredMarket): Promise<bigint> {
378
+ const b = await c.public.getBlock({ blockNumber: m.blockNumber });
379
+ return b.timestamp;
380
+ }
381
+
382
+ /**
383
+ * Create a C market: `initialize(key, x0)` on the manager — a call, not a deployment. Manager-
384
+ * direct by design: creation moves no funds, so the router adds nothing. The pool id is
385
+ * `poolId(key)` (pure) and the market is tradeable immediately; the first mint funds it.
386
+ */
387
+ export async function initializeMarket(c: LogswapClient, a: { key: PoolKey; x0: bigint; account: Address }) {
388
+ const { request } = await c.public.simulateContract({
389
+ address: c.addresses.cPoolManager,
390
+ abi: cPoolManagerAbi,
391
+ functionName: "initialize",
392
+ args: [a.key, a.x0],
393
+ account: a.account,
394
+ } as never);
395
+ return c.wallet!.writeContract(request as never);
396
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * positions.ts — position and fee reads.
3
+ *
4
+ * A position CLASS is fungible at its full key: uncapped by (pool, floor), capped by
5
+ * (pool, floor, cap). Holders own ERC-6909 shares of a class; fees are tracked per holder, so two
6
+ * accounts holding the same class can have different claimable amounts.
7
+ */
8
+
9
+ import { parseAbiItem } from "viem";
10
+ import type { Address } from "viem";
11
+ import { logswapLensAbi, cPoolManagerAbi } from "./generated.js";
12
+ import type { LogswapClient } from "./client.js";
13
+ import { poolId, type PoolKey } from "./keys.js";
14
+ import { isPosition, NO_CAP, positionId, unpackFloorCap } from "./ids.js";
15
+
16
+ /** A class's aggregate state — the whole class, not one holder's slice. */
17
+ export interface PositionClass {
18
+ /** Total exposure across every holder. */
19
+ L: bigint;
20
+ /** Total shares outstanding. */
21
+ shares: bigint;
22
+ /** Fee-per-share accumulator (masterchef-style). */
23
+ aps: bigint;
24
+ /** Snapshot of `F_above[floor] − F_above[cap]` at the class's last touch. */
25
+ insideLast: bigint;
26
+ }
27
+
28
+ export async function getPositionClass(c: LogswapClient, id: bigint): Promise<PositionClass> {
29
+ const r = (await c.public.readContract({
30
+ address: c.addresses.cPoolManager,
31
+ abi: cPoolManagerAbi,
32
+ functionName: "positions",
33
+ args: [id],
34
+ })) as readonly [bigint, bigint, bigint, bigint];
35
+ return { L: r[0], shares: r[1], aps: r[2], insideLast: r[3] };
36
+ }
37
+
38
+ /** A holder's share balance in a class. */
39
+ export async function balanceOf(c: LogswapClient, owner: Address, id: bigint): Promise<bigint> {
40
+ return c.public.readContract({
41
+ address: c.addresses.cPoolManager,
42
+ abi: cPoolManagerAbi,
43
+ functionName: "balanceOf",
44
+ args: [owner, id],
45
+ }) as Promise<bigint>;
46
+ }
47
+
48
+ /**
49
+ * A holder's claimable fees, in quote.
50
+ *
51
+ * For a capped class this is credit-minus-debit — two accumulator reads, `F_above[floor]` less
52
+ * `F_above[cap]` — which is monotone non-decreasing, so a capped position can never owe fees.
53
+ */
54
+ export async function feesOf(c: LogswapClient, id: bigint, owner: Address): Promise<bigint> {
55
+ return c.public.readContract({
56
+ address: c.addresses.lens,
57
+ abi: logswapLensAbi,
58
+ functionName: "feesOfById",
59
+ args: [id, owner],
60
+ }) as Promise<bigint>;
61
+ }
62
+
63
+ /** A holder's full view of one class: what they own and what they can claim. */
64
+ export interface HolderPosition {
65
+ id: bigint;
66
+ key: PoolKey;
67
+ floor: bigint;
68
+ cap: bigint;
69
+ capped: boolean;
70
+ shares: bigint;
71
+ /** The holder's pro-rata slice of the class's exposure. */
72
+ L: bigint;
73
+ claimableFees: bigint;
74
+ }
75
+
76
+ /**
77
+ * Everything a UI needs for one holder in one class. Computes the id offline, so this costs three
78
+ * reads and no id lookup.
79
+ */
80
+ export async function getHolderPosition(
81
+ c: LogswapClient,
82
+ key: PoolKey,
83
+ owner: Address,
84
+ floor: bigint,
85
+ cap: bigint = NO_CAP,
86
+ ): Promise<HolderPosition> {
87
+ const id = positionId(poolId(key), key.tickSpacing, floor, cap);
88
+ const [cls, shares, claimableFees] = await Promise.all([
89
+ getPositionClass(c, id),
90
+ balanceOf(c, owner, id),
91
+ feesOf(c, id, owner),
92
+ ]);
93
+ return {
94
+ id,
95
+ key,
96
+ floor,
97
+ cap,
98
+ capped: cap !== NO_CAP,
99
+ shares,
100
+ L: cls.shares === 0n ? 0n : (cls.L * shares) / cls.shares,
101
+ claimableFees,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Resolve an id the manager already knows back to its (pool, floor, cap).
107
+ *
108
+ * The inverse direction — id to class — needs the registry, because the id is a HASH: it cannot be
109
+ * unpacked offline the way `positionId` can be computed offline. `IdRegistered` carries the same
110
+ * information in the clear, so an indexer never needs this call.
111
+ *
112
+ * **Units.** The lens is inconsistent here and this wrapper hides it: `unpack` returns tick
113
+ * INDICES (`kFloor`, `kCap`), while `liveFloors` returns log-price floors. Pass `tickSpacing` and
114
+ * you get `floor`/`cap` in log-price WAD, the same units `mint` takes; omit it and you get the raw
115
+ * indices, honestly named.
116
+ */
117
+ export async function describeId(
118
+ c: LogswapClient,
119
+ id: bigint,
120
+ tickSpacing?: bigint,
121
+ ): Promise<{
122
+ poolId: `0x${string}`;
123
+ kFloor: bigint;
124
+ kCap: bigint;
125
+ capped: boolean;
126
+ floor?: bigint;
127
+ cap?: bigint;
128
+ }> {
129
+ const r = (await c.public.readContract({
130
+ address: c.addresses.lens,
131
+ abi: logswapLensAbi,
132
+ functionName: "unpack",
133
+ args: [id],
134
+ })) as readonly [`0x${string}`, bigint, bigint, boolean];
135
+ const out = { poolId: r[0], kFloor: r[1], kCap: r[2], capped: r[3] };
136
+ if (tickSpacing === undefined) return out;
137
+ return {
138
+ ...out,
139
+ floor: r[1] * tickSpacing,
140
+ cap: r[3] ? r[2] * tickSpacing : NO_CAP,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Split ERC-6909 ids into positions and token claims.
146
+ *
147
+ * **Do this before treating any `Transfer` as an LP event.** Bit 255 clear means a token claim — a
148
+ * bearer IOU a market maker uses to park inventory inside the manager — not a position. An indexer
149
+ * that skips this reports phantom LP positions for every parked balance.
150
+ */
151
+ export function partitionIds(ids: readonly bigint[]): { positions: bigint[]; claims: bigint[] } {
152
+ const positions: bigint[] = [];
153
+ const claims: bigint[] = [];
154
+ for (const id of ids) (isPosition(id) ? positions : claims).push(id);
155
+ return { positions, claims };
156
+ }
157
+
158
+ /** Decode a class's floor and cap from its packed field (as carried by `IdRegistered`). */
159
+ export function describePacked(tickSpacing: bigint, packed: bigint) {
160
+ return unpackFloorCap(tickSpacing, packed);
161
+ }
162
+
163
+ /**
164
+ * The QUOTE leg of a position at log-price `x`, in quote units.
165
+ *
166
+ * Mirrors the manager's `_assets` exactly, in its three cases:
167
+ *
168
+ * - **floor above spot** — the position is all base and holds no quote at all.
169
+ * - **in range** — `L·(x − ξ)`: the buffer built up as price rose from the floor.
170
+ * - **capped out** (spot at or above the cap) — `L·(cap − ξ)`, frozen. Past its cap a position
171
+ * stops converting, which is what makes a capped position a resting limit order.
172
+ *
173
+ * Exact: this is bigint arithmetic throughout, unlike the base leg, which needs `L/p` and is
174
+ * therefore transcendental. If you need base, read it from the chain rather than approximating it
175
+ * here — a display-only float has no business being subtracted from a balance.
176
+ */
177
+ export function positionQuote(
178
+ p: { L: bigint; floor: bigint; cap: bigint; capped: boolean },
179
+ x: bigint,
180
+ ): bigint {
181
+ if (p.floor > x) return 0n;
182
+ const upper = p.capped && p.cap <= x ? p.cap : x;
183
+ return (p.L * (upper - p.floor)) / 10n ** 18n;
184
+ }
185
+
186
+ /** The manager's `MINT_FEE`: 2 bp of newly-earning L, as a WAD fraction. */
187
+ export const MINT_FEE = 2n * 10n ** 14n;
188
+
189
+ /**
190
+ * The one-time mint fee at log-price `x`, in quote units — the amount `_mintCore` pulls ON TOP of
191
+ * the position's own legs, so a mint's total quote pull is `positionQuote(p, x) + mintFee(p, x)`.
192
+ *
193
+ * Mirrors the manager's exemption cases: a DORMANT mint pays nothing — floor above spot, or a
194
+ * capped class spot has already passed (`cap <= x`). Anything earning pays `MINT_FEE · L`, half
195
+ * accrued through `F` to the LPs already earning, half to the protocol. This is NOT the swap fee:
196
+ * that one is charged per unit of |Δ log p| on swaps; this is a flat charge on L, once, at mint.
197
+ *
198
+ * One rung of imprecision: for a floor or cap exactly AT spot the contract consults its `currentK`
199
+ * pointer (an up-move counts the tick, a down-move does not), which this offline mirror cannot see.
200
+ * Preview with it; let the chain be authoritative on that boundary.
201
+ */
202
+ export function mintFee(
203
+ p: { L: bigint; floor: bigint; cap: bigint; capped: boolean },
204
+ x: bigint,
205
+ ): bigint {
206
+ if (p.floor > x || (p.capped && p.cap <= x)) return 0n;
207
+ return (p.L * MINT_FEE) / 10n ** 18n;
208
+ }
209
+
210
+ /**
211
+ * Every UNCAPPED position `owner` holds in a market, enumerated fully on-chain by the lens — a
212
+ * bitmap walk plus a balance check per live floor. No indexer, no log scan. Capped classes are
213
+ * not listed here by contract (their candidate set is quadratic in live ticks); they come from
214
+ * `IdRegistered` replay when an indexer exists.
215
+ */
216
+ export interface HeldPositionRow {
217
+ id: bigint;
218
+ floor: bigint;
219
+ /** NO_CAP for an uncapped class. */
220
+ cap: bigint;
221
+ capped: boolean;
222
+ shares: bigint;
223
+ /** The holder's slice of the class's exposure. */
224
+ L: bigint;
225
+ /** Claimable now, quote units. */
226
+ fees: bigint;
227
+ }
228
+
229
+ export async function positionsOf(c: LogswapClient, key: PoolKey, owner: Address): Promise<HeldPositionRow[]> {
230
+ const rows = (await c.public.readContract({
231
+ address: c.addresses.lens,
232
+ abi: logswapLensAbi,
233
+ functionName: "positionsOf",
234
+ args: [key, owner],
235
+ } as never)) as readonly { id: bigint; floor: bigint; shares: bigint; L: bigint; fees: bigint }[];
236
+ return rows.map((r) => ({ id: r.id, floor: r.floor, cap: NO_CAP, capped: false, shares: r.shares, L: r.L, fees: r.fees }));
237
+ }
238
+
239
+ const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes32 indexed poolId, int256 floor, int256 cap, bool capped)");
240
+
241
+ /**
242
+ * EVERY class this owner holds in a pool, capped ones included — the complete book.
243
+ *
244
+ * `positionsOf` (the lens) enumerates the uncapped book from the ladder bitmap in one call and
245
+ * cannot list capped classes: a capped id is a (floor, cap) pair, quadratic in live ticks. This
246
+ * replays `IdRegistered` — every class is registered once, on first mint, with its floor and
247
+ * cap — then checks the owner's ERC-6909 balance on each and reads the class for its L, so the
248
+ * holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
249
+ * a read per registered class; fine for a pool's lifetime of classes.
250
+ */
251
+ export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n): Promise<HeldPositionRow[]> {
252
+ const pool = poolId(key);
253
+ const logs = await c.public.getLogs({ address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock, toBlock: "latest" });
254
+ const ids = logs.map((l) => (l.args as { id?: bigint }).id ?? 0n).filter((id) => id !== 0n);
255
+ if (ids.length === 0) return [];
256
+ // one lens call: balance, class L, fees, floor and cap for every id the owner holds
257
+ const rows = (await c.public.readContract({
258
+ address: c.addresses.lens,
259
+ abi: logswapLensAbi,
260
+ functionName: "heldByIds",
261
+ args: [key, owner, ids],
262
+ } as never)) as readonly { id: bigint; floor: bigint; cap: bigint; capped: boolean; shares: bigint; L: bigint; fees: bigint }[];
263
+ return rows.map((r) => ({ id: r.id, floor: r.floor, cap: r.capped ? r.cap : NO_CAP, capped: r.capped, shares: r.shares, L: r.L, fees: r.fees }));
264
+ }
265
+
266
+ /** A holding's legs, exact from the lens: base (base units), the quote leg, claimable fees, and
267
+ * value = base·p + quote + fees in quote units — L + quote + fees for an uncapped active class. */
268
+ export async function legsOf(c: LogswapClient, key: PoolKey, id: bigint, holder: Address): Promise<{ base: bigint; quote: bigint; fees: bigint; value: bigint }> {
269
+ const [base, quote, fees, value] = (await c.public.readContract({
270
+ address: c.addresses.lens,
271
+ abi: logswapLensAbi,
272
+ functionName: "legsOf",
273
+ args: [key, id, holder],
274
+ } as never)) as readonly [bigint, bigint, bigint, bigint];
275
+ return { base, quote, fees, value };
276
+ }