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