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,1049 @@
1
+ import { decodeAbiParameters, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, keccak256, stringToBytes, } from "viem";
2
+ import { recaptureAbi, rulesAbi, treasuryAbi } from "./abi.js";
3
+ import { RELEASE_CONSTANTS, STRUCT_COMPONENTS } from "./generated.js";
4
+ import { address, assert, uint, ZERO } from "./schema.js";
5
+ /** Gas every Rules admission, and so every pool bound to HookrRules, carries: exactly 2,000,000. */
6
+ export const RULES_GAS_LIMIT = RELEASE_CONSTANTS.REGISTRY_RULES_MIN_GAS_LIMIT;
7
+ /** Ceiling on any LP fee, in pips. */
8
+ export const MAX_LP_FEE_PIPS = 600_000;
9
+ /**
10
+ * Highest protocol share a pool binds with, in basis points (HookrRules._validate), and highest minimum a HookrRules
11
+ * can be built with (its constructor): 50%.
12
+ */
13
+ export const MAX_PROTOCOL_SHARE_BPS = 5_000;
14
+ /**
15
+ * Rule-fee terms the treasury governs (HookrTreasury, r23): the highest governed floor of a pool's protocolShareBps,
16
+ * the highest integrator rate (in basis points of Hookr's rule-fee share; HookrRules refuses a larger one at bind too)
17
+ * and the rate the app pre-fills, and how long a proposed change waits and then stays acceptable, in seconds.
18
+ */
19
+ export const MAX_RULE_SHARE_FLOOR_BPS = Number(RELEASE_CONSTANTS.TREASURY_MAX_RULE_SHARE_FLOOR_BPS);
20
+ export const MAX_INTEGRATOR_BPS = Number(RELEASE_CONSTANTS.RULES_MAX_INTEGRATOR_BPS);
21
+ export const DEFAULT_INTEGRATOR_BPS = Number(RELEASE_CONSTANTS.TREASURY_DEFAULT_INTEGRATOR_BPS);
22
+ export const FEE_TERMS_DELAY_SECONDS = Number(RELEASE_CONSTANTS.TREASURY_TERMS_DELAY);
23
+ export const FEE_TERMS_GRACE_SECONDS = Number(RELEASE_CONSTANTS.TREASURY_TERMS_GRACE);
24
+ /**
25
+ * The Hookr minimum (r25, HookrTreasury): the least Hookr takes of every trader swap on a Hookr token's pool, in pips
26
+ * of the swap, frozen at bind. The treasury holds a rate for pools without arb recapture and one for pools with it,
27
+ * each 0 to MAX_MIN_FEE_PIPS (10,000, 1%) and DEFAULT_MIN_FEE_PIPS (1,000, 0.1%) at deployment, and a per-integrator
28
+ * override (0 to 10,000; NO_MIN_FEE_OVERRIDE, 65,535, reads as none) that reaches only the pool ids that integrator
29
+ * approved (`approveMinFeePool`). HookrRules refuses a minimum above its own MAX_MIN_FEE_PIPS at bind.
30
+ */
31
+ export const MAX_MIN_FEE_PIPS = Number(RELEASE_CONSTANTS.TREASURY_MAX_MIN_FEE_PIPS);
32
+ export const DEFAULT_MIN_FEE_PIPS = Number(RELEASE_CONSTANTS.TREASURY_DEFAULT_MIN_FEE_PIPS);
33
+ export const NO_MIN_FEE_OVERRIDE = Number(RELEASE_CONSTANTS.TREASURY_NO_MIN_FEE_OVERRIDE);
34
+ export const RULES_MAX_MIN_FEE_PIPS = Number(RELEASE_CONSTANTS.RULES_MAX_MIN_FEE_PIPS);
35
+ /**
36
+ * The runtime keccak256 of HookrToken, the token HookrLauncher creates. It has no immutables, so every HookrToken has
37
+ * this codehash; HookrRules compares a pool's subject's codehash with it at bind and freezes the Hookr minimum only
38
+ * on a match, and then only on a pool without arb recapture whose rules earn Hookr nothing (`bindMinFeePips`). A token
39
+ * of any other code binds none; HookrToken bytecode deployed outside the launcher binds it.
40
+ */
41
+ export const HOOKR_TOKEN_RUNTIME_KECCAK256 = RELEASE_CONSTANTS.HOOKR_TOKEN_RUNTIME_KECCAK256;
42
+ /**
43
+ * Hookr's cut of every arb recapture, in basis points of the profit the root reads back from each push at the pool's
44
+ * frozen partner share (HookrRecapture.RECAPTURE_PROTOCOL_BPS): 25% since r31, on every pool, whatever its
45
+ * protocolShareBps or integrator.
46
+ */
47
+ export const RECAPTURE_PROTOCOL_BPS = Number(RELEASE_CONSTANTS.RECAPTURE_PROTOCOL_BPS);
48
+ /**
49
+ * HookrRegistryChecks.MAX_LANE_PARTNER_BPS: the most a lane partner keeps of each arb recapture's profit, 25% since
50
+ * r31, checked at every lane opening.
51
+ */
52
+ export const MAX_LANE_PARTNER_BPS = Number(RELEASE_CONSTANTS.MAX_LANE_PARTNER_BPS);
53
+ /** HookrRecapture.PRIZE_SHARE_OF_FEE_BPS: a King of the Pool prize reaches at most half the protocol fee it bounds. */
54
+ export const PRIZE_SHARE_OF_FEE_BPS = Number(RELEASE_CONSTANTS.RECAPTURE_PRIZE_SHARE_OF_FEE_BPS);
55
+ /**
56
+ * Longest Anti-Snipe decay any guard binds with, in seconds (HookrTypes.MAX_SNIPE_DECAY_SECONDS, an hour); since r31 a
57
+ * longer guard may decay over its whole length (`maxSnipeDecaySeconds`).
58
+ */
59
+ export const MAX_SNIPE_DECAY_SECONDS = Number(RELEASE_CONSTANTS.RULES_MAX_SNIPE_DECAY_SECONDS);
60
+ /** HookrLauncher.DYNAMIC_FEE_LIQUIDITY_DIVISOR: a dynamic fee member's launch liquidity over this is its minimum. */
61
+ export const DYNAMIC_FEE_LIQUIDITY_DIVISOR = BigInt(RELEASE_CONSTANTS.LAUNCHER_DYNAMIC_FEE_LIQUIDITY_DIVISOR);
62
+ /** Hookr dynamic fee reference windows (HookrRules.dynamicFeeParameters), in seconds, basis points and ticks. */
63
+ export const DYNAMIC_FEE_PARAMETERS = {
64
+ window: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_WINDOW),
65
+ reset: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_RESET),
66
+ carryBps: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_CARRY_BPS),
67
+ moveTicks: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_MOVE_TICKS),
68
+ };
69
+ /**
70
+ * RulesKnobs bounds HookrRules enforces at bind (HookrTypes): a dynamic fee pool's window, its reset (a multiple of the
71
+ * reset step from the larger of twice the window and the reset floor), its carry (a multiple of the carry step) and its
72
+ * counted move; the Snipe curves; and the parent block length a guard's wall-clock length is counted in.
73
+ */
74
+ export const RULES_KNOB_BOUNDS = {
75
+ minWindowSeconds: Number(RELEASE_CONSTANTS.RULES_MIN_WINDOW_SECONDS),
76
+ maxWindowSeconds: Number(RELEASE_CONSTANTS.RULES_MAX_WINDOW_SECONDS),
77
+ minResetSeconds: Number(RELEASE_CONSTANTS.RULES_MIN_RESET_SECONDS),
78
+ maxResetSeconds: Number(RELEASE_CONSTANTS.RULES_MAX_RESET_SECONDS),
79
+ resetStepSeconds: Number(RELEASE_CONSTANTS.RULES_RESET_STEP_SECONDS),
80
+ maxCarryBps: Number(RELEASE_CONSTANTS.RULES_MAX_CARRY_BPS),
81
+ carryStepBps: Number(RELEASE_CONSTANTS.RULES_CARRY_STEP_BPS),
82
+ minMoveTicks: Number(RELEASE_CONSTANTS.RULES_MIN_MOVE_TICKS),
83
+ maxMoveTicks: Number(RELEASE_CONSTANTS.RULES_MAX_MOVE_TICKS),
84
+ };
85
+ /** RulesKnobs.snipeCurve (HookrTypes.SNIPE_CURVE_*): linear (the default), front-loaded and step. */
86
+ export const SNIPE_CURVE = {
87
+ linear: Number(RELEASE_CONSTANTS.RULES_SNIPE_CURVE_LINEAR),
88
+ frontLoaded: Number(RELEASE_CONSTANTS.RULES_SNIPE_CURVE_FRONT_LOADED),
89
+ step: Number(RELEASE_CONSTANTS.RULES_SNIPE_CURVE_STEP),
90
+ };
91
+ /** HookrTypes.PARENT_BLOCK_SECONDS: a guard's length in parent blocks times this bounds a longer Snipe decay. */
92
+ export const PARENT_BLOCK_SECONDS = Number(RELEASE_CONSTANTS.RULES_PARENT_BLOCK_SECONDS);
93
+ /** The knobs of a pool bound without any (`abi.encode(RulesConfig)`): every field zero. */
94
+ export const NO_RULES_KNOBS = Object.freeze({
95
+ minDynamicFeeLiquidity: 0n,
96
+ windowSeconds: 0,
97
+ resetSeconds: 0,
98
+ carryBps: 0,
99
+ moveTicks: 0,
100
+ snipeCurve: 0,
101
+ });
102
+ /**
103
+ * HookrDynamicFee's reach bound (r31, N09): a dynamic fee pool binds only when its span (maxFeePips less the base fee)
104
+ * times its sensitivity squared is at most `maxReach`, and its span times its protocol share times (`reserveScale` less
105
+ * the sensitivity squared) at most `maxReserve` times `reserveScale`.
106
+ */
107
+ export const DYNAMIC_FEE_REACH = {
108
+ maxReach: BigInt(RELEASE_CONSTANTS.DYNAMIC_FEE_MAX_REACH),
109
+ maxReserve: BigInt(RELEASE_CONSTANTS.DYNAMIC_FEE_MAX_RESERVE),
110
+ reserveScale: BigInt(RELEASE_CONSTANTS.DYNAMIC_FEE_RESERVE_SCALE),
111
+ };
112
+ const MAX_MIN_LIQUIDITY = (1n << 96n) - 1n;
113
+ const WAD = 10n ** 18n;
114
+ /** Longest launch guard, in blocks after the binding block. */
115
+ export const MAX_GUARD_BLOCKS = 100_000;
116
+ const PIPS = 1000000n;
117
+ const BPS = 10000n;
118
+ const MAX_LP = BigInt(MAX_LP_FEE_PIPS);
119
+ const RULES_TUPLE = [{ type: "tuple", components: STRUCT_COMPONENTS.RulesConfig }];
120
+ const KNOBS_RULES_TUPLE = [...RULES_TUPLE, { type: "tuple", components: STRUCT_COMPONENTS.RulesKnobs }];
121
+ const RECAPTURE_RULES_TUPLE = [
122
+ ...KNOBS_RULES_TUPLE,
123
+ { type: "tuple", components: STRUCT_COMPONENTS.RecaptureConfig },
124
+ ];
125
+ /**
126
+ * RulesConfig's type string, `RulesConfig(uint40 guardEndBlock,...,address integrator)`, built from the struct's ABI
127
+ * components in declaration order (16 fields since r23 appended `integrator`).
128
+ */
129
+ export const RULES_CONFIG_TYPE = `RulesConfig(${STRUCT_COMPONENTS.RulesConfig.map((f) => `${f.type} ${f.name}`).join(",")})`;
130
+ /**
131
+ * The Rules' schema id (r26): HookrRules.configSchemaHash() is the keccak256 of RULES_CONFIG_TYPE, and
132
+ * HookrFeeAdvisory.RULES_SCHEMA and HookrSessionAdvisory's RULES_SCHEMA read the same id. A Rules admission carries it
133
+ * as its `schemaHash` (the lane checks the admission against the Rules' own answer), and a reader that gates on it
134
+ * refuses any other layout.
135
+ */
136
+ export const RULES_CONFIG_SCHEMA_HASH = RELEASE_CONSTANTS.RULES_CONFIG_SCHEMA_HASH;
137
+ /**
138
+ * The retired schema id of the 15-word layout before r23's `integrator` field, keccak256("hookr.rules.config"): these
139
+ * Rules never answer it, so an advisory or tool still pinned to it fails closed against them.
140
+ */
141
+ export const RETIRED_RULES_CONFIG_SCHEMA_HASH = keccak256(stringToBytes("hookr.rules.config"));
142
+ /**
143
+ * Words of each form of rules data HookrRules binds (r31): the config alone, the config and its RulesKnobs, and both
144
+ * with a RecaptureConfig too (16, 22 and 30 words).
145
+ */
146
+ export const RULES_DATA_WORDS = {
147
+ config: STRUCT_COMPONENTS.RulesConfig.length,
148
+ knobs: STRUCT_COMPONENTS.RulesConfig.length + STRUCT_COMPONENTS.RulesKnobs.length,
149
+ recapture: STRUCT_COMPONENTS.RulesConfig.length +
150
+ STRUCT_COMPONENTS.RulesKnobs.length +
151
+ STRUCT_COMPONENTS.RecaptureConfig.length,
152
+ };
153
+ /** The decoded config with its addresses in the lowercase form every SDK comparison uses. */
154
+ function normalized(c) {
155
+ return { ...c, royaltyTo: c.royaltyTo.toLowerCase(), integrator: c.integrator.toLowerCase() };
156
+ }
157
+ function int(value, bits, label) {
158
+ assert(Number.isSafeInteger(value) && value >= 0 && value < 2 ** bits, `Invalid ${label}`);
159
+ return BigInt(value);
160
+ }
161
+ function min(a, b) {
162
+ return a < b ? a : b;
163
+ }
164
+ /** ABI-encodes a RulesConfig alone: `abi.encode(RulesConfig)`, the rules data of a pool without dynamic fees. */
165
+ export function encodeRulesConfig(c) {
166
+ return encodeAbiParameters(RULES_TUPLE, [c]);
167
+ }
168
+ /** Decodes a config-only rules data (`abi.encode(RulesConfig)`), refusing any non-canonical encoding. */
169
+ export function decodeRulesConfig(data) {
170
+ const [c] = decodeAbiParameters(RULES_TUPLE, data);
171
+ const decoded = normalized(c);
172
+ assert(encodeRulesConfig(decoded).toLowerCase() === data.toLowerCase(), "Non-canonical rules config");
173
+ return decoded;
174
+ }
175
+ /** The knobs with every integer normalized: what an ABI decode and a caller's literal compare equal on. */
176
+ function knobsOf(k) {
177
+ return {
178
+ minDynamicFeeLiquidity: BigInt(k.minDynamicFeeLiquidity),
179
+ windowSeconds: Number(k.windowSeconds),
180
+ resetSeconds: Number(k.resetSeconds),
181
+ carryBps: Number(k.carryBps),
182
+ moveTicks: Number(k.moveTicks),
183
+ snipeCurve: Number(k.snipeCurve),
184
+ };
185
+ }
186
+ /** Whether `k` sets any knob: a pool whose knobs are all zero binds the config alone. */
187
+ function anyKnob(k) {
188
+ return (k.minDynamicFeeLiquidity !== 0n ||
189
+ k.windowSeconds !== 0 ||
190
+ k.resetSeconds !== 0 ||
191
+ k.carryBps !== 0 ||
192
+ k.moveTicks !== 0 ||
193
+ k.snipeCurve !== 0);
194
+ }
195
+ /**
196
+ * Refuses knobs HookrRules._checkKnobs refuses at bind for `c` in the given form (`knobs`: abi.encode(RulesConfig,
197
+ * RulesKnobs); `recapture`: with a RecaptureConfig after them; `config`: the config alone, whose knobs read as zero). A
198
+ * pool without dynamic fees takes none of the five dynamic fee fields; a dynamic fee pool needs the knobbed form, a
199
+ * minimum dynamic fee liquidity of 1 to 2^96 - 1 and its tempo in bounds; a curve other than linear needs a decaying
200
+ * Snipe; and a knobbed pool without arb recapture or dynamic fees must set a knob off its default (the curve).
201
+ * Returns whether the pool stores knobs (`custom`) and whether its tempo is off the default (`tempo`).
202
+ */
203
+ export function checkRulesKnobs(c, knobs, form) {
204
+ const k = knobsOf(knobs);
205
+ uint(k.minDynamicFeeLiquidity, 96);
206
+ int(k.windowSeconds, 16, "windowSeconds");
207
+ int(k.resetSeconds, 16, "resetSeconds");
208
+ int(k.carryBps, 16, "carryBps");
209
+ int(k.moveTicks, 24, "moveTicks");
210
+ int(k.snipeCurve, 8, "snipeCurve");
211
+ const knobbed = form !== "config";
212
+ if (!knobbed)
213
+ assert(!anyKnob(k), "Rules data without knobs carries no knob");
214
+ let tempo = false;
215
+ let custom = false;
216
+ if (c.dynamicFeeSens === 0) {
217
+ assert(k.minDynamicFeeLiquidity === 0n &&
218
+ k.windowSeconds === 0 &&
219
+ k.resetSeconds === 0 &&
220
+ k.carryBps === 0 &&
221
+ k.moveTicks === 0, "A pool without dynamic fees takes no dynamic fee knob");
222
+ }
223
+ else {
224
+ const b = RULES_KNOB_BOUNDS;
225
+ assert(knobbed, "A dynamic fee pool binds its RulesKnobs (abi.encode(RulesConfig, RulesKnobs))");
226
+ assert(k.minDynamicFeeLiquidity > 0n, "A dynamic fee pool needs a minimum dynamic fee liquidity in [1, 2^96 - 1]");
227
+ assert(k.windowSeconds >= b.minWindowSeconds && k.windowSeconds <= b.maxWindowSeconds, `windowSeconds ${k.windowSeconds} outside [${b.minWindowSeconds}, ${b.maxWindowSeconds}]`);
228
+ assert(k.resetSeconds >= 2 * k.windowSeconds &&
229
+ k.resetSeconds >= b.minResetSeconds &&
230
+ k.resetSeconds <= b.maxResetSeconds &&
231
+ k.resetSeconds % b.resetStepSeconds === 0, `resetSeconds ${k.resetSeconds} outside [max(2 x window, ${b.minResetSeconds}), ${b.maxResetSeconds}] in steps of ${b.resetStepSeconds}`);
232
+ assert(k.carryBps <= b.maxCarryBps && k.carryBps % b.carryStepBps === 0, `carryBps ${k.carryBps} above ${b.maxCarryBps} or off its ${b.carryStepBps} step`);
233
+ assert(k.moveTicks >= b.minMoveTicks && k.moveTicks <= b.maxMoveTicks, `moveTicks ${k.moveTicks} outside [${b.minMoveTicks}, ${b.maxMoveTicks}]`);
234
+ const d = DYNAMIC_FEE_PARAMETERS;
235
+ tempo =
236
+ k.windowSeconds !== d.window ||
237
+ k.resetSeconds !== d.reset ||
238
+ k.carryBps !== d.carryBps ||
239
+ k.moveTicks !== d.moveTicks;
240
+ custom = tempo;
241
+ }
242
+ assert(k.snipeCurve <= SNIPE_CURVE.step, `Unknown Snipe curve ${k.snipeCurve}`);
243
+ assert(k.snipeCurve === 0 || c.snipeDecaySeconds !== 0, "A Snipe curve other than linear needs a decaying Snipe");
244
+ if (k.snipeCurve !== 0)
245
+ custom = true;
246
+ assert(!(form === "knobs" && !custom && c.dynamicFeeSens === 0), "Knobs at their defaults on a pool without dynamic fees or arb recapture bind as the config alone");
247
+ return { custom, tempo };
248
+ }
249
+ /**
250
+ * The canonical rules data HookrRules.bind takes for `c` (r31, three forms told apart by length): `abi.encode(RulesConfig)`
251
+ * for a pool without dynamic fees, arb recapture or a knob off its default; `abi.encode(RulesConfig, RulesKnobs)` for
252
+ * every other pool without arb recapture; `abi.encode(RulesConfig, RulesKnobs, RecaptureConfig)` for a recapture pool
253
+ * (`recapture.on`). Refuses knobs bind would refuse (`checkRulesKnobs`); the RecaptureConfig's own bounds are
254
+ * HookrRecapture's, checked at bind.
255
+ */
256
+ export function encodeRulesData(c, knobs = NO_RULES_KNOBS, recapture) {
257
+ const k = knobsOf(knobs);
258
+ if (recapture?.on) {
259
+ checkRulesKnobs(c, k, "recapture");
260
+ return encodeAbiParameters(RECAPTURE_RULES_TUPLE, [c, k, recapture]);
261
+ }
262
+ if (c.dynamicFeeSens === 0 && !anyKnob(k))
263
+ return encodeRulesConfig(c);
264
+ checkRulesKnobs(c, k, "knobs");
265
+ return encodeAbiParameters(KNOBS_RULES_TUPLE, [c, k]);
266
+ }
267
+ /**
268
+ * Decodes `RulesBound.config` in any form HookrRules.bind accepts (16, 22 or, for a recapture pool, 30 words) and
269
+ * refuses what bind refuses of its shape: a non-canonical encoding, knobs its config cannot take (`checkRulesKnobs`)
270
+ * and recapture data with recapture off. `knobs` is NO_RULES_KNOBS for the config-only form; `minLiquidity` is the
271
+ * knobs' minimum dynamic fee liquidity, zero without dynamic fees.
272
+ */
273
+ export function decodeRulesData(data) {
274
+ const words = (data.length - 2) / 64;
275
+ if (words === RULES_DATA_WORDS.config)
276
+ return { config: decodeRulesConfig(data), knobs: { ...NO_RULES_KNOBS }, minLiquidity: 0n };
277
+ if (words === RULES_DATA_WORDS.recapture) {
278
+ const [c, k, rc] = decodeAbiParameters(RECAPTURE_RULES_TUPLE, data);
279
+ const config = normalized(c);
280
+ const knobs = knobsOf(k);
281
+ const recapture = { ...rc };
282
+ assert(encodeAbiParameters(RECAPTURE_RULES_TUPLE, [config, knobs, recapture]).toLowerCase() === data.toLowerCase(), "Non-canonical recapture rules data");
283
+ assert(recapture.on, "Recapture rules data with recapture off");
284
+ checkRulesKnobs(config, knobs, "recapture");
285
+ return { config, knobs, minLiquidity: knobs.minDynamicFeeLiquidity, recapture };
286
+ }
287
+ assert(words === RULES_DATA_WORDS.knobs, "Rules data is none of the forms HookrRules binds");
288
+ const [c, k] = decodeAbiParameters(KNOBS_RULES_TUPLE, data);
289
+ const config = normalized(c);
290
+ const knobs = knobsOf(k);
291
+ assert(encodeAbiParameters(KNOBS_RULES_TUPLE, [config, knobs]).toLowerCase() === data.toLowerCase(), "Non-canonical rules data");
292
+ checkRulesKnobs(config, knobs, "knobs");
293
+ return { config, knobs, minLiquidity: knobs.minDynamicFeeLiquidity };
294
+ }
295
+ /**
296
+ * HookrLauncher's minimum dynamic fee liquidity for a member launched with `liquidity`: liquidity / divisor, in
297
+ * [1, 2^96 - 1]. `divisor` is launchFamily's MemberKnobs.dynamicFeeLiquidityDivisor; zero (and every other launch) is
298
+ * DYNAMIC_FEE_LIQUIDITY_DIVISOR.
299
+ */
300
+ export function dynamicFeeMinLiquidity(liquidity, divisor = 0) {
301
+ int(divisor, 16, "dynamicFeeLiquidityDivisor");
302
+ const minimum = liquidity / (divisor === 0 ? DYNAMIC_FEE_LIQUIDITY_DIVISOR : BigInt(divisor));
303
+ if (minimum === 0n)
304
+ return 1n;
305
+ return minimum > MAX_MIN_LIQUIDITY ? MAX_MIN_LIQUIDITY : minimum;
306
+ }
307
+ /**
308
+ * The rules data HookrLauncher binds for a member with `rules` and launch `liquidity` (HookrLauncher._rulesData, r31):
309
+ * launchFamily's MemberKnobs pass the member's Rules knobs as given (`knobs`; every other launch passes none); a
310
+ * dynamic fee member's minimum is its liquidity over the divisor, and its tempo the default when all four tempo knobs
311
+ * are zero. A recapture member (`recapture.on`) binds the 30-word form, a member with every knob zero the config alone,
312
+ * any other the 22-word form. Like the Launcher it does not check the knobs; HookrRules refuses bad ones at bind
313
+ * (`checkRulesKnobs`).
314
+ */
315
+ export function launcherRulesData(c, liquidity, opts = {}) {
316
+ const m = opts.knobs ?? {};
317
+ const k = {
318
+ minDynamicFeeLiquidity: 0n,
319
+ windowSeconds: m.windowSeconds ?? 0,
320
+ resetSeconds: m.resetSeconds ?? 0,
321
+ carryBps: m.carryBps ?? 0,
322
+ moveTicks: m.moveTicks ?? 0,
323
+ snipeCurve: m.snipeCurve ?? 0,
324
+ };
325
+ if (c.dynamicFeeSens !== 0) {
326
+ k.minDynamicFeeLiquidity = dynamicFeeMinLiquidity(liquidity, m.dynamicFeeLiquidityDivisor ?? 0);
327
+ if (k.windowSeconds === 0 && k.resetSeconds === 0 && k.carryBps === 0 && k.moveTicks === 0) {
328
+ k.windowSeconds = DYNAMIC_FEE_PARAMETERS.window;
329
+ k.resetSeconds = DYNAMIC_FEE_PARAMETERS.reset;
330
+ k.carryBps = DYNAMIC_FEE_PARAMETERS.carryBps;
331
+ k.moveTicks = DYNAMIC_FEE_PARAMETERS.moveTicks;
332
+ }
333
+ }
334
+ if (opts.recapture?.on)
335
+ return encodeAbiParameters(RECAPTURE_RULES_TUPLE, [c, k, opts.recapture]);
336
+ if (!anyKnob(k))
337
+ return encodeRulesConfig(c);
338
+ return encodeAbiParameters(KNOBS_RULES_TUPLE, [c, k]);
339
+ }
340
+ /**
341
+ * The longest snipeDecaySeconds a pool binds with at `blockNumber` (HookrRules._validate, r31): MAX_SNIPE_DECAY_SECONDS,
342
+ * or the guard's length in seconds when that is longer, (guardEndBlock - blockNumber) x PARENT_BLOCK_SECONDS, at most
343
+ * 65,535. Without a block number only MAX_SNIPE_DECAY_SECONDS is certain.
344
+ */
345
+ export function maxSnipeDecaySeconds(guardEndBlock, blockNumber) {
346
+ const guard = BigInt(guardEndBlock);
347
+ if (blockNumber === undefined || guard <= blockNumber)
348
+ return MAX_SNIPE_DECAY_SECONDS;
349
+ const length = (guard - blockNumber) * BigInt(PARENT_BLOCK_SECONDS);
350
+ const longest = length > 65535n ? 65535n : length;
351
+ return Number(longest) > MAX_SNIPE_DECAY_SECONDS ? Number(longest) : MAX_SNIPE_DECAY_SECONDS;
352
+ }
353
+ /** Whether a dynamic fee of `spanPips` (maxFeePips less the base fee) at `sens` and `shareBps` binds (HookrDynamicFee.withinReach). */
354
+ export function withinDynamicFeeReach(spanPips, sens, shareBps) {
355
+ const span = BigInt(spanPips);
356
+ const sens2 = BigInt(sens) * BigInt(sens);
357
+ const r = DYNAMIC_FEE_REACH;
358
+ return (span * sens2 <= r.maxReach && span * BigInt(shareBps) * (r.reserveScale - sens2) <= r.maxReserve * r.reserveScale);
359
+ }
360
+ /**
361
+ * The Anti-Snipe tax, in pips before clipping, of a guarded buy at `timestamp` in a pool bound at `launchedAt`
362
+ * (HookrRules._snipePips and _knobbedSnipe, r31): snipeTaxPips flat without a decay, otherwise falling from snipeTaxPips
363
+ * at launch to snipeFloorPips at snipeDecaySeconds along the pool's Snipe curve, every division rounded down. With
364
+ * d = snipeDecaySeconds, e the launch age, x = snipeTaxPips - snipeFloorPips and n = 8e / d: linear (the default),
365
+ * floor + x(d - e)/d; step, floor + x(8 - n)/8; front-loaded, what is left halves every eighth of the decay in a
366
+ * straight line between the halvings (the last eighth falls the rest of the way). The launch age is taken modulo 2^32
367
+ * and a clock behind the launch reads as the launch. The guard itself is counted in blocks (guardEndBlock).
368
+ */
369
+ export function snipeTaxAt(c, launchedAt, timestamp, curve = SNIPE_CURVE.linear) {
370
+ int(curve, 8, "snipeCurve");
371
+ assert(curve <= SNIPE_CURVE.step, `Unknown Snipe curve ${curve}`);
372
+ const start = BigInt(c.snipeTaxPips);
373
+ const duration = BigInt(c.snipeDecaySeconds);
374
+ if (duration === 0n)
375
+ return Number(start);
376
+ let elapsed = (BigInt(timestamp) - BigInt(launchedAt)) & 0xffffffffn;
377
+ if (elapsed > 0xffffffffn >> 1n)
378
+ elapsed = 0n;
379
+ const floor = BigInt(c.snipeFloorPips);
380
+ if (elapsed >= duration)
381
+ return Number(floor);
382
+ if (curve === SNIPE_CURVE.linear)
383
+ return Number(floor + ((start - floor) * (duration - elapsed)) / duration);
384
+ const n = (elapsed * 8n) / duration;
385
+ if (curve === SNIPE_CURVE.step)
386
+ return Number(floor + ((start - floor) * (8n - n)) / 8n);
387
+ const h = (start - floor) >> n;
388
+ const l = n === 7n ? 0n : h >> 1n;
389
+ return Number(floor + l + ((h - l) * (duration - (elapsed * 8n - n * duration))) / duration);
390
+ }
391
+ /**
392
+ * The guarded per-parent-block buy cap at Anti-Snipe tax `snipePips` (HookrRules.settleSwap): maxBuyQuoteAmount
393
+ * without a decay; with one, maxBuyQuoteAmount * snipeTaxPips / snipePips, so a block that fills the cap pays the
394
+ * same tax at every point of the decay. `null` when there is no cap: a zero maxBuyQuoteAmount, or a zero tax.
395
+ */
396
+ export function guardBuyLimit(c, snipePips) {
397
+ if (c.maxBuyQuoteAmount === 0n)
398
+ return null;
399
+ if (c.snipeDecaySeconds === 0)
400
+ return c.maxBuyQuoteAmount;
401
+ if (snipePips === 0)
402
+ return null;
403
+ return (c.maxBuyQuoteAmount * BigInt(c.snipeTaxPips)) / BigInt(snipePips);
404
+ }
405
+ /**
406
+ * HookrDynamicFee.averageRate: the average of min(1, (d / knee)^2), knee = 1 / (2 * sens), over the distances [a, b]
407
+ * from the reference, as a fraction of the dynamic fee span in WAD. Distances are in WAD; requires b > a.
408
+ */
409
+ export function dynamicFeeAverageRate(a, b, sens) {
410
+ assert(b > a && a >= 0n && Number.isInteger(sens) && sens >= 1 && sens <= 10, "Invalid dynamic fee interval");
411
+ const s = BigInt(sens);
412
+ const knee = WAD / (2n * s);
413
+ if (a >= knee)
414
+ return WAD;
415
+ if (b <= knee)
416
+ return (4n * s * s * (a * a + a * b + b * b)) / (3n * WAD);
417
+ const area = (knee - (a * a * a) / (knee * knee)) / 3n + (b - knee);
418
+ const ratio = (area * WAD) / (b - a);
419
+ return ratio > WAD ? WAD : ratio;
420
+ }
421
+ /**
422
+ * HookrDynamicFee.distance: the signed distance of `sqrtPriceX96` from the reference `sqrtRefX96` in the direction of
423
+ * a swap, in WAD: sqrt(ref) / sqrt(p) - 1 for zeroForOne, sqrt(p) / sqrt(ref) - 1 otherwise.
424
+ */
425
+ export function dynamicFeeDistance(sqrtRefX96, sqrtPriceX96, zeroForOne) {
426
+ return (zeroForOne ? (sqrtRefX96 * WAD) / sqrtPriceX96 : (sqrtPriceX96 * WAD) / sqrtRefX96) - WAD;
427
+ }
428
+ /** Dynamic fee pips before clipping for a move over the distances [a, b]: the span times the average rate. */
429
+ export function dynamicFeePips(c, baseLpFeePips, a, b) {
430
+ if (c.dynamicFeeSens === 0 || b <= a)
431
+ return 0;
432
+ const span = BigInt(c.maxFeePips) - BigInt(baseLpFeePips);
433
+ return Number((span * dynamicFeeAverageRate(a, b, c.dynamicFeeSens)) / WAD);
434
+ }
435
+ /**
436
+ * The protocol's share of a `bps` slice of a buy, in pips of the gross buyer spend, rounded up
437
+ * (HookrRules._protocolPips): ceil(bps x 100 x shareBps / 10,000). Exact whenever shareBps is a whole percent.
438
+ */
439
+ export function protocolSharePips(bps, shareBps) {
440
+ return (BigInt(bps) * BigInt(shareBps) + 99n) / 100n;
441
+ }
442
+ /**
443
+ * LP Rewards net of the protocol share, before the royalty, in pips: lpBps x 100 less its protocol share in pips,
444
+ * rounded up (HookrRules._buyParts, HookrFeeAdvisory._nativeCeiling, HookrSessionAdvisory._rulesCeiling).
445
+ */
446
+ function lpNetPips(c) {
447
+ return BigInt(c.lpBps) * 100n - protocolSharePips(c.lpBps, c.protocolShareBps);
448
+ }
449
+ /**
450
+ * LP Rewards, quote take, net subject burn and the royalty's part of the take of a buy (HookrRules._buyParts). The
451
+ * protocol's share of LP Rewards and of Auto Burn is taken in pips, rounded up; the burn left after it is whole basis
452
+ * points, rounded down.
453
+ */
454
+ function buyParts(c) {
455
+ const lpSlice = protocolSharePips(c.lpBps, c.protocolShareBps);
456
+ const lpNet = BigInt(c.lpBps) * 100n - lpSlice;
457
+ const royaltyPips = (lpNet * BigInt(c.royaltyBps)) / BPS;
458
+ const burnSlice = protocolSharePips(c.burnBps, c.protocolShareBps);
459
+ return {
460
+ lpReward: lpNet - royaltyPips,
461
+ take: lpSlice + royaltyPips + burnSlice,
462
+ burnBps: (BigInt(c.burnBps) * 100n - burnSlice) / 100n,
463
+ royaltyPips,
464
+ };
465
+ }
466
+ /**
467
+ * The royalty's part of a buy's quote take, in pips of the gross buyer spend (HookrRules._buyParts): LP Rewards net of
468
+ * the protocol share, times royaltyBps, rounded down. It is the creator's, not Hookr's: the Hookr minimum compares
469
+ * Hookr's rule-fee share alone with the minimum and adds the royalty on top. Sells pay no royalty.
470
+ */
471
+ export function royaltyPips(c) {
472
+ return Number((lpNetPips(c) * BigInt(c.royaltyBps)) / BPS);
473
+ }
474
+ /**
475
+ * LP Rewards of a buy, in pips of the pool input: lpBps x 100 less the protocol share (in pips, rounded up), less
476
+ * the royalty (HookrRules._buyParts). A pool binds only when base + this is at most 600,000 pips.
477
+ */
478
+ export function lpRewardPips(c) {
479
+ return Number(buyParts({ ...c, burnBps: 0 }).lpReward);
480
+ }
481
+ /**
482
+ * The smallest caps HookrRules accepts for `c` at base fee `baseLpFeePips` on a pool that freezes the Hookr minimum
483
+ * `minFeePips` (zero for a pool without one, `bindMinFeePips`): the worst-case total LP fee, quote take and subject
484
+ * burn over every swap (HookrRules._validate). The quote take is at least royalty + minimum, so a Hookr token's pool
485
+ * whose caps do not admit its minimum does not bind. These are the caps the Rules sees, after the Root has reserved the
486
+ * advisory's admitted maximum.
487
+ */
488
+ export function rulesFeeCeilings(c, baseLpFeePips, minFeePips = 0) {
489
+ const base = int(baseLpFeePips, 24, "base fee");
490
+ const minimum = BigInt(minFee(minFeePips));
491
+ assert(BigInt(c.maxFeePips) >= base && c.maxFeePips <= MAX_LP_FEE_PIPS, "maxFeePips outside [base, 600000]");
492
+ const { lpReward, take, burnBps, royaltyPips } = buyParts(c);
493
+ assert(base + lpReward <= MAX_LP, "Base fee plus LP Rewards above 600000 pips");
494
+ const share = BigInt(c.protocolShareBps);
495
+ const span = BigInt(c.maxFeePips) - base;
496
+ const snipeTax = BigInt(c.snipeTaxPips);
497
+ const room = MAX_LP - base - lpReward;
498
+ const dynamicFee = min(span, room);
499
+ const snipe = min(snipeTax, room - dynamicFee);
500
+ let slice = (dynamicFee * share) / BPS + (snipe * share) / BPS;
501
+ let maximumLp = base + lpReward + dynamicFee + snipe - slice;
502
+ if (span + snipeTax > room && share !== 0n) {
503
+ // A clipped dynamic fee and Snipe round their shares separately: allow one pip less protocol share.
504
+ slice = ((dynamicFee + snipe) * share) / BPS;
505
+ maximumLp = base + lpReward + dynamicFee + snipe - (slice === 0n ? 0n : slice - 1n);
506
+ }
507
+ if (maximumLp < BigInt(c.maxFeePips))
508
+ maximumLp = BigInt(c.maxFeePips);
509
+ let maximumQuote = slice + take;
510
+ if ((span * share) / BPS > maximumQuote)
511
+ maximumQuote = (span * share) / BPS;
512
+ // The Hookr minimum replaces a smaller rule share (r25): a buy takes at most royalty + max(rule share, minimum), a
513
+ // sell max(dynamic fee share, minimum), so the bound is the larger of the rules' own and royalty + minimum.
514
+ if (royaltyPips + minimum > maximumQuote)
515
+ maximumQuote = royaltyPips + minimum;
516
+ return {
517
+ maxLpFeePips: Number(maximumLp),
518
+ maxQuoteTakePips: Number(maximumQuote),
519
+ maxSubjectTakeBps: Number(burnBps),
520
+ };
521
+ }
522
+ /**
523
+ * Largest native LP fee (base + Rules surcharge) HookrRules can quote with Anti-Snipe at `snipeTax`
524
+ * (HookrFeeAdvisory._nativeCeiling). The fee advisory's room on a HookrRoot pool is the pool's LP cap minus this.
525
+ */
526
+ export function nativeLpCeiling(c, baseLpFeePips, snipeTax = 0) {
527
+ const base = BigInt(baseLpFeePips);
528
+ const share = BigInt(c.protocolShareBps);
529
+ const lpNet = lpNetPips(c);
530
+ const lpReward = lpNet - (lpNet * BigInt(c.royaltyBps)) / BPS;
531
+ if (base + lpReward >= MAX_LP)
532
+ return MAX_LP_FEE_PIPS;
533
+ const room = MAX_LP - base - lpReward;
534
+ const span = BigInt(c.maxFeePips) - base;
535
+ const dynamicFee = min(span, room);
536
+ const snipe = min(BigInt(snipeTax), room - dynamicFee);
537
+ let slice = (dynamicFee * share) / BPS + (snipe * share) / BPS;
538
+ if (span + BigInt(snipeTax) > room && share !== 0n) {
539
+ slice = ((dynamicFee + snipe) * share) / BPS;
540
+ if (slice !== 0n)
541
+ slice -= 1n;
542
+ }
543
+ const ceiling = base + lpReward + dynamicFee + snipe - slice;
544
+ return Number(ceiling < BigInt(c.maxFeePips) ? BigInt(c.maxFeePips) : ceiling);
545
+ }
546
+ function bps(value, max, label) {
547
+ assert(Number.isSafeInteger(value) && value >= 0 && value <= max, `${label} ${value} outside [0, ${max}]`);
548
+ return value;
549
+ }
550
+ /** A Hookr minimum a pool can freeze: 0 to 10,000 pips (HookrRules refuses a larger one at bind, InvalidConfig). */
551
+ function minFee(pips) {
552
+ return bps(pips, RULES_MAX_MIN_FEE_PIPS, "minFeePips");
553
+ }
554
+ /** Whether a runtime codehash is HookrToken's (`HOOKR_TOKEN_RUNTIME_KECCAK256`), the test HookrRules applies at bind. */
555
+ export function isHookrTokenRuntime(codeHash) {
556
+ return codeHash.toLowerCase() === HOOKR_TOKEN_RUNTIME_KECCAK256.toLowerCase();
557
+ }
558
+ /**
559
+ * HookrRules._earnsHookr (r31): whether a rule can pay Hookr at least a pip of some swap at the pool's
560
+ * protocolShareBps. Nothing does at a zero share; LP Rewards and Auto Burn do at any nonzero share (the Rules round
561
+ * their protocol slices up); the dynamic fee does when its whole span (maxFeePips less the base fee) times the share is
562
+ * at least 10,000, and the Anti-Snipe tax when snipeTaxPips times the share is (the Rules round both shares down).
563
+ */
564
+ export function earnsHookr(rules, baseLpFeePips) {
565
+ const share = BigInt(rules.protocolShareBps);
566
+ if (share === 0n)
567
+ return false;
568
+ if (rules.lpBps !== 0 || rules.burnBps !== 0)
569
+ return true;
570
+ const base = BigInt(baseLpFeePips);
571
+ const span = BigInt(rules.maxFeePips) > base ? BigInt(rules.maxFeePips) - base : 0n;
572
+ return span * share >= BPS || BigInt(rules.snipeTaxPips) * share >= BPS;
573
+ }
574
+ /**
575
+ * The Hookr minimum a pool freezes at bind (HookrRules.bind, r31): the treasury's `feeTerms(integrator, false, id)`
576
+ * minimum (`decodeFeeTerms`) only on a Hookr token's pool that binds no arb recapture, whose rules earn Hookr no share
577
+ * (`earnsHookr`) and whose advisory is not Tax + Conversion at a nonzero share; zero on every other pool. Whatever
578
+ * pays Hookr a share replaces the minimum, never adds to it. `subject` is `"new"` for a launch that creates its token
579
+ * (HookrLauncher deploys a HookrToken, so it always is one) or the existing subject's runtime codehash (`eth_getCode`,
580
+ * then keccak256; a Hookr token exactly when it equals HOOKR_TOKEN_RUNTIME_KECCAK256). A treasury answer above 10,000
581
+ * pips is refused whatever follows, as the Rules refuse it. A Rules paying an address without code reads no terms and
582
+ * binds no minimum: pass 0 as `treasuryMinFeePips` for it.
583
+ */
584
+ export function bindMinFeePips(subject, treasuryMinFeePips, terms) {
585
+ minFee(treasuryMinFeePips);
586
+ if (subject !== "new" && !isHookrTokenRuntime(subject))
587
+ return 0;
588
+ int(terms.baseLpFeePips, 24, "base fee");
589
+ if (terms.recapture || earnsHookr(terms.rules, terms.baseLpFeePips))
590
+ return 0;
591
+ const tax = terms.taxConversion;
592
+ if (tax && terms.rules.protocolShareBps !== 0 && tax.maxQuoteTakePips !== 0)
593
+ return 0;
594
+ return treasuryMinFeePips;
595
+ }
596
+ /**
597
+ * The minimum HookrTreasury.feeTerms answers for a pool (r25): the integrator's override when it has one
598
+ * (`overridePips` other than NO_MIN_FEE_OVERRIDE) and approved the pool's id, else the rate for its kind of pool (with
599
+ * arb recapture on, `recapturePips`; without, `plainPips`). An override never reaches a pool its integrator did not
600
+ * approve, so naming the integrator alone does not bind it. Offline mirror; read `feeTerms` on chain before signing.
601
+ */
602
+ export function treasuryMinFeePips(t) {
603
+ const rate = minFee(t.recapture ? t.recapturePips : t.plainPips);
604
+ const override = t.overridePips ?? NO_MIN_FEE_OVERRIDE;
605
+ if (override === NO_MIN_FEE_OVERRIDE || !t.approved)
606
+ return rate;
607
+ return minFee(override);
608
+ }
609
+ /**
610
+ * The effective protocolShareBps floor HookrRules binds a pool at (HookrRules._feeTerms): the larger of its immutable
611
+ * minProtocolShareBps and the treasury's governed floor, both 0 to 5,000.
612
+ */
613
+ export function effectiveShareFloor(minProtocolShareBps, ruleShareFloorBps = 0) {
614
+ return Math.max(bps(minProtocolShareBps, MAX_PROTOCOL_SHARE_BPS, "minProtocolShareBps"), bps(ruleShareFloorBps, MAX_RULE_SHARE_FLOOR_BPS, "ruleShareFloorBps"));
615
+ }
616
+ /**
617
+ * The rate a pool naming `integrator` freezes, from the treasury's list as read (`RulesContext.integrators`): zero for
618
+ * no integrator; refused when the integrator is not listed at 1 to 5,000. Undefined when the list was not given.
619
+ */
620
+ export function listedIntegratorBps(integrator, integrators) {
621
+ const account = address(integrator, true);
622
+ if (account === ZERO)
623
+ return 0;
624
+ if (integrators === undefined)
625
+ return undefined;
626
+ const rate = integrators[account] ?? 0;
627
+ assert(Number.isSafeInteger(rate) && rate >= 1 && rate <= MAX_INTEGRATOR_BPS, `Integrator ${account} is not on the treasury's integrator list (HookrRules UnknownIntegrator)`);
628
+ return rate;
629
+ }
630
+ /**
631
+ * Splits Hookr's share of one swap's rule fees (the Snipe, dynamic fee, LP Rewards and Auto Burn, royalty already
632
+ * out) on a pool with an integrator frozen at `integratorBps` (HookrRules.settleSwap): the integrator gets
633
+ * share x rate / 10,000 rounded down as a Rules claim (`IntegratorPaid`), the protocol recipient the rest, the odd
634
+ * unit included (`FeesAllocated.protocol`). A pool without an integrator (rate 0) pays the protocol all of it.
635
+ */
636
+ export function splitProtocolShare(share, integratorBps) {
637
+ uint(share);
638
+ const cut = (share * BigInt(bps(integratorBps, MAX_INTEGRATOR_BPS, "integratorBps"))) / BPS;
639
+ return { protocol: share - cut, integrator: cut };
640
+ }
641
+ /**
642
+ * A swap's HookrRules quote (HookrRules._quote, r25) on a pool bound with `c` at `baseLpFeePips` that froze the Hookr
643
+ * minimum `minFeePips`. `dynamicFeePips` is the dynamic fee before clipping (zero without one) and `snipePips` the
644
+ * Anti-Snipe tax (`snipeTaxAt`, only on a buy while the guard lasts). The dynamic fee is clipped to the room base and
645
+ * LP Rewards leave under 600,000 pips, the Snipe to what remains. Hookr's rule-fee share is its part of LP Rewards and
646
+ * Auto Burn (buys) and of the dynamic fee and the Snipe; the royalty is the creator's. The minimum is a top-up to the
647
+ * quote take only: `quoteTakePips` = royalty + max(ruleSharePips, minFeePips) and the LP fee surcharge is unchanged, so
648
+ * a pool without rules takes exactly the minimum and a pool whose rules give Hookr at least the minimum pays nothing
649
+ * more. Sells pay no LP Rewards, Auto Burn, royalty or Snipe, so a sell's share is its dynamic fee share alone. Since
650
+ * r31 only a pool whose rules earn Hookr nothing binds a minimum (`bindMinFeePips`), so there the top-up is the whole
651
+ * minimum, and no arb recapture pool binds one.
652
+ */
653
+ export function rulesQuote(c, s) {
654
+ const base = int(s.baseLpFeePips, 24, "base fee");
655
+ const minimum = BigInt(minFee(s.minFeePips ?? 0));
656
+ let lpReward = 0n;
657
+ let take = 0n;
658
+ let burnBps = 0n;
659
+ let royalty = 0n;
660
+ if (s.isBuy)
661
+ ({ lpReward, take, burnBps, royaltyPips: royalty } = buyParts(c));
662
+ assert(base + lpReward <= MAX_LP, "Base fee plus LP Rewards above 600000 pips");
663
+ const room = MAX_LP - base - lpReward;
664
+ let dynamicFee = BigInt(int(s.dynamicFeePips ?? 0, 24, "dynamic fee"));
665
+ let snipe = s.isBuy ? BigInt(int(s.snipePips ?? 0, 24, "Snipe")) : 0n;
666
+ if (dynamicFee > room)
667
+ dynamicFee = room;
668
+ if (snipe > room - dynamicFee)
669
+ snipe = room - dynamicFee;
670
+ const share = BigInt(c.protocolShareBps);
671
+ const protocolPips = (dynamicFee * share) / BPS + (snipe * share) / BPS;
672
+ take += protocolPips;
673
+ const ruleShare = take - royalty;
674
+ const topUp = ruleShare < minimum ? minimum - ruleShare : 0n;
675
+ return {
676
+ lpFeeSurchargePips: Number(lpReward + dynamicFee + snipe - protocolPips),
677
+ quoteTakePips: Number(take + topUp),
678
+ subjectBurnBps: Number(burnBps),
679
+ royaltyPips: Number(royalty),
680
+ ruleSharePips: Number(ruleShare),
681
+ topUpPips: Number(topUp),
682
+ };
683
+ }
684
+ /**
685
+ * The quote HookrRules.simulationQuote gives the root for its simulated swap on a dynamic fee pool (r26): the swap's
686
+ * quote (`rulesQuote`) without a dynamic fee, and on an exact-output sell a quote take of the larger of the largest
687
+ * dynamic fee share the sell can pay (the span, clipped to 600,000 pips less the base fee, times protocolShareBps) and
688
+ * that quote's take, which on a sell is the pool's Hookr minimum (zero without one). The real take of such a sell is
689
+ * max(its dynamic fee share, the minimum), so the simulation reaches at least as far as the real swap and no further
690
+ * than that bound; before r26 it reserved their sum and simulated a longer move. Every other shape is quoted without
691
+ * the dynamic fee.
692
+ */
693
+ export function simulationQuote(c, s) {
694
+ const q = rulesQuote(c, {
695
+ baseLpFeePips: s.baseLpFeePips,
696
+ isBuy: s.isBuy,
697
+ snipePips: s.snipePips,
698
+ minFeePips: s.minFeePips,
699
+ });
700
+ if (s.isBuy || s.exactInput)
701
+ return q;
702
+ const base = BigInt(s.baseLpFeePips);
703
+ const span = BigInt(c.maxFeePips) - base;
704
+ const room = MAX_LP - base;
705
+ const largest = ((span > room ? room : span) * BigInt(c.protocolShareBps)) / BPS;
706
+ if (largest <= BigInt(q.quoteTakePips))
707
+ return q;
708
+ return { ...q, quoteTakePips: Number(largest) };
709
+ }
710
+ /**
711
+ * What one swap without an advisory pays the Rules, in the pool's quote (HookrRoot's settlement, then
712
+ * HookrRules.settleSwap): `rulesFee`, the royalty inside it (buys: quoteBasis x royalty pips / 1e6, at most the fee),
713
+ * Hookr's part (`hookr`, the rest: its rule-fee share with the minimum's top-up) and Hookr's part split with the
714
+ * pool's integrator (`splitProtocolShare`: the integrator's frozen rate rounded down, the protocol recipient the rest).
715
+ * `actualQuote` is the quote the pool swap took in (buys) or paid out (sells), `requested` the swap's specified
716
+ * amount. Buys are priced on the gross buyer spend, quoteBasis = actualQuote x 1e6 / (1e6 - take) (at most the
717
+ * specified input of an exact-input buy); an exact-input sell on the quote out; an exact-output sell reserves
718
+ * ceil(requested x take / (1e6 - take)) on top of the requested output, all of it the Rules' fee without an advisory.
719
+ */
720
+ export function rulesSwapFees(s) {
721
+ const rate = BigInt(int(s.quoteTakePips, 24, "quote take"));
722
+ assert(rate < PIPS, "Quote take at or above 1000000 pips");
723
+ const actual = uint(s.actualQuote);
724
+ const requested = uint(s.requested);
725
+ let quoteBasis;
726
+ let rulesFee;
727
+ let royalty = 0n;
728
+ if (s.shape === "exactInputBuy" || s.shape === "exactOutputBuy") {
729
+ quoteBasis = (actual * PIPS) / (PIPS - rate);
730
+ if (s.shape === "exactInputBuy" && quoteBasis > requested)
731
+ quoteBasis = requested;
732
+ rulesFee = (quoteBasis * rate) / PIPS;
733
+ royalty = (quoteBasis * BigInt(int(s.royaltyPips, 24, "royalty"))) / PIPS;
734
+ if (royalty > rulesFee)
735
+ royalty = rulesFee;
736
+ }
737
+ else if (s.shape === "exactInputSell") {
738
+ quoteBasis = actual;
739
+ rulesFee = (quoteBasis * rate) / PIPS;
740
+ }
741
+ else {
742
+ quoteBasis = actual;
743
+ rulesFee = rate === 0n ? 0n : (requested * rate + (PIPS - rate) - 1n) / (PIPS - rate);
744
+ }
745
+ const hookr = rulesFee - royalty;
746
+ const split = splitProtocolShare(hookr, s.integratorBps ?? 0);
747
+ return { quoteBasis, rulesFee, royalty, hookr, ...split };
748
+ }
749
+ /**
750
+ * The profit one currency's push stands for (HookrLane._run, r28): the root reads it back from the push at the pool's
751
+ * frozen partner share, pushed x 10,000 / (10,000 - partnerBps) rounded down, and emits it as CorrectionSucceeded's
752
+ * `reportedProfit`. What the executor's call returns is not read. An executor that keeps floor(P x partnerBps /
753
+ * 10,000) of a profit P and pushes the rest is read back as P or P + 1.
754
+ */
755
+ export function recaptureProfit(pushed, partnerBps) {
756
+ return (uint(pushed) * BPS) / (BPS - BigInt(bps(partnerBps, MAX_LANE_PARTNER_BPS, "partnerBps")));
757
+ }
758
+ /**
759
+ * One arb recapture's split of `amount` pushed in `currency` (HookrRecapture.settleRecapture): the protocol takes
760
+ * RECAPTURE_PROTOCOL_BPS (25% since r31) of the profit the push stands for (`reportedProfit`: the root's read-back,
761
+ * recaptureProfit(amount, partnerBps)), at most the push, on every pool alike and never split with an integrator; on
762
+ * a push in the pool's quote (`inQuote`) the King of the Pool pot takes potBps of the rest and the trader named on the
763
+ * after-phase traderBps of the rest's part its own move accounts for (min(amount, traderBasis) x rest / amount); the
764
+ * LPs take what is left, rounding dust included. A push in any other currency pays no trader or pot; a frame that
765
+ * pushed in several currencies splits each on its own. With WTH keeping 2,500 bps and the default traderBps of 2,500,
766
+ * a profit P splits 25% to the partner, 25% to the protocol, 12.5% to the trader and 37.5% to the LPs.
767
+ */
768
+ export function recaptureSplit(p) {
769
+ assert(uint(p.amount) > 0n, "An arb recapture settles a nonzero push (HookrRecapture InvalidSettlement)");
770
+ const cut = (uint(p.reportedProfit) * BigInt(RECAPTURE_PROTOCOL_BPS)) / BPS;
771
+ const protocol = cut < p.amount ? cut : p.amount;
772
+ const rest = p.amount - protocol;
773
+ let trader = 0n;
774
+ let pot = 0n;
775
+ if (p.inQuote) {
776
+ if (address(p.trader, true) !== ZERO && uint(p.traderBasis) !== 0n) {
777
+ const part = p.traderBasis < p.amount ? p.traderBasis : p.amount;
778
+ trader = (((part * rest) / p.amount) * BigInt(bps(p.traderBps, 5_000, "traderBps"))) / BPS;
779
+ }
780
+ pot = (rest * BigInt(bps(p.potBps, 5_000, "potBps"))) / BPS;
781
+ }
782
+ return { protocol, trader, pot, lp: rest - trader - pot };
783
+ }
784
+ /**
785
+ * The largest King of the Pool maxPrizeBps a recapture pool binds with (HookrRecapture._validate): half of what the
786
+ * protocol keeps of LP Rewards and Auto Burn in basis points of a buy, each rounded down, where the kept share is
787
+ * protocolShareBps x (10,000 - integratorBps) / 10,000 rounded down. It is HookrRecapture.prizeBoundFor(lpBps, burnBps,
788
+ * protocolShareBps, integratorBps) (r26), read on chain with `encodePrizeBoundForCall`; for a pool naming an integrator,
789
+ * take the rate from `integration(id)` or `feeTerms`, never `prizeBound` on the pool's whole share.
790
+ */
791
+ export function kingOfTheHillPrizeBound(lpBps, burnBps, protocolShareBps, integratorBps = 0) {
792
+ const kept = (BigInt(bps(protocolShareBps, MAX_PROTOCOL_SHARE_BPS, "protocolShareBps")) *
793
+ (BPS - BigInt(bps(integratorBps, MAX_INTEGRATOR_BPS, "integratorBps")))) /
794
+ BPS;
795
+ return (((BigInt(lpBps) * kept) / BPS + (BigInt(burnBps) * kept) / BPS) * BigInt(PRIZE_SHARE_OF_FEE_BPS)) / BPS;
796
+ }
797
+ /**
798
+ * Refuses a RulesConfig that HookrRules.bind would refuse, and returns its fee ceilings (`rulesFeeCeilings`).
799
+ * Mirrors HookrRules._validate, including the effective protocol share floor (`effectiveShareFloor`), the integrator
800
+ * list when the context carries it (`listedIntegratorBps`), the Anti-Snipe decay bounds and, on a pool that freezes the
801
+ * Hookr minimum (`RulesContext.minFeePips`), caps that admit royalty + minimum. A dynamic fee pool binds at any tick
802
+ * spacing. An integrator changes no fee ceiling: it is paid from Hookr's share after the take.
803
+ */
804
+ export function checkRulesConfig(c, ctx) {
805
+ int(c.guardEndBlock, 40, "guardEndBlock");
806
+ for (const [k, bits] of [
807
+ ["maxFeePips", 24],
808
+ ["snipeTaxPips", 24],
809
+ ["snipeFloorPips", 24],
810
+ ["snipeDecaySeconds", 16],
811
+ ["dynamicFeeSens", 16],
812
+ ["burnBps", 16],
813
+ ["lpBps", 16],
814
+ ["potBps", 16],
815
+ ["royaltyBps", 16],
816
+ ["protocolShareBps", 16],
817
+ ["potEveryNBuys", 32],
818
+ ])
819
+ int(c[k], bits, k);
820
+ uint(c.maxBuyQuoteAmount, 128);
821
+ uint(c.potMinBuyQuote, 128);
822
+ const royaltyTo = address(c.royaltyTo, true);
823
+ listedIntegratorBps(c.integrator, ctx.integrators);
824
+ const integrator = address(c.integrator, true);
825
+ assert(integrator === ZERO || !(ctx.refusedIntegrators ?? []).some((a) => address(a, true) === integrator), `Integrator ${integrator} is an account of the Rules' own release that never claims (HookrRules UnknownIntegrator)`);
826
+ assert(c.potBps === 0 && c.potEveryNBuys === 0 && c.potMinBuyQuote === 0n, "Pot unavailable in this release");
827
+ assert(c.protocolShareBps >= effectiveShareFloor(ctx.minProtocolShareBps, ctx.ruleShareFloorBps), "Protocol share below the Rules minimum (the larger of minProtocolShareBps and the treasury's governed floor)");
828
+ assert(c.protocolShareBps <= MAX_PROTOCOL_SHARE_BPS, "Protocol share above 5000 bps");
829
+ assert(c.dynamicFeeSens <= 10, "Dynamic fee sensitivity above 10");
830
+ assert((c.dynamicFeeSens === 0) === (c.maxFeePips === ctx.baseLpFeePips), "Dynamic fees must be on exactly when maxFeePips is above base");
831
+ // r31: a decay may run past MAX_SNIPE_DECAY_SECONDS up to the guard's own length at bind (maxSnipeDecaySeconds).
832
+ assert(c.snipeDecaySeconds <= maxSnipeDecaySeconds(c.guardEndBlock, ctx.blockNumber), "Anti-Snipe decay above the longest the Rules accept: an hour, or the guard's length when it is longer");
833
+ // r31 (N09): a dynamic fee binds only within HookrDynamicFee's reach bound.
834
+ assert(withinDynamicFeeReach(c.maxFeePips - ctx.baseLpFeePips, c.dynamicFeeSens, c.protocolShareBps), "Dynamic fee past the reach bound: lower maxFeePips, the sensitivity or the protocol share");
835
+ assert(c.snipeDecaySeconds === 0 ? c.snipeFloorPips === 0 : c.snipeFloorPips < c.snipeTaxPips, "Anti-Snipe floor must be below the launch tax with a decay, and zero without one");
836
+ assert(ctx.baseLpFeePips + c.snipeTaxPips <= MAX_LP_FEE_PIPS, "Base fee plus Snipe above 600000 pips");
837
+ assert(c.burnBps + c.lpBps <= 1_000, "Burn plus LP Rewards above 1000 bps");
838
+ assert(c.royaltyBps <= 1_000, "Royalty above 1000 bps");
839
+ assert((c.royaltyBps === 0) === (royaltyTo === ZERO), "Royalty needs exactly one of rate and recipient");
840
+ assert(royaltyTo === ZERO || !(ctx.refusedRoyaltyRecipients ?? []).some((a) => address(a, true) === royaltyTo), `Royalty recipient ${royaltyTo} is an account of the Rules' own release that never claims (HookrRules InvalidConfig)`);
841
+ assert(c.royaltyBps === 0 || c.lpBps !== 0, "Royalty needs LP Rewards");
842
+ if (c.guardEndBlock === 0) {
843
+ assert(c.snipeTaxPips === 0 && c.maxBuyQuoteAmount === 0n && c.snipeDecaySeconds === 0, "Snipe, its decay and the buy limit need a launch guard");
844
+ }
845
+ else {
846
+ assert(ctx.liquidityOwner !== undefined && address(ctx.liquidityOwner) !== ZERO, "Launch guard needs a liquidity owner");
847
+ if (ctx.blockNumber !== undefined)
848
+ assert(BigInt(c.guardEndBlock) > ctx.blockNumber &&
849
+ BigInt(c.guardEndBlock) <= ctx.blockNumber + BigInt(MAX_GUARD_BLOCKS), "Launch guard outside the next 100000 blocks");
850
+ }
851
+ const ceilings = rulesFeeCeilings(c, ctx.baseLpFeePips, ctx.minFeePips ?? 0);
852
+ if (ctx.caps)
853
+ assert(ceilings.maxLpFeePips <= ctx.caps.maxLpFeePips &&
854
+ ceilings.maxQuoteTakePips <= ctx.caps.maxQuoteTakePips &&
855
+ ceilings.maxSubjectTakeBps <= ctx.caps.maxSubjectTakeBps, "Rules fee ceilings exceed the caps");
856
+ return ceilings;
857
+ }
858
+ function checkCaps(caps) {
859
+ int(caps.maxLpFeePips, 24, "maxLpFeePips");
860
+ int(caps.maxQuoteTakePips, 24, "maxQuoteTakePips");
861
+ int(caps.maxSubjectTakeBps, 16, "maxSubjectTakeBps");
862
+ assert(caps.maxLpFeePips <= MAX_LP_FEE_PIPS, "LP cap above 600000 pips");
863
+ assert(BigInt(caps.maxQuoteTakePips) < PIPS, "Quote take cap at or above 1000000 pips");
864
+ assert(caps.maxSubjectTakeBps <= 1_000, "Subject cap above 1000 bps");
865
+ }
866
+ /**
867
+ * The pool caps (PoolConfig.caps) of a pool whose Rules needs `rules` and whose advisory is admitted with
868
+ * `advisory` caps: the Rules ceilings plus the advisory's admitted maximum, so the Root's reservation leaves the
869
+ * Rules its full ceilings. Pass the result of `rulesFeeCeilings` (or `checkRulesConfig`) as `rules`.
870
+ */
871
+ export function poolCapsFor(rules, baseLpFeePips, advisory) {
872
+ const caps = {
873
+ maxLpFeePips: rules.maxLpFeePips + (advisory?.maxLpFeePips ?? 0),
874
+ maxQuoteTakePips: rules.maxQuoteTakePips + (advisory?.maxQuoteTakePips ?? 0),
875
+ maxSubjectTakeBps: rules.maxSubjectTakeBps,
876
+ };
877
+ checkCaps(caps);
878
+ assert(baseLpFeePips <= caps.maxLpFeePips, "Base fee above the LP cap");
879
+ return caps;
880
+ }
881
+ /**
882
+ * The caps HookrRoot.initializePool hands the Rules: min(pool, Rules admission) per field, less the advisory's
883
+ * admitted maximum. Refuses what the Root refuses: pool caps out of range, or an advisory maximum above the room.
884
+ */
885
+ export function rulesBindCaps(pool, rulesAdmission, advisoryAdmission) {
886
+ checkCaps(pool);
887
+ const limits = {
888
+ maxLpFeePips: Math.min(pool.maxLpFeePips, rulesAdmission.maxLpFeePips),
889
+ maxQuoteTakePips: Math.min(pool.maxQuoteTakePips, rulesAdmission.maxQuoteTakePips),
890
+ maxSubjectTakeBps: Math.min(pool.maxSubjectTakeBps, rulesAdmission.maxSubjectTakeBps),
891
+ };
892
+ if (!advisoryAdmission)
893
+ return limits;
894
+ assert(advisoryAdmission.maxLpFeePips <= limits.maxLpFeePips &&
895
+ advisoryAdmission.maxQuoteTakePips <= limits.maxQuoteTakePips, "Advisory admitted maximum above the pool room");
896
+ return {
897
+ maxLpFeePips: limits.maxLpFeePips - advisoryAdmission.maxLpFeePips,
898
+ maxQuoteTakePips: limits.maxQuoteTakePips - advisoryAdmission.maxQuoteTakePips,
899
+ maxSubjectTakeBps: limits.maxSubjectTakeBps,
900
+ };
901
+ }
902
+ const ZERO_ID = `0x${"0".repeat(64)}`;
903
+ /**
904
+ * `feeTerms(integrator, recapture, id)` calldata for the treasury a Rules pays (r25): its governed floor, `integrator`'s
905
+ * listed rate and the Hookr minimum of pool `id` (with arb recapture on when `recapture`; `integrator`'s override only
906
+ * when it approved `id`). It is exactly the read HookrRules makes at bind, with the pool's own id.
907
+ */
908
+ export function encodeFeeTermsCall(integrator = ZERO, recapture = false, poolId = ZERO_ID) {
909
+ assert(/^0x[0-9a-fA-F]{64}$/.test(poolId), "Invalid pool id");
910
+ return encodeFunctionData({
911
+ abi: treasuryAbi,
912
+ functionName: "feeTerms",
913
+ args: [address(integrator, true), recapture, poolId],
914
+ });
915
+ }
916
+ /**
917
+ * Decodes a `feeTerms(integrator, recapture, id)` result: `ruleShareFloorBps` for `RulesContext.ruleShareFloorBps`,
918
+ * the integrator's rate (zero when it is not listed) for `RulesContext.integrators`, and the minimum the treasury
919
+ * gives the pool (`bindMinFeePips` applies the r31 rule for `RulesContext.minFeePips`). Refuses values no
920
+ * HookrTreasury holds, and a minimum above the 10,000 pips the Rules accept.
921
+ */
922
+ export function decodeFeeTerms(data) {
923
+ const [floor, rate, minimum] = decodeFunctionResult({ abi: treasuryAbi, functionName: "feeTerms", data });
924
+ return {
925
+ ruleShareFloorBps: bps(Number(floor), MAX_RULE_SHARE_FLOOR_BPS, "ruleShareFloorBps"),
926
+ integratorBps: bps(Number(rate), MAX_INTEGRATOR_BPS, "integratorBps"),
927
+ minFeePips: minFee(Number(minimum)),
928
+ };
929
+ }
930
+ /** `minimumFee(poolId)` calldata for a HookrRules (served by its fallback): the Hookr minimum the pool froze. */
931
+ export function encodeMinimumFeeCall(poolId) {
932
+ return encodeFunctionData({ abi: recaptureAbi, functionName: "minimumFee", args: [poolId] });
933
+ }
934
+ /** Decodes a `minimumFee(poolId)` result: the pool's frozen minimum in pips, zero for a pool without one. */
935
+ export function decodeMinimumFee(data) {
936
+ return minFee(Number(decodeFunctionResult({ abi: recaptureAbi, functionName: "minimumFee", data })));
937
+ }
938
+ /**
939
+ * `approveMinFeePool(id, approved)` calldata for the treasury, sent by an integrator account itself: it approves (or
940
+ * withdraws) pool `id` binding that account's Hookr minimum override when the pool names it. Only the sender's own
941
+ * approvals change; an approval does nothing while the sender has no override, and it is read once, at bind. How Hookr
942
+ * runs chosen pools at a lower minimum: list an account it controls as an integrator, set that account's override
943
+ * through the treasury timelock, then have the account approve each chosen pool id before its launch (for a new
944
+ * token the id follows from `HookrLauncher.predictToken`, and nobody can initialize it first).
945
+ */
946
+ export function encodeApproveMinFeePoolCall(poolId, approved) {
947
+ assert(/^0x[0-9a-fA-F]{64}$/.test(poolId), "Invalid pool id");
948
+ return encodeFunctionData({ abi: treasuryAbi, functionName: "approveMinFeePool", args: [poolId, approved] });
949
+ }
950
+ /** `integration(poolId)` calldata: the integrator and rate a bound pool froze. */
951
+ export function encodeIntegrationCall(poolId) {
952
+ return encodeFunctionData({ abi: rulesAbi, functionName: "integration", args: [poolId] });
953
+ }
954
+ /** Decodes an `integration(poolId)` result; zeros for a pool without an integrator. */
955
+ export function decodeIntegration(data) {
956
+ const [integrator, rate] = decodeFunctionResult({ abi: rulesAbi, functionName: "integration", data });
957
+ return {
958
+ integrator: integrator.toLowerCase(),
959
+ integratorBps: bps(Number(rate), MAX_INTEGRATOR_BPS, "integratorBps"),
960
+ };
961
+ }
962
+ /** `minProtocolShareBps()` calldata: the floor a HookrRules applies to every pool's protocol share. */
963
+ export function encodeMinProtocolShareCall() {
964
+ return encodeFunctionData({ abi: rulesAbi, functionName: "minProtocolShareBps" });
965
+ }
966
+ /**
967
+ * Decodes a `minProtocolShareBps()` result, the `ParamContext.minProtocolShareBps` of pools that bind to that Rules.
968
+ * Refuses a value no HookrRules can hold (its constructor caps it at 5,000 bps).
969
+ */
970
+ export function decodeMinProtocolShareBps(data) {
971
+ const v = Number(decodeFunctionResult({ abi: rulesAbi, functionName: "minProtocolShareBps", data }));
972
+ assert(v <= MAX_PROTOCOL_SHARE_BPS, `minProtocolShareBps ${v} above ${MAX_PROTOCOL_SHARE_BPS}`);
973
+ return v;
974
+ }
975
+ /**
976
+ * `protocolShareFloor()` calldata for a HookrRules (r26, served by its HookrRecapture module through the fallback): the
977
+ * effective floor a pool binding now must meet, the larger of the Rules' minProtocolShareBps and the governed floor of
978
+ * the treasury they pay (zero governed floor for a recipient without code). One read in place of
979
+ * `encodeMinProtocolShareCall` plus `encodeFeeTermsCall`; it moves when the treasury's floor changes.
980
+ */
981
+ export function encodeProtocolShareFloorCall() {
982
+ return encodeFunctionData({ abi: recaptureAbi, functionName: "protocolShareFloor" });
983
+ }
984
+ /**
985
+ * Decodes a `protocolShareFloor()` result: the least protocolShareBps a pool binds with now, for
986
+ * `ParamContext.minProtocolShareBps` (with `ruleShareFloorBps` left at zero, since it already includes the governed
987
+ * floor). Refuses a value above 5,000 bps, which neither floor can hold.
988
+ */
989
+ export function decodeProtocolShareFloor(data) {
990
+ const v = Number(decodeFunctionResult({ abi: recaptureAbi, functionName: "protocolShareFloor", data }));
991
+ assert(v <= MAX_PROTOCOL_SHARE_BPS, `protocolShareFloor ${v} above ${MAX_PROTOCOL_SHARE_BPS}`);
992
+ return v;
993
+ }
994
+ /**
995
+ * `prizeBoundFor(lpBps, burnBps, protocolShareBps, integratorBps)` calldata for a HookrRules (r26, served by its
996
+ * HookrRecapture module): the widest King of the Pool maxPrizeBps the bind admits for a pool naming an integrator at
997
+ * that rate (0 for none). `kingOfTheHillPrizeBound` computes the same offline.
998
+ */
999
+ export function encodePrizeBoundForCall(lpBps, burnBps, protocolShareBps, integratorBps = 0) {
1000
+ return encodeFunctionData({
1001
+ abi: recaptureAbi,
1002
+ functionName: "prizeBoundFor",
1003
+ args: [
1004
+ bps(lpBps, 65_535, "lpBps"),
1005
+ bps(burnBps, 65_535, "burnBps"),
1006
+ bps(protocolShareBps, 65_535, "protocolShareBps"),
1007
+ bps(integratorBps, MAX_INTEGRATOR_BPS, "integratorBps"),
1008
+ ],
1009
+ });
1010
+ }
1011
+ /** Decodes a `prizeBoundFor(...)` result: the widest maxPrizeBps, in basis points of a buy. */
1012
+ export function decodePrizeBoundFor(data) {
1013
+ return decodeFunctionResult({ abi: recaptureAbi, functionName: "prizeBoundFor", data });
1014
+ }
1015
+ /** `configSchemaHash()` calldata for a HookrRules: its schema id, which must equal RULES_CONFIG_SCHEMA_HASH. */
1016
+ export function encodeConfigSchemaHashCall() {
1017
+ return encodeFunctionData({ abi: rulesAbi, functionName: "configSchemaHash" });
1018
+ }
1019
+ /**
1020
+ * Decodes a `configSchemaHash()` result and refuses any id but RULES_CONFIG_SCHEMA_HASH: a Rules of another layout
1021
+ * (the retired keccak256("hookr.rules.config") among them) is not the Rules this SDK encodes for.
1022
+ */
1023
+ export function decodeConfigSchemaHash(data) {
1024
+ const id = decodeFunctionResult({ abi: rulesAbi, functionName: "configSchemaHash", data });
1025
+ assert(id.toLowerCase() === RULES_CONFIG_SCHEMA_HASH.toLowerCase(), `Rules schema id ${id} is not RULES_CONFIG_SCHEMA_HASH (the keccak256 of ${RULES_CONFIG_TYPE})` +
1026
+ (id.toLowerCase() === RETIRED_RULES_CONFIG_SCHEMA_HASH.toLowerCase()
1027
+ ? "; it is the retired 15-word layout's"
1028
+ : ""));
1029
+ return id;
1030
+ }
1031
+ /**
1032
+ * The accounts HookrRules refuses as a pool's integrator whatever its treasury answers (r26, HookrRules._feeTerms),
1033
+ * lowercase, for `RulesContext.refusedIntegrators` / `ParamContext.refusedIntegrators`: the Rules, their trusted root,
1034
+ * their protocol recipient (the treasury), the PoolManager and their HookrRecapture module (`recaptureModule()`).
1035
+ * None of them ever claims what the Rules would credit it. The treasury also refuses, at proposal and acceptance, the
1036
+ * zero address, itself, the PoolManager and every admitted source.
1037
+ */
1038
+ export function bindRefusedIntegrators(a) {
1039
+ return [a.rules, a.trustedRoot, a.protocolRecipient, a.poolManager, a.recaptureModule].map((x) => address(x));
1040
+ }
1041
+ /**
1042
+ * The accounts HookrRules refuses as a pool's royalty recipient (r31, HookrRules._validate), lowercase, for
1043
+ * `RulesContext.refusedRoyaltyRecipients` / `ParamContext.refusedRoyaltyRecipients`: the Rules, their trusted root, the
1044
+ * PoolManager and their HookrRecapture module (`recaptureModule()`), the integrator refusals less the protocol
1045
+ * recipient. None of them ever claims what settleSwap credits it.
1046
+ */
1047
+ export function bindRefusedRoyaltyRecipients(a) {
1048
+ return [a.rules, a.trustedRoot, a.poolManager, a.recaptureModule].map((x) => address(x));
1049
+ }