@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/src/client.ts CHANGED
@@ -165,7 +165,19 @@ export const EXPECTED_VERSION = 5n;
165
165
  // is the policy at birth, `migrate` moves a pool for the registry's owner; `stamp` / `setAsks` are
166
166
  // the policy's; the desk's `setAllowed` / `isAllowed` are `setMinters` / `setTakers` / `isMinter` /
167
167
  // `isTaker`; a pad and a POL hold one base (`OneBase`).
168
- export const EXPECTED_F_VERSION = 19n;
168
+ // 18–19: no role, no kind and no name on the manager (049); the events carry what a reader needs (050).
169
+ // 20 (2026-09-25, logswap-contract #53): policy isolation — `PolicyRegistry.migrate(manager, key, to)`
170
+ // takes the pool's KEY (was its id), `kindOf(policy)` is the tag recorded at approval, the
171
+ // registry emits `Migrated`; `IFPolicy.onMigrate` is brought the creator, authority and name;
172
+ // the desk's lists are keyed by `listEpoch`. The manager's ABI did not move.
173
+ // 21 (2026-09-26, #54): `regenerate` is the generation step — the policy's `Terms` name a
174
+ // `carried` holder (a fifth word, 160 B), every other holder is frozen out pro rata into
175
+ // generation g's bundle and redeems it (`fPoolRedeem` / `fPoolRollIn` with gen = g), and the
176
+ // pool goes on as g + 1. The manager lost `FEE_MIN` / `FEE_MAX` (see keys.ts), `harvestable`
177
+ // (the bid: `getPool().Q` when L > 0), `idOf(FPoolKey)` (hashed locally, `fPoolIdOfKey`) and
178
+ // `reserveOf` (`legOf`'s fourth word). `PadPolicy.rung` is gone: the pad's rung is `gen`, the
179
+ // next threshold `threshold(gen)`. A pad never retires; a public pool refuses `regenerate`.
180
+ export const EXPECTED_F_VERSION = 21n;
169
181
 
170
182
  export async function assertVersion(c: LogswapClient, expected = EXPECTED_VERSION): Promise<void> {
171
183
  const { cPoolManagerAbi, fPoolManagerAbi } = await import("./generated.js");
package/src/fpool.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  * `addresses.router`.
31
31
  */
32
32
 
33
- import { encodeFunctionData, toFunctionSelector, type Address, type Hash, type Hex } from "viem";
33
+ import { encodeAbiParameters, encodeFunctionData, keccak256, toFunctionSelector, type Address, type Hash, type Hex } from "viem";
34
34
  import { fPoolManagerAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
35
35
 
36
36
  /**
@@ -99,9 +99,12 @@ export interface FPoolState {
99
99
  /** False before the first seed — and between a retire and the next one. */
100
100
  seeded: boolean;
101
101
  /**
102
- * The live GENERATION. Every retire freezes one into a bundle its shares
103
- * redeem ({@link fPoolRedeem}) and opens the next; the share id carries it
104
- * ({@link fPoolShareId}), so generations `0 … gen-1` are retired claims and `gen` is the pool.
102
+ * The live GENERATION. Every retire freezes one into a bundle its shares redeem
103
+ * ({@link fPoolRedeem}) and opens the next; since F version 21 so does every `regenerate`
104
+ * (the generation step, {@link fPoolRegenerate}): the carried holder and the dead shares go on
105
+ * as `gen + 1`, every other holder of `gen` is frozen out into its bundle. The share id carries
106
+ * it ({@link fPoolShareId}), so generations `0 … gen-1` are claims ({@link fPoolClaimsOf}) and
107
+ * `gen` is the pool. On a pad, `gen` is the graduation count — the rung.
105
108
  */
106
109
  gen: number;
107
110
  totalSupply: bigint;
@@ -272,7 +275,7 @@ export interface FPoolRoles {
272
275
  pendingOperator: Address;
273
276
  gateMint: boolean;
274
277
  gateSwap: boolean;
275
- /** The policy's own ceiling on a harvest now — the manager's `harvestable` narrowed by the policy's rule. */
278
+ /** The policy's own ceiling on a harvest now — the bid ({@link FPoolDescription.harvestable}) narrowed by the policy's rule. */
276
279
  maxHarvest: bigint;
277
280
  }
278
281
 
@@ -314,7 +317,12 @@ export interface FPoolDescription {
314
317
  * from the first wei after it.
315
318
  */
316
319
  unharvested: bigint;
317
- /** 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}. */
320
+ /**
321
+ * The most a harvest may take now by the MANAGER's rule: the bid, `Q` once seeded (0 before).
322
+ * The manager's `harvestable()` getter was cut in F version 21; this is the same number, read
323
+ * off `getPool`. The policy may allow less (a pad: the income; a POL: above `keepQuote`) —
324
+ * {@link FPoolRoles.maxHarvest}.
325
+ */
318
326
  harvestable: bigint;
319
327
  /** The dead shares' banked dividend — a pad's pot, the creator's income until a rung is climbed. */
320
328
  pot: bigint;
@@ -328,10 +336,12 @@ export async function describeFPool(c: LogswapClient, s: FPoolState): Promise<FP
328
336
  functionName,
329
337
  args,
330
338
  } as never) as Promise<T>;
331
- // an unseeded pool has no L: nothing earned, nothing to harvest, no pot
332
- const [unharvested, harvestable, pot] = s.L > 0n
333
- ? await Promise.all([rd<bigint>("unharvested"), rd<bigint>("harvestable"), rd<bigint>("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
334
- : [0n, 0n, 0n];
339
+ // an unseeded pool has no L: nothing earned, nothing to harvest, no pot. The bid is the
340
+ // manager's ceiling (`harvestable()` until F version 21, which cut the getter): Q once seeded.
341
+ const harvestable = s.L > 0n ? s.Q : 0n;
342
+ const [unharvested, pot] = s.L > 0n
343
+ ? await Promise.all([rd<bigint>("unharvested"), rd<bigint>("dividendOf", [s.poolId, BigInt(s.gen), DEAD_ADDRESS])])
344
+ : [0n, 0n];
335
345
  return {
336
346
  shape: s.kind === "pad" ? "launch": "basket", // the kind, not n = 1
337
347
  legs: s.bases.length,
@@ -985,7 +995,50 @@ export function sortFPoolLegs<T>(bases: Address[], ...companions: T[][]): { base
985
995
  }
986
996
 
987
997
  /** The key as the manager hashes it: legs sorted, the kind's policy filled in when none is named; no kind in it (049). */
988
- export function fPoolKeyOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">) {
998
+ /**
999
+ * The tuple an F pool's id hashes — `src/types/FPoolKey.sol`, field order load-bearing (it is
1000
+ * the ABI encoding order). Stored by the manager at `initialize` and never in calldata again,
1001
+ * except `PolicyRegistry.migrate` (F version 20), which is handed it to check against the id.
1002
+ */
1003
+ export interface FPoolKey {
1004
+ quote: Address;
1005
+ /** Sorted ascending, unique. */
1006
+ bases: Address[];
1007
+ /** WAD, aligned with `bases`, summing to 1e18. */
1008
+ weights: bigint[];
1009
+ phi: bigint;
1010
+ /** The policy AT BIRTH — not the live one after a move. */
1011
+ policy: Address;
1012
+ creator: Address;
1013
+ salt: Hex;
1014
+ }
1015
+
1016
+ const F_POOL_KEY_ABI = [
1017
+ {
1018
+ type: "tuple",
1019
+ components: [
1020
+ { name: "quote", type: "address" },
1021
+ { name: "bases", type: "address[]" },
1022
+ { name: "weights", type: "uint256[]" },
1023
+ { name: "phi", type: "uint256" },
1024
+ { name: "policy", type: "address" },
1025
+ { name: "creator", type: "address" },
1026
+ { name: "salt", type: "bytes32" },
1027
+ ],
1028
+ },
1029
+ ] as const;
1030
+
1031
+ /**
1032
+ * An F pool's id: `keccak256(abi.encode(key))`, offline — what `initialize` computes inline. The
1033
+ * manager's `idOf(FPoolKey)` getter was cut in F version 21; this is the same hash (checked
1034
+ * against Foundry's encoder and the local deployment's pools in the tests). The key must be the
1035
+ * one the pool was initialized with: legs sorted ({@link fPoolKeyOf} sorts them), the birth policy.
1036
+ */
1037
+ export function fPoolIdOfKey(key: FPoolKey): Hex {
1038
+ return keccak256(encodeAbiParameters(F_POOL_KEY_ABI, [key]));
1039
+ }
1040
+
1041
+ export function fPoolKeyOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): FPoolKey {
989
1042
  const { bases, companions } = sortFPoolLegs(a.bases, a.weights);
990
1043
  return {
991
1044
  quote: a.quote,
@@ -1014,15 +1067,47 @@ export async function fPoolInitialize(c: LogswapClient, a: FPoolCreateArgs) {
1014
1067
  return c.wallet!.writeContract(request as never);
1015
1068
  }
1016
1069
 
1017
- /** The pool id is the key's hash — pure, so it can be read before or after creation. */
1070
+ /**
1071
+ * The pool id is the key's hash — pure, so it can be read before or after creation. Computed
1072
+ * locally since F version 21 ({@link fPoolIdOfKey}; the manager's `idOf` is gone): no RPC. Still
1073
+ * async, so callers written against the read keep working.
1074
+ */
1018
1075
  export async function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "account">): Promise<Hex> {
1019
- const key = fPoolKeyOf(c, a);
1020
- return c.public.readContract({
1076
+ return fPoolIdOfKey(fPoolKeyOf(c, a));
1077
+ }
1078
+
1079
+ /**
1080
+ * Rebuild a live pool's KEY from its id: the `FInitialize` log carries every field but the salt,
1081
+ * which is taken from `opts.salt` (zero by default — every pool the SDK's create flow makes
1082
+ * without naming one). The result is checked against the id, so a wrong salt throws rather than
1083
+ * returning a key that names another pool. What {@link fPoolMigrate} needs when handed an id.
1084
+ */
1085
+ export async function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts: { salt?: Hex; fromBlock?: bigint } = {}): Promise<FPoolKey> {
1086
+ type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "FInitialize" }>;
1087
+ const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "FInitialize")!;
1088
+ const logs = await c.public.getLogs({
1021
1089
  address: c.addresses.fPoolManager!,
1022
- abi: fPoolManagerAbi,
1023
- functionName: "idOf",
1024
- args: [key],
1025
- } as never) as Promise<Hex>;
1090
+ event: ev,
1091
+ args: { poolId },
1092
+ fromBlock: opts.fromBlock ?? 0n,
1093
+ toBlock: "latest",
1094
+ });
1095
+ const l = logs[0];
1096
+ if (!l) throw new Error(`logswap: no FInitialize for F pool ${poolId}`);
1097
+ const a = l.args as { quote: Address; creator: Address; bases: readonly Address[]; weights: readonly bigint[]; phi: bigint; policy: Address };
1098
+ const key: FPoolKey = {
1099
+ quote: a.quote,
1100
+ bases: [...a.bases],
1101
+ weights: [...a.weights],
1102
+ phi: a.phi,
1103
+ policy: a.policy,
1104
+ creator: a.creator,
1105
+ salt: opts.salt ?? ZERO_SALT,
1106
+ };
1107
+ if (fPoolIdOfKey(key).toLowerCase() !== poolId.toLowerCase()) {
1108
+ throw new Error(`logswap: F pool ${poolId} was not born with salt ${key.salt} — pass its salt, or its whole key`);
1109
+ }
1110
+ return key;
1026
1111
  }
1027
1112
 
1028
1113
  /**
@@ -1068,7 +1153,7 @@ export async function fPoolRetireRaw(c: LogswapClient, a: { poolId: Hex; account
1068
1153
  * Retire the live generation as ONE transaction: the harvest that empties the bid
1069
1154
  * ({@link fPoolEmptyingHarvests} — a dividend to every share; none when it is already empty),
1070
1155
  * then `retire` — the manager's `multicall`, the operator as sender. What a sponsor's
1071
- * "retire" button should call. Throws before sending on a pad that still bids.
1156
+ * "retire" button should call. Throws before sending on a pad: a pad never retires (F version 21).
1072
1157
  */
1073
1158
  export async function fPoolRetire(c: LogswapClient, a: { poolId: Hex; account: Address }) {
1074
1159
  const harvests = await fPoolEmptyingHarvests(c, a.poolId);
@@ -1083,26 +1168,39 @@ export async function fPoolRetire(c: LogswapClient, a: { poolId: Hex; account: A
1083
1168
  * The harvests that empty an unlocked pool's bid, in order — what a relaunch sends before its
1084
1169
  * `retire` (a retire needs an empty bid on every pool). Since F version 14 one
1085
1170
  * `harvest(Q)` does it: the protocol's cut leaves the pool with the amount (with no collector
1086
- * there is no cut). `[]` when the bid is already empty. Throws on a pad that still bids (its bid
1087
- * empties only as the market sells it down), and when the pool's policy keeps a floor on the bid
1171
+ * there is no cut). `[]` when the bid is already empty. Throws on a pad (F version 21: a pad
1172
+ * never retires, whatever its bid), and when the pool's policy keeps a floor on the bid
1088
1173
  * (`PolPolicy.keepQuote`): a `retire` under that policy waits for the market.
1089
1174
  */
1090
1175
  export async function fPoolEmptyingHarvests(c: LogswapClient, poolId: Hex): Promise<bigint[]> {
1091
1176
  const s = await getFPool(c, poolId);
1177
+ // F version 21: `PadPolicy` refuses `retire` at any bid — a pad graduates (regenerate), it
1178
+ // never closes; a fresh start is a new pad with a new id
1179
+ if (s.kind === "pad") throw new Error("logswap: a pad never retires (F version 21) — it graduates by regenerate; a fresh start is a new pad");
1092
1180
  if (s.Q === 0n) return [];
1093
- if (s.kind === "pad") throw new Error("logswap: a pad is retired only once its bid is empty");
1094
1181
  const { maxHarvest } = await getFPoolRoles(c, poolId, s);
1095
1182
  if (maxHarvest < s.Q) throw new Error("logswap: the pool's policy keeps a floor on the bid — it cannot be harvested empty");
1096
1183
  return [s.Q];
1097
1184
  }
1098
1185
 
1099
1186
  /**
1100
- * Release the dead shares' pot under the policy's terms — a pad's graduation.
1101
- * The manager harvests the generation's income first (the pot then holds everything the dead
1102
- * shares earned), asks the policy, and applies its answer: a share to the payee it names (the pad's
1103
- * creator — the pool's authority), a cut to the collector, the rest bought in as more of the
1104
- * float the pool owns, a slice of every reserve burned. The policy refuses under its rung's
1105
- * threshold (`PadPolicy.NotGraduated`). Whoever runs the pool.
1187
+ * The GENERATION STEP (F version 21) — on a pad, its graduation. The manager harvests the
1188
+ * generation's income first, releases the dead shares' pot, and asks the policy
1189
+ * (`onRegenerate`), whose {@link FPolicyTerms} name a `carried` holder. Then, for live
1190
+ * generation `g`:
1191
+ *
1192
+ * - the carried holder's and the dead shares' `g` shares are re-minted 1:1 as `g + 1`: they go on
1193
+ * as the pool;
1194
+ * - every OTHER holder of `g` is frozen out pro rata into generation `g`'s bundle (`Retired(g, …)`)
1195
+ * and redeems it with {@link fPoolRedeem} / rolls it with {@link fPoolRollIn} (`gen: g`); their
1196
+ * dividends on `g` stay claimable ({@link fPoolClaim} with `gen: g`). {@link fPoolClaimsOf}
1197
+ * lists them;
1198
+ * - the pot: `toPayee` to the payee, `protocolCut` to the collector, the rest bought in as dead
1199
+ * shares of `g + 1`; then the burn.
1200
+ *
1201
+ * A pad's gate is `threshold(gen)` (`PadPolicy.NotGraduated` below it); POL and private carry the
1202
+ * authority and send it the whole pot; a public pool refuses (`Refused`). A carried holder with no
1203
+ * share is `BadTerms`. Whoever runs the pool.
1106
1204
  */
1107
1205
  export async function fPoolRegenerate(c: LogswapClient, a: { poolId: Hex; to?: Address; account: Address }) {
1108
1206
  return writeFPool(c, a.poolId, "regenerate", [a.to ?? a.account], a.account);
@@ -1534,13 +1632,26 @@ export interface FPoolControlled {
1534
1632
  pool: DiscoveredFPool;
1535
1633
  roles: FPoolRoles;
1536
1634
  state: FPoolState;
1537
- /** The policy's numbers for this pool's kind — a pad's rung and threshold, a POL's floors, the desk's list flags. */
1635
+ /** 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. */
1538
1636
  rules: FPoolRules;
1539
1637
  }
1540
1638
 
1541
1639
  /** What a pool's policy says about it, by kind: the terms a buyer reads before entering. */
1542
1640
  export type FPoolRules =
1543
- | { kind: "pad"; harvestCut: bigint; rung: number; threshold: bigint; pot: bigint; creatorShare: bigint; protocolShare: bigint; burnPerRung: bigint }
1641
+ | {
1642
+ kind: "pad";
1643
+ harvestCut: bigint;
1644
+ /** The pad's graduation count: the pool's generation (F version 21 — `PadPolicy.rung` is gone). */
1645
+ gen: number;
1646
+ /** @deprecated Equal to {@link gen} since F version 21, kept for callers; read `gen`. */
1647
+ rung: number;
1648
+ /** The pot the next graduation needs: `PadPolicy.threshold(gen)`. */
1649
+ threshold: bigint;
1650
+ pot: bigint;
1651
+ creatorShare: bigint;
1652
+ protocolShare: bigint;
1653
+ burnPerRung: bigint;
1654
+ }
1544
1655
  | { kind: "pol"; harvestCut: bigint; keepQuote: bigint; minQuoteAtSeed: bigint }
1545
1656
  | { kind: "public"; harvestCut: bigint; curated: boolean }
1546
1657
  | { kind: "private"; harvestCut: bigint; gateMint: boolean; gateSwap: boolean; operator: Address };
@@ -1552,15 +1663,16 @@ export async function getFPoolRules(c: LogswapClient, s: Pick<FPoolState, "poolI
1552
1663
  const harvestCut = await rd<bigint>(fPolicyBaseAbi, "harvestCut");
1553
1664
  switch (s.kind) {
1554
1665
  case "pad": {
1555
- const [rung, creatorShare, protocolShare, burnPerRung, pot] = await Promise.all([
1556
- rd<bigint>(padPolicyAbi, "rung", [s.poolId]),
1666
+ // the rung is the manager's generation (F version 21): a move carries it, nothing to ask the policy
1667
+ const gen = s.gen;
1668
+ const [creatorShare, protocolShare, burnPerRung, pot, threshold] = await Promise.all([
1557
1669
  rd<bigint>(padPolicyAbi, "creatorShare"),
1558
1670
  rd<bigint>(padPolicyAbi, "protocolShare"),
1559
1671
  rd<bigint>(padPolicyAbi, "burnPerRung"),
1560
- fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, s.gen),
1672
+ fPoolDividendOf(c, s.poolId, DEAD_ADDRESS, gen),
1673
+ rd<bigint>(padPolicyAbi, "threshold", [BigInt(gen)]),
1561
1674
  ]);
1562
- const threshold = await rd<bigint>(padPolicyAbi, "threshold", [rung]);
1563
- return { kind: "pad", harvestCut, rung: Number(rung), threshold, pot, creatorShare, protocolShare, burnPerRung };
1675
+ return { kind: "pad", harvestCut, gen, rung: gen, threshold, pot, creatorShare, protocolShare, burnPerRung };
1564
1676
  }
1565
1677
  case "pol": {
1566
1678
  const [keepQuote, minQuoteAtSeed] = await Promise.all([rd<bigint>(polPolicyAbi, "keepQuote"), rd<bigint>(polPolicyAbi, "minQuoteAtSeed")]);
@@ -1640,6 +1752,19 @@ export async function fPoolShareHolders(
1640
1752
  .sort((a, b) => (b.shares > a.shares ? 1 : -1));
1641
1753
  }
1642
1754
 
1755
+ /**
1756
+ * What a policy answers `onRegenerate` with (and `onHarvest`'s split, the first four words):
1757
+ * `IFPolicy.Terms`. `carried` is F version 21's fifth word — the holder the next generation
1758
+ * carries beside the dead shares; the reply is exactly 160 bytes.
1759
+ */
1760
+ export interface FPolicyTerms {
1761
+ payee: Address;
1762
+ toPayee: bigint;
1763
+ protocolCut: bigint;
1764
+ burn: bigint;
1765
+ carried: Address;
1766
+ }
1767
+
1643
1768
  /** 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). */
1644
1769
  export async function getFPoolsRaw(c: LogswapClient, poolIds: Hex[]): Promise<readonly unknown[]> {
1645
1770
  const { logswapLensAbi } = await import("./generated.js");
package/src/generated.ts CHANGED
@@ -1100,20 +1100,6 @@ export const fPoolManagerAbi = [
1100
1100
  outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
1101
1101
  stateMutability: 'view',
1102
1102
  },
1103
- {
1104
- type: 'function',
1105
- inputs: [],
1106
- name: 'FEE_MAX',
1107
- outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
1108
- stateMutability: 'view',
1109
- },
1110
- {
1111
- type: 'function',
1112
- inputs: [],
1113
- name: 'FEE_MIN',
1114
- outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
1115
- stateMutability: 'view',
1116
- },
1117
1103
  {
1118
1104
  type: 'function',
1119
1105
  inputs: [],
@@ -1330,35 +1316,6 @@ export const fPoolManagerAbi = [
1330
1316
  outputs: [],
1331
1317
  stateMutability: 'nonpayable',
1332
1318
  },
1333
- {
1334
- type: 'function',
1335
- inputs: [{ name: 'poolId', internalType: 'bytes32', type: 'bytes32' }],
1336
- name: 'harvestable',
1337
- outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
1338
- stateMutability: 'view',
1339
- },
1340
- {
1341
- type: 'function',
1342
- inputs: [
1343
- {
1344
- name: 'key',
1345
- internalType: 'struct FPoolKey',
1346
- type: 'tuple',
1347
- components: [
1348
- { name: 'quote', internalType: 'address', type: 'address' },
1349
- { name: 'bases', internalType: 'address[]', type: 'address[]' },
1350
- { name: 'weights', internalType: 'uint256[]', type: 'uint256[]' },
1351
- { name: 'phi', internalType: 'uint256', type: 'uint256' },
1352
- { name: 'policy', internalType: 'address', type: 'address' },
1353
- { name: 'creator', internalType: 'address', type: 'address' },
1354
- { name: 'salt', internalType: 'bytes32', type: 'bytes32' },
1355
- ],
1356
- },
1357
- ],
1358
- name: 'idOf',
1359
- outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }],
1360
- stateMutability: 'pure',
1361
- },
1362
1319
  {
1363
1320
  type: 'function',
1364
1321
  inputs: [
@@ -1516,16 +1473,6 @@ export const fPoolManagerAbi = [
1516
1473
  ],
1517
1474
  stateMutability: 'view',
1518
1475
  },
1519
- {
1520
- type: 'function',
1521
- inputs: [
1522
- { name: 'poolId', internalType: 'bytes32', type: 'bytes32' },
1523
- { name: 'j', internalType: 'uint256', type: 'uint256' },
1524
- ],
1525
- name: 'reserveOf',
1526
- outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
1527
- stateMutability: 'view',
1528
- },
1529
1476
  {
1530
1477
  type: 'function',
1531
1478
  inputs: [{ name: 'poolId', internalType: 'bytes32', type: 'bytes32' }],
@@ -3951,6 +3898,7 @@ export const padPolicyAbi = [
3951
3898
  { name: 'toPayee', internalType: 'uint256', type: 'uint256' },
3952
3899
  { name: 'protocolCut', internalType: 'uint256', type: 'uint256' },
3953
3900
  { name: 'burn', internalType: 'uint256', type: 'uint256' },
3901
+ { name: 'carried', internalType: 'address', type: 'address' },
3954
3902
  ],
3955
3903
  },
3956
3904
  ],
@@ -3994,13 +3942,6 @@ export const padPolicyAbi = [
3994
3942
  outputs: [{ name: '', internalType: 'bool', type: 'bool' }],
3995
3943
  stateMutability: 'view',
3996
3944
  },
3997
- {
3998
- type: 'function',
3999
- inputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }],
4000
- name: 'rung',
4001
- outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
4002
- stateMutability: 'view',
4003
- },
4004
3945
  {
4005
3946
  type: 'function',
4006
3947
  inputs: [{ name: '_harvestCut', internalType: 'uint256', type: 'uint256' }],
@@ -4178,26 +4119,6 @@ export const padPolicyAbi = [
4178
4119
  ],
4179
4120
  name: 'QuoteListed',
4180
4121
  },
4181
- {
4182
- type: 'event',
4183
- anonymous: false,
4184
- inputs: [
4185
- {
4186
- name: 'poolId',
4187
- internalType: 'bytes32',
4188
- type: 'bytes32',
4189
- indexed: true,
4190
- },
4191
- { name: 'from', internalType: 'address', type: 'address', indexed: true },
4192
- {
4193
- name: 'rung',
4194
- internalType: 'uint256',
4195
- type: 'uint256',
4196
- indexed: false,
4197
- },
4198
- ],
4199
- name: 'RungCarried',
4200
- },
4201
4122
  {
4202
4123
  type: 'event',
4203
4124
  anonymous: false,
@@ -4470,6 +4391,7 @@ export const polPolicyAbi = [
4470
4391
  { name: 'toPayee', internalType: 'uint256', type: 'uint256' },
4471
4392
  { name: 'protocolCut', internalType: 'uint256', type: 'uint256' },
4472
4393
  { name: 'burn', internalType: 'uint256', type: 'uint256' },
4394
+ { name: 'carried', internalType: 'address', type: 'address' },
4473
4395
  ],
4474
4396
  },
4475
4397
  ],
@@ -5240,6 +5162,7 @@ export const privatePolicyAbi = [
5240
5162
  { name: 'toPayee', internalType: 'uint256', type: 'uint256' },
5241
5163
  { name: 'protocolCut', internalType: 'uint256', type: 'uint256' },
5242
5164
  { name: 'burn', internalType: 'uint256', type: 'uint256' },
5165
+ { name: 'carried', internalType: 'address', type: 'address' },
5243
5166
  ],
5244
5167
  },
5245
5168
  ],
@@ -5770,6 +5693,7 @@ export const publicPolicyAbi = [
5770
5693
  { name: 'toPayee', internalType: 'uint256', type: 'uint256' },
5771
5694
  { name: 'protocolCut', internalType: 'uint256', type: 'uint256' },
5772
5695
  { name: 'burn', internalType: 'uint256', type: 'uint256' },
5696
+ { name: 'carried', internalType: 'address', type: 'address' },
5773
5697
  ],
5774
5698
  },
5775
5699
  ],
package/src/keys.ts CHANGED
@@ -13,6 +13,16 @@
13
13
 
14
14
  import { encodeAbiParameters, keccak256, type Address, type Hex } from "viem";
15
15
 
16
+ /**
17
+ * The fee domain of the family, `FeeParams.FEE_MIN` / `FEE_MAX` (decisions 043): one bound for a
18
+ * C market's φ and an F pool's alike, WAD. 1 bp and 1%. The F manager no longer answers
19
+ * `FEE_MIN()` / `FEE_MAX()` (F version 21 cut the getters for bytecode); every F policy still
20
+ * checks φ against these at `onInitialize`, and the C side re-exposes them through the lens's
21
+ * `cParams()`. Constants here, so a create form bounds φ with no read.
22
+ */
23
+ export const FEE_MIN = 10n ** 14n;
24
+ export const FEE_MAX = 10n ** 16n;
25
+
16
26
  /**
17
27
  * The economic tuple that IS the market. Field order is load-bearing: it is the ABI encoding order,
18
28
  * so it must match `src/types/PoolKey.sol` exactly.
@@ -23,7 +33,7 @@ export interface PoolKey {
23
33
  /** Log-tick spacing, WAD. The LP-boundary ladder — position floors and caps. */
24
34
  tickSpacing: bigint;
25
35
  /**
26
- * The swap fee, WAD, fixed for the life of the market: `FEE_MIN` (1 bp) ≤ φ ≤ `FEE_MAX` (25%),
36
+ * The swap fee, WAD, fixed for the life of the market: {@link FEE_MIN} (1 bp) ≤ φ ≤ {@link FEE_MAX} (1%),
27
37
  * or `0n`, which selects `DEFAULT_FEE` (2 bp) at read time. Four fields since C version 4
28
38
  *: `kappa` and `alpha` left with the variance kernel, and `phiMin` — a floor with
29
39
  * nothing above it — became `phi`. Every pool id changed with the shape.
package/src/pools.ts CHANGED
@@ -82,6 +82,46 @@ export function priceScale(baseDecimals: number, quoteDecimals: number): number
82
82
  return 10 ** (baseDecimals - quoteDecimals);
83
83
  }
84
84
 
85
+ /** The C manager's log-price wall: |x| ≤ 46 on the WEI ratio (`MAX_ABS_FLOOR`). */
86
+ export const C_MAX_ABS_X = 46;
87
+
88
+ /** The least derived base reserve a C book keeps, in base wei (`MIN_RESERVE`, decisions 023). */
89
+ export const C_MIN_RESERVE = 10n ** 6n;
90
+
91
+ /**
92
+ * The prices a C market can hold, in human units — quote per whole base token (decisions 054).
93
+ *
94
+ * - `listMin`, `listMax`: the wall, `10^(base − quote) · e^{∓46}`. A market cannot be initialized,
95
+ * and no floor or cap placed, outside it. An 18-dec token against 6-dec USDC cannot list under
96
+ * 1.05e-8; against an 18-dec quote the same token lists down to 1.05e-20.
97
+ * - `ceiling`: the highest price the book can trade to with `lActive` of active exposure (quote
98
+ * wei): the derived reserve `lActive / p` must stay at or above `C_MIN_RESERVE` base wei, so
99
+ * `ceiling = lActive · 10^(base − quote) / 1e6`, capped by `listMax`. In whole units that is
100
+ * `L · 10^(base − 6)`: the quote's decimals cancel and the base's set it — harmless on an
101
+ * 18-dec base, but a WBTC book of 1,000 USDC of depth stops at 100,000. Omit `lActive` for the
102
+ * wall alone.
103
+ *
104
+ * Floats, for display and warnings; the chain is the authority.
105
+ */
106
+ export function cPriceRange(
107
+ baseDecimals: number,
108
+ quoteDecimals: number,
109
+ lActive?: bigint,
110
+ ): { listMin: number; listMax: number; ceiling: number } {
111
+ const scale = priceScale(baseDecimals, quoteDecimals);
112
+ const listMin = Math.exp(-C_MAX_ABS_X) * scale;
113
+ const listMax = Math.exp(C_MAX_ABS_X) * scale;
114
+ const ceiling =
115
+ lActive === undefined || lActive === 0n ? listMax : Math.min(listMax, (Number(lActive) / Number(C_MIN_RESERVE)) * scale);
116
+ return { listMin, listMax, ceiling };
117
+ }
118
+
119
+ /** Whether a C market can be initialized at `price` (human units) for this pair's decimals. */
120
+ export function cListable(price: number, baseDecimals: number, quoteDecimals: number): boolean {
121
+ const { listMin, listMax } = cPriceRange(baseDecimals, quoteDecimals);
122
+ return price >= listMin && price <= listMax;
123
+ }
124
+
85
125
  /**
86
126
  * The model-free LP edge, $F - \tfrac12\Sigma$ — the protocol's headline metric, in quote per unit
87
127
  * of exposure.
package/src/write.ts CHANGED
@@ -15,6 +15,7 @@
15
15
 
16
16
  import type { Address, Hash, Hex } from "viem";
17
17
  import type { LogswapClient } from "./client.js";
18
+ import { cPoolManagerAbi, fPoolManagerAbi } from "./generated.js";
18
19
 
19
20
  /** Disables the deadline check. Deliberate opt-out, never a default. */
20
21
  export const NEVER = (1n << 256n) - 1n;
@@ -87,6 +88,64 @@ export function withSlippage(quoted: bigint, bps: number, side: "output" | "inpu
87
88
  return side === "output" ? (quoted * (10_000n - b)) / 10_000n : (quoted * (10_000n + b) + 9_999n) / 10_000n;
88
89
  }
89
90
 
91
+ type AbiError = { type: "error"; name: string; inputs: readonly { type: string }[] };
92
+
93
+ /** Permit2's pull errors — Uniswap's and the devnet mock's — which a router write can revert with. */
94
+ const permit2Errors: readonly AbiError[] = [
95
+ { type: "error", name: "AllowanceExpired", inputs: [{ type: "uint256" }] },
96
+ { type: "error", name: "InsufficientAllowance", inputs: [{ type: "uint256" }] },
97
+ { type: "error", name: "AllowanceExpired", inputs: [] },
98
+ { type: "error", name: "NotAllowed", inputs: [] },
99
+ ];
100
+
101
+ const errorKey = (e: AbiError) => `${e.name}(${e.inputs.map((i) => i.type).join(",")})`;
102
+
103
+ /**
104
+ * Every custom error a router call can revert with that the router's own ABI does not declare: the
105
+ * C and F managers' (a mint, a swap or a settlement bubbles them up unchanged) and Permit2's.
106
+ * Without them viem can name only the router's errors, and anything raised in the manager comes
107
+ * back as a bare selector.
108
+ */
109
+ export const routerRevertErrors: readonly AbiError[] = (() => {
110
+ const seen = new Set<string>();
111
+ const out: AbiError[] = [];
112
+ for (const x of [...cPoolManagerAbi, ...fPoolManagerAbi, ...permit2Errors] as readonly { type: string }[]) {
113
+ if (x.type !== "error") continue;
114
+ const e = x as AbiError;
115
+ if (!seen.has(errorKey(e))) {
116
+ seen.add(errorKey(e));
117
+ out.push(e);
118
+ }
119
+ }
120
+ return out;
121
+ })();
122
+
123
+ /** `abi` plus every error in {@link routerRevertErrors} it does not already declare. */
124
+ export function withRevertErrors(abi: readonly unknown[]): readonly unknown[] {
125
+ const have = new Set(
126
+ (abi as readonly { type: string }[]).filter((x) => x.type === "error").map((x) => errorKey(x as AbiError)),
127
+ );
128
+ return [...abi, ...routerRevertErrors.filter((e) => !have.has(errorKey(e)))];
129
+ }
130
+
131
+ /**
132
+ * The decoded revert of a contract call, e.g. `SlippageIn()` or `AllowanceExpired(1700000000)`,
133
+ * found however deep viem nested it; the raw selector when the error is not in the ABI; undefined
134
+ * when the failure was not a revert. viem's `message` puts the name on its second line, so a UI
135
+ * that shows only the first line ("The contract function … reverted.") loses it.
136
+ */
137
+ export function revertName(e: unknown): string | undefined {
138
+ const seen = new Set<unknown>();
139
+ for (let x: unknown = e; x && typeof x === "object" && !seen.has(x); x = (x as { cause?: unknown }).cause) {
140
+ seen.add(x);
141
+ const o = x as { name?: string; data?: { errorName?: string; args?: readonly unknown[] }; signature?: string };
142
+ if (o.name !== "ContractFunctionRevertedError") continue;
143
+ if (o.data?.errorName) return `${o.data.errorName}(${(o.data.args ?? []).map(String).join(", ")})`;
144
+ if (o.signature) return o.signature;
145
+ }
146
+ return undefined;
147
+ }
148
+
90
149
  /**
91
150
  * Send a router write with a gas buffer over an explicit estimate.
92
151
  *
@@ -102,7 +161,7 @@ export async function sendRouterWrite(
102
161
  const { account } = requireWallet(c);
103
162
  const params = {
104
163
  address: c.addresses.router,
105
- abi: args.abi,
164
+ abi: withRevertErrors(args.abi),
106
165
  functionName: args.functionName,
107
166
  args: args.args,
108
167
  account,
@@ -147,7 +206,7 @@ export async function simulateRouterWrite<T>(
147
206
  const account = args.account ?? c.wallet?.account?.address;
148
207
  const { result } = await c.public.simulateContract({
149
208
  address: c.addresses.router,
150
- abi: args.abi,
209
+ abi: withRevertErrors(args.abi),
151
210
  functionName: args.functionName,
152
211
  args: args.args,
153
212
  account,