@logswap/sdk 0.5.45 → 0.5.47

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/dist/fpool.js 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
@@ -23,17 +30,26 @@
23
30
  * `addresses.router`.
24
31
  */
25
32
  import { encodeFunctionData, toFunctionSelector } from "viem";
26
- import { fPoolManagerAbi, logswapRouterAbi } from "./generated.js";
33
+ import { fPoolManagerAbi, logswapRouterAbi, padHookAbi, polHookAbi, privateHookAbi, publicHookAbi } from "./generated.js";
34
+ /**
35
+ * One contract, one ABI (decisions 039, F version 12): `harvest`, `claim`, `retire`, `regenerate`,
36
+ * `setLegs`, the names, the fee parameters are `FPoolManager`'s own code. The roles left it for
37
+ * the hooks in F version 15 (decisions 044): {@link fHookBaseAbi} is what every hook shares,
38
+ * {@link privateHookAbi} the desk's, {@link padHookAbi} / {@link polHookAbi} / {@link publicHookAbi}
39
+ * the kinds'.
40
+ */
41
+ export { fPoolManagerAbi, padHookAbi, polHookAbi, privateHookAbi, publicHookAbi };
27
42
  /**
28
- * One contract, one ABI (decisions 039, F version 12): the sponsor surface — `harvest`, `claim`,
29
- * `dissolve`, the handover, the gates, the operator, `setLegs`, the names, the fee parameters —
30
- * is `FPoolManager`'s own code again. From 031 to 038 it was a delegatecall facet
31
- * (`FPoolSponsor`) and this ABI was the union of the two compiled halves; the union is gone with
32
- * the facet, and the generated ABI is the whole surface.
43
+ * What every protocol hook shares (`FHookBase`): the roles and their handover, `harvestCut`,
44
+ * `maxHarvest`. `polHookAbi` is a superset of it and carries nothing that collides, so it stands
45
+ * in for the base — a call through it to any of the four hooks decodes the base's functions.
33
46
  */
34
- export { fPoolManagerAbi };
47
+ export const fHookBaseAbi = polHookAbi;
35
48
  const WAD = 10n ** 18n;
36
49
  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
50
+ /** The optional questions a hook declared at birth (decisions 044), as bits of `Pool.asks`. */
51
+ export const ASK_SWAP = 1;
52
+ export const ASK_MINT = 2;
37
53
  /** Read a basket pool whole. Batched — viem multicalls these when the transport allows. */
38
54
  export async function getFPool(c, poolId) {
39
55
  // viem types `functionName` as a literal union; this reader is deliberately generic, so the
@@ -45,7 +61,7 @@ export async function getFPool(c, poolId) {
45
61
  args,
46
62
  });
47
63
  // `getPool` returns the whole scalar struct in one call — the singleton's shape makes what used
48
- // to be a dozen getters a single read.
64
+ // to be a dozen getters a single read. No role in it since F version 15: those are the hook's.
49
65
  const p = await rd("getPool", [poolId]);
50
66
  // A pool that was never created reads as an all-zero struct rather than reverting, so the
51
67
  // absence has to be named here or every caller mistakes "no such pool" for "a pool holding
@@ -67,7 +83,7 @@ export async function getFPool(c, poolId) {
67
83
  const weights = legs.map((l) => l[1]);
68
84
  const x = legs.map((l) => l[2]); // derived by the manager: x_j = ln(w_j·L / R_j) (decisions 026)
69
85
  const reserves = legs.map((l) => l[3]); // the state
70
- const { quote, Q, L, phi, theta0, bigSigma, authority, pendingAuthority, operator, pendingOperator, gateMint, gateSwap, restructureBlock, seeded, gen } = p;
86
+ const { quote, Q, L, phi, theta0, bigSigma, hook, restructureBlock, seeded, gen } = p;
71
87
  const totalSupply = p.shares;
72
88
  return {
73
89
  poolId,
@@ -82,13 +98,9 @@ export async function getFPool(c, poolId) {
82
98
  theta,
83
99
  compositeX,
84
100
  bigSigma,
85
- authority,
86
- pendingAuthority,
87
- operator,
88
- pendingOperator,
101
+ hook,
102
+ asks: Number(p.asks),
89
103
  name: bytes32ToText(p.name),
90
- gateMint,
91
- gateSwap,
92
104
  restructureBlock: Number(restructureBlock),
93
105
  kind: fPoolKindOf(p.kind),
94
106
  seeded,
@@ -106,24 +118,65 @@ export function fPoolKindOf(n) {
106
118
  throw new Error(`logswap: unknown pool kind ${n}`);
107
119
  return k;
108
120
  }
121
+ /** The deployment's hook for a kind (decisions 044): what a key of that kind names. */
122
+ export function fPoolHookFor(c, kind) {
123
+ const a = c.addresses;
124
+ const hook = kind === "public" ? a.publicHook : kind === "private" ? a.privateHook : kind === "pol" ? a.polHook : a.padHook;
125
+ if (!hook)
126
+ throw new Error(`logswap: this deployment carries no ${kind} hook`);
127
+ return hook;
128
+ }
129
+ /** The hook a pool named in its key — one read; {@link getFPool} carries it too. */
130
+ export async function fPoolHookOf(c, poolId) {
131
+ const p = (await c.public.readContract({
132
+ address: c.addresses.fPoolManager,
133
+ abi: fPoolManagerAbi,
134
+ functionName: "getPool",
135
+ args: [poolId],
136
+ }));
137
+ if (/^0x0{40}$/i.test(p.quote))
138
+ throw new Error(`logswap: no F pool with id ${poolId}`);
139
+ return p.hook;
140
+ }
141
+ /** Read a pool's roles from its hook. `state` saves the `getPool` round trip when already in hand. */
142
+ export async function getFPoolRoles(c, poolId, state) {
143
+ const s = state ?? (await getFPool(c, poolId));
144
+ const rdHook = (abi, functionName) => c.public.readContract({ address: s.hook, abi, functionName, args: [poolId] });
145
+ const [creator, authority, pendingAuthority, maxHarvest] = await Promise.all([
146
+ rdHook(fHookBaseAbi, "creator"),
147
+ rdHook(fHookBaseAbi, "authority"),
148
+ rdHook(fHookBaseAbi, "pendingAuthority"),
149
+ rdHook(fHookBaseAbi, "maxHarvest"),
150
+ ]);
151
+ const desk = s.kind === "private";
152
+ const [operator, pendingOperator, gateMint, gateSwap] = desk
153
+ ? await Promise.all([
154
+ rdHook(privateHookAbi, "operator"),
155
+ rdHook(privateHookAbi, "pendingOperator"),
156
+ rdHook(privateHookAbi, "gateMint"),
157
+ rdHook(privateHookAbi, "gateSwap"),
158
+ ])
159
+ : [ZERO_ADDRESS, ZERO_ADDRESS, false, false];
160
+ return { hook: s.hook, creator, authority, pendingAuthority, operator, pendingOperator, gateMint, gateSwap, maxHarvest };
161
+ }
109
162
  export async function describeFPool(c, s) {
110
- const rd = (functionName) => c.public.readContract({
163
+ const rd = (functionName, args = [s.poolId]) => c.public.readContract({
111
164
  address: c.addresses.fPoolManager,
112
165
  abi: fPoolManagerAbi,
113
166
  functionName,
114
- args: [s.poolId],
167
+ args,
115
168
  });
116
- // an unseeded pool has no L: nothing earned, and θ (hence feePerL, hence edgePerL) is not
117
- // defined at L = 0. Older deployments revert with DivWadFailed rather than answering zero.
118
- const [feePerL, edgePerL] = s.L > 0n
119
- ? await Promise.all([rd("feePerL"), rd("edgePerL")])
120
- : [0n, 0n];
169
+ // an unseeded pool has no L: nothing earned, nothing to harvest, no pot
170
+ const [unharvested, harvestable, pot] = s.L > 0n
171
+ ? await Promise.all([rd("unharvested"), rd("harvestable"), rd("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
172
+ : [0n, 0n, 0n];
121
173
  return {
122
174
  shape: s.kind === "pad" ? "launch" : "basket", // the kind, not n = 1 (decisions 036)
123
175
  legs: s.bases.length,
124
176
  atFloor: s.Q === 0n,
125
- feePerL,
126
- edgePerL,
177
+ unharvested,
178
+ harvestable,
179
+ pot,
127
180
  };
128
181
  }
129
182
  /** `p_j = e^{x_j}`, lossy the way any float conversion is. Compare in log space when it matters. */
@@ -553,13 +606,26 @@ export function sortFPoolLegs(bases, ...companions) {
553
606
  const idx = bases.map((_, i) => i).sort((a, b) => (bases[a].toLowerCase() < bases[b].toLowerCase() ? -1 : 1));
554
607
  return { bases: idx.map((i) => bases[i]), companions: companions.map((arr) => idx.map((i) => arr[i])) };
555
608
  }
609
+ /** The key as the manager hashes it: legs sorted, the kind's hook filled in when none is named. */
610
+ export function fPoolKeyOf(c, a) {
611
+ const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
612
+ return {
613
+ quote: a.quote,
614
+ bases,
615
+ weights: companions[0],
616
+ phi: a.phi,
617
+ kind: F_KIND[a.kind],
618
+ hook: a.hook ?? fPoolHookFor(c, a.kind),
619
+ creator: a.creator,
620
+ salt: a.salt ?? ZERO_SALT,
621
+ };
622
+ }
556
623
  /** Create the pool (no funds move — `seed` arms it). Returns the tx hash; read the id with `fPoolIdOf`. */
557
624
  export async function fPoolInitialize(c, a) {
558
625
  const sum = a.weights.reduce((x, y) => x + y, 0n);
559
626
  if (sum !== 10n ** 18n)
560
627
  throw new Error(`logswap: weights sum to ${sum}, expected 1e18`);
561
- const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
562
- 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 };
628
+ const key = fPoolKeyOf(c, a);
563
629
  const wallet = requireWallet(c);
564
630
  const { request } = await c.public.simulateContract({
565
631
  address: c.addresses.fPoolManager,
@@ -572,8 +638,7 @@ export async function fPoolInitialize(c, a) {
572
638
  }
573
639
  /** The pool id is the key's hash — pure, so it can be read before or after creation. */
574
640
  export async function fPoolIdOf(c, a) {
575
- const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
576
- 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 };
641
+ const key = fPoolKeyOf(c, a);
577
642
  return c.public.readContract({
578
643
  address: c.addresses.fPoolManager,
579
644
  abi: fPoolManagerAbi,
@@ -604,81 +669,71 @@ export async function fPoolSeed(c, a) {
604
669
  return writeFPool(c, a.poolId, "seed", [a.L0, r0, a.Q0], a.account);
605
670
  }
606
671
  /**
607
- * Retire the live generation (decisions 025, 032, 035) — the raw entry. Allowed only at an empty
608
- * bid, Q ≤ L/1e9, on every pool: the market took it, or the authority harvested it out first
609
- * ({@link fPoolRetire} does both in one transaction). The live (S, Q, R_j) become a frozen bundle
610
- * every share of this generation redeems ({@link fPoolRedeem}) or rolls ({@link fPoolRollIn}),
611
- * forever; the pool is unseeded until the next {@link fPoolSeed}, which opens generation
612
- * `gen + 1` under the same id, gates and lists.
672
+ * Retire the live generation (decisions 025, 032, 035) — the raw entry (`dissolve` before F
673
+ * version 14). Allowed only at an empty bid, Q ≤ L/1e9, on every pool: the market took it, or the
674
+ * operator harvested it out first ({@link fPoolRetire} does both in one transaction). The live
675
+ * (S, Q, R_j) become a frozen bundle every share of this generation redeems ({@link fPoolRedeem})
676
+ * or rolls ({@link fPoolRollIn}), forever; the pool is unseeded until the next {@link fPoolSeed},
677
+ * which opens generation `gen + 1` under the same id, hook, roles, gates and lists.
613
678
  */
614
- export async function fPoolDissolve(c, a) {
615
- return writeFPool(c, a.poolId, "dissolve", [], a.account);
679
+ export async function fPoolRetireRaw(c, a) {
680
+ return writeFPool(c, a.poolId, "retire", [], a.account);
616
681
  }
617
682
  /**
618
- * Retire the live generation as ONE transaction: the harvests that empty the bid
683
+ * Retire the live generation as ONE transaction: the harvest that empties the bid
619
684
  * ({@link fPoolEmptyingHarvests} — a dividend to every share; none when it is already empty),
620
- * then `dissolve` — the manager's `multicall`, the authority as sender. What a sponsor's
621
- * "dissolve" button should call. Throws before sending on a pad that still bids.
685
+ * then `retire` — the manager's `multicall`, the operator as sender. What a sponsor's
686
+ * "retire" button should call. Throws before sending on a pad that still bids.
622
687
  */
623
688
  export async function fPoolRetire(c, a) {
624
689
  const harvests = await fPoolEmptyingHarvests(c, a.poolId);
625
690
  if (harvests.length === 0)
626
- return fPoolDissolve(c, a);
691
+ return fPoolRetireRaw(c, a);
627
692
  const enc = (functionName, args) => encodeFunctionData({ abi: fPoolManagerAbi, functionName, args });
628
- const calls = [...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])), enc("dissolve", [a.poolId])];
693
+ const calls = [...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])), enc("retire", [a.poolId])];
629
694
  return writeFPool0(c, "multicall", [calls], a.account);
630
695
  }
631
696
  /**
632
697
  * The harvests that empty an unlocked pool's bid, in order — what a relaunch sends before its
633
- * `dissolve` (decisions 035: a dissolve needs an empty bid on every pool). One `harvest(Q)` when a
634
- * collector is set. With none, the cut on income STAYS in the pool as LP quote (decisions 002)
635
- * and reads as taken income, so a second harvest — capital, no cut — takes the rest; the
636
- * contract's own arithmetic, replayed. `[]` when the bid is already empty. Throws on a
637
- * pad that still bids (its bid empties only as the market sells it down), and in
638
- * the one shape two harvests cannot empty (no collector, income above Q — harvest by hand).
698
+ * `retire` (decisions 035: a retire needs an empty bid on every pool). Since F version 14 one
699
+ * `harvest(Q)` does it: the protocol's cut leaves the pool with the amount (with no collector
700
+ * there is no cut). `[]` when the bid is already empty. Throws on a pad that still bids (its bid
701
+ * empties only as the market sells it down), and when the pool's hook keeps a floor on the bid
702
+ * (`PolHook.keepQuote`, decisions 044): a `retire` under that hook waits for the market.
639
703
  */
640
704
  export async function fPoolEmptyingHarvests(c, poolId) {
641
- const rd = (functionName, args = []) => c.public.readContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName, args });
642
- const [p, feePerL, fee, exempt, collector] = await Promise.all([
643
- rd("getPool", [poolId]),
644
- rd("feePerL", [poolId]),
645
- rd("HARVEST_PROTOCOL_CUT"),
646
- rd("harvestCutExempt", [poolId]),
647
- rd("feeCollector"),
648
- ]);
649
- if (p.Q === 0n)
705
+ const s = await getFPool(c, poolId);
706
+ if (s.Q === 0n)
650
707
  return [];
651
- if (p.kind === F_KIND.pad)
708
+ if (s.kind === "pad")
652
709
  throw new Error("logswap: a pad is retired only once its bid is empty (decisions 035)");
653
- const earned = feePerL > 0n ? (feePerL * p.L) / WAD : 0n;
654
- let taken = p.incomeTaken;
655
- let q = p.Q;
656
- const amounts = [];
657
- for (let i = 0; i < 2 && q > 0n; i++) {
658
- const amount = q;
659
- const left = earned > taken ? earned - taken : 0n;
660
- const taxable = amount < left ? amount : left;
661
- taken += taxable;
662
- const pFee = exempt ? 0n : (taxable * fee) / WAD;
663
- const leaving = pFee !== 0n && collector !== ZERO_ADDRESS ? amount : amount - pFee;
664
- q -= leaving;
665
- amounts.push(amount);
666
- }
667
- if (q > 0n)
668
- throw new Error("logswap: the bid does not empty in two harvests (no collector, income above Q) — harvest by hand first");
669
- return amounts;
710
+ const { maxHarvest } = await getFPoolRoles(c, poolId, s);
711
+ if (maxHarvest < s.Q)
712
+ throw new Error("logswap: the pool's hook keeps a floor on the bid — it cannot be harvested empty (decisions 044)");
713
+ return [s.Q];
714
+ }
715
+ /**
716
+ * Release the dead shares' pot under the hook's terms (decisions 041, 044) — a pad's graduation.
717
+ * The manager harvests the generation's income first (the pot then holds everything the dead
718
+ * shares earned), asks the hook, and applies its answer: a share to the payee it names (the pad's
719
+ * creator — the pool's authority), a cut to the collector, the rest bought in as more of the
720
+ * float the pool owns, a slice of every reserve burned. The hook refuses under its rung's
721
+ * threshold (`PadHook.NotGraduated`). Whoever runs the pool.
722
+ */
723
+ export async function fPoolRegenerate(c, a) {
724
+ return writeFPool(c, a.poolId, "regenerate", [a.to ?? a.account], a.account);
670
725
  }
671
726
  /**
672
727
  * The relaunch as ONE transaction (decisions 025, 032, 035, 037): `harvest` the bid out — a
673
- * dividend to every share, {@link fPoolEmptyingHarvests} — `dissolve` the live generation at the
728
+ * dividend to every share, {@link fPoolEmptyingHarvests} — `retire` the live generation at the
674
729
  * empty bid, `redeem` the caller's own shares of it (when `shares` > 0), optionally `setLegs` the
675
730
  * next generation's composition (`legs`: a private pool's reshape — retire, admit, reweight), and
676
731
  * `seed` it with what came back — the manager's `multicall`, one sender throughout, the same
677
732
  * pool. The operator's when one is appointed and the seed's inventory is its own; by default the
678
- * authority's. Gates, allowlist and names carry over; anything else goes in `extra`, pre-encoded
679
- * against {@link fPoolManagerAbi}. The seed names reserves (decisions 029): `r0` outright — one
680
- * per leg of the NEXT table, 0 for a retired leg — or `x0` marks against the next table's
681
- * `weights`.
733
+ * authority's. The hook, its roles, gates, allowlist and the name carry over; anything else goes
734
+ * in `extra`, pre-encoded against {@link fPoolManagerAbi}. The seed names reserves (decisions
735
+ * 029): `r0` outright — one per leg of the NEXT table, 0 for a retired leg — or `x0` marks
736
+ * against the next table's `weights`.
682
737
  */
683
738
  export async function fPoolRelaunch(c, a) {
684
739
  const [gen, harvests] = await Promise.all([fPoolGen(c, a.poolId), fPoolEmptyingHarvests(c, a.poolId)]);
@@ -686,7 +741,7 @@ export async function fPoolRelaunch(c, a) {
686
741
  const enc = (functionName, args) => encodeFunctionData({ abi: fPoolManagerAbi, functionName, args });
687
742
  const calls = [
688
743
  ...harvests.map((amount) => enc("harvest", [a.poolId, amount, a.account])),
689
- enc("dissolve", [a.poolId]),
744
+ enc("retire", [a.poolId]),
690
745
  ...(a.shares > 0n ? [enc("redeem", [a.poolId, BigInt(gen), a.account, a.shares])] : []),
691
746
  ...(a.legs ? [enc("setLegs", [a.poolId, a.legs.weights, a.legs.newBases ?? []])] : []),
692
747
  enc("seed", [a.poolId, a.L0, r0, a.Q0]),
@@ -712,7 +767,9 @@ export function fPoolLaunchCalls(poolId, a) {
712
767
  if (given.length !== a.bases.length)
713
768
  throw new Error(`logswap: ${given.length} reserves for ${a.bases.length} legs`);
714
769
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights, given);
715
- 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 };
770
+ if (!a.hook)
771
+ throw new Error("logswap: fPoolLaunchCalls needs the key's hook — fPoolLaunch fills it from the deployment");
772
+ 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 };
716
773
  const enc = (functionName, args) => encodeFunctionData({ abi: fPoolManagerAbi, functionName, args });
717
774
  const calls = [];
718
775
  if (!a.initialized)
@@ -728,15 +785,23 @@ export function fPoolLaunchCalls(poolId, a) {
728
785
  }
729
786
  if (a.name)
730
787
  calls.push(enc("setName", [poolId, textToBytes32(a.name)]));
731
- if (a.kind === "private") {
732
- // the desk's opening posture: gated minting, the creator listed. No appointment: the
733
- // authority is the operator until one is appointed (decisions 037)
734
- calls.push(enc("setGates", [poolId, true, false]));
735
- calls.push(enc("setAllowed", [poolId, [a.account], true]));
736
- }
788
+ // the desk's gates and list are the HOOK's (decisions 044), not the manager's: they cannot ride
789
+ // in this batch — `fPoolDeskOpening` sends them right after, as the same creator
737
790
  calls.push(...(a.extra ?? []));
738
791
  return calls;
739
792
  }
793
+ /**
794
+ * The desk's opening posture, on its hook (decisions 044): gated minting, the creator listed. No
795
+ * appointment: the authority is the operator until one is appointed (decisions 037). Two writes
796
+ * to `PrivateHook`; the launch batch cannot carry them (they are not manager calls).
797
+ */
798
+ export async function fPoolDeskOpening(c, a) {
799
+ const hook = a.hook ?? fPoolHookFor(c, "private");
800
+ return [
801
+ await fPoolSetGates(c, { poolId: a.poolId, gateMint: true, gateSwap: false, account: a.account, hook }),
802
+ await fPoolSetAllowed(c, { poolId: a.poolId, who: [a.account], allowed: true, account: a.account, hook }),
803
+ ];
804
+ }
740
805
  /**
741
806
  * Create a pool as ONE transaction: `initialize`, `seed`, the seed lock, the opening buy, the
742
807
  * name, the private-pool gates — the manager's `multicall`, the creator as sender throughout.
@@ -756,36 +821,62 @@ export function fPoolLaunchCalls(poolId, a) {
756
821
  */
757
822
  export async function fPoolLaunch(c, a) {
758
823
  const poolId = await fPoolIdOf(c, a);
759
- const hash = await writeFPool0(c, "multicall", [fPoolLaunchCalls(poolId, a)], a.account);
824
+ const calls = fPoolLaunchCalls(poolId, { ...a, hook: a.hook ?? fPoolHookFor(c, a.kind) });
825
+ const hash = await writeFPool0(c, "multicall", [calls], a.account);
760
826
  return { hash, poolId };
761
827
  }
828
+ // ─── the roles, on the hook (decisions 044) ─────────────────────────────────────────────
829
+ //
830
+ // Every write here goes to the pool's HOOK, not the manager: the manager holds no role. `hook`
831
+ // may be passed when in hand (`FPoolState.hook`, `FPoolRoles.hook`); otherwise it is read once.
832
+ /** A write keyed by poolId, on the pool's hook. */
833
+ async function writeHook(c, abi, a, functionName, args) {
834
+ const wallet = requireWallet(c);
835
+ const hook = a.hook ?? (await fPoolHookOf(c, a.poolId));
836
+ const { request } = await c.public.simulateContract({
837
+ address: hook,
838
+ abi,
839
+ functionName,
840
+ args: [a.poolId, ...args],
841
+ account: a.account,
842
+ });
843
+ return wallet.writeContract(request);
844
+ }
762
845
  /**
763
- * Propose the next authority — step one of two (contracts `2aa6b7b`, decisions 020). Nothing moves
846
+ * Propose the next authority — step one of two (decisions 020), on the pool's hook. Nothing moves
764
847
  * until `next` accepts; proposing again (the zero address included) cancels a pending proposal.
765
- * Indexed on-chain via AuthorityProposed.
848
+ * Indexed on-chain via the hook's AuthorityProposed.
766
849
  */
767
850
  export async function fPoolProposeAuthority(c, a) {
768
- return writeFPool(c, a.poolId, "proposeAuthority", [a.next], a.account);
851
+ return writeHook(c, fHookBaseAbi, a, "proposeAuthority", [a.next]);
769
852
  }
770
- /** Accept a pending proposal — step two; only the proposed address can. Indexed via AuthoritySet. */
853
+ /**
854
+ * Accept a pending proposal — step two; only the proposed address can. Indexed via the hook's
855
+ * AuthoritySet. On the desk's hook this clears the operator (pending and accepted) and voids the
856
+ * seller's list (decisions 040, 044).
857
+ */
771
858
  export async function fPoolAcceptAuthority(c, a) {
772
- return writeFPool(c, a.poolId, "acceptAuthority", [], a.account);
859
+ return writeHook(c, fHookBaseAbi, a, "acceptAuthority", []);
773
860
  }
774
- // ─── the sponsor's controls and the desk (contracts bd3dd87, decisions 021) ───────────────
775
- /** Turn the mint / swap gates on or off (authority). `burn` is never gated. */
861
+ // ─── the desk's controls (PrivateHook; decisions 021, 044) ───────────────────────────────
862
+ /** Turn the mint / swap gates on or off (the authority, on the desk's hook). `burn` is never gated. */
776
863
  export async function fPoolSetGates(c, a) {
777
- return writeFPool(c, a.poolId, "setGates", [a.gateMint, a.gateSwap], a.account);
864
+ return writeHook(c, privateHookAbi, a, "setGates", [a.gateMint, a.gateSwap]);
778
865
  }
779
- /** Add to or remove from the pool's list, in one call (authority). */
866
+ /**
867
+ * Add to or remove from the pool's list, in one call (the authority, on the desk's hook). The list
868
+ * is keyed by the authority that granted it: a handover starts the buyer with an empty one.
869
+ */
780
870
  export async function fPoolSetAllowed(c, a) {
781
- return writeFPool(c, a.poolId, "setAllowed", [a.who, a.allowed], a.account);
871
+ return writeHook(c, privateHookAbi, a, "setAllowed", [a.who, a.allowed]);
782
872
  }
783
- /** Whether `who` is on the pool's list (consulted only while a gate is on). */
784
- export async function fPoolIsAllowed(c, poolId, who) {
873
+ /** Whether `who` is on the pool's list under its current authority (consulted only while a gate is on). */
874
+ export async function fPoolIsAllowed(c, poolId, who, hook) {
875
+ const address = hook ?? (await fPoolHookOf(c, poolId));
785
876
  return (await c.public.readContract({
786
- address: c.addresses.fPoolManager,
787
- abi: fPoolManagerAbi,
788
- functionName: "allowed",
877
+ address,
878
+ abi: privateHookAbi,
879
+ functionName: "isAllowed",
789
880
  args: [poolId, who],
790
881
  }));
791
882
  }
@@ -823,20 +914,20 @@ export function fPoolOperatorOf(s) {
823
914
  return s.operator === ZERO_ADDRESS ? s.authority : s.operator;
824
915
  }
825
916
  /**
826
- * Propose the operator (authority; private pools only) — the hot key that runs the pool: `harvest`,
827
- * `dissolve`, `setLegs`. Two-step: the proposed address takes the role with
917
+ * Propose the operator (the authority, on the desk's hook) — the hot key that runs the pool:
918
+ * `harvest`, `retire`, `regenerate`, `setLegs`. Two-step: the proposed address takes the role with
828
919
  * {@link fPoolAcceptOperator}. The zero address clears at once — the role returns to the authority.
829
920
  */
830
921
  export async function fPoolAppointOperator(c, a) {
831
- return writeFPool(c, a.poolId, "appointOperator", [a.operator], a.account);
922
+ return writeHook(c, privateHookAbi, a, "appointOperator", [a.operator]);
832
923
  }
833
924
  /** The proposed operator takes the role. */
834
925
  export async function fPoolAcceptOperator(c, a) {
835
- return writeFPool(c, a.poolId, "acceptOperator", [], a.account);
926
+ return writeHook(c, privateHookAbi, a, "acceptOperator", []);
836
927
  }
837
928
  /**
838
929
  * The next generation's composition (operator; private pools only; ONLY while the pool is
839
- * unseeded — between a dissolve and the seed that prices it, decisions 037). `weights` has one
930
+ * unseeded — between a retire and the seed that prices it, decisions 037). `weights` has one
840
931
  * entry per leg the pool has ever had, in index order (0 retires a leg; indices are never reused),
841
932
  * then one per base in `newBases`, appended; they sum to 1e18 exactly. No value moves: the
842
933
  * following {@link fPoolSeed} names every reserve. A live reshape is {@link fPoolRelaunch} with
@@ -898,7 +989,8 @@ export async function discoverFPools(c, opts = {}) {
898
989
  weights: [...a.weights],
899
990
  phi: a.phi,
900
991
  kind: fPoolKindOf(a.kind),
901
- authority: a.authority,
992
+ hook: a.hook,
993
+ creator: a.creator,
902
994
  block: l.blockNumber ?? 0n,
903
995
  seeded: q0 !== undefined,
904
996
  q0: q0 ?? 0n,
@@ -906,6 +998,52 @@ export async function discoverFPools(c, opts = {}) {
906
998
  };
907
999
  });
908
1000
  }
1001
+ /** The hook's terms for one pool, by its kind. */
1002
+ export async function getFPoolPolicy(c, s, roles) {
1003
+ const rd = (abi, functionName, args = []) => c.public.readContract({ address: s.hook, abi, functionName, args });
1004
+ const harvestCut = await rd(fHookBaseAbi, "harvestCut");
1005
+ switch (s.kind) {
1006
+ case "pad": {
1007
+ const [rung, creatorShare, protocolShare, burnPerRung, pot] = await Promise.all([
1008
+ rd(padHookAbi, "rung", [s.poolId]),
1009
+ rd(padHookAbi, "creatorShare"),
1010
+ rd(padHookAbi, "protocolShare"),
1011
+ rd(padHookAbi, "burnPerRung"),
1012
+ fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, s.gen),
1013
+ ]);
1014
+ const threshold = await rd(padHookAbi, "threshold", [rung]);
1015
+ return { kind: "pad", harvestCut, rung: Number(rung), threshold, pot, creatorShare, protocolShare, burnPerRung };
1016
+ }
1017
+ case "pol": {
1018
+ const [keepQuote, minQuoteAtSeed] = await Promise.all([rd(polHookAbi, "keepQuote"), rd(polHookAbi, "minQuoteAtSeed")]);
1019
+ return { kind: "pol", harvestCut, keepQuote, minQuoteAtSeed };
1020
+ }
1021
+ case "public": {
1022
+ const curated = await rd(publicHookAbi, "curated");
1023
+ return { kind: "public", harvestCut, curated };
1024
+ }
1025
+ case "private": {
1026
+ const r = roles ?? (await getFPoolRoles(c, s.poolId, s));
1027
+ return { kind: "private", harvestCut, gateMint: r.gateMint, gateSwap: r.gateSwap, operator: fPoolOperatorOf(r) };
1028
+ }
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Every F pool `who` is the AUTHORITY of, across the four hooks — the policy page's list. Pools
1033
+ * `who` created and handed over are not here (the buyer's page shows them); pools handed TO
1034
+ * `who` are. `pools` may be passed (from the indexer, say) to skip the log scan.
1035
+ */
1036
+ export async function fPoolsControlledBy(c, who, opts = {}) {
1037
+ const pools = opts.pools ?? (await discoverFPools(c, { fromBlock: opts.fromBlock }));
1038
+ const authorities = await Promise.all(pools.map((p) => c.public.readContract({ address: p.hook, abi: fHookBaseAbi, functionName: "authority", args: [p.poolId] })));
1039
+ const mine = pools.filter((_, i) => authorities[i].toLowerCase() === who.toLowerCase());
1040
+ return Promise.all(mine.map(async (pool) => {
1041
+ const state = await getFPool(c, pool.poolId);
1042
+ const roles = await getFPoolRoles(c, pool.poolId, state);
1043
+ const policy = await getFPoolPolicy(c, state, roles);
1044
+ return { pool, roles, state, policy };
1045
+ }));
1046
+ }
909
1047
  /**
910
1048
  * Every holder of a pool's shares, from the ERC-6909 Transfer stream — the id is an indexed
911
1049
  * topic, so one filtered log query aggregates the whole ledger. The 0xdead entry is the