hookr-sdk 0.2.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATING.md +89 -0
  4. package/README.md +308 -105
  5. package/dist/core/abi.d.ts +10930 -0
  6. package/dist/core/abi.js +455 -0
  7. package/dist/core/advisories.d.ts +184 -0
  8. package/dist/core/advisories.js +291 -0
  9. package/dist/core/client.d.ts +55 -0
  10. package/dist/core/client.js +107 -0
  11. package/dist/core/generated.d.ts +14295 -0
  12. package/dist/core/generated.js +1058 -0
  13. package/dist/core/index.d.ts +17 -0
  14. package/dist/core/index.js +17 -0
  15. package/dist/core/launch.d.ts +200 -0
  16. package/dist/core/launch.js +427 -0
  17. package/dist/core/pair.d.ts +64 -0
  18. package/dist/core/pair.js +136 -0
  19. package/dist/core/params.d.ts +415 -0
  20. package/dist/core/params.js +1235 -0
  21. package/dist/core/policies.d.ts +136 -0
  22. package/dist/core/policies.js +125 -0
  23. package/dist/core/proofs.d.ts +25 -0
  24. package/dist/core/proofs.js +118 -0
  25. package/dist/core/quotes.d.ts +185 -0
  26. package/dist/core/quotes.js +550 -0
  27. package/dist/core/referrals.d.ts +32 -0
  28. package/dist/core/referrals.js +36 -0
  29. package/dist/core/registry.d.ts +357 -0
  30. package/dist/core/registry.js +378 -0
  31. package/dist/core/rules.d.ts +675 -0
  32. package/dist/core/rules.js +1049 -0
  33. package/dist/core/schema.d.ts +358 -0
  34. package/dist/core/schema.js +105 -0
  35. package/dist/core/stocks.generated.d.ts +11 -0
  36. package/dist/core/stocks.generated.js +1228 -0
  37. package/dist/core/vault.d.ts +59 -0
  38. package/dist/core/vault.js +112 -0
  39. package/dist/core/wallet.d.ts +142 -0
  40. package/dist/core/wallet.js +304 -0
  41. package/dist/deployments.d.ts +8 -0
  42. package/dist/deployments.js +26 -0
  43. package/dist/index.d.ts +42 -15
  44. package/dist/index.js +40 -15
  45. package/dist/releases.d.ts +219 -0
  46. package/dist/releases.js +365 -0
  47. package/dist/wallet-cap.d.ts +159 -0
  48. package/dist/wallet-cap.js +234 -0
  49. package/package.json +46 -10
  50. package/provenance.json +31 -0
  51. package/dist/abi.d.ts +0 -6404
  52. package/dist/abi.js +0 -8300
  53. package/dist/chain.d.ts +0 -87
  54. package/dist/chain.js +0 -86
  55. package/dist/claims.d.ts +0 -32
  56. package/dist/claims.js +0 -31
  57. package/dist/fees.d.ts +0 -59
  58. package/dist/fees.js +0 -142
  59. package/dist/links.d.ts +0 -48
  60. package/dist/links.js +0 -93
  61. package/dist/market.d.ts +0 -264
  62. package/dist/market.js +0 -310
  63. package/dist/reads.d.ts +0 -136
  64. package/dist/reads.js +0 -219
  65. package/dist/rules.d.ts +0 -84
  66. package/dist/rules.js +0 -296
  67. package/dist/swap.d.ts +0 -108
  68. package/dist/swap.js +0 -144
@@ -0,0 +1,1235 @@
1
+ import { decodeAbiParameters, encodeAbiParameters } from "viem";
2
+ import { RELEASE_CONSTANTS } from "./generated.js";
3
+ import { checkFeeAdvisoryConfig, checkSessionSchedule, checkSessionTiers, encodeFeeAdvisoryBind, encodeSessionSchedule, feeAdvisoryMinGas, } from "./advisories.js";
4
+ import { checkPairRootParams } from "./pair.js";
5
+ import { ADMISSION_KIND, PHASE, checkAdmission, checkPairRootAdmission } from "./registry.js";
6
+ import { DYNAMIC_FEE_LIQUIDITY_DIVISOR, DYNAMIC_FEE_PARAMETERS, DYNAMIC_FEE_REACH, MAX_GUARD_BLOCKS, MAX_LP_FEE_PIPS, MAX_PROTOCOL_SHARE_BPS, MAX_RULE_SHARE_FLOOR_BPS, RULES_KNOB_BOUNDS, SNIPE_CURVE, checkRulesConfig, maxSnipeDecaySeconds, withinDynamicFeeReach, effectiveShareFloor, dynamicFeeMinLiquidity, encodeRulesData, lpRewardPips, nativeLpCeiling, poolCapsFor, rulesBindCaps, rulesFeeCeilings, } from "./rules.js";
7
+ import { address, assert, hash, ZERO, ZERO_HASH } from "./schema.js";
8
+ const U24 = 2n ** 24n - 1n;
9
+ const U32 = 2n ** 32n - 1n;
10
+ const U40 = 2n ** 40n - 1n;
11
+ const U96 = 2n ** 96n - 1n;
12
+ const U128 = 2n ** 128n - 1n;
13
+ const U160 = 2n ** 160n - 1n;
14
+ const U256 = 2n ** 256n - 1n;
15
+ const Q96 = 1n << 96n;
16
+ const I128 = 2n ** 127n - 1n;
17
+ const MAX_LP = MAX_LP_FEE_PIPS;
18
+ /** v4 TickMath.MAX_TICK_SPACING. */
19
+ const MAX_TICK_SPACING = 32_767;
20
+ const DEFAULT_BLOCK_TIME = 12;
21
+ /** Anti-Snipe guard length a launch starts with, in seconds. */
22
+ const DEFAULT_GUARD_SECONDS = 600;
23
+ function n(cfg, key) {
24
+ return Number(paramValue(cfg[key]));
25
+ }
26
+ function big(cfg, key) {
27
+ return paramValue(cfg[key]);
28
+ }
29
+ /** A knob value as an unsigned integer. Refuses fractions, negatives and malformed hex. */
30
+ export function paramValue(v) {
31
+ if (typeof v === "boolean")
32
+ return v ? 1n : 0n;
33
+ if (typeof v === "bigint")
34
+ return v;
35
+ if (typeof v === "number") {
36
+ assert(Number.isSafeInteger(v), `Invalid knob value ${v}`);
37
+ return BigInt(v);
38
+ }
39
+ assert(typeof v === "string" && /^0x[\da-fA-F]+$/.test(v), `Invalid knob value ${String(v)}`);
40
+ return BigInt(v);
41
+ }
42
+ function lpReward(cfg) {
43
+ return lpRewardPips({
44
+ lpBps: n(cfg, "lpBps"),
45
+ protocolShareBps: n(cfg, "protocolShareBps"),
46
+ royaltyBps: n(cfg, "royaltyBps"),
47
+ });
48
+ }
49
+ /** First v in [lo, hi] with ok(v), for predicates that stay true once true; hi + 1 when there is none. */
50
+ function first(lo, hi, ok) {
51
+ let a = lo;
52
+ let b = hi + 1;
53
+ while (a < b) {
54
+ const m = Math.floor((a + b) / 2);
55
+ if (ok(m))
56
+ b = m;
57
+ else
58
+ a = m + 1;
59
+ }
60
+ return a;
61
+ }
62
+ /** Last v in [lo, hi] with ok(v), for predicates that stay false once false; lo - 1 when there is none. */
63
+ function last(lo, hi, ok) {
64
+ let a = lo - 1;
65
+ let b = hi;
66
+ while (a < b) {
67
+ const m = Math.ceil((a + b) / 2);
68
+ if (ok(m))
69
+ a = m;
70
+ else
71
+ b = m - 1;
72
+ }
73
+ return a;
74
+ }
75
+ /** base + LP Rewards within the 600,000-pip LP-fee ceiling (HookrRules._validate). */
76
+ function lpRoom(cfg, patch) {
77
+ const c = { ...cfg, ...patch };
78
+ return n(c, "baseLpFeePips") + lpReward(c) <= MAX_LP;
79
+ }
80
+ const guarded = (cfg) => big(cfg, "guardEndBlock") !== 0n;
81
+ /** The dynamic fee's span, maxFeePips less the base fee (zero without dynamic fees). */
82
+ const span = (cfg) => Math.max(n(cfg, "maxFeePips") - n(cfg, "baseLpFeePips"), 0);
83
+ /** The widest span HookrDynamicFee's reach bound admits at the configuration's sensitivity and protocol share. */
84
+ const reachSpan = (cfg) => last(0, MAX_LP, (v) => withinDynamicFeeReach(v, n(cfg, "dynamicFeeSens"), n(cfg, "protocolShareBps")));
85
+ const decaying = (cfg) => n(cfg, "snipeDecaySeconds") !== 0;
86
+ const dynamicFeeOff = (cfg) => n(cfg, "maxFeePips") === n(cfg, "baseLpFeePips");
87
+ const dynamicFeeOn = (cfg) => n(cfg, "dynamicFeeSens") !== 0;
88
+ /** The default per-block guarded buy cap, in basis points of the launch's quote depth (`quoteDepth`). */
89
+ export const DEFAULT_MAX_BUY_DEPTH_BPS = 50n;
90
+ /**
91
+ * A launch member's quote depth in raw quote units: the virtual quote reserve of its liquidity at the opening price,
92
+ * L x 2^96 / sqrtPriceX96 when the quote is currency0 and L x sqrtPriceX96 / 2^96 when it is currency1. A buy of
93
+ * 0.5% of it moves the opening price about 1% while the price stays in the band. Single-sided launches seed no quote,
94
+ * so the seed is no measure of their depth; for a two-sided full-range seed the two are equal. Without the opening
95
+ * state it is the quote seed, and undefined without either.
96
+ */
97
+ export function quoteDepth(ctx) {
98
+ const { launchLiquidity: l, launchSqrtPriceX96: p, quoteIsCurrency0: q0 } = ctx;
99
+ if (l !== undefined && p !== undefined && q0 !== undefined) {
100
+ if (p <= 0n)
101
+ throw new RangeError("launchSqrtPriceX96 must be positive");
102
+ return q0 ? (l * Q96) / p : (l * p) / Q96;
103
+ }
104
+ return ctx.quoteSeed;
105
+ }
106
+ /** Pool knobs of a HookrRoot launch member (PoolKey and PoolConfig). */
107
+ const POOL = [
108
+ {
109
+ name: "baseLpFeePips",
110
+ type: "uint24",
111
+ unit: "pips",
112
+ where: "launch",
113
+ // With dynamic fees off maxFeePips equals base, so the two move together; with them the span stays within the
114
+ // reach bound (r31, N09).
115
+ min: (cfg) => n(cfg, "dynamicFeeSens") === 0 ? n(cfg, "maxFeePips") : Math.max(0, n(cfg, "maxFeePips") - reachSpan(cfg)),
116
+ max: (cfg) => Math.min(n(cfg, "dynamicFeeSens") === 0 ? n(cfg, "maxFeePips") : n(cfg, "maxFeePips") - 1, MAX_LP - n(cfg, "snipeTaxPips"), MAX_LP - lpReward(cfg)),
117
+ default: 3_000,
118
+ dependsOn: ["maxFeePips", "dynamicFeeSens", "snipeTaxPips", "lpBps", "royaltyBps", "protocolShareBps"],
119
+ },
120
+ // A Hookr dynamic fee pool binds at any tick spacing: the root simulates the swap, so no spacing floor applies.
121
+ { name: "tickSpacing", type: "int24", unit: "ticks", where: "launch", min: 1, max: MAX_TICK_SPACING, default: 60 },
122
+ ];
123
+ /**
124
+ * Anti-Snipe: a launch guard with a per-block buy cap and a tax on guarded buys. The tax may decay linearly from
125
+ * snipeTaxPips at launch to snipeFloorPips over snipeDecaySeconds; the per-block cap grows as it falls.
126
+ */
127
+ const ANTI_SNIPE = [
128
+ {
129
+ name: "guardEndBlock",
130
+ type: "uint40",
131
+ unit: "blocks",
132
+ where: "bind",
133
+ zeroIsOff: true,
134
+ min: (_c, ctx) => (ctx.blockNumber === undefined ? 1n : ctx.blockNumber + 1n),
135
+ max: (_c, ctx) => (ctx.blockNumber === undefined ? U40 : ctx.blockNumber + BigInt(MAX_GUARD_BLOCKS)),
136
+ default: (ctx) => ctx.blockNumber === undefined
137
+ ? 0n
138
+ : ctx.blockNumber + guardBlocksFor(DEFAULT_GUARD_SECONDS, ctx.blockTimeSeconds),
139
+ disclose: "While the guard lasts only the launcher adds liquidity, exact-output buys are refused and no member's principal " +
140
+ "can be withdrawn. It is counted in parent blocks.",
141
+ },
142
+ {
143
+ name: "maxBuyQuoteAmount",
144
+ type: "uint128",
145
+ unit: "raw",
146
+ where: "bind",
147
+ min: 0,
148
+ max: (cfg) => (guarded(cfg) ? U128 : 0n),
149
+ default: (ctx, cfg) => {
150
+ const depth = guarded(cfg) ? quoteDepth(ctx) : undefined;
151
+ if (depth === undefined)
152
+ return 0n;
153
+ const cap = (depth * DEFAULT_MAX_BUY_DEPTH_BPS) / 10000n;
154
+ return cap > U128 ? U128 : cap;
155
+ },
156
+ dependsOn: ["guardEndBlock"],
157
+ disclose: "Zero leaves guarded buys uncapped. Caps apply per pool, so a family's per-block cap is the sum of its members'. " +
158
+ "The default is 0.5% of the launch's quote depth, the buy that moves the opening price about 1%.",
159
+ },
160
+ {
161
+ name: "snipeTaxPips",
162
+ type: "uint24",
163
+ unit: "pips",
164
+ where: "bind",
165
+ // With a decay the launch tax sits above the floor it decays to.
166
+ min: (cfg) => (decaying(cfg) ? n(cfg, "snipeFloorPips") + 1 : 0),
167
+ max: (cfg) => (guarded(cfg) ? MAX_LP - n(cfg, "baseLpFeePips") : 0),
168
+ default: (_ctx, cfg) => (guarded(cfg) ? Math.min(100_000, MAX_LP - n(cfg, "baseLpFeePips")) : 0),
169
+ dependsOn: ["guardEndBlock", "baseLpFeePips", "snipeFloorPips", "snipeDecaySeconds"],
170
+ },
171
+ {
172
+ name: "snipeFloorPips",
173
+ type: "uint24",
174
+ unit: "pips",
175
+ where: "bind",
176
+ min: 0,
177
+ max: (cfg) => (decaying(cfg) ? n(cfg, "snipeTaxPips") - 1 : 0),
178
+ default: 0,
179
+ dependsOn: ["snipeTaxPips", "snipeDecaySeconds"],
180
+ disclose: "The tax a guarded buy pays once the decay ends, for the rest of the guard.",
181
+ },
182
+ {
183
+ name: "snipeDecaySeconds",
184
+ type: "uint16",
185
+ unit: "seconds",
186
+ where: "bind",
187
+ // A floor exists only with a decay; a decay needs a guard and a launch tax above its floor (HookrRules._validate).
188
+ min: (cfg) => (n(cfg, "snipeFloorPips") > 0 ? 1 : 0),
189
+ // r31: an hour, or the guard's own length at the launch block when it is longer (HookrRules._validate).
190
+ max: (cfg, ctx) => guarded(cfg) && n(cfg, "snipeTaxPips") > n(cfg, "snipeFloorPips")
191
+ ? maxSnipeDecaySeconds(big(cfg, "guardEndBlock"), ctx.blockNumber)
192
+ : 0,
193
+ default: (ctx, cfg) => guarded(cfg) && n(cfg, "snipeTaxPips") > 0
194
+ ? Math.min(300, maxSnipeDecaySeconds(big(cfg, "guardEndBlock"), ctx.blockNumber))
195
+ : 0,
196
+ dependsOn: ["guardEndBlock", "snipeTaxPips", "snipeFloorPips"],
197
+ disclose: "Zero charges the launch tax flat for the whole guard. The decay is counted in seconds from the launch, the " +
198
+ "guard in parent blocks, and the per-block buy cap grows as the tax falls. It runs up to an hour, or over the " +
199
+ "whole guard when the guard is longer.",
200
+ },
201
+ {
202
+ name: "snipeCurve",
203
+ type: "uint8",
204
+ unit: "raw",
205
+ where: "bind",
206
+ // RulesKnobs.snipeCurve (r31): another curve than linear only where the Snipe decays (HookrRules._checkKnobs).
207
+ min: 0,
208
+ max: (cfg) => (decaying(cfg) ? SNIPE_CURVE.step : SNIPE_CURVE.linear),
209
+ default: SNIPE_CURVE.linear,
210
+ dependsOn: ["snipeDecaySeconds"],
211
+ disclose: "How the tax falls over the decay: 0 linear (the default), 1 front-loaded (what is left to fall halves every " +
212
+ "eighth of the decay), 2 step (eight equal steps, held through each eighth). A curve other than linear goes " +
213
+ "through launchFamily; every other launch binds the linear curve.",
214
+ },
215
+ ];
216
+ /**
217
+ * Hookr dynamic fees: the root simulates each swap and the Rules price its move away from a time-anchored reference
218
+ * price, up to maxFeePips. A dynamic fee pool binds `abi.encode(RulesConfig, RulesKnobs)` (r31): a swap carries the
219
+ * pool's anchor to its end price only when its quote covers minLiquidity between the two prices, and the reference
220
+ * follows the pool's tempo (window, reset, carry, counted move). Its span, sensitivity and protocol share stay within
221
+ * HookrDynamicFee's reach bound.
222
+ */
223
+ const dynamicTempo = (name) => (_ctx, cfg) => !dynamicFeeOn(cfg)
224
+ ? 0
225
+ : {
226
+ windowSeconds: DYNAMIC_FEE_PARAMETERS.window,
227
+ resetSeconds: DYNAMIC_FEE_PARAMETERS.reset,
228
+ carryBps: DYNAMIC_FEE_PARAMETERS.carryBps,
229
+ moveTicks: DYNAMIC_FEE_PARAMETERS.moveTicks,
230
+ }[name];
231
+ const TEMPO_DISCLOSURE = "A tempo off the default goes through launchFamily; every other launch binds the default (30 s window, 120 s " +
232
+ "reset, 75% carry, 200-tick move).";
233
+ const DYNAMIC_FEE = [
234
+ {
235
+ name: "maxFeePips",
236
+ type: "uint24",
237
+ unit: "pips",
238
+ where: "bind",
239
+ min: (cfg) => (n(cfg, "dynamicFeeSens") === 0 ? n(cfg, "baseLpFeePips") : n(cfg, "baseLpFeePips") + 1),
240
+ // r31 (N09): within the reach bound at the configuration's sensitivity and protocol share.
241
+ max: (cfg) => n(cfg, "dynamicFeeSens") === 0
242
+ ? n(cfg, "baseLpFeePips")
243
+ : Math.min(MAX_LP, n(cfg, "baseLpFeePips") + reachSpan(cfg)),
244
+ default: (_ctx, cfg) => Math.min(n(cfg, "baseLpFeePips") + 20_000, MAX_LP),
245
+ dependsOn: ["baseLpFeePips", "dynamicFeeSens", "protocolShareBps"],
246
+ },
247
+ {
248
+ name: "dynamicFeeSens",
249
+ type: "uint16",
250
+ unit: "raw",
251
+ where: "bind",
252
+ // On exactly when maxFeePips is above base; any sensitivity from 1 to 10 at any tick spacing that keeps the span
253
+ // within the reach bound (span x sens^2 at most MAX_REACH, and span x share x (432 - sens^2) at most
254
+ // MAX_RESERVE x 432): the first term caps the sensitivity, the second floors it.
255
+ min: (cfg) => dynamicFeeOff(cfg)
256
+ ? 0
257
+ : first(1, 10, (v) => BigInt(span(cfg)) *
258
+ BigInt(n(cfg, "protocolShareBps")) *
259
+ (DYNAMIC_FEE_REACH.reserveScale - BigInt(v * v)) <=
260
+ DYNAMIC_FEE_REACH.maxReserve * DYNAMIC_FEE_REACH.reserveScale),
261
+ max: (cfg) => dynamicFeeOff(cfg) ? 0 : last(1, 10, (v) => BigInt(span(cfg)) * BigInt(v * v) <= DYNAMIC_FEE_REACH.maxReach),
262
+ default: (_ctx, cfg) => (dynamicFeeOff(cfg) ? 0 : 5),
263
+ dependsOn: ["maxFeePips", "baseLpFeePips", "protocolShareBps"],
264
+ },
265
+ {
266
+ name: "minLiquidity",
267
+ type: "uint96",
268
+ unit: "raw",
269
+ where: "bind",
270
+ // RulesKnobs.minDynamicFeeLiquidity: HookrRules binds 1 to 2^96 - 1 with dynamic fees and nothing without. A
271
+ // Launcher member's is its launch liquidity over its divisor (HookrLauncher._rulesData), so with
272
+ // `launchLiquidity` it is pinned.
273
+ min: (cfg, ctx) => !dynamicFeeOn(cfg)
274
+ ? 0n
275
+ : ctx.launchLiquidity !== undefined
276
+ ? dynamicFeeMinLiquidity(ctx.launchLiquidity, divisorOf(cfg))
277
+ : 1n,
278
+ max: (cfg, ctx) => !dynamicFeeOn(cfg)
279
+ ? 0n
280
+ : ctx.launchLiquidity !== undefined
281
+ ? dynamicFeeMinLiquidity(ctx.launchLiquidity, divisorOf(cfg))
282
+ : U96,
283
+ default: (ctx, cfg) => !dynamicFeeOn(cfg) ? 0n : dynamicFeeMinLiquidity(ctx.launchLiquidity ?? 0n, divisorOf(cfg)),
284
+ dependsOn: ["dynamicFeeSens", "dynamicFeeLiquidityDivisor"],
285
+ hidden: true,
286
+ disclose: `Set by the Launcher: launch liquidity / ${DYNAMIC_FEE_LIQUIDITY_DIVISOR}, or the member's own divisor through launchFamily.`,
287
+ },
288
+ {
289
+ name: "dynamicFeeLiquidityDivisor",
290
+ type: "uint16",
291
+ unit: "raw",
292
+ where: "launch",
293
+ // launchFamily's MemberKnobs.dynamicFeeLiquidityDivisor (HookrLaunchChecks.familyBounds): 2 to 10,000 on a dynamic
294
+ // fee member, zero for DYNAMIC_FEE_LIQUIDITY_DIVISOR; a member without dynamic fees takes only zero.
295
+ zeroIsOff: true,
296
+ min: (cfg) => (dynamicFeeOn(cfg) ? Number(RELEASE_CONSTANTS.LAUNCH_MIN_DYNAMIC_FEE_LIQUIDITY_DIVISOR) : 0),
297
+ max: (cfg) => (dynamicFeeOn(cfg) ? Number(RELEASE_CONSTANTS.LAUNCH_MAX_DYNAMIC_FEE_LIQUIDITY_DIVISOR) : 0),
298
+ default: 0,
299
+ dependsOn: ["dynamicFeeSens"],
300
+ disclose: `Through launchFamily only: the pool's minimum dynamic fee liquidity is its launch liquidity over this; zero keeps ${DYNAMIC_FEE_LIQUIDITY_DIVISOR}.`,
301
+ },
302
+ {
303
+ name: "windowSeconds",
304
+ type: "uint16",
305
+ unit: "seconds",
306
+ where: "bind",
307
+ min: (cfg) => (dynamicFeeOn(cfg) ? RULES_KNOB_BOUNDS.minWindowSeconds : 0),
308
+ // The reset runs at least two windows (HookrRules._checkKnobs), so the window is at most half the reset.
309
+ max: (cfg) => dynamicFeeOn(cfg) ? Math.min(RULES_KNOB_BOUNDS.maxWindowSeconds, Math.floor(n(cfg, "resetSeconds") / 2)) : 0,
310
+ default: dynamicTempo("windowSeconds"),
311
+ dependsOn: ["dynamicFeeSens", "resetSeconds"],
312
+ disclose: "Without an anchor move the reference steps toward the anchor once per window. " + TEMPO_DISCLOSURE,
313
+ },
314
+ {
315
+ name: "resetSeconds",
316
+ type: "uint16",
317
+ unit: "seconds",
318
+ where: "bind",
319
+ // At least twice the window and the reset floor, on the reset step's grid.
320
+ min: (cfg) => {
321
+ if (!dynamicFeeOn(cfg))
322
+ return 0;
323
+ const least = Math.max(2 * n(cfg, "windowSeconds"), RULES_KNOB_BOUNDS.minResetSeconds);
324
+ const step = RULES_KNOB_BOUNDS.resetStepSeconds;
325
+ return Math.ceil(least / step) * step;
326
+ },
327
+ max: (cfg) => (dynamicFeeOn(cfg) ? RULES_KNOB_BOUNDS.maxResetSeconds : 0),
328
+ grid: RULES_KNOB_BOUNDS.resetStepSeconds,
329
+ default: dynamicTempo("resetSeconds"),
330
+ dependsOn: ["dynamicFeeSens", "windowSeconds"],
331
+ disclose: "The reference joins the anchor after the reset, and steps once per reset while the anchor keeps moving.",
332
+ },
333
+ {
334
+ name: "carryBps",
335
+ type: "uint16",
336
+ unit: "bps",
337
+ where: "bind",
338
+ min: 0,
339
+ max: (cfg) => (dynamicFeeOn(cfg) ? RULES_KNOB_BOUNDS.maxCarryBps : 0),
340
+ grid: RULES_KNOB_BOUNDS.carryStepBps,
341
+ default: dynamicTempo("carryBps"),
342
+ dependsOn: ["dynamicFeeSens"],
343
+ disclose: "The share of its distance to the anchor the reference keeps at each step.",
344
+ },
345
+ {
346
+ name: "moveTicks",
347
+ type: "uint24",
348
+ unit: "ticks",
349
+ where: "bind",
350
+ min: (cfg) => (dynamicFeeOn(cfg) ? RULES_KNOB_BOUNDS.minMoveTicks : 0),
351
+ max: (cfg) => (dynamicFeeOn(cfg) ? RULES_KNOB_BOUNDS.maxMoveTicks : 0),
352
+ default: dynamicTempo("moveTicks"),
353
+ dependsOn: ["dynamicFeeSens"],
354
+ disclose: "A move counts when it carries the anchor at least this far from where the last counted move left it.",
355
+ },
356
+ ];
357
+ /** launchFamily's MemberKnobs.dynamicFeeLiquidityDivisor of a configuration, zero when it has none. */
358
+ function divisorOf(cfg) {
359
+ return cfg.dynamicFeeLiquidityDivisor === undefined ? 0 : n(cfg, "dynamicFeeLiquidityDivisor");
360
+ }
361
+ /** Auto Burn: a share of every buy's subject output is burned. */
362
+ const AUTO_BURN = [
363
+ {
364
+ name: "burnBps",
365
+ type: "uint16",
366
+ unit: "bps",
367
+ where: "bind",
368
+ min: 0,
369
+ max: (cfg) => 1_000 - n(cfg, "lpBps"),
370
+ default: 100,
371
+ dependsOn: ["lpBps"],
372
+ disclose: "The protocol share turns that part of the burn into a quote take.",
373
+ },
374
+ ];
375
+ /** LP Rewards: an LP fee on buys on top of the base fee. */
376
+ const LP_REWARDS = [
377
+ {
378
+ name: "lpBps",
379
+ type: "uint16",
380
+ unit: "bps",
381
+ where: "bind",
382
+ min: (cfg) => (n(cfg, "royaltyBps") !== 0 ? 1 : 0),
383
+ max: (cfg) => last(0, 1_000 - n(cfg, "burnBps"), (v) => lpRoom(cfg, { lpBps: v })),
384
+ default: 200,
385
+ dependsOn: ["burnBps", "royaltyBps", "protocolShareBps", "baseLpFeePips"],
386
+ disclose: "LPs keep lpBps less the protocol share.",
387
+ },
388
+ ];
389
+ /** Royalty: a share of LP Rewards paid to a fixed recipient. */
390
+ const ROYALTY = [
391
+ {
392
+ name: "royaltyBps",
393
+ type: "uint16",
394
+ unit: "bps",
395
+ where: "bind",
396
+ min: (cfg) => (big(cfg, "royaltyTo") === 0n ? 0 : first(1, 1_000, (v) => lpRoom(cfg, { royaltyBps: v }))),
397
+ max: (cfg) => (big(cfg, "royaltyTo") === 0n || n(cfg, "lpBps") === 0 ? 0 : 1_000),
398
+ default: 0,
399
+ dependsOn: ["royaltyTo", "lpBps"],
400
+ disclose: "Royalty is a share of LP Rewards, not of volume.",
401
+ },
402
+ {
403
+ name: "royaltyTo",
404
+ type: "address",
405
+ unit: "address",
406
+ where: "bind",
407
+ min: (cfg) => (n(cfg, "royaltyBps") !== 0 ? 1 : 0),
408
+ max: (cfg) => (n(cfg, "royaltyBps") !== 0 ? U160 : 0),
409
+ default: (ctx, cfg) => (n(cfg, "royaltyBps") !== 0 && ctx.creator ? address(ctx.creator) : ZERO),
410
+ dependsOn: ["royaltyBps"],
411
+ },
412
+ ];
413
+ /** The protocol share a launch starts with: 2,000 bps (20%), or the Rules minimum when that is higher. */
414
+ export const DEFAULT_PROTOCOL_SHARE_BPS = 2_000;
415
+ /**
416
+ * The effective floor from the context: the larger of the Rules minimum and the treasury's governed floor, each
417
+ * refused unless a HookrRules or a HookrTreasury could hold it (an integer from 0 to 5,000).
418
+ */
419
+ function minShareOf(ctx) {
420
+ const m = ctx.minProtocolShareBps ?? 0;
421
+ assert(Number.isSafeInteger(m) && m >= 0 && m <= MAX_PROTOCOL_SHARE_BPS, `minProtocolShareBps ${m} is not a HookrRules minimum (an integer from 0 to ${MAX_PROTOCOL_SHARE_BPS})`);
422
+ const g = ctx.ruleShareFloorBps ?? 0;
423
+ assert(Number.isSafeInteger(g) && g >= 0 && g <= MAX_RULE_SHARE_FLOOR_BPS, `ruleShareFloorBps ${g} is not a HookrTreasury floor (an integer from 0 to ${MAX_RULE_SHARE_FLOOR_BPS})`);
424
+ return effectiveShareFloor(m, g);
425
+ }
426
+ /**
427
+ * Protocol share: the part of LP Rewards, Auto Burn, the dynamic fee and the Snipe tax paid to the protocol as a
428
+ * quote take. The creator chooses it from the effective floor (the larger of `HookrRules.minProtocolShareBps` and the
429
+ * treasury's governed floor) to 5,000 bps. `integrator` names the app the launch goes through: on a pool with one,
430
+ * the integrator is paid its listed rate of every Hookr share of those rules (`splitProtocolShare`), never of the arb
431
+ * recapture cut.
432
+ */
433
+ const PROTOCOL_SHARE = [
434
+ {
435
+ name: "protocolShareBps",
436
+ type: "uint16",
437
+ unit: "bps",
438
+ where: "bind",
439
+ // At least the Rules minimum, and enough that base + LP Rewards stay within 600,000 pips: the share comes out of
440
+ // LP Rewards, so near the ceiling it has a floor of its own.
441
+ min: (cfg, ctx) => first(minShareOf(ctx), MAX_PROTOCOL_SHARE_BPS, (v) => lpRoom(cfg, { protocolShareBps: v })),
442
+ // r31 (N09): a dynamic fee pool's share also keeps its span within the reach bound.
443
+ max: (cfg) => !dynamicFeeOn(cfg)
444
+ ? MAX_PROTOCOL_SHARE_BPS
445
+ : last(0, MAX_PROTOCOL_SHARE_BPS, (v) => withinDynamicFeeReach(span(cfg), n(cfg, "dynamicFeeSens"), v)),
446
+ default: (ctx) => Math.max(DEFAULT_PROTOCOL_SHARE_BPS, minShareOf(ctx)),
447
+ dependsOn: ["lpBps", "royaltyBps", "baseLpFeePips", "maxFeePips", "dynamicFeeSens"],
448
+ disclose: "The protocol's share of LP Rewards, Auto Burn, the dynamic fee and the Snipe tax, paid as a quote take, from " +
449
+ "the Rules minimum to 50%. Any take-bearing rule turns off the fee-only path for the pool's whole life.",
450
+ },
451
+ {
452
+ name: "integrator",
453
+ type: "address",
454
+ unit: "address",
455
+ where: "bind",
456
+ // A set, not a range: zero, or an address on the treasury's integrator list, which only the chain holds. With
457
+ // ParamContext.integrators the checked encoder refuses one off the list; HookrRules refuses it at bind either way.
458
+ min: 0,
459
+ max: U160,
460
+ default: (ctx) => (ctx.integrator ? address(ctx.integrator, true) : ZERO),
461
+ hidden: true,
462
+ disclose: "The app the launch goes through, if it is a listed integrator: it is paid its listed rate (at most half) of " +
463
+ "the protocol's share of these rules on this pool, frozen at launch. The buyer pays the same either way.",
464
+ },
465
+ ];
466
+ /** Fee advisory Config: a keeper-set surcharge within an immutable cap. */
467
+ const FEE_ADVISORY = [
468
+ {
469
+ name: "cap",
470
+ type: "uint24",
471
+ unit: "pips",
472
+ where: "advisoryData",
473
+ min: (cfg) => Math.max(1, n(cfg, "surcharge") + Math.max(n(cfg, "premium0"), n(cfg, "premium1"))),
474
+ max: (_c, ctx) => limitOf(ctx),
475
+ default: (ctx) => Math.min(10_000, limitOf(ctx)),
476
+ dependsOn: ["surcharge", "premium0", "premium1"],
477
+ },
478
+ {
479
+ name: "surcharge",
480
+ type: "uint24",
481
+ unit: "pips",
482
+ where: "advisoryData",
483
+ min: 0,
484
+ max: (cfg) => n(cfg, "cap") - Math.max(n(cfg, "premium0"), n(cfg, "premium1")),
485
+ default: (_ctx, cfg) => Math.min(500, n(cfg, "cap")),
486
+ dependsOn: ["cap", "premium0", "premium1"],
487
+ },
488
+ {
489
+ name: "premium0",
490
+ type: "uint24",
491
+ unit: "pips",
492
+ where: "advisoryData",
493
+ min: 0,
494
+ max: (cfg) => n(cfg, "cap") - n(cfg, "surcharge"),
495
+ default: 0,
496
+ dependsOn: ["cap", "surcharge"],
497
+ disclose: "Premium on zeroForOne swaps: buys when the quote is currency0 (`feePremiumSides`).",
498
+ },
499
+ {
500
+ name: "premium1",
501
+ type: "uint24",
502
+ unit: "pips",
503
+ where: "advisoryData",
504
+ min: 0,
505
+ max: (cfg) => n(cfg, "cap") - n(cfg, "surcharge"),
506
+ default: 0,
507
+ dependsOn: ["cap", "surcharge"],
508
+ disclose: "Premium on oneForZero swaps: buys when the quote is currency1 (`feePremiumSides`).",
509
+ },
510
+ { name: "maxStep", type: "uint24", unit: "pips", where: "advisoryData", min: 1, max: U24, default: 1_000 },
511
+ {
512
+ name: "minInterval",
513
+ type: "uint32",
514
+ unit: "seconds",
515
+ where: "advisoryData",
516
+ min: 1,
517
+ // HookrFeeAdvisory.MAX_MIN_INTERVAL: one day.
518
+ max: RELEASE_CONSTANTS.FEE_ADVISORY_MAX_MIN_INTERVAL,
519
+ default: 60,
520
+ ui: { min: 60, max: RELEASE_CONSTANTS.FEE_ADVISORY_MAX_MIN_INTERVAL },
521
+ },
522
+ {
523
+ name: "maxTtl",
524
+ type: "uint32",
525
+ unit: "seconds",
526
+ where: "advisoryData",
527
+ min: 1,
528
+ max: RELEASE_CONSTANTS.FEE_ADVISORY_MAX_TTL,
529
+ default: 3_600,
530
+ },
531
+ ];
532
+ function limitOf(ctx) {
533
+ assert(ctx.advisoryLimit !== undefined, "Fee advisory knobs need the advisory limit (feeAdvisoryLimit)");
534
+ return ctx.advisoryLimit;
535
+ }
536
+ function tierCapOf(ctx) {
537
+ assert(ctx.tierCap !== undefined, "Session knobs need the tier cap");
538
+ return ctx.tierCap;
539
+ }
540
+ /** The five session tiers, two ramps and flags shared by fee-advisory tiers and the session advisory. */
541
+ function sessionParams(maxRamp, defaults) {
542
+ const tiers = ["regularPips", "preMarketPips", "afterHoursPips", "overnightPips", "closedPips"].map((name, i) => ({
543
+ name,
544
+ type: "uint24",
545
+ unit: "pips",
546
+ where: "advisoryData",
547
+ min: 0,
548
+ max: (_c, ctx) => tierCapOf(ctx),
549
+ default: (ctx) => Math.min(defaults[i], tierCapOf(ctx)),
550
+ }));
551
+ return [
552
+ ...tiers,
553
+ {
554
+ name: "openRampSeconds",
555
+ type: "uint16",
556
+ unit: "seconds",
557
+ where: "advisoryData",
558
+ min: 0,
559
+ max: (cfg) => maxRamp - n(cfg, "closeRampSeconds"),
560
+ default: defaults[5],
561
+ dependsOn: ["closeRampSeconds"],
562
+ },
563
+ {
564
+ name: "closeRampSeconds",
565
+ type: "uint16",
566
+ unit: "seconds",
567
+ where: "advisoryData",
568
+ min: 0,
569
+ max: (cfg) => maxRamp - n(cfg, "openRampSeconds"),
570
+ default: defaults[6],
571
+ dependsOn: ["openRampSeconds"],
572
+ },
573
+ {
574
+ name: "flags",
575
+ type: "uint8",
576
+ unit: "mask",
577
+ where: "advisoryData",
578
+ min: 0,
579
+ max: RELEASE_CONSTANTS.SESSION_UNCOVERED_AS_CLOSED,
580
+ default: RELEASE_CONSTANTS.SESSION_UNCOVERED_AS_CLOSED,
581
+ options: [
582
+ { bit: 0, name: "UNCOVERED_AS_CLOSED", disclose: "A weekday outside the calendar is charged the closed tier." },
583
+ ],
584
+ },
585
+ ];
586
+ }
587
+ const SESSION_DEFAULTS = [0, 2_000, 2_000, 4_000, 6_000, 1_800, 900];
588
+ const PAIR_GAS_FLOOR = {
589
+ fee: RELEASE_CONSTANTS.FEE_ADVISORY_MIN_PAIR_GAS,
590
+ feeTiered: RELEASE_CONSTANTS.FEE_ADVISORY_MIN_TIERED_GAS,
591
+ session: RELEASE_CONSTANTS.SESSION_ADVISORY_MIN_PAIR_GAS,
592
+ };
593
+ const advised = (cfg) => big(cfg, "advisory") !== 0n;
594
+ /** Pair root: a Hookr root for one existing pair, with an optional fee or session advisory. */
595
+ const PAIR_ROOT = [
596
+ { name: "tickSpacing", type: "int24", unit: "ticks", where: "launch", min: 1, max: MAX_TICK_SPACING, default: 60 },
597
+ {
598
+ name: "baseLpFeePips",
599
+ type: "uint24",
600
+ unit: "pips",
601
+ where: "launch",
602
+ min: 0,
603
+ max: (cfg) => RELEASE_CONSTANTS.PAIR_ROOT_MAX_LP_FEE_PIPS - n(cfg, "advisoryCapPips"),
604
+ default: 3_000,
605
+ dependsOn: ["advisoryCapPips"],
606
+ },
607
+ { name: "advisory", type: "address", unit: "address", where: "launch", min: 0, max: U160, default: ZERO },
608
+ {
609
+ name: "advisoryCapPips",
610
+ type: "uint24",
611
+ unit: "pips",
612
+ where: "launch",
613
+ min: (cfg) => (advised(cfg) ? 1 : 0),
614
+ max: (cfg, ctx) => advised(cfg)
615
+ ? Math.min(pairAdmissionOf(ctx).maxLpFeePips, RELEASE_CONSTANTS.PAIR_ROOT_MAX_LP_FEE_PIPS - n(cfg, "baseLpFeePips"))
616
+ : 0,
617
+ default: (ctx, cfg) => advised(cfg)
618
+ ? Math.min(10_000, pairAdmissionOf(ctx).maxLpFeePips, RELEASE_CONSTANTS.PAIR_ROOT_MAX_LP_FEE_PIPS - n(cfg, "baseLpFeePips"))
619
+ : 0,
620
+ dependsOn: ["advisory", "baseLpFeePips"],
621
+ },
622
+ {
623
+ name: "advisoryFailOpen",
624
+ type: "bool",
625
+ unit: "bool",
626
+ where: "launch",
627
+ min: 0,
628
+ // HookrRootFactory deploys a fail-open root only on a fail-open, fee-only admission without a quote take, the
629
+ // rule HookrRoot applies to its own pools; it defaults to what the admission allows.
630
+ max: (cfg, ctx) => (advised(cfg) && failOpenAdmitted(ctx) ? 1 : 0),
631
+ default: (ctx, cfg) => advised(cfg) && failOpenAdmitted(ctx),
632
+ dependsOn: ["advisory"],
633
+ disclose: "On advisory failure, true charges the cap and false reverts the swap.",
634
+ },
635
+ {
636
+ name: "maxLpFeePips",
637
+ type: "uint24",
638
+ unit: "pips",
639
+ where: "launch",
640
+ min: (cfg) => n(cfg, "baseLpFeePips") + n(cfg, "advisoryCapPips"),
641
+ max: RELEASE_CONSTANTS.PAIR_ROOT_MAX_LP_FEE_PIPS,
642
+ default: (_ctx, cfg) => n(cfg, "baseLpFeePips") + n(cfg, "advisoryCapPips"),
643
+ dependsOn: ["baseLpFeePips", "advisoryCapPips"],
644
+ hidden: true,
645
+ },
646
+ {
647
+ name: "advisoryGasLimit",
648
+ type: "uint32",
649
+ unit: "gas",
650
+ where: "launch",
651
+ min: (cfg, ctx) => (advised(cfg) ? (ctx.pairAdvisoryKind ? PAIR_GAS_FLOOR[ctx.pairAdvisoryKind] : 1) : 0),
652
+ max: (cfg, ctx) => advised(cfg) ? Math.min(pairAdmissionOf(ctx).gasLimit, RELEASE_CONSTANTS.PAIR_ROOT_MAX_ADVISORY_GAS) : 0,
653
+ default: (ctx, cfg) => advised(cfg) ? Math.min(pairAdmissionOf(ctx).gasLimit, RELEASE_CONSTANTS.PAIR_ROOT_MAX_ADVISORY_GAS) : 0,
654
+ dependsOn: ["advisory"],
655
+ hidden: true,
656
+ },
657
+ ];
658
+ /** Whether the pair root's factory admission lets it fail open: fail-open, fee-only and without a quote take. */
659
+ function failOpenAdmitted(ctx) {
660
+ const a = ctx.pairAdmission;
661
+ return a?.failOpen === true && a.feeOnly === true && (a.maxQuoteTakePips ?? 0) === 0;
662
+ }
663
+ function pairAdmissionOf(ctx) {
664
+ assert(ctx.pairAdmission !== undefined, "An advised pair root needs its advisory's factory admission");
665
+ return ctx.pairAdmission;
666
+ }
667
+ /** HookrComplianceGuard flag: also check the recipient. */
668
+ export const COMPLIANCE_CHECK_BENEFICIARY = 1;
669
+ /** HookrComplianceGuard flag: sellers need a credential too. */
670
+ export const COMPLIANCE_KYC_ON_SELL = 2;
671
+ /** HookrComplianceGuard flag: sells stop too while the sanctions source is down. */
672
+ export const COMPLIANCE_HALT_SELLS_WHEN_SOURCE_DOWN = 4;
673
+ /** HookrComplianceGuard flag: the subject enforces transfer restrictions; needs a zero subject take. */
674
+ export const COMPLIANCE_RESTRICTED_SUBJECT = 8;
675
+ /** HookrComplianceGuard flag: accept curated-router swaps; needs RESTRICTED_SUBJECT or no list. */
676
+ export const COMPLIANCE_ACCEPT_CURATED = 16;
677
+ /** HookrComplianceGuard flag: permitted senders other than the launcher may add liquidity. */
678
+ export const COMPLIANCE_DIRECT_LP_IF_PERMITTED = 32;
679
+ /** Every HookrComplianceGuard flag. */
680
+ export const COMPLIANCE_ALL_FLAGS = 63;
681
+ /** HookrComplianceGuard.MIN_ADVISORY_GAS. */
682
+ export const COMPLIANCE_MIN_ADVISORY_GAS = 150_000;
683
+ /** Compliance guard terms: a credential list, required and blocked tiers, and flags. */
684
+ const COMPLIANCE = [
685
+ { name: "listId", type: "bytes32", unit: "bytes32", where: "advisoryData", min: 0, max: U256, default: ZERO_HASH },
686
+ { name: "requireAll", type: "uint32", unit: "mask", where: "advisoryData", min: 0, max: U32, default: 0 },
687
+ {
688
+ name: "blockAny",
689
+ type: "uint32",
690
+ unit: "mask",
691
+ where: "advisoryData",
692
+ min: 0,
693
+ max: U32,
694
+ default: 0,
695
+ disclose: "No tier may be both required and blocked.",
696
+ },
697
+ {
698
+ name: "flags",
699
+ type: "uint16",
700
+ unit: "mask",
701
+ where: "advisoryData",
702
+ min: 0,
703
+ max: COMPLIANCE_ALL_FLAGS,
704
+ default: COMPLIANCE_CHECK_BENEFICIARY,
705
+ options: [
706
+ { bit: 0, name: "CHECK_BENEFICIARY" },
707
+ { bit: 1, name: "KYC_ON_SELL" },
708
+ {
709
+ bit: 2,
710
+ name: "HALT_SELLS_WHEN_SOURCE_DOWN",
711
+ disclose: "An outage of the sanctions source freezes every exit until it recovers. Opt in explicitly.",
712
+ },
713
+ { bit: 3, name: "RESTRICTED_SUBJECT", hidden: true, disclose: "Derived from the subject token." },
714
+ { bit: 4, name: "ACCEPT_CURATED" },
715
+ { bit: 5, name: "DIRECT_LP_IF_PERMITTED" },
716
+ ],
717
+ },
718
+ ];
719
+ /** HookrLauncher.MAX_DEV_BUY_BPS: the most of the supply, in basis points, a creator holds when a dev-buy launch returns. */
720
+ export const MAX_DEV_BUY_BPS = Number(RELEASE_CONSTANTS.LAUNCHER_MAX_DEV_BUY_BPS);
721
+ /** HookrLauncher.MAX_DEV_BUY_LOCK_BLOCKS: the longest DevBuy.lockBlocks, 365 days of 12-second parent blocks. */
722
+ export const MAX_DEV_BUY_LOCK_BLOCKS = Number(RELEASE_CONSTANTS.LAUNCHER_MAX_DEV_BUY_LOCK_BLOCKS);
723
+ /** Largest DevBuy.quoteIn the Launcher swaps: it negates it into an int256 amountSpecified bounded by int128. */
724
+ export const MAX_DEV_BUY_QUOTE_IN = I128;
725
+ /**
726
+ * The lock a launch with a dev buy starts with, in seconds: 7 days, the owner's default. The `lockBlocks` knob takes it
727
+ * in parent blocks at the chain's clock, the measured block time the Anti-Snipe guard default also uses
728
+ * (`ParamContext.blockTimeSeconds`, default 12): 50,400 blocks at 12 seconds.
729
+ */
730
+ export const DEFAULT_DEV_BUY_LOCK_SECONDS = 7 * 86_400;
731
+ /** Parent blocks covering a dev-buy lock of `seconds` at the measured block time, rounded up (`guardBlocksFor`) and at
732
+ * most MAX_DEV_BUY_LOCK_BLOCKS. */
733
+ export function devBuyLockBlocksFor(seconds, blockTimeSeconds) {
734
+ const blocks = guardBlocksFor(seconds, blockTimeSeconds);
735
+ return blocks < BigInt(MAX_DEV_BUY_LOCK_BLOCKS) ? Number(blocks) : MAX_DEV_BUY_LOCK_BLOCKS;
736
+ }
737
+ /**
738
+ * Why the member a dev buy names cannot take it, or null when it can: HookrLauncher._checkBuy's InvalidDevBuy checks on
739
+ * the member (r31, T39). An unguarded member (guardEndBlock zero) takes a dev buy with no Snipe and no cap; a guarded
740
+ * one needs its guard past `blockNumber`, a Snipe and a per-block cap of at least `quoteIn`.
741
+ */
742
+ export function devBuyMemberRefusal(r, quoteIn, blockNumber) {
743
+ if (r === undefined)
744
+ return "The dev buy names no family member";
745
+ const guardEnd = BigInt(r.guardEndBlock);
746
+ if (guardEnd === 0n)
747
+ return null;
748
+ if (blockNumber !== undefined && guardEnd <= blockNumber)
749
+ return "The dev buy's guarded member needs its Anti-Snipe guard past the launch block";
750
+ if (r.snipeTaxPips === 0)
751
+ return "The dev buy's guarded member needs an Anti-Snipe tax (snipeTaxPips above zero)";
752
+ if (r.maxBuyQuoteAmount < quoteIn)
753
+ return `The dev buy's quoteIn ${quoteIn} is above its member's per-block buy cap ${r.maxBuyQuoteAmount}`;
754
+ return null;
755
+ }
756
+ /**
757
+ * The largest quoteIn the dev buy's member admits: its per-block buy cap on a guarded member, the type's bound on an
758
+ * unguarded one, zero when it cannot take a dev buy at all.
759
+ */
760
+ function devBuyRoom(cfg, ctx) {
761
+ if (ctx.newToken === false || ctx.launchMembers === undefined)
762
+ return 0n;
763
+ const r = ctx.launchMembers[n(cfg, "member")];
764
+ if (r === undefined || devBuyMemberRefusal(r, 0n, ctx.blockNumber) !== null)
765
+ return 0n;
766
+ if (BigInt(r.guardEndBlock) === 0n)
767
+ return I128;
768
+ return BigInt(r.maxBuyQuoteAmount) < I128 ? BigInt(r.maxBuyQuoteAmount) : I128;
769
+ }
770
+ const buying = (cfg) => big(cfg, "quoteIn") !== 0n;
771
+ /**
772
+ * Dev buy (HookrLauncher.DevBuy): the creator's own first buy on one member of a new-token family, made inside the
773
+ * launch right after that member's liquidity add and delivered to the creator. Off by default (quoteIn zero, a plain
774
+ * launch). It pays every guarded-buy leg at the launch tax, counts against the member's per-block cap and closes the
775
+ * launch block to other buys on that member; the creator holds at most MAX_DEV_BUY_BPS (5%) of the supply, dev-bought
776
+ * and unplaced together, when the launch returns.
777
+ */
778
+ const DEV_BUY = [
779
+ {
780
+ name: "member",
781
+ type: "uint8",
782
+ unit: "raw",
783
+ where: "launch",
784
+ min: 0,
785
+ max: (cfg, ctx) => !buying(cfg) ? 0 : ctx.launchMembers !== undefined ? Math.max(ctx.launchMembers.length - 1, 0) : 7,
786
+ default: 0,
787
+ dependsOn: ["quoteIn"],
788
+ disclose: "The member whose pool takes the dev buy. A guarded member needs its guard past the launch block, a Snipe tax " +
789
+ "and a per-block buy cap of at least the buy; an unguarded member takes it with no Snipe and no cap.",
790
+ },
791
+ {
792
+ name: "quoteIn",
793
+ type: "uint128",
794
+ unit: "raw",
795
+ where: "launch",
796
+ zeroIsOff: true,
797
+ min: 1,
798
+ // HookrLauncher._checkBuy: at most a guarded member's maxBuyQuoteAmount (it needs a Snipe); any amount up to
799
+ // 2^127 - 1 on an unguarded member.
800
+ max: (cfg, ctx) => devBuyRoom(cfg, ctx),
801
+ default: 0,
802
+ dependsOn: ["member"],
803
+ disclose: "The exact quote the dev buy spends, collected with the member's liquidity budget. On a guarded member it pays " +
804
+ "the full launch Anti-Snipe tax, and on any member every other buy leg; the creator ends the launch holding at " +
805
+ "most 5% of the supply, dev-bought and unplaced together.",
806
+ },
807
+ {
808
+ name: "minSubjectOut",
809
+ type: "uint128",
810
+ unit: "raw",
811
+ where: "launch",
812
+ min: (cfg) => (buying(cfg) ? 1 : 0),
813
+ max: (cfg) => (buying(cfg) ? U128 : 0n),
814
+ default: 0,
815
+ dependsOn: ["quoteIn"],
816
+ hidden: true,
817
+ disclose: "Set by the SDK from an eth_call quote of the launch less the slippage (quoteDevBuy).",
818
+ },
819
+ {
820
+ name: "lockBlocks",
821
+ type: "uint32",
822
+ unit: "blocks",
823
+ where: "launch",
824
+ min: 0,
825
+ max: (cfg) => (buying(cfg) ? MAX_DEV_BUY_LOCK_BLOCKS : 0),
826
+ // With a dev buy: 7 days in parent blocks at the measured block time; without one it stays zero (a plain launch).
827
+ default: (ctx, cfg) => (buying(cfg) ? devBuyLockBlocksFor(DEFAULT_DEV_BUY_LOCK_SECONDS, ctx.blockTimeSeconds) : 0),
828
+ dependsOn: ["quoteIn"],
829
+ disclose: "Keeps the family's principal locked at least this many parent blocks past the launch block, on top of the " +
830
+ "guards (at most 2,628,000, 365 days of 12-second blocks). LP fees stay collectable. A launch with a dev buy " +
831
+ "starts at 7 days (50,400 blocks at 12 seconds).",
832
+ },
833
+ ];
834
+ /**
835
+ * launchFamily's family-wide terms (HookrLauncher.LaunchParams, HookrLaunchChecks.checkFamily, r31): the lock end every
836
+ * member's MemberKnobs.lockEndBlock may set, the retained-supply cap and the opening price check's tolerance.
837
+ */
838
+ const FAMILY = [
839
+ {
840
+ name: "lockEndBlock",
841
+ type: "uint64",
842
+ unit: "blocks",
843
+ where: "launch",
844
+ // Zero, or a parent block from the family's last guard end to the launch block + MAX_LOCK_END_BLOCKS.
845
+ zeroIsOff: true,
846
+ min: (_c, ctx) => (ctx.launchMembers ?? []).reduce((m, r) => (BigInt(r.guardEndBlock) > m ? BigInt(r.guardEndBlock) : m), ctx.blockNumber === undefined ? 1n : ctx.blockNumber),
847
+ max: (_c, ctx) => ctx.blockNumber === undefined
848
+ ? 2n ** 64n - 1n
849
+ : ctx.blockNumber + BigInt(RELEASE_CONSTANTS.LAUNCH_MAX_LOCK_END_BLOCKS),
850
+ default: 0,
851
+ disclose: "Keeps the family's principal locked until this parent block, on top of the guards and any dev buy's lock " +
852
+ "(at most 100,000 blocks past the launch block). LP fees stay collectable.",
853
+ },
854
+ {
855
+ name: "maxRetainedBps",
856
+ type: "uint16",
857
+ unit: "bps",
858
+ where: "launch",
859
+ // A new token's cap on the supply the creator holds when the launch returns; an existing token takes only 10,000.
860
+ min: (_c, ctx) => (ctx.newToken === false ? 10_000 : 0),
861
+ max: 10_000,
862
+ default: 10_000,
863
+ disclose: "The most of a new token's supply the creator may hold when the launch returns, unplaced supply and dev buys " +
864
+ "together; 100% is no cap. The dev buy bound of 5% applies on top whenever a dev buy runs.",
865
+ },
866
+ {
867
+ name: "openingToleranceBps",
868
+ type: "uint16",
869
+ unit: "bps",
870
+ where: "launch",
871
+ // OpeningCheck.toleranceBps: zero checks nothing; otherwise 1 to MAX_OPENING_TOLERANCE_BPS, with one reference
872
+ // pool per member after the first.
873
+ zeroIsOff: true,
874
+ min: 1,
875
+ max: Number(RELEASE_CONSTANTS.LAUNCH_MAX_OPENING_TOLERANCE_BPS),
876
+ default: 0,
877
+ disclose: "Checks that every later pool opens within this distance of pool 1's price, converted through a reference pool " +
878
+ "you name per pool. It guards your own launch against a mistake; it is not an oracle.",
879
+ },
880
+ ];
881
+ /**
882
+ * Knob manifest per rule, for the release in this package. `pool` is the PoolKey and PoolConfig of a HookrRoot
883
+ * launch member; `antiSnipe`, `dynamicFee`, `autoBurn`, `lpRewards`, `royalty` and `protocolShare` together are one
884
+ * `RulesConfig` and its `RulesKnobs` (r31: with dynamic fees the minimum liquidity and the tempo, and the Snipe curve;
885
+ * `dynamicFeeLiquidityDivisor` is the member's launchFamily divisor); `feeAdvisory` and `sessionTiers` are
886
+ * fee-advisory bind data; `sessionAdvisory` is a session Schedule; `pairRoot` the HookrPairRoot params; `compliance`
887
+ * the HookrComplianceGuard terms; `devBuy` the launch's optional HookrLauncher.DevBuy (`buildLaunchWithBuy`,
888
+ * `quoteDevBuy`, and each of launchFamily's dev buys); `family` launchFamily's family-wide terms (`buildLaunchFamily`).
889
+ */
890
+ export const RULE_PARAMS = {
891
+ pool: POOL,
892
+ antiSnipe: ANTI_SNIPE,
893
+ dynamicFee: DYNAMIC_FEE,
894
+ autoBurn: AUTO_BURN,
895
+ lpRewards: LP_REWARDS,
896
+ royalty: ROYALTY,
897
+ protocolShare: PROTOCOL_SHARE,
898
+ feeAdvisory: FEE_ADVISORY,
899
+ sessionTiers: sessionParams(RELEASE_CONSTANTS.SESSION_TIERS_MAX_RAMP_SECONDS, SESSION_DEFAULTS),
900
+ sessionAdvisory: sessionParams(RELEASE_CONSTANTS.SESSION_ADVISORY_MAX_RAMP_SECONDS, SESSION_DEFAULTS),
901
+ pairRoot: PAIR_ROOT,
902
+ compliance: COMPLIANCE,
903
+ devBuy: DEV_BUY,
904
+ family: FAMILY,
905
+ };
906
+ /** The rules whose knobs make up a HookrRoot launch member: its pool knobs and one RulesConfig. */
907
+ export const RULES_MEMBER_RULES = [
908
+ "pool",
909
+ "antiSnipe",
910
+ "dynamicFee",
911
+ "autoBurn",
912
+ "lpRewards",
913
+ "royalty",
914
+ "protocolShare",
915
+ ];
916
+ /** Resolves one knob's bounds for `cfg`. */
917
+ export function paramBounds(p, cfg, ctx = {}) {
918
+ const at = (b) => BigInt(typeof b === "function" ? b(cfg, ctx) : b);
919
+ return { min: at(p.min), max: at(p.max), grid: BigInt(p.grid ?? 1), zeroIsOff: p.zeroIsOff === true };
920
+ }
921
+ /** Refuses a knob value outside its bounds or off its grid. */
922
+ export function checkParam(rule, p, cfg, ctx = {}) {
923
+ assert(p.name in cfg, `${rule}.${p.name} missing`);
924
+ const v = paramValue(cfg[p.name]);
925
+ const b = paramBounds(p, cfg, ctx);
926
+ if (b.zeroIsOff && v === 0n)
927
+ return;
928
+ assert(v >= b.min && v <= b.max, `${rule}.${p.name} ${v} outside [${b.min}, ${b.max}]`);
929
+ assert(v % b.grid === 0n, `${rule}.${p.name} ${v} off its ${b.grid} grid`);
930
+ }
931
+ /** Every knob of `rule` outside its bounds in `cfg`, one message each; empty when all are inside. */
932
+ export function ruleParamErrors(rule, cfg, ctx = {}) {
933
+ const errors = [];
934
+ for (const p of RULE_PARAMS[rule]) {
935
+ try {
936
+ checkParam(rule, p, cfg, ctx);
937
+ }
938
+ catch (e) {
939
+ errors.push(e.message);
940
+ }
941
+ }
942
+ return errors;
943
+ }
944
+ /** Refuses a configuration with any knob of `rule` outside its bounds, naming every such knob. */
945
+ export function checkRuleParams(rule, cfg, ctx = {}) {
946
+ const errors = ruleParamErrors(rule, cfg, ctx);
947
+ assert(errors.length === 0, errors.join("; "));
948
+ }
949
+ /** The default configuration of `rule`, knob by knob in manifest order; `base` fixes knobs already chosen. */
950
+ export function defaultRuleParams(rule, ctx = {}, base = {}) {
951
+ const cfg = { ...base };
952
+ for (const p of RULE_PARAMS[rule])
953
+ if (!(p.name in cfg))
954
+ cfg[p.name] = typeof p.default === "function" ? p.default(ctx, cfg) : p.default;
955
+ return cfg;
956
+ }
957
+ /** Parent blocks covering `seconds` at the measured block time, rounded up. */
958
+ export function guardBlocksFor(seconds, blockTimeSeconds = DEFAULT_BLOCK_TIME) {
959
+ assert(Number.isSafeInteger(seconds) && seconds > 0, "Invalid guard length");
960
+ assert(blockTimeSeconds > 0, "Invalid block time");
961
+ return BigInt(Math.ceil(seconds / blockTimeSeconds));
962
+ }
963
+ function flat(l) {
964
+ return {
965
+ ...l.rules,
966
+ baseLpFeePips: l.baseLpFeePips,
967
+ tickSpacing: l.tickSpacing,
968
+ minLiquidity: l.minLiquidity,
969
+ windowSeconds: l.windowSeconds,
970
+ resetSeconds: l.resetSeconds,
971
+ carryBps: l.carryBps,
972
+ moveTicks: l.moveTicks,
973
+ snipeCurve: l.snipeCurve,
974
+ dynamicFeeLiquidityDivisor: l.dynamicFeeLiquidityDivisor,
975
+ };
976
+ }
977
+ /** The RulesKnobs a launch member binds: its minLiquidity, its tempo and its Snipe curve. */
978
+ export function rulesKnobsOf(l) {
979
+ return {
980
+ minDynamicFeeLiquidity: l.minLiquidity,
981
+ windowSeconds: l.windowSeconds,
982
+ resetSeconds: l.resetSeconds,
983
+ carryBps: l.carryBps,
984
+ moveTicks: l.moveTicks,
985
+ snipeCurve: l.snipeCurve,
986
+ };
987
+ }
988
+ /** Every knob of a HookrRoot launch member at its default. */
989
+ export function defaultRulesLaunch(ctx = {}) {
990
+ let cfg = {};
991
+ for (const rule of RULES_MEMBER_RULES)
992
+ cfg = defaultRuleParams(rule, ctx, cfg);
993
+ return rulesLaunchOf(cfg);
994
+ }
995
+ /**
996
+ * Builds a RulesLaunch from a flat knob configuration. The retired pot fields are zero. A Rules knob the configuration
997
+ * lacks (one written before r31) takes what the Launcher binds without it: the default tempo on a dynamic fee member,
998
+ * zero otherwise, the linear Snipe curve and the default divisor.
999
+ */
1000
+ export function rulesLaunchOf(cfg) {
1001
+ const dynamic = n(cfg, "dynamicFeeSens") !== 0;
1002
+ const tempo = (key, fallback) => (cfg[key] === undefined ? (dynamic ? fallback : 0) : n(cfg, key));
1003
+ const knob = (key) => (cfg[key] === undefined ? 0 : n(cfg, key));
1004
+ return {
1005
+ baseLpFeePips: n(cfg, "baseLpFeePips"),
1006
+ tickSpacing: n(cfg, "tickSpacing"),
1007
+ minLiquidity: big(cfg, "minLiquidity"),
1008
+ windowSeconds: tempo("windowSeconds", DYNAMIC_FEE_PARAMETERS.window),
1009
+ resetSeconds: tempo("resetSeconds", DYNAMIC_FEE_PARAMETERS.reset),
1010
+ carryBps: tempo("carryBps", DYNAMIC_FEE_PARAMETERS.carryBps),
1011
+ moveTicks: tempo("moveTicks", DYNAMIC_FEE_PARAMETERS.moveTicks),
1012
+ snipeCurve: knob("snipeCurve"),
1013
+ dynamicFeeLiquidityDivisor: knob("dynamicFeeLiquidityDivisor"),
1014
+ rules: {
1015
+ guardEndBlock: n(cfg, "guardEndBlock"),
1016
+ maxFeePips: n(cfg, "maxFeePips"),
1017
+ snipeTaxPips: n(cfg, "snipeTaxPips"),
1018
+ snipeFloorPips: n(cfg, "snipeFloorPips"),
1019
+ snipeDecaySeconds: n(cfg, "snipeDecaySeconds"),
1020
+ dynamicFeeSens: n(cfg, "dynamicFeeSens"),
1021
+ burnBps: n(cfg, "burnBps"),
1022
+ lpBps: n(cfg, "lpBps"),
1023
+ potBps: 0,
1024
+ royaltyBps: n(cfg, "royaltyBps"),
1025
+ protocolShareBps: n(cfg, "protocolShareBps"),
1026
+ potEveryNBuys: 0,
1027
+ maxBuyQuoteAmount: big(cfg, "maxBuyQuoteAmount"),
1028
+ potMinBuyQuote: 0n,
1029
+ royaltyTo: address(`0x${big(cfg, "royaltyTo").toString(16).padStart(40, "0")}`, true),
1030
+ integrator: address(`0x${(cfg.integrator === undefined ? 0n : big(cfg, "integrator")).toString(16).padStart(40, "0")}`, true),
1031
+ },
1032
+ };
1033
+ }
1034
+ /**
1035
+ * Checks every knob of a launch member against the manifest, HookrRules._validate and _checkKnobs, and returns its
1036
+ * `rulesData`: the bytes HookrRules binds (`encodeRulesData`, r31): `abi.encode(RulesConfig)` without dynamic fees or
1037
+ * a knob off its default, `abi.encode(RulesConfig, RulesKnobs)` otherwise. `ctx.blockNumber` should be the block the
1038
+ * launch lands in; the guard is then checked against the 100,000-block window. A guarded pool needs
1039
+ * `liquidityOwner`, the Launcher. For a Launcher member pass `ctx.launchLiquidity`: the Launcher derives minLiquidity
1040
+ * from it (and the member's divisor) and the manifest pins the knob to that value, so the returned bytes are the ones
1041
+ * launchFamily binds with these MemberKnobs (`launcherRulesData`); launch, launchAdvised and launchWithBuy bind the
1042
+ * default tempo and the linear curve.
1043
+ */
1044
+ export function encodeRulesBind(l, ctx = {}) {
1045
+ const cfg = flat(l);
1046
+ const errors = RULES_MEMBER_RULES.flatMap((rule) => ruleParamErrors(rule, cfg, ctx));
1047
+ assert(errors.length === 0, errors.join("; "));
1048
+ checkRulesConfig(l.rules, {
1049
+ baseLpFeePips: l.baseLpFeePips,
1050
+ minProtocolShareBps: ctx.minProtocolShareBps ?? 0,
1051
+ ruleShareFloorBps: ctx.ruleShareFloorBps,
1052
+ integrators: ctx.integrators,
1053
+ refusedIntegrators: ctx.refusedIntegrators,
1054
+ refusedRoyaltyRecipients: ctx.refusedRoyaltyRecipients,
1055
+ minFeePips: ctx.minFeePips,
1056
+ blockNumber: ctx.blockNumber,
1057
+ liquidityOwner: ctx.liquidityOwner,
1058
+ });
1059
+ return encodeRulesData(l.rules, rulesKnobsOf(l));
1060
+ }
1061
+ /**
1062
+ * Smallest advisory gas each advisory binds with on a HookrRoot member: the fee advisory's bind floor
1063
+ * (`feeAdvisoryMinGas`), HookrSessionAdvisory.MIN_ADVISORY_GAS and HookrComplianceGuard.MIN_ADVISORY_GAS.
1064
+ */
1065
+ export const ROOT_ADVISORY_MIN_GAS = {
1066
+ fee: feeAdvisoryMinGas("root", false),
1067
+ feeTiered: feeAdvisoryMinGas("root", true),
1068
+ session: RELEASE_CONSTANTS.SESSION_ADVISORY_MIN_ADVISORY_GAS,
1069
+ compliance: COMPLIANCE_MIN_ADVISORY_GAS,
1070
+ };
1071
+ /**
1072
+ * Pool caps and gas limits for a launch member (`PoolConfig.caps`, `rulesGasLimit`, `advisoryGasLimit`,
1073
+ * `advisoryPhases`). Caps are the tightest the Rules accept, plus the advisory's admitted maximum so the Root's
1074
+ * reservation leaves the Rules their full ceilings. On a Hookr token's pool pass the minimum it binds as
1075
+ * `ctx.minFeePips`: the quote cap then admits royalty + minimum, and a Rules admission whose quote cap cannot carry it
1076
+ * is refused here, so no launch is built that the bind refuses. The advisory gas is the admission's, which HookrRoot requires
1077
+ * exactly and runs the advisory's bind within, so `advisory.kind` is required: the admission's gas must clear that
1078
+ * advisory's floor (`ROOT_ADVISORY_MIN_GAS`) and the phases must include BEFORE_SWAP (exactly BEFORE_SWAP for the
1079
+ * session advisory). Refuses a configuration the Rules admission caps cannot carry.
1080
+ */
1081
+ export function rulesPoolTerms(l, ctx, rulesAdmission, advisory) {
1082
+ checkAdmission(rulesAdmission);
1083
+ assert(rulesAdmission.kind === ADMISSION_KIND.RULES, "Not a Rules admission");
1084
+ const rulesData = encodeRulesBind(l, ctx);
1085
+ // A Hookr token's pool freezes the Hookr minimum: its quote cap must admit royalty + minimum or the bind reverts.
1086
+ const ceilings = rulesFeeCeilings(l.rules, l.baseLpFeePips, ctx.minFeePips ?? 0);
1087
+ let advisoryGasLimit = 0;
1088
+ let advisoryPhases = 0;
1089
+ if (advisory) {
1090
+ const a = advisory.admission;
1091
+ checkAdmission(a);
1092
+ assert(a.kind === ADMISSION_KIND.ADVISORY, "Not an advisory admission");
1093
+ const kind = advisory.kind;
1094
+ assert(Object.hasOwn(ROOT_ADVISORY_MIN_GAS, kind), "An advised member needs its advisory kind");
1095
+ const floor = ROOT_ADVISORY_MIN_GAS[kind];
1096
+ assert(a.gasLimit >= floor, `Advisory admission gas ${a.gasLimit} below the ${kind} bind floor ${floor}`);
1097
+ advisoryGasLimit = a.gasLimit;
1098
+ advisoryPhases = advisory.phases ?? a.phaseMask;
1099
+ assert(advisoryPhases !== 0 && (advisoryPhases & ~a.phaseMask) === 0, "Advisory phases outside the admission");
1100
+ assert(kind === "session" ? advisoryPhases === PHASE.BEFORE_SWAP : (advisoryPhases & PHASE.BEFORE_SWAP) !== 0, `The ${kind} advisory binds only with BEFORE_SWAP${kind === "session" ? " alone" : ""}`);
1101
+ }
1102
+ const caps = poolCapsFor(ceilings, l.baseLpFeePips, advisory?.admission.caps);
1103
+ const seen = rulesBindCaps(caps, rulesAdmission.caps, advisory?.admission.caps);
1104
+ assert(ceilings.maxLpFeePips <= seen.maxLpFeePips &&
1105
+ ceilings.maxQuoteTakePips <= seen.maxQuoteTakePips &&
1106
+ ceilings.maxSubjectTakeBps <= seen.maxSubjectTakeBps, "Rules admission caps below the configuration's ceilings");
1107
+ return { rulesData, caps, rulesGasLimit: rulesAdmission.gasLimit, advisoryGasLimit, advisoryPhases };
1108
+ }
1109
+ /**
1110
+ * The largest fee-advisory cap a HookrRoot member may bind: min(admission cap, pool LP cap above the Rules' native
1111
+ * ceiling). With `rulesPoolTerms` caps this is the admission cap.
1112
+ */
1113
+ export function feeAdvisoryLimit(l, poolCaps, admissionCap) {
1114
+ const room = poolCaps.maxLpFeePips - nativeLpCeiling(l.rules, l.baseLpFeePips, 0);
1115
+ return Math.max(0, Math.min(admissionCap, room));
1116
+ }
1117
+ /** Which side each fee premium prices: premium0 is zeroForOne, a buy when the quote is currency0. */
1118
+ export function feePremiumSides(quoteIsCurrency0) {
1119
+ return quoteIsCurrency0 ? { premium0: "buy", premium1: "sell" } : { premium0: "sell", premium1: "buy" };
1120
+ }
1121
+ /**
1122
+ * Checked fee-advisory bind data: the Config (and session tiers when given) against the manifest with
1123
+ * `ctx.advisoryLimit`, then the advisory's own checks and the gas floor `feeAdvisoryMinGas(root, tiered)`.
1124
+ * `gasLimit` is the pool's advisory gas. HookrRoot member (`root` "root", the default): the advisory admission's
1125
+ * gas, which HookrRoot requires exactly and runs `bind` within, so at least `FEE_ADVISORY_ROOT_BIND_GAS` (80,000
1126
+ * plain, 110,000 tiered), above the advisory's MIN_ADVISORY_GAS (30,000) and MIN_TIERED_GAS (60,000). Pair root
1127
+ * (`"pair"`): the root's advisoryGasLimit, at least MIN_PAIR_GAS (10,000), or MIN_TIERED_GAS with tiers.
1128
+ */
1129
+ export function encodeFeeAdvisoryParams(c, ctx, gasLimit, tiers, root = "root") {
1130
+ checkRuleParams("feeAdvisory", c, ctx);
1131
+ checkFeeAdvisoryConfig(c, limitOf(ctx));
1132
+ if (tiers) {
1133
+ checkRuleParams("sessionTiers", tiers, { ...ctx, tierCap: c.cap });
1134
+ checkSessionTiers(tiers, c.cap);
1135
+ }
1136
+ return encodeFeeAdvisoryBind(c, limitOf(ctx), gasLimit, tiers, root);
1137
+ }
1138
+ /** Checked session-advisory bind data: the Schedule against the manifest with `ctx.tierCap`. */
1139
+ export function encodeSessionScheduleParams(s, ctx) {
1140
+ checkRuleParams("sessionAdvisory", s, ctx);
1141
+ checkSessionSchedule(s, tierCapOf(ctx));
1142
+ return encodeSessionSchedule(s);
1143
+ }
1144
+ /**
1145
+ * HookrPairRoot params from creator knobs: maxLpFeePips is base plus the advisory cap, advisoryGasLimit the factory
1146
+ * admission's gas (at most 500,000) and advisoryFailOpen false unless `ctx.pairAdmission` is fail-open, fee-only and
1147
+ * without a quote take, each unless given. Checked against the manifest, the constructor and, with `admission` and
1148
+ * `advisoryCodeHash`, the root factory's admission check, which refuses a fail-open root on any other admission.
1149
+ */
1150
+ export function pairRootParamsFor(k, ctx = {}, admission) {
1151
+ const partial = { ...k };
1152
+ for (const hidden of ["maxLpFeePips", "advisoryGasLimit", "advisoryFailOpen"])
1153
+ if (partial[hidden] === undefined)
1154
+ delete partial[hidden];
1155
+ const cfg = defaultRuleParams("pairRoot", ctx, partial);
1156
+ checkRuleParams("pairRoot", cfg, ctx);
1157
+ const p = checkPairRootParams({
1158
+ currency0: k.currency0,
1159
+ currency1: k.currency1,
1160
+ tickSpacing: n(cfg, "tickSpacing"),
1161
+ baseLpFeePips: n(cfg, "baseLpFeePips"),
1162
+ maxLpFeePips: n(cfg, "maxLpFeePips"),
1163
+ advisory: k.advisory,
1164
+ advisoryCapPips: n(cfg, "advisoryCapPips"),
1165
+ advisoryGasLimit: n(cfg, "advisoryGasLimit"),
1166
+ advisoryFailOpen: big(cfg, "advisoryFailOpen") === 1n,
1167
+ });
1168
+ if (p.advisory !== ZERO && admission)
1169
+ checkPairRootAdmission(p, admission.admission, admission.advisoryCodeHash);
1170
+ return p;
1171
+ }
1172
+ const TERMS = [
1173
+ {
1174
+ type: "tuple",
1175
+ components: [
1176
+ { name: "listId", type: "bytes32" },
1177
+ { name: "requireAll", type: "uint32" },
1178
+ { name: "blockAny", type: "uint32" },
1179
+ { name: "flags", type: "uint16" },
1180
+ ],
1181
+ },
1182
+ ];
1183
+ /**
1184
+ * Compliance flags from named options. RESTRICTED_SUBJECT is derived from the subject token, never chosen.
1185
+ */
1186
+ export function complianceFlags(o) {
1187
+ return ((o.checkBeneficiary ? COMPLIANCE_CHECK_BENEFICIARY : 0) |
1188
+ (o.kycOnSell ? COMPLIANCE_KYC_ON_SELL : 0) |
1189
+ (o.haltSellsWhenSourceDown ? COMPLIANCE_HALT_SELLS_WHEN_SOURCE_DOWN : 0) |
1190
+ (o.restrictedSubject ? COMPLIANCE_RESTRICTED_SUBJECT : 0) |
1191
+ (o.acceptCurated ? COMPLIANCE_ACCEPT_CURATED : 0) |
1192
+ (o.directLpIfPermitted ? COMPLIANCE_DIRECT_LP_IF_PERMITTED : 0));
1193
+ }
1194
+ /**
1195
+ * Refuses terms HookrComplianceGuard.bind would refuse: unknown flags, a tier both required and blocked, a missing
1196
+ * list (with `ctx.listExists`), curated swaps on a listed unrestricted subject, and a restricted subject with a
1197
+ * subject take (with `ctx.subjectTakeBps`).
1198
+ */
1199
+ export function checkComplianceTerms(t, ctx = {}) {
1200
+ hash(t.listId);
1201
+ checkRuleParams("compliance", t, ctx);
1202
+ assert((t.requireAll & t.blockAny) === 0, "A tier is both required and blocked");
1203
+ if (t.listId !== ZERO_HASH && ctx.listExists !== undefined)
1204
+ assert(ctx.listExists, "Unknown compliance list");
1205
+ assert((t.flags & COMPLIANCE_ACCEPT_CURATED) === 0 ||
1206
+ (t.flags & COMPLIANCE_RESTRICTED_SUBJECT) !== 0 ||
1207
+ BigInt(t.listId) === 0n, "Curated swaps need a restricted subject or no list");
1208
+ if ((t.flags & COMPLIANCE_RESTRICTED_SUBJECT) !== 0 && ctx.subjectTakeBps !== undefined)
1209
+ assert(ctx.subjectTakeBps === 0, "A restricted subject needs a zero subject take");
1210
+ }
1211
+ /** Checked compliance terms as the guard's canonical 128-byte bind data. */
1212
+ export function encodeComplianceTerms(t, ctx = {}) {
1213
+ checkComplianceTerms(t, ctx);
1214
+ return encodeAbiParameters(TERMS, [{ ...t, listId: hash(t.listId) }]);
1215
+ }
1216
+ /** Decodes compliance bind data, refusing any size or encoding the guard refuses. */
1217
+ export function decodeComplianceTerms(data) {
1218
+ assert(/^0x[\da-fA-F]{256}$/.test(data), "Compliance terms must be 128 bytes");
1219
+ // The guard refuses a word above its field's type before decoding (HookrComplianceGuard._decode).
1220
+ const word = (i) => BigInt(`0x${data.slice(2 + 64 * i, 2 + 64 * (i + 1))}`);
1221
+ assert(word(1) < 2n ** 32n && word(2) < 2n ** 32n && word(3) < 2n ** 16n, "Non-canonical compliance terms");
1222
+ const [t] = decodeAbiParameters(TERMS, data);
1223
+ assert(encodeAbiParameters(TERMS, [t]).toLowerCase() === data.toLowerCase(), "Non-canonical compliance terms");
1224
+ return { ...t, listId: t.listId.toLowerCase() };
1225
+ }
1226
+ /**
1227
+ * A compliance-guarded member's advisory pool terms: fail closed, BEFORE_SWAP, and the admission's gas, which must
1228
+ * clear the guard's 150,000 floor.
1229
+ */
1230
+ export function complianceAdvisoryTerms(admission) {
1231
+ checkAdmission(admission);
1232
+ assert(admission.kind === ADMISSION_KIND.ADVISORY && (admission.phaseMask & PHASE.BEFORE_SWAP) !== 0, "Guard admission needs BEFORE_SWAP");
1233
+ assert(admission.gasLimit >= COMPLIANCE_MIN_ADVISORY_GAS, "Guard admission gas below 150000");
1234
+ return { advisoryGasLimit: admission.gasLimit, advisoryPhases: PHASE.BEFORE_SWAP, advisoryFailOpen: false };
1235
+ }