@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/fpool.ts ADDED
@@ -0,0 +1,935 @@
1
+ /**
2
+ * fpool.ts — the `FPoolManager` surface: n bases, one quote, one shared floor.
3
+ *
4
+ * The other primitive. `CPoolManager` gives every position its own floor on a tick ladder;
5
+ * `FPoolManager` gives every share the SAME floor and deletes the ladder, the accumulators and the
6
+ * per-position accounting with it. Reading the two side by side is the fastest way to understand
7
+ * either (logswap-docs:docs/multi/f-pool.md).
8
+ *
9
+ * **A pool is an ID, not an address.** Both primitives are singletons now, so neither has a
10
+ * per-pool deployment — but they are addressed differently and for a reason. The C manager rides
11
+ * its whole `PoolKey` in calldata because that key is small, fixed and wholly needed. An F-pool
12
+ * key is variable-length and only ever partially needed (no swap path is O(n)), so it is stored
13
+ * once at `initialize` and every later call names `(poolId, j)`. Callers hold a `bytes32`.
14
+ *
15
+ * **Two products, one contract.** The basket (n > 1, public authority) and the launch (n = 1, the
16
+ * creator alone on the lever) are the same bytecode under different settings — what separates them
17
+ * is who holds `authority` and how many bases there are, and nothing in the maths. `describeFPool`
18
+ * below reports which shape a given pool is, because a UI has to decide what to render.
19
+ *
20
+ * **Zaps live on the ROUTER, not here.** Permit2 is AllowanceTransfer mode, whose standing
21
+ * allowance is keyed by spender, so a second router would cost the user an allowance per token
22
+ * twice, forever. One router is one spender — see `fPoolZapIn`/`fPoolZapOut` below, which target
23
+ * `addresses.router`.
24
+ */
25
+
26
+ import { toFunctionSelector, type Address, type Hash, type Hex } from "viem";
27
+ import { fPoolManagerAbi, logswapRouterAbi } from "./generated.js";
28
+ import type { LogswapClient } from "./client.js";
29
+
30
+ const WAD = 10n ** 18n;
31
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
32
+
33
+ /** Everything scalar about a basket pool, in one round trip's worth of reads. */
34
+ export interface FPoolState {
35
+ /** The pool's id — the hash of its key. There is no per-pool address. */
36
+ poolId: Hex;
37
+ quote: Address;
38
+ bases: Address[];
39
+ /** WAD weights, summing to WAD. `L_j = w_j · L`. */
40
+ weights: bigint[];
41
+ /** Per-asset log prices, WAD. Price is derived; `x` is the state. */
42
+ x: bigint[];
43
+ /** Quote reserve. The one leg a base→quote swap can exhaust. */
44
+ Q: bigint;
45
+ /** Total exposure. Grows on mint, shrinks on burn; never moves on a swap. */
46
+ L: bigint;
47
+ /** Fixed fee, WAD. At or above `FEE_MIN` — a zero-fee basket is not constructible. */
48
+ phi: bigint;
49
+ /** The strike: θ at seeding, and the fees-only harvest bound. */
50
+ theta0: bigint;
51
+ /** θ = X − Q/L, the shared floor. Derived from the holdings, never stored. */
52
+ theta: bigint;
53
+ /** X = Σ w_j x_j, the composite index. */
54
+ compositeX: bigint;
55
+ /** Σ = ∫ Σ_j w_j (dx_j)², accrued even though the fee is fixed — the kernel stays calculable. */
56
+ bigSigma: bigint;
57
+ /** Creator (private) or a governance contract (public). Holds the floor lever. */
58
+ authority: Address;
59
+ /** The proposed next authority, until it accepts — zero when nothing is pending (decisions 020). */
60
+ pendingAuthority: Address;
61
+ /** The desk's hot key (`ma-private.md` §6): may reshape composition, never move value out. Zero when none. */
62
+ operator: Address;
63
+ /** The sponsor's own label for this pool, mutable and never identity. "" when unnamed. */
64
+ name: string;
65
+ /** `harvest` may not take Q/L under this (WAD log-distance); raise-only (decisions 021). */
66
+ minBuffer: bigint;
67
+ /** Only `allowed` accounts may receive minted shares. */
68
+ gateMint: boolean;
69
+ /** Only `allowed` takers may swap. */
70
+ gateSwap: boolean;
71
+ /** The block of the last lever / composition action: no swap runs in it. */
72
+ restructureBlock: number;
73
+ /** Whether harvest is bounded by θ ≤ θ₀ — income only, never the floor's backing. */
74
+ feesOnly: boolean;
75
+ seeded: boolean;
76
+ dissolved: boolean;
77
+ totalSupply: bigint;
78
+ /** Base holdings the state implies, `R_j = w_j·L·e^{−x_j}`. */
79
+ reserves: bigint[];
80
+ }
81
+
82
+ /** Read a basket pool whole. Batched — viem multicalls these when the transport allows. */
83
+ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolState> {
84
+ // viem types `functionName` as a literal union; this reader is deliberately generic, so the
85
+ // cast lives here once rather than at each call site below.
86
+ const rd = <T>(functionName: string, args: readonly unknown[]) =>
87
+ c.public.readContract({
88
+ address: c.addresses.fPoolManager!,
89
+ abi: fPoolManagerAbi,
90
+ functionName,
91
+ args,
92
+ } as never) as Promise<T>;
93
+
94
+ // `getPool` returns the whole scalar struct in one call — the singleton's shape makes what used
95
+ // to be a dozen getters a single read.
96
+ const p = await rd<{
97
+ quote: Address;
98
+ phi: bigint;
99
+ authority: Address;
100
+ feesOnly: boolean;
101
+ seeded: boolean;
102
+ dissolved: boolean;
103
+ n: number;
104
+ restructureBlock: number;
105
+ gateMint: boolean;
106
+ gateSwap: boolean;
107
+ Q: bigint;
108
+ L: bigint;
109
+ shares: bigint;
110
+ theta0: bigint;
111
+ bigSigma: bigint;
112
+ leverTheta: bigint;
113
+ pendingAuthority: Address;
114
+ operator: Address;
115
+ minBuffer: bigint;
116
+ incomeTaken: bigint;
117
+ name: Hex;
118
+ }>("getPool", [poolId]);
119
+
120
+ // A pool that was never created reads as an all-zero struct rather than reverting, so the
121
+ // absence has to be named here or every caller mistakes "no such pool" for "a pool holding
122
+ // nothing". `quote` is the manager's own existence test (`_live`), and it is what the create
123
+ // flow's resumability turns on: an initialized-but-unseeded pool MUST still read, so this
124
+ // cannot be a check on `seeded`.
125
+ if (/^0x0{40}$/i.test(p.quote)) throw new Error(`logswap: no F pool with id ${poolId}`);
126
+
127
+ const n = Number(p.n);
128
+ const idx = Array.from({ length: n }, (_, i) => BigInt(i));
129
+ const [theta, compositeX, legs] = await Promise.all([
130
+ // before the seed there is no L and no floor: θ is X by convention (the manager says so since
131
+ // bd3dd87's successor; older deployments revert with DivWadFailed, so do not ask them)
132
+ p.L > 0n ? rd<bigint>("theta", [poolId]) : rd<bigint>("compositeX", [poolId]),
133
+ rd<bigint>("compositeX", [poolId]),
134
+ Promise.all(idx.map((i) => rd<readonly [Address, bigint, bigint, bigint]>("legOf", [poolId, i]))),
135
+ ]);
136
+
137
+ const bases = legs.map((l) => l[0]);
138
+ const weights = legs.map((l) => l[1]);
139
+ const x = legs.map((l) => l[2]);
140
+ // the ISOLATION SHADOW, not the derived reserve: what this pool actually holds of the token.
141
+ // In a singleton the contract's balance backs many pools, so this is the only per-pool figure.
142
+ const reserves = legs.map((l) => l[3]);
143
+ const { quote, Q, L, phi, theta0, bigSigma, authority, pendingAuthority, operator, minBuffer, gateMint, gateSwap, restructureBlock, feesOnly, seeded, dissolved } = p;
144
+ const totalSupply = p.shares;
145
+
146
+ return {
147
+ poolId,
148
+ quote,
149
+ bases,
150
+ weights,
151
+ x,
152
+ Q,
153
+ L,
154
+ phi,
155
+ theta0,
156
+ theta,
157
+ compositeX,
158
+ bigSigma,
159
+ authority,
160
+ pendingAuthority,
161
+ operator,
162
+ name: bytes32ToText(p.name),
163
+ minBuffer,
164
+ gateMint,
165
+ gateSwap,
166
+ restructureBlock: Number(restructureBlock),
167
+ feesOnly,
168
+ seeded,
169
+ dissolved,
170
+ totalSupply,
171
+ reserves,
172
+ };
173
+ }
174
+
175
+ /** Which of the two products a pool is configured as. A UI has to choose what to render. */
176
+ export type FPoolShape = "launch" | "basket";
177
+
178
+ export interface FPoolDescription {
179
+ shape: FPoolShape;
180
+ /** n = 1 with a private authority is the launch shape (f-pool §6.5). */
181
+ legs: number;
182
+ /** True when the pool holds no quote: an all-base resting ask. base→quote is the one blocked path. */
183
+ atFloor: boolean;
184
+ /** Fee income earned per unit L so far, `θ₀ − θ` with the lever's displacement backed out. */
185
+ feePerL: bigint;
186
+ /** The model-free LP edge, `F − ½Σ`. Exact at any n: V is separable in the logs. */
187
+ edgePerL: bigint;
188
+ }
189
+
190
+ export async function describeFPool(c: LogswapClient, s: FPoolState): Promise<FPoolDescription> {
191
+ const rd = <T>(functionName: string) =>
192
+ c.public.readContract({
193
+ address: c.addresses.fPoolManager!,
194
+ abi: fPoolManagerAbi,
195
+ functionName,
196
+ args: [s.poolId],
197
+ } as never) as Promise<T>;
198
+ // an unseeded pool has no L: nothing earned, and θ (hence feePerL, hence edgePerL) is not
199
+ // defined at L = 0. Older deployments revert with DivWadFailed rather than answering zero.
200
+ const [feePerL, edgePerL] = s.L > 0n
201
+ ? await Promise.all([rd<bigint>("feePerL"), rd<bigint>("edgePerL")])
202
+ : [0n, 0n];
203
+ return {
204
+ shape: s.bases.length === 1 ? "launch" : "basket",
205
+ legs: s.bases.length,
206
+ atFloor: s.Q === 0n,
207
+ feePerL,
208
+ edgePerL,
209
+ };
210
+ }
211
+
212
+ /** `p_j = e^{x_j}`, lossy the way any float conversion is. Compare in log space when it matters. */
213
+ export function fPoolPriceOf(x: bigint, scale = 1): number {
214
+ return Math.exp(Number(x) / 1e18) * scale;
215
+ }
216
+
217
+ /** Value per share at the pool's own marks. `V = L + Q`, so this is just that, pro-rated. */
218
+ export function fPoolShareValue(s: FPoolState): bigint {
219
+ if (s.totalSupply === 0n) return 0n;
220
+ return ((s.L + s.Q) * WAD) / s.totalSupply;
221
+ }
222
+
223
+ // ─── swaps ────────────────────────────────────────────────────────────────────
224
+ // Exact-in only (f-pool §10). Three paths; the third is the one that makes a basket a basket.
225
+
226
+ export interface FPoolSwapArgs {
227
+ poolId: Hex;
228
+ /** Leg index into `bases`. */
229
+ j: number;
230
+ amountIn: bigint;
231
+ minOut?: bigint;
232
+ /** Who receives the out leg. Defaults to the account. */
233
+ to?: Address;
234
+ /** Absolute unix seconds. Defaulted to a bounded window, as every router write is. */
235
+ deadline?: bigint;
236
+ account: Address;
237
+ }
238
+
239
+ /**
240
+ * All three F swaps go through the ROUTER's `swapExactIn` overload — the same surface as a C
241
+ * swap: Permit2 pull, `minOut`, recipient, deadline. The manager's direct `swapQuoteIn` etc.
242
+ * remain callable (plain allowance to the manager, settle to caller) but are the primitive's
243
+ * interface, not the SDK's: routing everywhere is what keeps the two pools identical to use.
244
+ */
245
+ async function fSwap(c: LogswapClient, a: FPoolSwapArgs, kind: FPoolQuoteKind, k = 0) {
246
+ const st = await getFPool(c, a.poolId);
247
+ const pull = kind === FPoolQuoteKind.QuoteIn ? st.quote : st.bases[a.j]!;
248
+ return writeRouter(
249
+ c,
250
+ "swapExactIn",
251
+ [a.poolId, kind, BigInt(a.j), BigInt(k), a.amountIn, a.minOut ?? 0n, a.to ?? a.account, a.deadline ?? defaultDeadline()],
252
+ a.account,
253
+ [pull],
254
+ );
255
+ }
256
+
257
+ export async function fPoolSwapQuoteIn(c: LogswapClient, a: FPoolSwapArgs) {
258
+ return fSwap(c, a, FPoolQuoteKind.QuoteIn);
259
+ }
260
+
261
+ export async function fPoolSwapBaseIn(c: LogswapClient, a: FPoolSwapArgs) {
262
+ return fSwap(c, a, FPoolQuoteKind.BaseIn);
263
+ }
264
+
265
+ /**
266
+ * Base j → base k. Needs no quote, works at Q = 0, and cannot cross the floor: X is held
267
+ * exactly up to the in-kind fee. Note it moves BOTH quote prices — p_j down, p_k up — because the
268
+ * pool has one coordinate per asset and every trade moves at least one.
269
+ */
270
+ export async function fPoolSwapBaseForBase(c: LogswapClient, a: FPoolSwapArgs & { k: number }) {
271
+ return fSwap(c, a, FPoolQuoteKind.BaseForBase, a.k);
272
+ }
273
+
274
+ // ─── liquidity ────────────────────────────────────────────────────────────────
275
+
276
+ /** Deposit dL/L of every reserve, receive shares ∝ dL. θ is untouched by construction. */
277
+ export async function fPoolMint(
278
+ c: LogswapClient,
279
+ a: { poolId: Hex; dL: bigint; maxQuoteIn?: bigint; account: Address; to?: Address },
280
+ ) {
281
+ // shares go to `to` (the account by default); on a gated pool `to` must be allowed
282
+ return writeFPool(c, a.poolId, "mint", [a.to ?? a.account, a.dL, a.maxQuoteIn ?? 2n ** 256n - 1n], a.account);
283
+ }
284
+
285
+ /**
286
+ * Burn shares for the pro-rata slice of every reserve. Available at any Q, and after dissolution.
287
+ * By owner since contracts `c782811` (decisions 019): the account burns its own shares; an
288
+ * operator may pass another owner's address, as on the C manager's `burnById`.
289
+ */
290
+ export async function fPoolBurn(
291
+ c: LogswapClient,
292
+ a: { poolId: Hex; shares: bigint; minQuoteOut?: bigint; account: Address; owner?: Address },
293
+ ) {
294
+ return writeFPool(c, a.poolId, "burn", [a.owner ?? a.account, a.shares, a.minQuoteOut ?? 0n], a.account);
295
+ }
296
+
297
+ // ─── zaps (on the router — one router, one Permit2 spender) ───────────────────
298
+
299
+ /**
300
+ * Quote needed to zap into exposure `dL`. An UPPER bound: buying each leg raises its mark, so the
301
+ * mint consumes less base than was bought for it and every zap ends with a refund. Size from this
302
+ * and expect change back — do not treat it as exact.
303
+ */
304
+ export async function fPoolPreviewZapIn(c: LogswapClient, poolId: Hex, dL: bigint): Promise<bigint> {
305
+ // read-only lives on the LENS (the router's F flows moved to a facet; its previews came here)
306
+ const { logswapLensAbi } = await import("./generated.js");
307
+ return c.public.readContract({
308
+ address: c.addresses.lens,
309
+ abi: logswapLensAbi,
310
+ functionName: "previewZapIn",
311
+ args: [poolId, dL],
312
+ } as never) as Promise<bigint>;
313
+ }
314
+
315
+ /**
316
+ * Buy `dL` of the basket with quote alone.
317
+ *
318
+ * The imbalanced part goes through the pool's own curve before anything is minted. That is not a
319
+ * penalty: minting against a quote-only deposit would be a zero-slippage, zero-fee swap at stale
320
+ * spot, paid for by the existing LPs — 27.4% in f-pool §6's worked example. Routing it through
321
+ * the curve deletes the subsidy and hands the fee and impact to the incumbents instead.
322
+ */
323
+ export async function fPoolZapIn(
324
+ c: LogswapClient,
325
+ a: { poolId: Hex; dL: bigint; maxQuoteIn: bigint; minShares?: bigint; to?: Address; account: Address; deadline?: bigint },
326
+ ) {
327
+ const to = a.to ?? a.account;
328
+ const st = await getFPool(c, a.poolId);
329
+ return writeRouter(
330
+ c,
331
+ "zapIn",
332
+ [a.poolId, a.dL, a.maxQuoteIn, a.minShares ?? 0n, to, a.deadline ?? defaultDeadline()],
333
+ a.account,
334
+ [st.quote],
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Sell shares into ONE token — the quote, or any base.
340
+ *
341
+ * Into a base the other legs route through `swapBaseForBase`, which needs no quote and cannot
342
+ * cross the floor; only the burn's own quote slice touches the quote leg. Into the quote every
343
+ * base leg is sold, which is the one path a drained quote leg can block.
344
+ */
345
+ export async function fPoolZapOut(
346
+ c: LogswapClient,
347
+ a: { poolId: Hex; shares: bigint; tokenOut: Address; minOut?: bigint; to?: Address; account: Address; deadline?: bigint },
348
+ ) {
349
+ const to = a.to ?? a.account;
350
+ return writeRouter(
351
+ c,
352
+ "zapOut",
353
+ [a.poolId, a.shares, a.tokenOut, a.minOut ?? 0n, to, a.deadline ?? defaultDeadline()],
354
+ a.account,
355
+ );
356
+ }
357
+
358
+ // ─── the floor lever (authority only) ─────────────────────────────────────────
359
+
360
+ /**
361
+ * Remove quote, lifting θ — price-neutral, pro-rata, no base moves. Under `feesOnly` the lift
362
+ * stops at θ₀, so the authority extracts income and never the proceeds backing the floor.
363
+ */
364
+ export async function fPoolHarvest(c: LogswapClient, a: { poolId: Hex; amount: bigint; to?: Address; account: Address }) {
365
+ return writeFPool(c, a.poolId, "harvest", [a.amount, a.to ?? a.account], a.account);
366
+ }
367
+
368
+ /** Commit quote, deepening θ — the other sign of `harvest`, the same word as the C floor edit (decisions 011). The mark does not move: governance may write θ, never x. */
369
+ export async function fPoolDeepen(c: LogswapClient, a: { poolId: Hex; amount: bigint; account: Address }) {
370
+ return writeFPool(c, a.poolId, "deepen", [a.amount], a.account);
371
+ }
372
+
373
+ // ─── internals ────────────────────────────────────────────────────────────────
374
+
375
+ function defaultDeadline(): bigint {
376
+ return BigInt(Math.floor(Date.now() / 1000) + 1800);
377
+ }
378
+
379
+ function requireWallet(c: LogswapClient) {
380
+ if (!c.wallet) throw new Error("logswap: this call needs a wallet client");
381
+ return c.wallet;
382
+ }
383
+
384
+ async function writeFPool(c: LogswapClient, poolId: Hex, functionName: string, args: unknown[], account: Address) {
385
+ const wallet = requireWallet(c);
386
+ const { request } = await c.public.simulateContract({
387
+ address: c.addresses.fPoolManager!,
388
+ abi: fPoolManagerAbi,
389
+ functionName,
390
+ // Every manager write is keyed by poolId as its FIRST parameter; prepending it here, in the
391
+ // one place all seven helpers share, is what keeps a caller from ever omitting it. The `as
392
+ // never` casts blind tsc to arg counts, so this file's encode test is the real guard.
393
+ args: [poolId, ...args],
394
+ account,
395
+ } as never);
396
+ return wallet.writeContract(request as never);
397
+ }
398
+
399
+ async function writeRouter(
400
+ c: LogswapClient,
401
+ functionName: string,
402
+ args: unknown[],
403
+ account: Address,
404
+ /** Tokens the call pulls: missing Permit2 allowances are SIGNED and ride in the same tx. */
405
+ pullTokens: Address[] = [],
406
+ ) {
407
+ const wallet = requireWallet(c);
408
+ let fn = functionName;
409
+ let sendArgs: unknown[] = args;
410
+ if (pullTokens.length) {
411
+ const { permit2SigCalls } = await import("./onboard.js");
412
+ const sigCalls = await permit2SigCalls(c, pullTokens).catch(() => []);
413
+ if (sigCalls.length) {
414
+ const { encodeFunctionData } = await import("viem");
415
+ const inner = encodeFunctionData({ abi: logswapRouterAbi, functionName, args } as never);
416
+ fn = "multicall";
417
+ sendArgs = [[...sigCalls, inner]];
418
+ }
419
+ }
420
+ const { request } = await c.public.simulateContract({
421
+ address: c.addresses.router,
422
+ abi: logswapRouterAbi,
423
+ functionName: fn,
424
+ args: sendArgs,
425
+ account,
426
+ } as never);
427
+ return wallet.writeContract(request as never);
428
+ }
429
+
430
+ // ─── token readiness (two pull paths, not one) ────────────────────────────────
431
+ //
432
+ // The F screens have TWO spenders to satisfy, and confusing them was a shipped bug. Direct
433
+ // manager calls (swaps, mint, burn) pull with plain `transferFrom`, so the MANAGER needs an
434
+ // ERC-20 allowance. The router's zaps pull through Permit2 when the deployment carries one, so a
435
+ // plain approval to the router does nothing there and the zap reverts `NotAllowed()` — that leg
436
+ // is `onboardToken`'s job. This helper covers the manager leg; a UI needs both.
437
+
438
+ /** Plain ERC-20 approval to the F manager — the allowance every DIRECT call pulls against. */
439
+ export async function approveTokenForFPoolManager(c: LogswapClient, token: Address): Promise<Hash> {
440
+ const wallet = requireWallet(c);
441
+ return c.wallet!.writeContract({
442
+ address: token,
443
+ abi: [{ type: "function", name: "approve", stateMutability: "nonpayable", inputs: [{ name: "s", type: "address" }, { name: "a", type: "uint256" }], outputs: [{ type: "bool" }] }],
444
+ functionName: "approve",
445
+ args: [c.addresses.fPoolManager!, (1n << 256n) - 1n],
446
+ account: wallet.account,
447
+ chain: c.wallet!.chain,
448
+ } as never);
449
+ }
450
+
451
+ /** Does `owner` have enough allowance on `token` for the MANAGER to pull `need`? */
452
+ export async function fPoolManagerAllowanceOk(c: LogswapClient, token: Address, owner: Address, need: bigint): Promise<boolean> {
453
+ const allowance = (await c.public.readContract({
454
+ address: token,
455
+ abi: [{ type: "function", name: "allowance", stateMutability: "view", inputs: [{ name: "o", type: "address" }, { name: "s", type: "address" }], outputs: [{ type: "uint256" }] }],
456
+ functionName: "allowance",
457
+ args: [owner, c.addresses.fPoolManager!],
458
+ } as never)) as bigint;
459
+ return allowance >= need;
460
+ }
461
+
462
+ // ─── ERC-6909 operator (zapOut needs it) ──────────────────────────────────────
463
+ //
464
+ // The singleton's shares are ERC-6909, so the router cannot `transferFrom` them the way it could
465
+ // an ERC-20 LP token. The holder approves it once as an operator. This is the one place the move
466
+ // off a per-pool ERC-20 costs a user-visible step, so the SDK names it rather than letting the
467
+ // first `fPoolZapOut` revert with something unreadable.
468
+
469
+ export async function isRouterOperatorForFPool(c: LogswapClient, owner: Address): Promise<boolean> {
470
+ return c.public.readContract({
471
+ address: c.addresses.fPoolManager!,
472
+ abi: fPoolManagerAbi,
473
+ functionName: "isOperator",
474
+ args: [owner, c.addresses.router],
475
+ } as never) as Promise<boolean>;
476
+ }
477
+
478
+ /** Approve the router to move this account's F-pool shares. Needed once, before any `fPoolZapOut`. */
479
+ export async function approveRouterForFPool(c: LogswapClient, account: Address) {
480
+ const wallet = requireWallet(c);
481
+ const { request } = await c.public.simulateContract({
482
+ address: c.addresses.fPoolManager!,
483
+ abi: fPoolManagerAbi,
484
+ functionName: "setOperator",
485
+ args: [c.addresses.router, true],
486
+ account,
487
+ } as never);
488
+ return wallet.writeContract(request as never);
489
+ }
490
+
491
+ // ─── ids ──────────────────────────────────────────────────────────────────────
492
+ //
493
+ // One ERC-6909 space, split structurally on bit 255 — the C manager's scheme. Derived here rather
494
+ // than read from the chain: it is pure, and a client that has to call to learn an id cannot build
495
+ // a multicall that uses it.
496
+
497
+ /** The ERC-6909 id carrying a pool's shares. Bit 255 set. */
498
+ export function fPoolShareId(poolId: Hex): bigint {
499
+ return (1n << 255n) | (BigInt(poolId) >> 1n);
500
+ }
501
+
502
+ /** The ERC-6909 id carrying claims on a token. Bit 255 clear, so it can never meet a share id. */
503
+ export function fPoolClaimId(token: Address): bigint {
504
+ return BigInt(token);
505
+ }
506
+
507
+ export async function fPoolShareBalance(c: LogswapClient, poolId: Hex, owner: Address): Promise<bigint> {
508
+ return c.public.readContract({
509
+ address: c.addresses.fPoolManager!,
510
+ abi: fPoolManagerAbi,
511
+ functionName: "balanceOf",
512
+ args: [owner, fPoolShareId(poolId)],
513
+ } as never) as Promise<bigint>;
514
+ }
515
+
516
+ export async function fPoolClaimBalance(c: LogswapClient, token: Address, owner: Address): Promise<bigint> {
517
+ return c.public.readContract({
518
+ address: c.addresses.fPoolManager!,
519
+ abi: fPoolManagerAbi,
520
+ functionName: "balanceOf",
521
+ args: [owner, fPoolClaimId(token)],
522
+ } as never) as Promise<bigint>;
523
+ }
524
+
525
+ // ─── the quoter ───────────────────────────────────────────────────────────────
526
+
527
+ export enum FPoolQuoteKind {
528
+ QuoteIn = 0,
529
+ BaseIn = 1,
530
+ BaseForBase = 2,
531
+ }
532
+
533
+ /**
534
+ * An EXACT quote — what execution returns, not an estimate.
535
+ *
536
+ * `quoteSwap` runs the real swap and reverts with the answer, so the value has to be decoded out
537
+ * of the revert rather than returned. That is the point of the pattern: there is no second pricing
538
+ * path to drift from the first. It needs no tokens, no approvals and no balance, so it works from
539
+ * a disconnected wallet — unlike `fPoolPreviewZapIn`, which is a genuine upper bound.
540
+ */
541
+ export async function fPoolQuoteSwap(
542
+ c: LogswapClient,
543
+ a: { poolId: Hex; kind: FPoolQuoteKind; j: number; k?: number; amountIn: bigint; taker?: Address },
544
+ ): Promise<bigint> {
545
+ try {
546
+ await c.public.simulateContract({
547
+ address: c.addresses.fPoolManager!,
548
+ abi: fPoolManagerAbi,
549
+ functionName: "quoteSwap",
550
+ // the taker matters only on a gated pool: the connected account, or the zero address
551
+ args: [a.poolId, a.kind, BigInt(a.j), BigInt(a.k ?? 0), a.amountIn, a.taker ?? c.wallet?.account?.address ?? ZERO_ADDRESS],
552
+ } as never);
553
+ } catch (err) {
554
+ const hit = findQuoteResult(err);
555
+ if (hit !== null) return hit;
556
+ throw err; // a real revert — the floor stop, a bad leg — belongs to the caller
557
+ }
558
+ throw new Error("logswap: quoteSwap returned without reverting, which it must never do");
559
+ }
560
+
561
+ /**
562
+ * `QuoteResult(uint256)` — selector then one word. viem nests the revert data several layers deep
563
+ * depending on the transport, so walk for it rather than guessing the shape.
564
+ *
565
+ * The selector is DERIVED, not written down. A hardcoded one is wrong silently: the decode simply
566
+ * never matches and every quote rethrows as if the swap had failed.
567
+ */
568
+ function findQuoteResult(err: unknown): bigint | null {
569
+ const SELECTOR = toFunctionSelector("QuoteResult(uint256)");
570
+ const seen = new Set<unknown>();
571
+ const walk = (e: unknown): bigint | null => {
572
+ if (!e || typeof e !== "object" || seen.has(e)) return null;
573
+ seen.add(e);
574
+ const anyE = e as Record<string, unknown>;
575
+ const data = anyE.data;
576
+ if (typeof data === "string" && data.startsWith(SELECTOR) && data.length >= 10 + 64) {
577
+ return BigInt("0x" + data.slice(10, 10 + 64));
578
+ }
579
+ for (const k of ["cause", "walk", "error", "details"]) {
580
+ const found = walk(anyE[k]);
581
+ if (found !== null) return found;
582
+ }
583
+ return null;
584
+ };
585
+ return walk(err);
586
+ }
587
+
588
+ // ─── the pool lifecycle (creation to dissolution) ─────────────────────────────
589
+ //
590
+ // Everything here is MANAGER-DIRECT by design: creation and governance are one-time,
591
+ // authority-gated acts with no pull to route and no slippage to bound, so the router adds
592
+ // nothing and its 128 bytes of EIP-170 margin are not spent on them.
593
+
594
+ export interface FPoolCreateArgs {
595
+ quote: Address;
596
+ /** Legs in any order; sorted (with weights and marks kept aligned) before the call. */
597
+ bases: Address[];
598
+ /** WAD each; must sum to 1e18 — validated here so the revert is readable. */
599
+ weights: bigint[];
600
+ /** Fixed fee, WAD. The base-for-base fee is pinned at 2φ on-chain. */
601
+ phi: bigint;
602
+ /** true: the authority's harvest stops at the seed strike θ₀ (the launch posture). */
603
+ feesOnly: boolean;
604
+ authority: Address;
605
+ /**
606
+ * The differentiator, and nothing else. Identity is the key's hash, so one sponsor cannot run
607
+ * two pools of the same shape without varying it — the second `initialize` would land on the
608
+ * first pool's id. Defaults to zero, which is the first book of any shape. A pool's NAME is not
609
+ * this: names are mutable state (`fPoolSetName`), so a rename forks nothing.
610
+ */
611
+ salt?: Hex;
612
+ account: Address;
613
+ }
614
+
615
+ const ZERO_SALT = `0x${"0".repeat(64)}` as const;
616
+
617
+ /** `bytes32` ⇄ short UTF-8 text, for the pool and sponsor labels. */
618
+ export function textToBytes32(text: string): Hex {
619
+ const b = new TextEncoder().encode(text);
620
+ if (b.length > 31) throw new Error(`logswap: "${text}" is ${b.length} bytes; a label holds 31`);
621
+ const out = new Uint8Array(32);
622
+ out.set(b);
623
+ return `0x${Array.from(out, (x) => x.toString(16).padStart(2, "0")).join("")}` as Hex;
624
+ }
625
+ export function bytes32ToText(word: Hex): string {
626
+ if (!word) return ""; // a manager older than the field answers with nothing at all
627
+ const bytes = (word.slice(2).match(/../g) ?? []).map((h) => parseInt(h, 16));
628
+ const end = bytes.findIndex((x) => x === 0);
629
+ return new TextDecoder().decode(new Uint8Array(end === -1 ? bytes : bytes.slice(0, end)));
630
+ }
631
+
632
+ /** Sort legs by base address (the key's canonical order), carrying companion arrays along. */
633
+ export function sortFPoolLegs<T>(bases: Address[], ...companions: T[][]): { bases: Address[]; companions: T[][] } {
634
+ const idx = bases.map((_, i) => i).sort((a, b) => (bases[a]!.toLowerCase() < bases[b]!.toLowerCase() ? -1 : 1));
635
+ return { bases: idx.map((i) => bases[i]!), companions: companions.map((arr) => idx.map((i) => arr[i]!)) };
636
+ }
637
+
638
+ /** Create the pool (no funds move — `seed` arms it). Returns the tx hash; read the id with `fPoolIdOf`. */
639
+ export async function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs) {
640
+ const sum = a.weights.reduce((x, y) => x + y, 0n);
641
+ if (sum !== 10n ** 18n) throw new Error(`logswap: weights sum to ${sum}, expected 1e18`);
642
+ const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
643
+ const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, feesOnly: a.feesOnly, authority: a.authority, salt: a.salt ?? ZERO_SALT };
644
+ const wallet = requireWallet(c);
645
+ const { request } = await c.public.simulateContract({
646
+ address: c.addresses.fPoolManager!,
647
+ abi: fPoolManagerAbi,
648
+ functionName: "initialize",
649
+ args: [key],
650
+ account: wallet.account,
651
+ } as never);
652
+ return c.wallet!.writeContract(request as never);
653
+ }
654
+
655
+ /** The pool id is the key's hash — pure, so it can be read before or after creation. */
656
+ export async function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): Promise<Hex> {
657
+ const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
658
+ const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, feesOnly: a.feesOnly, authority: a.authority, salt: a.salt ?? ZERO_SALT };
659
+ return c.public.readContract({
660
+ address: c.addresses.fPoolManager!,
661
+ abi: fPoolManagerAbi,
662
+ functionName: "idOf",
663
+ args: [key],
664
+ } as never) as Promise<Hex>;
665
+ }
666
+
667
+ /**
668
+ * Arm the pool: deposit the full basket at marks `x0` plus `Q0` of quote. Authority-only on-chain
669
+ * (an open seed lets anyone front-run the creator for the strike). `x0` must align with the
670
+ * SORTED bases — use {@link sortFPoolLegs} on (bases, weights, x0) together when building forms.
671
+ */
672
+ export async function fPoolSeed(
673
+ c: LogswapClient,
674
+ a: { poolId: Hex; L0: bigint; x0: bigint[]; Q0: bigint; account: Address },
675
+ ) {
676
+ return writeFPool(c, a.poolId, "seed", [a.L0, a.x0, a.Q0], a.account);
677
+ }
678
+
679
+ /** End of life. Gated on-chain to the authority and to Q at dust (the raise is not abortable). */
680
+ export async function fPoolDissolve(c: LogswapClient, a: { poolId: Hex; account: Address }) {
681
+ return writeFPool(c, a.poolId, "dissolve", [], a.account);
682
+ }
683
+
684
+ /**
685
+ * Propose the next authority — step one of two (contracts `2aa6b7b`, decisions 020). Nothing moves
686
+ * until `next` accepts; proposing again (the zero address included) cancels a pending proposal.
687
+ * Indexed on-chain via AuthorityProposed.
688
+ */
689
+ export async function fPoolProposeAuthority(c: LogswapClient, a: { poolId: Hex; next: Address; account: Address }) {
690
+ return writeFPool(c, a.poolId, "proposeAuthority", [a.next], a.account);
691
+ }
692
+
693
+ /** Accept a pending proposal — step two; only the proposed address can. Indexed via AuthoritySet. */
694
+ export async function fPoolAcceptAuthority(c: LogswapClient, a: { poolId: Hex; account: Address }) {
695
+ return writeFPool(c, a.poolId, "acceptAuthority", [], a.account);
696
+ }
697
+
698
+ // ─── the sponsor's controls and the desk (contracts bd3dd87, decisions 021) ───────────────
699
+
700
+ /** `setLegL`'s "keep the stored mark" sentinel — the only legal `x` for a live leg. */
701
+ export const NO_X = -(2n ** 255n);
702
+
703
+ /** Turn the mint / swap gates on or off (authority). `burn` is never gated. */
704
+ export async function fPoolSetGates(
705
+ c: LogswapClient,
706
+ a: { poolId: Hex; gateMint: boolean; gateSwap: boolean; account: Address },
707
+ ) {
708
+ return writeFPool(c, a.poolId, "setGates", [a.gateMint, a.gateSwap], a.account);
709
+ }
710
+
711
+ /** Add to or remove from the pool's list, in one call (authority). */
712
+ export async function fPoolSetAllowed(
713
+ c: LogswapClient,
714
+ a: { poolId: Hex; who: readonly Address[]; allowed: boolean; account: Address },
715
+ ) {
716
+ return writeFPool(c, a.poolId, "setAllowed", [a.who, a.allowed], a.account);
717
+ }
718
+
719
+ /** Whether `who` is on the pool's list (consulted only while a gate is on). */
720
+ export async function fPoolIsAllowed(c: LogswapClient, poolId: Hex, who: Address): Promise<boolean> {
721
+ return (await c.public.readContract({
722
+ address: c.addresses.fPoolManager!,
723
+ abi: fPoolManagerAbi,
724
+ functionName: "allowed",
725
+ args: [poolId, who],
726
+ } as never)) as boolean;
727
+ }
728
+
729
+ /** Label the pool — the sponsor's own words, mutable, never identity. At most 31 bytes of UTF-8. */
730
+ export async function fPoolSetName(c: LogswapClient, a: { poolId: Hex; name: string; account: Address }) {
731
+ return writeFPool(c, a.poolId, "setName", [textToBytes32(a.name)], a.account);
732
+ }
733
+
734
+ /**
735
+ * What the PROTOCOL says an address is — "Keyrock". Only `feeCollector` may write it, which is
736
+ * what makes it an attestation rather than a claim anyone could make about themselves.
737
+ */
738
+ export async function fPoolSetSponsorName(c: LogswapClient, a: { who: Address; name: string; account: Address }) {
739
+ const wallet = requireWallet(c);
740
+ const { request } = await c.public.simulateContract({
741
+ address: c.addresses.fPoolManager!,
742
+ abi: fPoolManagerAbi,
743
+ functionName: "setSponsorName",
744
+ args: [a.who, textToBytes32(a.name)],
745
+ account: a.account,
746
+ } as never);
747
+ return wallet.writeContract(request as never);
748
+ }
749
+
750
+ /** The protocol's attestation for an address, or "" when unattested — which most addresses are. */
751
+ export async function fPoolSponsorName(c: LogswapClient, who: Address): Promise<string> {
752
+ const word = (await c.public.readContract({
753
+ address: c.addresses.fPoolManager!,
754
+ abi: fPoolManagerAbi,
755
+ functionName: "sponsorName",
756
+ args: [who],
757
+ } as never)) as Hex;
758
+ return bytes32ToText(word);
759
+ }
760
+
761
+ /** Appoint (or clear, with the zero address) the operator — the hot key that reshapes and never moves value out. */
762
+ export async function fPoolAppointOperator(c: LogswapClient, a: { poolId: Hex; operator: Address; account: Address }) {
763
+ return writeFPool(c, a.poolId, "appointOperator", [a.operator], a.account);
764
+ }
765
+
766
+ /**
767
+ * Raise the floor guard: `harvest` may not take Q/L under `minBuffer` (WAD log-distance; 20% below
768
+ * spot is ln 1.25 ≈ 0.223e18). Raise-only — a commitment the authority cannot walk back.
769
+ */
770
+ export async function fPoolRaiseMinBuffer(c: LogswapClient, a: { poolId: Hex; minBuffer: bigint; account: Address }) {
771
+ return writeFPool(c, a.poolId, "raiseMinBuffer", [a.minBuffer], a.account);
772
+ }
773
+
774
+ /**
775
+ * Set leg `j`'s liquidity (operator, sole LP only): grow, shrink, retire (`newLj = 0`), or re-admit
776
+ * a retired leg at a named mark `x`. For a live leg leave `x` undefined — its mark is the market's.
777
+ * The base moves single-sidedly through the lock; shares adjust to hold the share value.
778
+ */
779
+ export async function fPoolSetLegL(
780
+ c: LogswapClient,
781
+ a: { poolId: Hex; j: number; newLj: bigint; x?: bigint; account: Address },
782
+ ) {
783
+ return writeFPool(c, a.poolId, "setLegL", [BigInt(a.j), a.newLj, a.x ?? NO_X], a.account);
784
+ }
785
+
786
+ /** Admit a base the pool has never held, at a named mark, with liquidity `Lj` (operator, sole LP only). */
787
+ export async function fPoolAdmitLeg(
788
+ c: LogswapClient,
789
+ a: { poolId: Hex; base: Address; Lj: bigint; x: bigint; account: Address },
790
+ ) {
791
+ return writeFPool(c, a.poolId, "admitLeg", [a.base, a.Lj, a.x], a.account);
792
+ }
793
+
794
+ /**
795
+ * Move shares (ERC-6909 `transfer`) — to another holder, or to `0xdead` to lock them for good.
796
+ * Burning to the dead address is a sponsor's way to make its liquidity permanent: the shares can
797
+ * never be redeemed, and they never count against the sole-LP precondition.
798
+ */
799
+ export async function fPoolTransferShares(
800
+ c: LogswapClient,
801
+ a: { poolId: Hex; to: Address; shares: bigint; account: Address },
802
+ ) {
803
+ const wallet = requireWallet(c);
804
+ const { request } = await c.public.simulateContract({
805
+ address: c.addresses.fPoolManager!,
806
+ abi: fPoolManagerAbi,
807
+ functionName: "transfer",
808
+ args: [a.to, fPoolShareId(a.poolId), a.shares],
809
+ account: a.account,
810
+ } as never);
811
+ return wallet.writeContract(request as never);
812
+ }
813
+
814
+ // ─── discovery from the log stream ────────────────────────────────────────────
815
+
816
+ export interface DiscoveredFPool {
817
+ poolId: Hex;
818
+ quote: Address;
819
+ bases: Address[];
820
+ weights: bigint[];
821
+ phi: bigint;
822
+ feesOnly: boolean;
823
+ authority: Address;
824
+ /** The block the pool was initialized in — its age, for annualising realized income. */
825
+ block: bigint;
826
+ /** Whether `seed` has run; an unseeded pool has no floor and no float, so nothing to show. */
827
+ seeded: boolean;
828
+ /** The quote the pool was born with. Zero is the launchpad's signature (launchpad.md §3). */
829
+ q0: bigint;
830
+ /**
831
+ * The PRODUCT the pool is, by the spec's definition rather than a guess: a LAUNCH is one asset
832
+ * born at Q = 0 — a resting ask under a token, nothing raised, the creator alone on the lever;
833
+ * everything else is a liquidity BASKET (public, quote-funded). Read from the Seeded log, so a
834
+ * one-asset pool seeded WITH quote files correctly as liquidity, not as a pad.
835
+ */
836
+ shape: FPoolShape;
837
+ }
838
+
839
+ /**
840
+ * Every F pool ever created, from `Initialize` logs alone. Possible only because the event
841
+ * carries the full key preimage (bases, weights, phi, feesOnly) — the poolId is a hash and could
842
+ * never be inverted; contracts PR #17 exists for exactly this call.
843
+ */
844
+ export async function discoverFPools(
845
+ c: LogswapClient,
846
+ opts: { fromBlock?: bigint; toBlock?: bigint } = {},
847
+ ): Promise<DiscoveredFPool[]> {
848
+ type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "Initialize" }>;
849
+ const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Initialize");
850
+ if (!ev) throw new Error("logswap: F Initialize event missing from the generated ABI");
851
+ type SeedEv = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "Seeded" }>;
852
+ const seedEv = fPoolManagerAbi.find((x): x is SeedEv => x.type === "event" && x.name === "Seeded");
853
+ const [logs, seeds] = await Promise.all([
854
+ c.public.getLogs({
855
+ address: c.addresses.fPoolManager!,
856
+ event: ev,
857
+ fromBlock: opts.fromBlock ?? 0n,
858
+ toBlock: opts.toBlock ?? "latest",
859
+ }),
860
+ seedEv
861
+ ? c.public.getLogs({ address: c.addresses.fPoolManager!, event: seedEv, fromBlock: opts.fromBlock ?? 0n, toBlock: opts.toBlock ?? "latest" })
862
+ : Promise.resolve([]),
863
+ ]);
864
+ const q0Of = new Map<string, bigint>();
865
+ for (const l of seeds) {
866
+ const a = l.args as { poolId: Hex; Q0: bigint };
867
+ q0Of.set(a.poolId.toLowerCase(), a.Q0);
868
+ }
869
+ return logs.map((l) => {
870
+ const a = l.args as {
871
+ poolId: Hex; quote: Address; authority: Address; bases: readonly Address[];
872
+ weights: readonly bigint[]; phi: bigint; feesOnly: boolean;
873
+ };
874
+ const q0 = q0Of.get(a.poolId.toLowerCase());
875
+ return {
876
+ poolId: a.poolId,
877
+ quote: a.quote,
878
+ bases: [...a.bases],
879
+ weights: [...a.weights],
880
+ phi: a.phi,
881
+ feesOnly: a.feesOnly,
882
+ authority: a.authority,
883
+ block: l.blockNumber ?? 0n,
884
+ seeded: q0 !== undefined,
885
+ q0: q0 ?? 0n,
886
+ shape: a.bases.length === 1 && (q0 ?? 0n) === 0n ? "launch" : "basket",
887
+ };
888
+ });
889
+ }
890
+
891
+
892
+ /**
893
+ * Every holder of a pool's shares, from the ERC-6909 Transfer stream — the id is an indexed
894
+ * topic, so one filtered log query aggregates the whole ledger. The 0xdead entry is the
895
+ * MIN_SHARES lock from seeding; callers usually label rather than hide it.
896
+ */
897
+ export async function fPoolShareHolders(
898
+ c: LogswapClient,
899
+ poolIdHex: Hex,
900
+ opts: { fromBlock?: bigint } = {},
901
+ ): Promise<Array<{ holder: Address; shares: bigint }>> {
902
+ type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "Transfer" }>;
903
+ const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Transfer");
904
+ if (!ev) throw new Error("logswap: 6909 Transfer event missing from the generated ABI");
905
+ const id = fPoolShareId(poolIdHex);
906
+ const logs = await c.public.getLogs({
907
+ address: c.addresses.fPoolManager!,
908
+ event: ev,
909
+ args: { id },
910
+ fromBlock: opts.fromBlock ?? 0n,
911
+ toBlock: "latest",
912
+ });
913
+ const bal = new Map<string, bigint>();
914
+ for (const l of logs) {
915
+ const a = l.args as { from: Address; to: Address; amount: bigint };
916
+ if (a.from !== "0x0000000000000000000000000000000000000000") bal.set(a.from, (bal.get(a.from) ?? 0n) - a.amount);
917
+ if (a.to !== "0x0000000000000000000000000000000000000000") bal.set(a.to, (bal.get(a.to) ?? 0n) + a.amount);
918
+ }
919
+ return [...bal.entries()]
920
+ .filter(([, v]) => v > 0n)
921
+ .map(([holder, shares]) => ({ holder: holder as Address, shares }))
922
+ .sort((a, b) => (b.shares > a.shares ? 1 : -1));
923
+ }
924
+
925
+ /** Every F pool's state in one lens call (the raw manager struct: quote, phi, authority, feesOnly, seeded, dissolved, n, Q, L, shares, theta0, bigSigma, leverTheta). */
926
+ export async function getFPoolsRaw(c: LogswapClient, poolIds: Hex[]): Promise<readonly unknown[]> {
927
+ const { logswapLensAbi } = await import("./generated.js");
928
+ return (await c.public.readContract({ address: c.addresses.lens, abi: logswapLensAbi, functionName: "getFPools", args: [poolIds] } as never)) as readonly unknown[];
929
+ }
930
+
931
+ /** Share value from the lens — (L + Q)/shares, WAD per share — the same figure as `fPoolShareValue`, from one source. */
932
+ export async function shareValueOnChain(c: LogswapClient, poolId: Hex): Promise<bigint> {
933
+ const { logswapLensAbi } = await import("./generated.js");
934
+ return (await c.public.readContract({ address: c.addresses.lens, abi: logswapLensAbi, functionName: "shareValue", args: [poolId] } as never)) as bigint;
935
+ }