@logswap/sdk 0.5.65 → 0.5.67

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/admin.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import type { Address, Hash } from "viem";
14
14
  import type { LogswapClient } from "./client.js";
15
- import { type FPoolKind } from "./fpool.js";
15
+ import { type FPoolKey, type FPoolKind } from "./fpool.js";
16
16
  /** The C manager's `setFeeParams(mintFeeProtocolCut)` — one cut. */
17
17
  export declare function setFeeParamsC(c: LogswapClient, mintFeeProtocolCutWad: bigint): Promise<Hash>;
18
18
  /** The F manager's `MINT_FEE_PROTOCOL_CUT` — a constant since F version 18; there is no setter. */
@@ -62,18 +62,36 @@ export declare function registryAcceptOwner(c: LogswapClient): Promise<Hash>;
62
62
  */
63
63
  export declare function setPolicyApproved(c: LogswapClient, policy: Address, approved: boolean): Promise<Hash>;
64
64
  /**
65
- * Move a pool to another approved policy of the same kind: the registry
66
- * owner's alone, orchestrated on the registry — `migrate(manager, poolId, to)`. The new policy
67
- * takes the authority and the name from the old; the operator and the lists void; no swap lands
68
- * in the block.
65
+ * Move a pool to another approved policy of the same kind: the registry owner's alone,
66
+ * orchestrated on the registry — `migrate(manager, key, to)` since F version 20 (it took the
67
+ * pool's id before). The registry checks the key against the manager (`NoPool`), checks the
68
+ * manager is its own (`WrongManager`), and compares the two policies' tags recorded at approval
69
+ * ({@link registryKindOf}). The new policy is brought the creator (the key's), the authority and
70
+ * the name (read off the old policy defensively, the key's creator standing in when it cannot
71
+ * answer); the operator and the lists void; no swap lands in the block.
72
+ *
73
+ * Pass the pool's `key` when in hand. Given only `poolId`, the key is rebuilt from the pool's
74
+ * `FInitialize` log ({@link fPoolKeyOfPool}) with `salt` (zero by default) and checked against
75
+ * the id — the log does not carry the salt, so a salted pool needs it named.
69
76
  */
70
77
  export declare function fPoolMigrate(c: LogswapClient, a: {
71
- poolId: `0x${string}`;
72
78
  to: Address;
73
- }): Promise<Hash>;
79
+ } & ({
80
+ key: FPoolKey;
81
+ } | {
82
+ poolId: `0x${string}`;
83
+ salt?: `0x${string}`;
84
+ fromBlock?: bigint;
85
+ })): Promise<Hash>;
86
+ /**
87
+ * The kind tag the registry recorded for `policy` at approval (`kindOf`, F version 20) — refreshed
88
+ * on re-approval and kept after a revoke; what `migrate` compares, never the policy's live
89
+ * answer. "" for a policy the registry never approved (or a deployment with no registry).
90
+ */
91
+ export declare function registryKindOf(c: LogswapClient, policy: Address): Promise<FPoolKind | "">;
74
92
  /** The protocol's rate on the income part of every harvest under `policy`, WAD ≤ `HARVEST_CUT_MAX`. */
75
93
  export declare function setHarvestCut(c: LogswapClient, policy: Address, rateWad: bigint): Promise<Hash>;
76
- /** `PadPolicy.setSchedule`: `T(0)`, the rung's growth, the creator's and the protocol's shares of the pot (≤ `POT_CUT_MAX`), the burn per rung (≤ `BURN_MAX`). */
94
+ /** `PadPolicy.setSchedule`: `T(0)`, the rung's growth, the creator's and the protocol's shares of the pot (≤ `POT_CUT_MAX`), the burn per rung (≤ `BURN_MAX`). The rung is the pool's generation since F version 21. */
77
95
  export declare function padSetSchedule(c: LogswapClient, policy: Address, s: {
78
96
  thresholdBase: bigint;
79
97
  thresholdGrowth: bigint;
package/dist/admin.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * protocol share likewise (`POT_CUT_MAX`).
12
12
  */
13
13
  import { cPoolManagerAbi, fPoolManagerAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, publicPolicyAbi } from "./generated.js";
14
- import { bytes32ToText, fPolicyBaseAbi, fPoolKindOfPolicy, textToBytes32 } from "./fpool.js";
14
+ import { bytes32ToText, fPolicyBaseAbi, fPoolKeyOfPool, fPoolKindOfPolicy, textToBytes32 } from "./fpool.js";
15
15
  function wallet(c) {
16
16
  if (!c.wallet)
17
17
  throw new Error("a wallet client is required");
@@ -111,20 +111,40 @@ export async function setPolicyApproved(c, policy, approved) {
111
111
  return approved ? registryApprove(c, policy, await fPoolKindOfPolicy(c, policy)) : registryRevoke(c, policy);
112
112
  }
113
113
  /**
114
- * Move a pool to another approved policy of the same kind: the registry
115
- * owner's alone, orchestrated on the registry — `migrate(manager, poolId, to)`. The new policy
116
- * takes the authority and the name from the old; the operator and the lists void; no swap lands
117
- * in the block.
114
+ * Move a pool to another approved policy of the same kind: the registry owner's alone,
115
+ * orchestrated on the registry — `migrate(manager, key, to)` since F version 20 (it took the
116
+ * pool's id before). The registry checks the key against the manager (`NoPool`), checks the
117
+ * manager is its own (`WrongManager`), and compares the two policies' tags recorded at approval
118
+ * ({@link registryKindOf}). The new policy is brought the creator (the key's), the authority and
119
+ * the name (read off the old policy defensively, the key's creator standing in when it cannot
120
+ * answer); the operator and the lists void; no swap lands in the block.
121
+ *
122
+ * Pass the pool's `key` when in hand. Given only `poolId`, the key is rebuilt from the pool's
123
+ * `FInitialize` log ({@link fPoolKeyOfPool}) with `salt` (zero by default) and checked against
124
+ * the id — the log does not carry the salt, so a salted pool needs it named.
118
125
  */
119
126
  export async function fPoolMigrate(c, a) {
120
- return registryWrite(c, "migrate", [c.addresses.fPoolManager, a.poolId, a.to]);
127
+ const key = "key" in a ? a.key : await fPoolKeyOfPool(c, a.poolId, { salt: a.salt, fromBlock: a.fromBlock });
128
+ return registryWrite(c, "migrate", [c.addresses.fPoolManager, key, a.to]);
129
+ }
130
+ /**
131
+ * The kind tag the registry recorded for `policy` at approval (`kindOf`, F version 20) — refreshed
132
+ * on re-approval and kept after a revoke; what `migrate` compares, never the policy's live
133
+ * answer. "" for a policy the registry never approved (or a deployment with no registry).
134
+ */
135
+ export async function registryKindOf(c, policy) {
136
+ const registry = await policyRegistryOf(c);
137
+ if (registry === ZERO)
138
+ return "";
139
+ const word = (await c.public.readContract({ address: registry, abi: policyRegistryAbi, functionName: "kindOf", args: [policy] }));
140
+ return bytes32ToText(word);
121
141
  }
122
142
  // ─── the policies' numbers (the collector's; each bounded where it reaches the manager) ─────
123
143
  /** The protocol's rate on the income part of every harvest under `policy`, WAD ≤ `HARVEST_CUT_MAX`. */
124
144
  export async function setHarvestCut(c, policy, rateWad) {
125
145
  return write(c, policy, fPolicyBaseAbi, "setHarvestCut", [rateWad]);
126
146
  }
127
- /** `PadPolicy.setSchedule`: `T(0)`, the rung's growth, the creator's and the protocol's shares of the pot (≤ `POT_CUT_MAX`), the burn per rung (≤ `BURN_MAX`). */
147
+ /** `PadPolicy.setSchedule`: `T(0)`, the rung's growth, the creator's and the protocol's shares of the pot (≤ `POT_CUT_MAX`), the burn per rung (≤ `BURN_MAX`). The rung is the pool's generation since F version 21. */
128
148
  export async function padSetSchedule(c, policy, s) {
129
149
  return write(c, policy, padPolicyAbi, "setSchedule", [s.thresholdBase, s.thresholdGrowth, s.creatorShare, s.protocolShare, s.burnPerRung]);
130
150
  }
package/dist/client.d.ts CHANGED
@@ -83,7 +83,7 @@ export declare const EXPECTED_VERSION = 5n;
83
83
  * passes it; when one lands, raise this constant in the same change, which is the discipline
84
84
  * `EXPECTED_VERSION` enforces mechanically and this one asks for.
85
85
  */
86
- export declare const EXPECTED_F_VERSION = 19n;
86
+ export declare const EXPECTED_F_VERSION = 21n;
87
87
  export declare function assertVersion(c: LogswapClient, expected?: bigint): Promise<void>;
88
88
  /**
89
89
  * The versions say the managers are the right code; this says the rest of the addresses belong
package/dist/client.js CHANGED
@@ -121,7 +121,19 @@ export const EXPECTED_VERSION = 5n;
121
121
  // is the policy at birth, `migrate` moves a pool for the registry's owner; `stamp` / `setAsks` are
122
122
  // the policy's; the desk's `setAllowed` / `isAllowed` are `setMinters` / `setTakers` / `isMinter` /
123
123
  // `isTaker`; a pad and a POL hold one base (`OneBase`).
124
- export const EXPECTED_F_VERSION = 19n;
124
+ // 18–19: no role, no kind and no name on the manager (049); the events carry what a reader needs (050).
125
+ // 20 (2026-09-25, logswap-contract #53): policy isolation — `PolicyRegistry.migrate(manager, key, to)`
126
+ // takes the pool's KEY (was its id), `kindOf(policy)` is the tag recorded at approval, the
127
+ // registry emits `Migrated`; `IFPolicy.onMigrate` is brought the creator, authority and name;
128
+ // the desk's lists are keyed by `listEpoch`. The manager's ABI did not move.
129
+ // 21 (2026-09-26, #54): `regenerate` is the generation step — the policy's `Terms` name a
130
+ // `carried` holder (a fifth word, 160 B), every other holder is frozen out pro rata into
131
+ // generation g's bundle and redeems it (`fPoolRedeem` / `fPoolRollIn` with gen = g), and the
132
+ // pool goes on as g + 1. The manager lost `FEE_MIN` / `FEE_MAX` (see keys.ts), `harvestable`
133
+ // (the bid: `getPool().Q` when L > 0), `idOf(FPoolKey)` (hashed locally, `fPoolIdOfKey`) and
134
+ // `reserveOf` (`legOf`'s fourth word). `PadPolicy.rung` is gone: the pad's rung is `gen`, the
135
+ // next threshold `threshold(gen)`. A pad never retires; a public pool refuses `regenerate`.
136
+ export const EXPECTED_F_VERSION = 21n;
125
137
  export async function assertVersion(c, expected = EXPECTED_VERSION) {
126
138
  const { cPoolManagerAbi, fPoolManagerAbi } = await import("./generated.js");
127
139
  // three independent reads: one round trip, not three in a row
package/dist/fpool.d.ts CHANGED
@@ -416,6 +416,10 @@ export declare const fPolicyBaseAbi: readonly [{
416
416
  readonly name: "burn";
417
417
  readonly internalType: "uint256";
418
418
  readonly type: "uint256";
419
+ }, {
420
+ readonly name: "carried";
421
+ readonly internalType: "address";
422
+ readonly type: "address";
419
423
  }];
420
424
  }];
421
425
  readonly stateMutability: "nonpayable";
@@ -732,9 +736,12 @@ export interface FPoolState {
732
736
  /** False before the first seed — and between a retire and the next one. */
733
737
  seeded: boolean;
734
738
  /**
735
- * The live GENERATION. Every retire freezes one into a bundle its shares
736
- * redeem ({@link fPoolRedeem}) and opens the next; the share id carries it
737
- * ({@link fPoolShareId}), so generations `0 … gen-1` are retired claims and `gen` is the pool.
739
+ * The live GENERATION. Every retire freezes one into a bundle its shares redeem
740
+ * ({@link fPoolRedeem}) and opens the next; since F version 21 so does every `regenerate`
741
+ * (the generation step, {@link fPoolRegenerate}): the carried holder and the dead shares go on
742
+ * as `gen + 1`, every other holder of `gen` is frozen out into its bundle. The share id carries
743
+ * it ({@link fPoolShareId}), so generations `0 … gen-1` are claims ({@link fPoolClaimsOf}) and
744
+ * `gen` is the pool. On a pad, `gen` is the graduation count — the rung.
738
745
  */
739
746
  gen: number;
740
747
  totalSupply: bigint;
@@ -785,7 +792,7 @@ export interface FPoolRoles {
785
792
  pendingOperator: Address;
786
793
  gateMint: boolean;
787
794
  gateSwap: boolean;
788
- /** The policy's own ceiling on a harvest now — the manager's `harvestable` narrowed by the policy's rule. */
795
+ /** The policy's own ceiling on a harvest now — the bid ({@link FPoolDescription.harvestable}) narrowed by the policy's rule. */
789
796
  maxHarvest: bigint;
790
797
  }
791
798
  /** Read a pool's roles from its policy. `state` saves the `getPool` round trip when already in hand. */
@@ -804,7 +811,12 @@ export interface FPoolDescription {
804
811
  * from the first wei after it.
805
812
  */
806
813
  unharvested: bigint;
807
- /** The most a harvest may take now by the KIND's promise: the bid, and on a pad the income. The policy may allow less — {@link FPoolRoles.maxHarvest}. */
814
+ /**
815
+ * The most a harvest may take now by the MANAGER's rule: the bid, `Q` once seeded (0 before).
816
+ * The manager's `harvestable()` getter was cut in F version 21; this is the same number, read
817
+ * off `getPool`. The policy may allow less (a pad: the income; a POL: above `keepQuote`) —
818
+ * {@link FPoolRoles.maxHarvest}.
819
+ */
808
820
  harvestable: bigint;
809
821
  /** The dead shares' banked dividend — a pad's pot, the creator's income until a rung is climbed. */
810
822
  pot: bigint;
@@ -1102,19 +1114,49 @@ export declare function sortFPoolLegs<T>(bases: Address[], ...companions: T[][])
1102
1114
  companions: T[][];
1103
1115
  };
1104
1116
  /** The key as the manager hashes it: legs sorted, the kind's policy filled in when none is named; no kind in it (049). */
1105
- export declare function fPoolKeyOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): {
1106
- quote: `0x${string}`;
1107
- bases: `0x${string}`[];
1117
+ /**
1118
+ * The tuple an F pool's id hashes — `src/types/FPoolKey.sol`, field order load-bearing (it is
1119
+ * the ABI encoding order). Stored by the manager at `initialize` and never in calldata again,
1120
+ * except `PolicyRegistry.migrate` (F version 20), which is handed it to check against the id.
1121
+ */
1122
+ export interface FPoolKey {
1123
+ quote: Address;
1124
+ /** Sorted ascending, unique. */
1125
+ bases: Address[];
1126
+ /** WAD, aligned with `bases`, summing to 1e18. */
1108
1127
  weights: bigint[];
1109
1128
  phi: bigint;
1110
- policy: `0x${string}`;
1111
- creator: `0x${string}`;
1112
- salt: `0x${string}`;
1113
- };
1129
+ /** The policy AT BIRTH — not the live one after a move. */
1130
+ policy: Address;
1131
+ creator: Address;
1132
+ salt: Hex;
1133
+ }
1134
+ /**
1135
+ * An F pool's id: `keccak256(abi.encode(key))`, offline — what `initialize` computes inline. The
1136
+ * manager's `idOf(FPoolKey)` getter was cut in F version 21; this is the same hash (checked
1137
+ * against Foundry's encoder and the local deployment's pools in the tests). The key must be the
1138
+ * one the pool was initialized with: legs sorted ({@link fPoolKeyOf} sorts them), the birth policy.
1139
+ */
1140
+ export declare function fPoolIdOfKey(key: FPoolKey): Hex;
1141
+ export declare function fPoolKeyOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): FPoolKey;
1114
1142
  /** Create the pool (no funds move — `seed` arms it). Returns the tx hash; read the id with `fPoolIdOf`. */
1115
1143
  export declare function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs): Promise<`0x${string}`>;
1116
- /** The pool id is the key's hash — pure, so it can be read before or after creation. */
1144
+ /**
1145
+ * The pool id is the key's hash — pure, so it can be read before or after creation. Computed
1146
+ * locally since F version 21 ({@link fPoolIdOfKey}; the manager's `idOf` is gone): no RPC. Still
1147
+ * async, so callers written against the read keep working.
1148
+ */
1117
1149
  export declare function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): Promise<Hex>;
1150
+ /**
1151
+ * Rebuild a live pool's KEY from its id: the `FInitialize` log carries every field but the salt,
1152
+ * which is taken from `opts.salt` (zero by default — every pool the SDK's create flow makes
1153
+ * without naming one). The result is checked against the id, so a wrong salt throws rather than
1154
+ * returning a key that names another pool. What {@link fPoolMigrate} needs when handed an id.
1155
+ */
1156
+ export declare function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts?: {
1157
+ salt?: Hex;
1158
+ fromBlock?: bigint;
1159
+ }): Promise<FPoolKey>;
1118
1160
  /**
1119
1161
  * The reserve a leg holds at a mark: `R = Lj·e^{−x}`, WAD math. The seed names RESERVES —
1120
1162
  * the sponsor states holdings and the price follows, `p = Lj / R` — so this is
@@ -1157,7 +1199,7 @@ export declare function fPoolRetireRaw(c: LogswapClient, a: {
1157
1199
  * Retire the live generation as ONE transaction: the harvest that empties the bid
1158
1200
  * ({@link fPoolEmptyingHarvests} — a dividend to every share; none when it is already empty),
1159
1201
  * then `retire` — the manager's `multicall`, the operator as sender. What a sponsor's
1160
- * "retire" button should call. Throws before sending on a pad that still bids.
1202
+ * "retire" button should call. Throws before sending on a pad: a pad never retires (F version 21).
1161
1203
  */
1162
1204
  export declare function fPoolRetire(c: LogswapClient, a: {
1163
1205
  poolId: Hex;
@@ -1167,18 +1209,29 @@ export declare function fPoolRetire(c: LogswapClient, a: {
1167
1209
  * The harvests that empty an unlocked pool's bid, in order — what a relaunch sends before its
1168
1210
  * `retire` (a retire needs an empty bid on every pool). Since F version 14 one
1169
1211
  * `harvest(Q)` does it: the protocol's cut leaves the pool with the amount (with no collector
1170
- * there is no cut). `[]` when the bid is already empty. Throws on a pad that still bids (its bid
1171
- * empties only as the market sells it down), and when the pool's policy keeps a floor on the bid
1212
+ * there is no cut). `[]` when the bid is already empty. Throws on a pad (F version 21: a pad
1213
+ * never retires, whatever its bid), and when the pool's policy keeps a floor on the bid
1172
1214
  * (`PolPolicy.keepQuote`): a `retire` under that policy waits for the market.
1173
1215
  */
1174
1216
  export declare function fPoolEmptyingHarvests(c: LogswapClient, poolId: Hex): Promise<bigint[]>;
1175
1217
  /**
1176
- * Release the dead shares' pot under the policy's terms — a pad's graduation.
1177
- * The manager harvests the generation's income first (the pot then holds everything the dead
1178
- * shares earned), asks the policy, and applies its answer: a share to the payee it names (the pad's
1179
- * creator — the pool's authority), a cut to the collector, the rest bought in as more of the
1180
- * float the pool owns, a slice of every reserve burned. The policy refuses under its rung's
1181
- * threshold (`PadPolicy.NotGraduated`). Whoever runs the pool.
1218
+ * The GENERATION STEP (F version 21) — on a pad, its graduation. The manager harvests the
1219
+ * generation's income first, releases the dead shares' pot, and asks the policy
1220
+ * (`onRegenerate`), whose {@link FPolicyTerms} name a `carried` holder. Then, for live
1221
+ * generation `g`:
1222
+ *
1223
+ * - the carried holder's and the dead shares' `g` shares are re-minted 1:1 as `g + 1`: they go on
1224
+ * as the pool;
1225
+ * - every OTHER holder of `g` is frozen out pro rata into generation `g`'s bundle (`Retired(g, …)`)
1226
+ * and redeems it with {@link fPoolRedeem} / rolls it with {@link fPoolRollIn} (`gen: g`); their
1227
+ * dividends on `g` stay claimable ({@link fPoolClaim} with `gen: g`). {@link fPoolClaimsOf}
1228
+ * lists them;
1229
+ * - the pot: `toPayee` to the payee, `protocolCut` to the collector, the rest bought in as dead
1230
+ * shares of `g + 1`; then the burn.
1231
+ *
1232
+ * A pad's gate is `threshold(gen)` (`PadPolicy.NotGraduated` below it); POL and private carry the
1233
+ * authority and send it the whole pot; a public pool refuses (`Refused`). A carried holder with no
1234
+ * share is `BadTerms`. Whoever runs the pool.
1182
1235
  */
1183
1236
  export declare function fPoolRegenerate(c: LogswapClient, a: {
1184
1237
  poolId: Hex;
@@ -1458,14 +1511,18 @@ export interface FPoolControlled {
1458
1511
  pool: DiscoveredFPool;
1459
1512
  roles: FPoolRoles;
1460
1513
  state: FPoolState;
1461
- /** The policy's numbers for this pool's kind — a pad's rung and threshold, a POL's floors, the desk's list flags. */
1514
+ /** The policy's numbers for this pool's kind — a pad's gen (its rung) and threshold, a POL's floors, the desk's list flags. */
1462
1515
  rules: FPoolRules;
1463
1516
  }
1464
1517
  /** What a pool's policy says about it, by kind: the terms a buyer reads before entering. */
1465
1518
  export type FPoolRules = {
1466
1519
  kind: "pad";
1467
1520
  harvestCut: bigint;
1521
+ /** The pad's graduation count: the pool's generation (F version 21 — `PadPolicy.rung` is gone). */
1522
+ gen: number;
1523
+ /** @deprecated Equal to {@link gen} since F version 21, kept for callers; read `gen`. */
1468
1524
  rung: number;
1525
+ /** The pot the next graduation needs: `PadPolicy.threshold(gen)`. */
1469
1526
  threshold: bigint;
1470
1527
  pot: bigint;
1471
1528
  creatorShare: bigint;
@@ -1510,6 +1567,18 @@ export declare function fPoolShareHolders(c: LogswapClient, poolIdHex: Hex, opts
1510
1567
  holder: Address;
1511
1568
  shares: bigint;
1512
1569
  }>>;
1570
+ /**
1571
+ * What a policy answers `onRegenerate` with (and `onHarvest`'s split, the first four words):
1572
+ * `IFPolicy.Terms`. `carried` is F version 21's fifth word — the holder the next generation
1573
+ * carries beside the dead shares; the reply is exactly 160 bytes.
1574
+ */
1575
+ export interface FPolicyTerms {
1576
+ payee: Address;
1577
+ toPayee: bigint;
1578
+ protocolCut: bigint;
1579
+ burn: bigint;
1580
+ carried: Address;
1581
+ }
1513
1582
  /** Every F pool's state in one lens call (the raw manager struct: quote, phi, authority, kind, seeded, gen, n, Q, L, shares, theta0, bigSigma, harvestedTheta). */
1514
1583
  export declare function getFPoolsRaw(c: LogswapClient, poolIds: Hex[]): Promise<readonly unknown[]>;
1515
1584
  /** Share value from the lens — (L + Q)/shares, WAD per share — the same figure as `fPoolShareValue`, from one source. */
package/dist/fpool.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * twice, forever. One router is one spender — see `fPoolZapIn`/`fPoolZapOut` below, which target
30
30
  * `addresses.router`.
31
31
  */
32
- import { encodeFunctionData, toFunctionSelector } from "viem";
32
+ import { encodeAbiParameters, encodeFunctionData, keccak256, toFunctionSelector } from "viem";
33
33
  import { fPoolManagerAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
34
34
  /**
35
35
  * One contract, one ABI (F version 12): `harvest`, `claim`, `retire`, `regenerate`,
@@ -192,10 +192,12 @@ export async function describeFPool(c, s) {
192
192
  functionName,
193
193
  args,
194
194
  });
195
- // an unseeded pool has no L: nothing earned, nothing to harvest, no pot
196
- const [unharvested, harvestable, pot] = s.L > 0n
197
- ? await Promise.all([rd("unharvested"), rd("harvestable"), rd("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
198
- : [0n, 0n, 0n];
195
+ // an unseeded pool has no L: nothing earned, nothing to harvest, no pot. The bid is the
196
+ // manager's ceiling (`harvestable()` until F version 21, which cut the getter): Q once seeded.
197
+ const harvestable = s.L > 0n ? s.Q : 0n;
198
+ const [unharvested, pot] = s.L > 0n
199
+ ? await Promise.all([rd("unharvested"), rd("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
200
+ : [0n, 0n];
199
201
  return {
200
202
  shape: s.kind === "pad" ? "launch" : "basket", // the kind, not n = 1
201
203
  legs: s.bases.length,
@@ -669,7 +671,29 @@ export function sortFPoolLegs(bases, ...companions) {
669
671
  const idx = bases.map((_, i) => i).sort((a, b) => (bases[a].toLowerCase() < bases[b].toLowerCase() ? -1 : 1));
670
672
  return { bases: idx.map((i) => bases[i]), companions: companions.map((arr) => idx.map((i) => arr[i])) };
671
673
  }
672
- /** The key as the manager hashes it: legs sorted, the kind's policy filled in when none is named; no kind in it (049). */
674
+ const F_POOL_KEY_ABI = [
675
+ {
676
+ type: "tuple",
677
+ components: [
678
+ { name: "quote", type: "address" },
679
+ { name: "bases", type: "address[]" },
680
+ { name: "weights", type: "uint256[]" },
681
+ { name: "phi", type: "uint256" },
682
+ { name: "policy", type: "address" },
683
+ { name: "creator", type: "address" },
684
+ { name: "salt", type: "bytes32" },
685
+ ],
686
+ },
687
+ ];
688
+ /**
689
+ * An F pool's id: `keccak256(abi.encode(key))`, offline — what `initialize` computes inline. The
690
+ * manager's `idOf(FPoolKey)` getter was cut in F version 21; this is the same hash (checked
691
+ * against Foundry's encoder and the local deployment's pools in the tests). The key must be the
692
+ * one the pool was initialized with: legs sorted ({@link fPoolKeyOf} sorts them), the birth policy.
693
+ */
694
+ export function fPoolIdOfKey(key) {
695
+ return keccak256(encodeAbiParameters(F_POOL_KEY_ABI, [key]));
696
+ }
673
697
  export function fPoolKeyOf(c, a) {
674
698
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
675
699
  return {
@@ -698,15 +722,46 @@ export async function fPoolInitialize(c, a) {
698
722
  });
699
723
  return c.wallet.writeContract(request);
700
724
  }
701
- /** The pool id is the key's hash — pure, so it can be read before or after creation. */
725
+ /**
726
+ * The pool id is the key's hash — pure, so it can be read before or after creation. Computed
727
+ * locally since F version 21 ({@link fPoolIdOfKey}; the manager's `idOf` is gone): no RPC. Still
728
+ * async, so callers written against the read keep working.
729
+ */
702
730
  export async function fPoolIdOf(c, a) {
703
- const key = fPoolKeyOf(c, a);
704
- return c.public.readContract({
731
+ return fPoolIdOfKey(fPoolKeyOf(c, a));
732
+ }
733
+ /**
734
+ * Rebuild a live pool's KEY from its id: the `FInitialize` log carries every field but the salt,
735
+ * which is taken from `opts.salt` (zero by default — every pool the SDK's create flow makes
736
+ * without naming one). The result is checked against the id, so a wrong salt throws rather than
737
+ * returning a key that names another pool. What {@link fPoolMigrate} needs when handed an id.
738
+ */
739
+ export async function fPoolKeyOfPool(c, poolId, opts = {}) {
740
+ const ev = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "FInitialize");
741
+ const logs = await c.public.getLogs({
705
742
  address: c.addresses.fPoolManager,
706
- abi: fPoolManagerAbi,
707
- functionName: "idOf",
708
- args: [key],
743
+ event: ev,
744
+ args: { poolId },
745
+ fromBlock: opts.fromBlock ?? 0n,
746
+ toBlock: "latest",
709
747
  });
748
+ const l = logs[0];
749
+ if (!l)
750
+ throw new Error(`logswap: no FInitialize for F pool ${poolId}`);
751
+ const a = l.args;
752
+ const key = {
753
+ quote: a.quote,
754
+ bases: [...a.bases],
755
+ weights: [...a.weights],
756
+ phi: a.phi,
757
+ policy: a.policy,
758
+ creator: a.creator,
759
+ salt: opts.salt ?? ZERO_SALT,
760
+ };
761
+ if (fPoolIdOfKey(key).toLowerCase() !== poolId.toLowerCase()) {
762
+ throw new Error(`logswap: F pool ${poolId} was not born with salt ${key.salt} — pass its salt, or its whole key`);
763
+ }
764
+ return key;
710
765
  }
711
766
  /**
712
767
  * The reserve a leg holds at a mark: `R = Lj·e^{−x}`, WAD math. The seed names RESERVES —
@@ -745,7 +800,7 @@ export async function fPoolRetireRaw(c, a) {
745
800
  * Retire the live generation as ONE transaction: the harvest that empties the bid
746
801
  * ({@link fPoolEmptyingHarvests} — a dividend to every share; none when it is already empty),
747
802
  * then `retire` — the manager's `multicall`, the operator as sender. What a sponsor's
748
- * "retire" button should call. Throws before sending on a pad that still bids.
803
+ * "retire" button should call. Throws before sending on a pad: a pad never retires (F version 21).
749
804
  */
750
805
  export async function fPoolRetire(c, a) {
751
806
  const harvests = await fPoolEmptyingHarvests(c, a.poolId);
@@ -759,28 +814,41 @@ export async function fPoolRetire(c, a) {
759
814
  * The harvests that empty an unlocked pool's bid, in order — what a relaunch sends before its
760
815
  * `retire` (a retire needs an empty bid on every pool). Since F version 14 one
761
816
  * `harvest(Q)` does it: the protocol's cut leaves the pool with the amount (with no collector
762
- * there is no cut). `[]` when the bid is already empty. Throws on a pad that still bids (its bid
763
- * empties only as the market sells it down), and when the pool's policy keeps a floor on the bid
817
+ * there is no cut). `[]` when the bid is already empty. Throws on a pad (F version 21: a pad
818
+ * never retires, whatever its bid), and when the pool's policy keeps a floor on the bid
764
819
  * (`PolPolicy.keepQuote`): a `retire` under that policy waits for the market.
765
820
  */
766
821
  export async function fPoolEmptyingHarvests(c, poolId) {
767
822
  const s = await getFPool(c, poolId);
823
+ // F version 21: `PadPolicy` refuses `retire` at any bid — a pad graduates (regenerate), it
824
+ // never closes; a fresh start is a new pad with a new id
825
+ if (s.kind === "pad")
826
+ throw new Error("logswap: a pad never retires (F version 21) — it graduates by regenerate; a fresh start is a new pad");
768
827
  if (s.Q === 0n)
769
828
  return [];
770
- if (s.kind === "pad")
771
- throw new Error("logswap: a pad is retired only once its bid is empty");
772
829
  const { maxHarvest } = await getFPoolRoles(c, poolId, s);
773
830
  if (maxHarvest < s.Q)
774
831
  throw new Error("logswap: the pool's policy keeps a floor on the bid — it cannot be harvested empty");
775
832
  return [s.Q];
776
833
  }
777
834
  /**
778
- * Release the dead shares' pot under the policy's terms — a pad's graduation.
779
- * The manager harvests the generation's income first (the pot then holds everything the dead
780
- * shares earned), asks the policy, and applies its answer: a share to the payee it names (the pad's
781
- * creator — the pool's authority), a cut to the collector, the rest bought in as more of the
782
- * float the pool owns, a slice of every reserve burned. The policy refuses under its rung's
783
- * threshold (`PadPolicy.NotGraduated`). Whoever runs the pool.
835
+ * The GENERATION STEP (F version 21) — on a pad, its graduation. The manager harvests the
836
+ * generation's income first, releases the dead shares' pot, and asks the policy
837
+ * (`onRegenerate`), whose {@link FPolicyTerms} name a `carried` holder. Then, for live
838
+ * generation `g`:
839
+ *
840
+ * - the carried holder's and the dead shares' `g` shares are re-minted 1:1 as `g + 1`: they go on
841
+ * as the pool;
842
+ * - every OTHER holder of `g` is frozen out pro rata into generation `g`'s bundle (`Retired(g, …)`)
843
+ * and redeems it with {@link fPoolRedeem} / rolls it with {@link fPoolRollIn} (`gen: g`); their
844
+ * dividends on `g` stay claimable ({@link fPoolClaim} with `gen: g`). {@link fPoolClaimsOf}
845
+ * lists them;
846
+ * - the pot: `toPayee` to the payee, `protocolCut` to the collector, the rest bought in as dead
847
+ * shares of `g + 1`; then the burn.
848
+ *
849
+ * A pad's gate is `threshold(gen)` (`PadPolicy.NotGraduated` below it); POL and private carry the
850
+ * authority and send it the whole pot; a public pool refuses (`Refused`). A carried holder with no
851
+ * share is `BadTerms`. Whoever runs the pool.
784
852
  */
785
853
  export async function fPoolRegenerate(c, a) {
786
854
  return writeFPool(c, a.poolId, "regenerate", [a.to ?? a.account], a.account);
@@ -1089,15 +1157,16 @@ export async function getFPoolRules(c, s, roles) {
1089
1157
  const harvestCut = await rd(fPolicyBaseAbi, "harvestCut");
1090
1158
  switch (s.kind) {
1091
1159
  case "pad": {
1092
- const [rung, creatorShare, protocolShare, burnPerRung, pot] = await Promise.all([
1093
- rd(padPolicyAbi, "rung", [s.poolId]),
1160
+ // the rung is the manager's generation (F version 21): a move carries it, nothing to ask the policy
1161
+ const gen = s.gen;
1162
+ const [creatorShare, protocolShare, burnPerRung, pot, threshold] = await Promise.all([
1094
1163
  rd(padPolicyAbi, "creatorShare"),
1095
1164
  rd(padPolicyAbi, "protocolShare"),
1096
1165
  rd(padPolicyAbi, "burnPerRung"),
1097
- fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, s.gen),
1166
+ fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, gen),
1167
+ rd(padPolicyAbi, "threshold", [BigInt(gen)]),
1098
1168
  ]);
1099
- const threshold = await rd(padPolicyAbi, "threshold", [rung]);
1100
- return { kind: "pad", harvestCut, rung: Number(rung), threshold, pot, creatorShare, protocolShare, burnPerRung };
1169
+ return { kind: "pad", harvestCut, gen, rung: gen, threshold, pot, creatorShare, protocolShare, burnPerRung };
1101
1170
  }
1102
1171
  case "pol": {
1103
1172
  const [keepQuote, minQuoteAtSeed] = await Promise.all([rd(polPolicyAbi, "keepQuote"), rd(polPolicyAbi, "minQuoteAtSeed")]);