@logswap/sdk 0.5.44 → 0.5.46

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.
package/src/fpool.ts CHANGED
@@ -12,10 +12,17 @@
12
12
  * key is variable-length and only ever partially needed (no swap path is O(n)), so it is stored
13
13
  * once at `initialize` and every later call names `(poolId, j)`. Callers hold a `bytes32`.
14
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.
15
+ * **Two products, one contract.** The basket (n > 1) and the launch (n = 1, the creator alone on
16
+ * the lever) are the same bytecode under two key fields — the KIND (what the manager promises
17
+ * structurally) and the HOOK (the pool's policy: its roles, its gates, the terms of every harvest
18
+ * and release; decisions 041, 044) — and nothing in the maths. `describeFPool` below reports
19
+ * which shape a given pool is, because a UI has to decide what to render.
20
+ *
21
+ * **The roles are the hook's, not the manager's** (F version 15). The manager holds no
22
+ * authority, operator, gate or list: it asks the pool's hook `may(who, action)` and acts on the
23
+ * answer. So `getFPool` carries the hook and what it declared, {@link getFPoolRoles} reads the
24
+ * roles FROM the hook, and every role write — the handover, the operator, the gates, the list —
25
+ * is a write to the hook ({@link fPoolProposeAuthority}, {@link fPoolSetGates}, …).
19
26
  *
20
27
  * **Zaps live on the ROUTER, not here.** Permit2 is AllowanceTransfer mode, whose standing
21
28
  * allowance is keyed by spender, so a second router would cost the user an allowance per token
@@ -24,21 +31,31 @@
24
31
  */
25
32
 
26
33
  import { encodeFunctionData, toFunctionSelector, type Address, type Hash, type Hex } from "viem";
27
- import { fPoolManagerAbi, logswapRouterAbi } from "./generated.js";
34
+ import { fPoolManagerAbi, logswapRouterAbi, padHookAbi, polHookAbi, privateHookAbi, publicHookAbi } from "./generated.js";
28
35
 
29
36
  /**
30
- * One contract, one ABI (decisions 039, F version 12): the sponsor surface — `harvest`, `claim`,
31
- * `dissolve`, the handover, the gates, the operator, `setLegs`, the names, the fee parameters —
32
- * is `FPoolManager`'s own code again. From 031 to 038 it was a delegatecall facet
33
- * (`FPoolSponsor`) and this ABI was the union of the two compiled halves; the union is gone with
34
- * the facet, and the generated ABI is the whole surface.
37
+ * One contract, one ABI (decisions 039, F version 12): `harvest`, `claim`, `retire`, `regenerate`,
38
+ * `setLegs`, the names, the fee parameters are `FPoolManager`'s own code. The roles left it for
39
+ * the hooks in F version 15 (decisions 044): {@link fHookBaseAbi} is what every hook shares,
40
+ * {@link privateHookAbi} the desk's, {@link padHookAbi} / {@link polHookAbi} / {@link publicHookAbi}
41
+ * the kinds'.
42
+ */
43
+ export { fPoolManagerAbi, padHookAbi, polHookAbi, privateHookAbi, publicHookAbi };
44
+ /**
45
+ * What every protocol hook shares (`FHookBase`): the roles and their handover, `harvestCut`,
46
+ * `maxHarvest`. `polHookAbi` is a superset of it and carries nothing that collides, so it stands
47
+ * in for the base — a call through it to any of the four hooks decodes the base's functions.
35
48
  */
36
- export { fPoolManagerAbi };
49
+ export const fHookBaseAbi = polHookAbi;
37
50
  import type { LogswapClient } from "./client.js";
38
51
 
39
52
  const WAD = 10n ** 18n;
40
53
  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
41
54
 
55
+ /** The optional questions a hook declared at birth (decisions 044), as bits of `Pool.asks`. */
56
+ export const ASK_SWAP = 1;
57
+ export const ASK_MINT = 2;
58
+
42
59
  /** Everything scalar about a basket pool, in one round trip's worth of reads. */
43
60
  export interface FPoolState {
44
61
  /** The pool's id — the hash of its key. There is no per-pool address. */
@@ -63,32 +80,25 @@ export interface FPoolState {
63
80
  compositeX: bigint;
64
81
  /** Σ = ∫ Σ_j w_j (dx_j)², accrued even though the fee is fixed — the kernel stays calculable. */
65
82
  bigSigma: bigint;
66
- /** The cold key (decisions 037): seeds the pool, hands it over, names it, gates it (private). A creator, a treasury, a governance contract. */
67
- authority: Address;
68
- /** The proposed next authority, until it accepts — zero when nothing is pending (decisions 020). */
69
- pendingAuthority: Address;
70
83
  /**
71
- * The hot key that RUNS the pool (decisions 037): `harvest`, `dissolve`, `setLegs`. Zero means
72
- * the authority itself — see {@link fPoolOperatorOf}. Appointed two-step on a private pool.
84
+ * The pool's policy (decisions 041, 044): the contract the key names, holding the pool's roles
85
+ * and answering every question the manager does not decide itself. Read its roles with
86
+ * {@link getFPoolRoles}; one of the deployment's four ({@link fPoolHookFor}) on every protocol pool.
73
87
  */
74
- operator: Address;
75
- /** The proposed next operator, until it accepts — zero when nothing is pending. */
76
- pendingOperator: Address;
88
+ hook: Address;
89
+ /** Whether the hook asked to be consulted on swaps ({@link ASK_SWAP}) and mints ({@link ASK_MINT}) — the desk's hook does, no other. */
90
+ asks: number;
77
91
  /** The sponsor's own label for this pool, mutable and never identity. "" when unnamed. */
78
92
  name: string;
79
- /** Only `allowed` accounts may receive minted shares. */
80
- gateMint: boolean;
81
- /** Only `allowed` takers may swap. */
82
- gateSwap: boolean;
83
93
  /** The block of the last harvest / composition action: no swap runs in it. */
84
94
  restructureBlock: number;
85
95
  /** The rung — see {@link FPoolKind}. */
86
96
  kind: FPoolKind;
87
- /** False before the first seed — and between a dissolve and the next one (decisions 032). */
97
+ /** False before the first seed — and between a retire and the next one (decisions 032). */
88
98
  seeded: boolean;
89
99
  /**
90
- * The live GENERATION (decisions 032). Every dissolve retires one into a frozen bundle its
91
- * shares redeem ({@link fPoolRedeem}) and opens the next; the share id carries it
100
+ * The live GENERATION (decisions 032). Every retire freezes one into a bundle its shares
101
+ * redeem ({@link fPoolRedeem}) and opens the next; the share id carries it
92
102
  * ({@link fPoolShareId}), so generations `0 … gen-1` are retired claims and `gen` is the pool.
93
103
  */
94
104
  gen: number;
@@ -110,28 +120,22 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
110
120
  } as never) as Promise<T>;
111
121
 
112
122
  // `getPool` returns the whole scalar struct in one call — the singleton's shape makes what used
113
- // to be a dozen getters a single read.
123
+ // to be a dozen getters a single read. No role in it since F version 15: those are the hook's.
114
124
  const p = await rd<{
115
125
  quote: Address;
116
126
  phi: bigint;
117
- authority: Address;
127
+ hook: Address;
118
128
  kind: number;
119
129
  seeded: boolean;
120
130
  gen: number;
121
131
  n: number;
122
132
  restructureBlock: number;
123
- gateMint: boolean;
124
- gateSwap: boolean;
133
+ asks: number;
125
134
  Q: bigint;
135
+ bigSigma: bigint;
126
136
  L: bigint;
127
137
  shares: bigint;
128
138
  theta0: bigint;
129
- bigSigma: bigint;
130
- harvestedTheta: bigint;
131
- pendingAuthority: Address;
132
- operator: Address;
133
- pendingOperator: Address;
134
- incomeTaken: bigint;
135
139
  name: Hex;
136
140
  }>("getPool", [poolId]);
137
141
 
@@ -156,7 +160,7 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
156
160
  const weights = legs.map((l) => l[1]);
157
161
  const x = legs.map((l) => l[2]); // derived by the manager: x_j = ln(w_j·L / R_j) (decisions 026)
158
162
  const reserves = legs.map((l) => l[3]); // the state
159
- const { quote, Q, L, phi, theta0, bigSigma, authority, pendingAuthority, operator, pendingOperator, gateMint, gateSwap, restructureBlock, seeded, gen } = p;
163
+ const { quote, Q, L, phi, theta0, bigSigma, hook, restructureBlock, seeded, gen } = p;
160
164
  const totalSupply = p.shares;
161
165
 
162
166
  return {
@@ -172,13 +176,9 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
172
176
  theta,
173
177
  compositeX,
174
178
  bigSigma,
175
- authority,
176
- pendingAuthority,
177
- operator,
178
- pendingOperator,
179
+ hook,
180
+ asks: Number(p.asks),
179
181
  name: bytes32ToText(p.name),
180
- gateMint,
181
- gateSwap,
182
182
  restructureBlock: Number(restructureBlock),
183
183
  kind: fPoolKindOf(p.kind),
184
184
  seeded,
@@ -189,14 +189,14 @@ export async function getFPool(c: LogswapClient, poolId: Hex): Promise<FPoolStat
189
189
  }
190
190
 
191
191
  /**
192
- * The kind of pool (decisions 036, 038). `public`: the protocol's own pool — born with the fee
193
- * collector as its authority (which may hand it to a governance contract); no gates, no operator.
194
- * `private`, the desk: every entry — gates, a list, an operator that reshapes the legs between
195
- * generations, an unbounded harvest, the seed kept. `pol`, a treasury's pool or a basket: no
196
- * gates, no operator — composition and access fixed at birth; harvest unbounded, seed the
197
- * seeder's. `pad`, a launch: a POL whose harvest never lifts the floor above the seed floor and
198
- * whose seed the contract minted to 0xdead — one asset, open to all. private → pol → pad is the
199
- * ladder of rigidity; public is a pol held by the protocol.
192
+ * The kind of pool (decisions 036, 038, 044) — what the manager promises structurally, whatever
193
+ * the pool's hook answers. `public`: the protocol's own pool — its hook requires the collector as
194
+ * creator; no gate, no operator. `private`, the desk: the one kind whose hook may gate swaps and
195
+ * mints, with an operator that reshapes the legs between generations. `pol`, a treasury's pool
196
+ * or a basket: composition and access fixed at birth. `pad`, a launch: a POL whose harvest is
197
+ * income only whatever its hook says and whose seed the contract minted to 0xdead — one asset,
198
+ * open to all. private → pol → pad is the ladder of rigidity; public is a pol held by the
199
+ * protocol. Every rate, role and gate is the hook's.
200
200
  */
201
201
  export type FPoolKind = "public" | "private" | "pol" | "pad";
202
202
  /** The key's enum values. */
@@ -208,40 +208,110 @@ export function fPoolKindOf(n: number | bigint): FPoolKind {
208
208
  return k;
209
209
  }
210
210
 
211
+ /** The deployment's hook for a kind (decisions 044): what a key of that kind names. */
212
+ export function fPoolHookFor(c: LogswapClient, kind: FPoolKind): Address {
213
+ const a = c.addresses;
214
+ const hook = kind === "public" ? a.publicHook : kind === "private" ? a.privateHook : kind === "pol" ? a.polHook : a.padHook;
215
+ if (!hook) throw new Error(`logswap: this deployment carries no ${kind} hook`);
216
+ return hook;
217
+ }
218
+
219
+ /** The hook a pool named in its key — one read; {@link getFPool} carries it too. */
220
+ export async function fPoolHookOf(c: LogswapClient, poolId: Hex): Promise<Address> {
221
+ const p = (await c.public.readContract({
222
+ address: c.addresses.fPoolManager!,
223
+ abi: fPoolManagerAbi,
224
+ functionName: "getPool",
225
+ args: [poolId],
226
+ } as never)) as { quote: Address; hook: Address };
227
+ if (/^0x0{40}$/i.test(p.quote)) throw new Error(`logswap: no F pool with id ${poolId}`);
228
+ return p.hook;
229
+ }
230
+
231
+ /**
232
+ * A pool's roles, as its HOOK holds them (decisions 044). `creator` is immutable identity —
233
+ * who was born holding the pool; `authority` is the cold key that owns it now, the creator until
234
+ * a two-step handover; on the desk's kind the `operator` runs it (zero = the authority itself,
235
+ * {@link fPoolOperatorOf}) and the two gates and the list are its. Every other kind has no
236
+ * operator and no gate: those fields read zero / false.
237
+ */
238
+ export interface FPoolRoles {
239
+ hook: Address;
240
+ creator: Address;
241
+ authority: Address;
242
+ /** The proposed next authority, until it accepts — zero when nothing is pending (decisions 020). */
243
+ pendingAuthority: Address;
244
+ operator: Address;
245
+ pendingOperator: Address;
246
+ gateMint: boolean;
247
+ gateSwap: boolean;
248
+ /** The hook's own ceiling on a harvest now — the manager's `harvestable` narrowed by the hook's rule. */
249
+ maxHarvest: bigint;
250
+ }
251
+
252
+ /** Read a pool's roles from its hook. `state` saves the `getPool` round trip when already in hand. */
253
+ export async function getFPoolRoles(c: LogswapClient, poolId: Hex, state?: Pick<FPoolState, "hook" | "kind">): Promise<FPoolRoles> {
254
+ const s = state ?? (await getFPool(c, poolId));
255
+ const rdHook = <T>(abi: readonly unknown[], functionName: string) =>
256
+ c.public.readContract({ address: s.hook, abi, functionName, args: [poolId] } as never) as Promise<T>;
257
+ const [creator, authority, pendingAuthority, maxHarvest] = await Promise.all([
258
+ rdHook<Address>(fHookBaseAbi, "creator"),
259
+ rdHook<Address>(fHookBaseAbi, "authority"),
260
+ rdHook<Address>(fHookBaseAbi, "pendingAuthority"),
261
+ rdHook<bigint>(fHookBaseAbi, "maxHarvest"),
262
+ ]);
263
+ const desk = s.kind === "private";
264
+ const [operator, pendingOperator, gateMint, gateSwap] = desk
265
+ ? await Promise.all([
266
+ rdHook<Address>(privateHookAbi, "operator"),
267
+ rdHook<Address>(privateHookAbi, "pendingOperator"),
268
+ rdHook<boolean>(privateHookAbi, "gateMint"),
269
+ rdHook<boolean>(privateHookAbi, "gateSwap"),
270
+ ])
271
+ : [ZERO_ADDRESS, ZERO_ADDRESS, false, false];
272
+ return { hook: s.hook, creator, authority, pendingAuthority, operator, pendingOperator, gateMint, gateSwap, maxHarvest };
273
+ }
274
+
211
275
  /** Which of the two products a pool is configured as — a pad is a launch, anything else a basket/pool. A UI has to choose what to render. */
212
276
  export type FPoolShape = "launch" | "basket";
213
277
 
214
278
  export interface FPoolDescription {
215
279
  shape: FPoolShape;
216
- /** n = 1 with a private authority is the launch shape (f-pool §6.5). */
280
+ /** The pad is the launch shape, by the kind (decisions 036). */
217
281
  legs: number;
218
282
  /** True when the pool holds no quote: an all-base resting ask. base→quote is the one blocked path. */
219
283
  atFloor: boolean;
220
- /** Fee income earned per unit L so far, `θ₀ − θ` with the lever's displacement backed out. */
221
- feePerL: bigint;
222
- /** The model-free LP edge, `F − ½Σ`. Exact at any n: V is separable in the logs. */
223
- edgePerL: bigint;
284
+ /**
285
+ * This generation's fee income still in the pool, quote: `L·(θ₀ − θ)` (F version 14 —
286
+ * `feePerL` before, per unit L). A harvest of capital moves the reference floor, so this counts
287
+ * from the first wei after it.
288
+ */
289
+ unharvested: bigint;
290
+ /** The most a harvest may take now by the KIND's promise: the bid, and on a pad the income. The hook may allow less — {@link FPoolRoles.maxHarvest}. */
291
+ harvestable: bigint;
292
+ /** The dead shares' banked dividend — a pad's pot, the creator's income until a rung is climbed (decisions 041). */
293
+ pot: bigint;
224
294
  }
225
295
 
226
296
  export async function describeFPool(c: LogswapClient, s: FPoolState): Promise<FPoolDescription> {
227
- const rd = <T>(functionName: string) =>
297
+ const rd = <T>(functionName: string, args: readonly unknown[] = [s.poolId]) =>
228
298
  c.public.readContract({
229
299
  address: c.addresses.fPoolManager!,
230
300
  abi: fPoolManagerAbi,
231
301
  functionName,
232
- args: [s.poolId],
302
+ args,
233
303
  } as never) as Promise<T>;
234
- // an unseeded pool has no L: nothing earned, and θ (hence feePerL, hence edgePerL) is not
235
- // defined at L = 0. Older deployments revert with DivWadFailed rather than answering zero.
236
- const [feePerL, edgePerL] = s.L > 0n
237
- ? await Promise.all([rd<bigint>("feePerL"), rd<bigint>("edgePerL")])
238
- : [0n, 0n];
304
+ // an unseeded pool has no L: nothing earned, nothing to harvest, no pot
305
+ const [unharvested, harvestable, pot] = s.L > 0n
306
+ ? await Promise.all([rd<bigint>("unharvested"), rd<bigint>("harvestable"), rd<bigint>("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
307
+ : [0n, 0n, 0n];
239
308
  return {
240
309
  shape: s.kind === "pad" ? "launch" : "basket", // the kind, not n = 1 (decisions 036)
241
310
  legs: s.bases.length,
242
311
  atFloor: s.Q === 0n,
243
- feePerL,
244
- edgePerL,
312
+ unharvested,
313
+ harvestable,
314
+ pot,
245
315
  };
246
316
  }
247
317
 
@@ -798,7 +868,14 @@ export interface FPoolCreateArgs {
798
868
  phi: bigint;
799
869
  /** The rung (decisions 036) — see {@link FPoolKind}. In the key: the pool's identity, never changed after. */
800
870
  kind: FPoolKind;
801
- authority: Address;
871
+ /**
872
+ * The pool's policy (decisions 041, 044): the contract the key names, approved by the protocol
873
+ * for the kind. Defaults to the deployment's hook for `kind` ({@link fPoolHookFor}) — the one
874
+ * every protocol pool names.
875
+ */
876
+ hook?: Address;
877
+ /** Who is born holding the pool: immutable identity, the pool's first authority (the hook records it). */
878
+ creator: Address;
802
879
  /**
803
880
  * The differentiator, and nothing else. Identity is the key's hash, so one sponsor cannot run
804
881
  * two pools of the same shape without varying it — the second `initialize` would land on the
@@ -832,12 +909,26 @@ export function sortFPoolLegs<T>(bases: Address[], ...companions: T[][]): { base
832
909
  return { bases: idx.map((i) => bases[i]!), companions: companions.map((arr) => idx.map((i) => arr[i]!)) };
833
910
  }
834
911
 
912
+ /** The key as the manager hashes it: legs sorted, the kind's hook filled in when none is named. */
913
+ export function fPoolKeyOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">) {
914
+ const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
915
+ return {
916
+ quote: a.quote,
917
+ bases,
918
+ weights: companions[0]!,
919
+ phi: a.phi,
920
+ kind: F_KIND[a.kind],
921
+ hook: a.hook ?? fPoolHookFor(c, a.kind),
922
+ creator: a.creator,
923
+ salt: a.salt ?? ZERO_SALT,
924
+ };
925
+ }
926
+
835
927
  /** Create the pool (no funds move — `seed` arms it). Returns the tx hash; read the id with `fPoolIdOf`. */
836
928
  export async function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs) {
837
929
  const sum = a.weights.reduce((x, y) => x + y, 0n);
838
930
  if (sum !== 10n ** 18n) throw new Error(`logswap: weights sum to ${sum}, expected 1e18`);
839
- const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
840
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], authority: a.authority, salt: a.salt ?? ZERO_SALT };
931
+ const key = fPoolKeyOf(c, a);
841
932
  const wallet = requireWallet(c);
842
933
  const { request } = await c.public.simulateContract({
843
934
  address: c.addresses.fPoolManager!,
@@ -851,8 +942,7 @@ export async function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs) {
851
942
 
852
943
  /** The pool id is the key's hash — pure, so it can be read before or after creation. */
853
944
  export async function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): Promise<Hex> {
854
- const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
855
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], authority: a.authority, salt: a.salt ?? ZERO_SALT };
945
+ const key = fPoolKeyOf(c, a);
856
946
  return c.public.readContract({
857
947
  address: c.addresses.fPoolManager!,
858
948
  abi: fPoolManagerAbi,
@@ -889,82 +979,72 @@ export async function fPoolSeed(
889
979
  }
890
980
 
891
981
  /**
892
- * Retire the live generation (decisions 025, 032, 035) — the raw entry. Allowed only at an empty
893
- * bid, Q ≤ L/1e9, on every pool: the market took it, or the authority harvested it out first
894
- * ({@link fPoolRetire} does both in one transaction). The live (S, Q, R_j) become a frozen bundle
895
- * every share of this generation redeems ({@link fPoolRedeem}) or rolls ({@link fPoolRollIn}),
896
- * forever; the pool is unseeded until the next {@link fPoolSeed}, which opens generation
897
- * `gen + 1` under the same id, gates and lists.
982
+ * Retire the live generation (decisions 025, 032, 035) — the raw entry (`dissolve` before F
983
+ * version 14). Allowed only at an empty bid, Q ≤ L/1e9, on every pool: the market took it, or the
984
+ * operator harvested it out first ({@link fPoolRetire} does both in one transaction). The live
985
+ * (S, Q, R_j) become a frozen bundle every share of this generation redeems ({@link fPoolRedeem})
986
+ * or rolls ({@link fPoolRollIn}), forever; the pool is unseeded until the next {@link fPoolSeed},
987
+ * which opens generation `gen + 1` under the same id, hook, roles, gates and lists.
898
988
  */
899
- export async function fPoolDissolve(c: LogswapClient, a: { poolId: Hex; account: Address }) {
900
- return writeFPool(c, a.poolId, "dissolve", [], a.account);
989
+ export async function fPoolRetireRaw(c: LogswapClient, a: { poolId: Hex; account: Address }) {
990
+ return writeFPool(c, a.poolId, "retire", [], a.account);
901
991
  }
902
992
 
903
993
  /**
904
- * Retire the live generation as ONE transaction: the harvests that empty the bid
994
+ * Retire the live generation as ONE transaction: the harvest that empties the bid
905
995
  * ({@link fPoolEmptyingHarvests} — a dividend to every share; none when it is already empty),
906
- * then `dissolve` — the manager's `multicall`, the authority as sender. What a sponsor's
907
- * "dissolve" button should call. Throws before sending on a pad that still bids.
996
+ * then `retire` — the manager's `multicall`, the operator as sender. What a sponsor's
997
+ * "retire" button should call. Throws before sending on a pad that still bids.
908
998
  */
909
999
  export async function fPoolRetire(c: LogswapClient, a: { poolId: Hex; account: Address }) {
910
1000
  const harvests = await fPoolEmptyingHarvests(c, a.poolId);
911
- if (harvests.length === 0) return fPoolDissolve(c, a);
1001
+ if (harvests.length === 0) return fPoolRetireRaw(c, a);
912
1002
  const enc = (functionName: string, args: unknown[]) =>
913
1003
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
914
- const calls: Hex[] = [...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])), enc("dissolve", [a.poolId])];
1004
+ const calls: Hex[] = [...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])), enc("retire", [a.poolId])];
915
1005
  return writeFPool0(c, "multicall", [calls], a.account);
916
1006
  }
917
1007
 
918
1008
  /**
919
1009
  * The harvests that empty an unlocked pool's bid, in order — what a relaunch sends before its
920
- * `dissolve` (decisions 035: a dissolve needs an empty bid on every pool). One `harvest(Q)` when a
921
- * collector is set. With none, the cut on income STAYS in the pool as LP quote (decisions 002)
922
- * and reads as taken income, so a second harvest — capital, no cut — takes the rest; the
923
- * contract's own arithmetic, replayed. `[]` when the bid is already empty. Throws on a
924
- * pad that still bids (its bid empties only as the market sells it down), and in
925
- * the one shape two harvests cannot empty (no collector, income above Q — harvest by hand).
1010
+ * `retire` (decisions 035: a retire needs an empty bid on every pool). Since F version 14 one
1011
+ * `harvest(Q)` does it: the protocol's cut leaves the pool with the amount (with no collector
1012
+ * there is no cut). `[]` when the bid is already empty. Throws on a pad that still bids (its bid
1013
+ * empties only as the market sells it down), and when the pool's hook keeps a floor on the bid
1014
+ * (`PolHook.keepQuote`, decisions 044): a `retire` under that hook waits for the market.
926
1015
  */
927
1016
  export async function fPoolEmptyingHarvests(c: LogswapClient, poolId: Hex): Promise<bigint[]> {
928
- const rd = <T>(functionName: string, args: readonly unknown[] = []) =>
929
- c.public.readContract({ address: c.addresses.fPoolManager!, abi: fPoolManagerAbi, functionName, args } as never) as Promise<T>;
930
- const [p, feePerL, fee, exempt, collector] = await Promise.all([
931
- rd<{ Q: bigint; L: bigint; kind: number; incomeTaken: bigint }>("getPool", [poolId]),
932
- rd<bigint>("feePerL", [poolId]),
933
- rd<bigint>("HARVEST_PROTOCOL_CUT"),
934
- rd<boolean>("harvestCutExempt", [poolId]),
935
- rd<Address>("feeCollector"),
936
- ]);
937
- if (p.Q === 0n) return [];
938
- if (p.kind === F_KIND.pad) throw new Error("logswap: a pad is retired only once its bid is empty (decisions 035)");
939
- const earned = feePerL > 0n ? (feePerL * p.L) / WAD : 0n;
940
- let taken = p.incomeTaken;
941
- let q = p.Q;
942
- const amounts: bigint[] = [];
943
- for (let i = 0; i < 2 && q > 0n; i++) {
944
- const amount = q;
945
- const left = earned > taken ? earned - taken : 0n;
946
- const taxable = amount < left ? amount : left;
947
- taken += taxable;
948
- const pFee = exempt ? 0n : (taxable * fee) / WAD;
949
- const leaving = pFee !== 0n && collector !== ZERO_ADDRESS ? amount : amount - pFee;
950
- q -= leaving;
951
- amounts.push(amount);
952
- }
953
- if (q > 0n) throw new Error("logswap: the bid does not empty in two harvests (no collector, income above Q) — harvest by hand first");
954
- return amounts;
1017
+ const s = await getFPool(c, poolId);
1018
+ if (s.Q === 0n) return [];
1019
+ if (s.kind === "pad") throw new Error("logswap: a pad is retired only once its bid is empty (decisions 035)");
1020
+ const { maxHarvest } = await getFPoolRoles(c, poolId, s);
1021
+ if (maxHarvest < s.Q) throw new Error("logswap: the pool's hook keeps a floor on the bid — it cannot be harvested empty (decisions 044)");
1022
+ return [s.Q];
1023
+ }
1024
+
1025
+ /**
1026
+ * Release the dead shares' pot under the hook's terms (decisions 041, 044) — a pad's graduation.
1027
+ * The manager harvests the generation's income first (the pot then holds everything the dead
1028
+ * shares earned), asks the hook, and applies its answer: a share to the payee it names (the pad's
1029
+ * creator — the pool's authority), a cut to the collector, the rest bought in as more of the
1030
+ * float the pool owns, a slice of every reserve burned. The hook refuses under its rung's
1031
+ * threshold (`PadHook.NotGraduated`). Whoever runs the pool.
1032
+ */
1033
+ export async function fPoolRegenerate(c: LogswapClient, a: { poolId: Hex; to?: Address; account: Address }) {
1034
+ return writeFPool(c, a.poolId, "regenerate", [a.to ?? a.account], a.account);
955
1035
  }
956
1036
 
957
1037
  /**
958
1038
  * The relaunch as ONE transaction (decisions 025, 032, 035, 037): `harvest` the bid out — a
959
- * dividend to every share, {@link fPoolEmptyingHarvests} — `dissolve` the live generation at the
1039
+ * dividend to every share, {@link fPoolEmptyingHarvests} — `retire` the live generation at the
960
1040
  * empty bid, `redeem` the caller's own shares of it (when `shares` > 0), optionally `setLegs` the
961
1041
  * next generation's composition (`legs`: a private pool's reshape — retire, admit, reweight), and
962
1042
  * `seed` it with what came back — the manager's `multicall`, one sender throughout, the same
963
1043
  * pool. The operator's when one is appointed and the seed's inventory is its own; by default the
964
- * authority's. Gates, allowlist and names carry over; anything else goes in `extra`, pre-encoded
965
- * against {@link fPoolManagerAbi}. The seed names reserves (decisions 029): `r0` outright — one
966
- * per leg of the NEXT table, 0 for a retired leg — or `x0` marks against the next table's
967
- * `weights`.
1044
+ * authority's. The hook, its roles, gates, allowlist and the name carry over; anything else goes
1045
+ * in `extra`, pre-encoded against {@link fPoolManagerAbi}. The seed names reserves (decisions
1046
+ * 029): `r0` outright — one per leg of the NEXT table, 0 for a retired leg — or `x0` marks
1047
+ * against the next table's `weights`.
968
1048
  */
969
1049
  export async function fPoolRelaunch(
970
1050
  c: LogswapClient,
@@ -986,7 +1066,7 @@ export async function fPoolRelaunch(
986
1066
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
987
1067
  const calls: Hex[] = [
988
1068
  ...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])),
989
- enc("dissolve", [a.poolId]),
1069
+ enc("retire", [a.poolId]),
990
1070
  ...(a.shares > 0n ? [enc("redeem", [a.poolId, BigInt(gen), a.account, a.shares])] : []),
991
1071
  ...(a.legs ? [enc("setLegs", [a.poolId, a.legs.weights, a.legs.newBases ?? []])] : []),
992
1072
  enc("seed", [a.poolId, a.L0, r0, a.Q0]),
@@ -1040,7 +1120,8 @@ export function fPoolLaunchCalls(
1040
1120
  const given = "r0" in a ? a.r0 : a.x0.map((x, j) => fPoolReserveAt((a.weights[j]! * a.L0) / WAD, x));
1041
1121
  if (given.length !== a.bases.length) throw new Error(`logswap: ${given.length} reserves for ${a.bases.length} legs`);
1042
1122
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights, given);
1043
- const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], authority: a.authority, salt: a.salt ?? ZERO_SALT };
1123
+ if (!a.hook) throw new Error("logswap: fPoolLaunchCalls needs the key's hook — fPoolLaunch fills it from the deployment");
1124
+ const key = { quote: a.quote, bases, weights: companions[0]!, phi: a.phi, kind: F_KIND[a.kind], hook: a.hook, creator: a.creator, salt: a.salt ?? ZERO_SALT };
1044
1125
  const enc = (functionName: string, args: unknown[]) =>
1045
1126
  encodeFunctionData({ abi: fPoolManagerAbi, functionName, args } as never) as Hex;
1046
1127
  const calls: Hex[] = [];
@@ -1054,16 +1135,25 @@ export function fPoolLaunchCalls(
1054
1135
  calls.push(enc("swapQuoteIn", [poolId, BigInt(j), a.openBuy.amountIn, a.openBuy.minOut ?? 0n, a.account]));
1055
1136
  }
1056
1137
  if (a.name) calls.push(enc("setName", [poolId, textToBytes32(a.name)]));
1057
- if (a.kind === "private") {
1058
- // the desk's opening posture: gated minting, the creator listed. No appointment: the
1059
- // authority is the operator until one is appointed (decisions 037)
1060
- calls.push(enc("setGates", [poolId, true, false]));
1061
- calls.push(enc("setAllowed", [poolId, [a.account], true]));
1062
- }
1138
+ // the desk's gates and list are the HOOK's (decisions 044), not the manager's: they cannot ride
1139
+ // in this batch — `fPoolDeskOpening` sends them right after, as the same creator
1063
1140
  calls.push(...(a.extra ?? []));
1064
1141
  return calls;
1065
1142
  }
1066
1143
 
1144
+ /**
1145
+ * The desk's opening posture, on its hook (decisions 044): gated minting, the creator listed. No
1146
+ * appointment: the authority is the operator until one is appointed (decisions 037). Two writes
1147
+ * to `PrivateHook`; the launch batch cannot carry them (they are not manager calls).
1148
+ */
1149
+ export async function fPoolDeskOpening(c: LogswapClient, a: { poolId: Hex; account: Address; hook?: Address }): Promise<Hash[]> {
1150
+ const hook = a.hook ?? fPoolHookFor(c, "private");
1151
+ return [
1152
+ await fPoolSetGates(c, { poolId: a.poolId, gateMint: true, gateSwap: false, account: a.account, hook }),
1153
+ await fPoolSetAllowed(c, { poolId: a.poolId, who: [a.account], allowed: true, account: a.account, hook }),
1154
+ ];
1155
+ }
1156
+
1067
1157
  /**
1068
1158
  * Create a pool as ONE transaction: `initialize`, `seed`, the seed lock, the opening buy, the
1069
1159
  * name, the private-pool gates — the manager's `multicall`, the creator as sender throughout.
@@ -1086,48 +1176,76 @@ export async function fPoolLaunch(
1086
1176
  a: FPoolLaunchArgs & ({ r0: bigint[] } | { x0: bigint[] }),
1087
1177
  ): Promise<{ hash: Hash; poolId: Hex }> {
1088
1178
  const poolId = await fPoolIdOf(c, a);
1089
- const hash = await writeFPool0(c, "multicall", [fPoolLaunchCalls(poolId, a)], a.account);
1179
+ const calls = fPoolLaunchCalls(poolId, { ...a, hook: a.hook ?? fPoolHookFor(c, a.kind) });
1180
+ const hash = await writeFPool0(c, "multicall", [calls], a.account);
1090
1181
  return { hash, poolId };
1091
1182
  }
1092
1183
 
1184
+ // ─── the roles, on the hook (decisions 044) ─────────────────────────────────────────────
1185
+ //
1186
+ // Every write here goes to the pool's HOOK, not the manager: the manager holds no role. `hook`
1187
+ // may be passed when in hand (`FPoolState.hook`, `FPoolRoles.hook`); otherwise it is read once.
1188
+
1189
+ /** A write keyed by poolId, on the pool's hook. */
1190
+ async function writeHook(c: LogswapClient, abi: readonly unknown[], a: { poolId: Hex; hook?: Address; account: Address }, functionName: string, args: unknown[]) {
1191
+ const wallet = requireWallet(c);
1192
+ const hook = a.hook ?? (await fPoolHookOf(c, a.poolId));
1193
+ const { request } = await c.public.simulateContract({
1194
+ address: hook,
1195
+ abi,
1196
+ functionName,
1197
+ args: [a.poolId, ...args],
1198
+ account: a.account,
1199
+ } as never);
1200
+ return wallet.writeContract(request as never);
1201
+ }
1202
+
1093
1203
  /**
1094
- * Propose the next authority — step one of two (contracts `2aa6b7b`, decisions 020). Nothing moves
1204
+ * Propose the next authority — step one of two (decisions 020), on the pool's hook. Nothing moves
1095
1205
  * until `next` accepts; proposing again (the zero address included) cancels a pending proposal.
1096
- * Indexed on-chain via AuthorityProposed.
1206
+ * Indexed on-chain via the hook's AuthorityProposed.
1097
1207
  */
1098
- export async function fPoolProposeAuthority(c: LogswapClient, a: { poolId: Hex; next: Address; account: Address }) {
1099
- return writeFPool(c, a.poolId, "proposeAuthority", [a.next], a.account);
1208
+ export async function fPoolProposeAuthority(c: LogswapClient, a: { poolId: Hex; next: Address; account: Address; hook?: Address }) {
1209
+ return writeHook(c, fHookBaseAbi, a, "proposeAuthority", [a.next]);
1100
1210
  }
1101
1211
 
1102
- /** Accept a pending proposal — step two; only the proposed address can. Indexed via AuthoritySet. */
1103
- export async function fPoolAcceptAuthority(c: LogswapClient, a: { poolId: Hex; account: Address }) {
1104
- return writeFPool(c, a.poolId, "acceptAuthority", [], a.account);
1212
+ /**
1213
+ * Accept a pending proposal — step two; only the proposed address can. Indexed via the hook's
1214
+ * AuthoritySet. On the desk's hook this clears the operator (pending and accepted) and voids the
1215
+ * seller's list (decisions 040, 044).
1216
+ */
1217
+ export async function fPoolAcceptAuthority(c: LogswapClient, a: { poolId: Hex; account: Address; hook?: Address }) {
1218
+ return writeHook(c, fHookBaseAbi, a, "acceptAuthority", []);
1105
1219
  }
1106
1220
 
1107
- // ─── the sponsor's controls and the desk (contracts bd3dd87, decisions 021) ───────────────
1221
+ // ─── the desk's controls (PrivateHook; decisions 021, 044) ───────────────────────────────
1108
1222
 
1109
- /** Turn the mint / swap gates on or off (authority). `burn` is never gated. */
1223
+ /** Turn the mint / swap gates on or off (the authority, on the desk's hook). `burn` is never gated. */
1110
1224
  export async function fPoolSetGates(
1111
1225
  c: LogswapClient,
1112
- a: { poolId: Hex; gateMint: boolean; gateSwap: boolean; account: Address },
1226
+ a: { poolId: Hex; gateMint: boolean; gateSwap: boolean; account: Address; hook?: Address },
1113
1227
  ) {
1114
- return writeFPool(c, a.poolId, "setGates", [a.gateMint, a.gateSwap], a.account);
1228
+ return writeHook(c, privateHookAbi, a, "setGates", [a.gateMint, a.gateSwap]);
1115
1229
  }
1116
1230
 
1117
- /** Add to or remove from the pool's list, in one call (authority). */
1231
+ /**
1232
+ * Add to or remove from the pool's list, in one call (the authority, on the desk's hook). The list
1233
+ * is keyed by the authority that granted it: a handover starts the buyer with an empty one.
1234
+ */
1118
1235
  export async function fPoolSetAllowed(
1119
1236
  c: LogswapClient,
1120
- a: { poolId: Hex; who: readonly Address[]; allowed: boolean; account: Address },
1237
+ a: { poolId: Hex; who: readonly Address[]; allowed: boolean; account: Address; hook?: Address },
1121
1238
  ) {
1122
- return writeFPool(c, a.poolId, "setAllowed", [a.who, a.allowed], a.account);
1239
+ return writeHook(c, privateHookAbi, a, "setAllowed", [a.who, a.allowed]);
1123
1240
  }
1124
1241
 
1125
- /** Whether `who` is on the pool's list (consulted only while a gate is on). */
1126
- export async function fPoolIsAllowed(c: LogswapClient, poolId: Hex, who: Address): Promise<boolean> {
1242
+ /** Whether `who` is on the pool's list under its current authority (consulted only while a gate is on). */
1243
+ export async function fPoolIsAllowed(c: LogswapClient, poolId: Hex, who: Address, hook?: Address): Promise<boolean> {
1244
+ const address = hook ?? (await fPoolHookOf(c, poolId));
1127
1245
  return (await c.public.readContract({
1128
- address: c.addresses.fPoolManager!,
1129
- abi: fPoolManagerAbi,
1130
- functionName: "allowed",
1246
+ address,
1247
+ abi: privateHookAbi,
1248
+ functionName: "isAllowed",
1131
1249
  args: [poolId, who],
1132
1250
  } as never)) as boolean;
1133
1251
  }
@@ -1165,27 +1283,27 @@ export async function fPoolSponsorName(c: LogswapClient, who: Address): Promise<
1165
1283
  }
1166
1284
 
1167
1285
  /** Who runs the pool (decisions 037): the appointed operator, or the authority until one is. */
1168
- export function fPoolOperatorOf(s: Pick<FPoolState, "authority" | "operator">): Address {
1286
+ export function fPoolOperatorOf(s: Pick<FPoolRoles, "authority" | "operator">): Address {
1169
1287
  return s.operator === ZERO_ADDRESS ? s.authority : s.operator;
1170
1288
  }
1171
1289
 
1172
1290
  /**
1173
- * Propose the operator (authority; private pools only) — the hot key that runs the pool: `harvest`,
1174
- * `dissolve`, `setLegs`. Two-step: the proposed address takes the role with
1291
+ * Propose the operator (the authority, on the desk's hook) — the hot key that runs the pool:
1292
+ * `harvest`, `retire`, `regenerate`, `setLegs`. Two-step: the proposed address takes the role with
1175
1293
  * {@link fPoolAcceptOperator}. The zero address clears at once — the role returns to the authority.
1176
1294
  */
1177
- export async function fPoolAppointOperator(c: LogswapClient, a: { poolId: Hex; operator: Address; account: Address }) {
1178
- return writeFPool(c, a.poolId, "appointOperator", [a.operator], a.account);
1295
+ export async function fPoolAppointOperator(c: LogswapClient, a: { poolId: Hex; operator: Address; account: Address; hook?: Address }) {
1296
+ return writeHook(c, privateHookAbi, a, "appointOperator", [a.operator]);
1179
1297
  }
1180
1298
 
1181
1299
  /** The proposed operator takes the role. */
1182
- export async function fPoolAcceptOperator(c: LogswapClient, a: { poolId: Hex; account: Address }) {
1183
- return writeFPool(c, a.poolId, "acceptOperator", [], a.account);
1300
+ export async function fPoolAcceptOperator(c: LogswapClient, a: { poolId: Hex; account: Address; hook?: Address }) {
1301
+ return writeHook(c, privateHookAbi, a, "acceptOperator", []);
1184
1302
  }
1185
1303
 
1186
1304
  /**
1187
1305
  * The next generation's composition (operator; private pools only; ONLY while the pool is
1188
- * unseeded — between a dissolve and the seed that prices it, decisions 037). `weights` has one
1306
+ * unseeded — between a retire and the seed that prices it, decisions 037). `weights` has one
1189
1307
  * entry per leg the pool has ever had, in index order (0 retires a leg; indices are never reused),
1190
1308
  * then one per base in `newBases`, appended; they sum to 1e18 exactly. No value moves: the
1191
1309
  * following {@link fPoolSeed} names every reserve. A live reshape is {@link fPoolRelaunch} with
@@ -1228,7 +1346,10 @@ export interface DiscoveredFPool {
1228
1346
  weights: bigint[];
1229
1347
  phi: bigint;
1230
1348
  kind: FPoolKind;
1231
- authority: Address;
1349
+ /** The pool's policy — the hook the key names (decisions 041, 044). */
1350
+ hook: Address;
1351
+ /** Who was born holding the pool (immutable). The LIVE authority is the hook's — {@link getFPoolRoles}. */
1352
+ creator: Address;
1232
1353
  /** The block the pool was initialized in — its age, for annualising realized income. */
1233
1354
  block: bigint;
1234
1355
  /** Whether `seed` has run; an unseeded pool has no floor and no float, so nothing to show. */
@@ -1276,8 +1397,8 @@ export async function discoverFPools(
1276
1397
  }
1277
1398
  return logs.map((l) => {
1278
1399
  const a = l.args as {
1279
- poolId: Hex; quote: Address; authority: Address; bases: readonly Address[];
1280
- weights: readonly bigint[]; phi: bigint; kind: number;
1400
+ poolId: Hex; quote: Address; creator: Address; bases: readonly Address[];
1401
+ weights: readonly bigint[]; phi: bigint; kind: number; hook: Address;
1281
1402
  };
1282
1403
  const q0 = q0Of.get(a.poolId.toLowerCase());
1283
1404
  return {
@@ -1287,7 +1408,8 @@ export async function discoverFPools(
1287
1408
  weights: [...a.weights],
1288
1409
  phi: a.phi,
1289
1410
  kind: fPoolKindOf(a.kind),
1290
- authority: a.authority,
1411
+ hook: a.hook,
1412
+ creator: a.creator,
1291
1413
  block: l.blockNumber ?? 0n,
1292
1414
  seeded: q0 !== undefined,
1293
1415
  q0: q0 ?? 0n,
@@ -1296,6 +1418,87 @@ export async function discoverFPools(
1296
1418
  });
1297
1419
  }
1298
1420
 
1421
+ // ─── the policy page: what an authority controls (decisions 044) ────────────────────────
1422
+ //
1423
+ // One hook serves every pool of its kind, so "the pools I control" is not a hook's list: it is
1424
+ // every pool whose hook answers `authority(poolId) == me`. Discovery from the log stream (or the
1425
+ // indexer), then one `authority` read per pool on its hook — viem multicalls them.
1426
+
1427
+ /** A pool an authority controls, with what the policy page shows of it. */
1428
+ export interface FPoolControlled {
1429
+ pool: DiscoveredFPool;
1430
+ roles: FPoolRoles;
1431
+ state: FPoolState;
1432
+ /** The hook's numbers for this pool's kind — a pad's rung and threshold, a POL's floors, the desk's list flags. */
1433
+ policy: FPoolPolicy;
1434
+ }
1435
+
1436
+ /** What a pool's hook says about it, by kind: the terms a buyer reads before entering. */
1437
+ export type FPoolPolicy =
1438
+ | { kind: "pad"; harvestCut: bigint; rung: number; threshold: bigint; pot: bigint; creatorShare: bigint; protocolShare: bigint; burnPerRung: bigint }
1439
+ | { kind: "pol"; harvestCut: bigint; keepQuote: bigint; minQuoteAtSeed: bigint }
1440
+ | { kind: "public"; harvestCut: bigint; curated: boolean }
1441
+ | { kind: "private"; harvestCut: bigint; gateMint: boolean; gateSwap: boolean; operator: Address };
1442
+
1443
+ /** The hook's terms for one pool, by its kind. */
1444
+ export async function getFPoolPolicy(c: LogswapClient, s: Pick<FPoolState, "poolId" | "hook" | "kind" | "gen">, roles?: FPoolRoles): Promise<FPoolPolicy> {
1445
+ const rd = <T>(abi: readonly unknown[], functionName: string, args: readonly unknown[] = []) =>
1446
+ c.public.readContract({ address: s.hook, abi, functionName, args } as never) as Promise<T>;
1447
+ const harvestCut = await rd<bigint>(fHookBaseAbi, "harvestCut");
1448
+ switch (s.kind) {
1449
+ case "pad": {
1450
+ const [rung, creatorShare, protocolShare, burnPerRung, pot] = await Promise.all([
1451
+ rd<bigint>(padHookAbi, "rung", [s.poolId]),
1452
+ rd<bigint>(padHookAbi, "creatorShare"),
1453
+ rd<bigint>(padHookAbi, "protocolShare"),
1454
+ rd<bigint>(padHookAbi, "burnPerRung"),
1455
+ fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, s.gen),
1456
+ ]);
1457
+ const threshold = await rd<bigint>(padHookAbi, "threshold", [rung]);
1458
+ return { kind: "pad", harvestCut, rung: Number(rung), threshold, pot, creatorShare, protocolShare, burnPerRung };
1459
+ }
1460
+ case "pol": {
1461
+ const [keepQuote, minQuoteAtSeed] = await Promise.all([rd<bigint>(polHookAbi, "keepQuote"), rd<bigint>(polHookAbi, "minQuoteAtSeed")]);
1462
+ return { kind: "pol", harvestCut, keepQuote, minQuoteAtSeed };
1463
+ }
1464
+ case "public": {
1465
+ const curated = await rd<boolean>(publicHookAbi, "curated");
1466
+ return { kind: "public", harvestCut, curated };
1467
+ }
1468
+ case "private": {
1469
+ const r = roles ?? (await getFPoolRoles(c, s.poolId, s));
1470
+ return { kind: "private", harvestCut, gateMint: r.gateMint, gateSwap: r.gateSwap, operator: fPoolOperatorOf(r) };
1471
+ }
1472
+ }
1473
+ }
1474
+
1475
+ /**
1476
+ * Every F pool `who` is the AUTHORITY of, across the four hooks — the policy page's list. Pools
1477
+ * `who` created and handed over are not here (the buyer's page shows them); pools handed TO
1478
+ * `who` are. `pools` may be passed (from the indexer, say) to skip the log scan.
1479
+ */
1480
+ export async function fPoolsControlledBy(
1481
+ c: LogswapClient,
1482
+ who: Address,
1483
+ opts: { pools?: DiscoveredFPool[]; fromBlock?: bigint } = {},
1484
+ ): Promise<FPoolControlled[]> {
1485
+ const pools = opts.pools ?? (await discoverFPools(c, { fromBlock: opts.fromBlock }));
1486
+ const authorities = await Promise.all(
1487
+ pools.map((p) =>
1488
+ c.public.readContract({ address: p.hook, abi: fHookBaseAbi, functionName: "authority", args: [p.poolId] } as never) as Promise<Address>,
1489
+ ),
1490
+ );
1491
+ const mine = pools.filter((_, i) => authorities[i]!.toLowerCase() === who.toLowerCase());
1492
+ return Promise.all(
1493
+ mine.map(async (pool) => {
1494
+ const state = await getFPool(c, pool.poolId);
1495
+ const roles = await getFPoolRoles(c, pool.poolId, state);
1496
+ const policy = await getFPoolPolicy(c, state, roles);
1497
+ return { pool, roles, state, policy };
1498
+ }),
1499
+ );
1500
+ }
1501
+
1299
1502
 
1300
1503
  /**
1301
1504
  * Every holder of a pool's shares, from the ERC-6909 Transfer stream — the id is an indexed