hookr-sdk 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # hookr-sdk
2
2
 
3
- Build a market on Hookr's live root hook with a custom rule configuration, quote and swap
4
- through Hookr's router, read markets, and claim payouts. There is exactly one root today:
5
- `HookrModularHookV6` on Robinhood Chain (id 4663), `ADDRESSES.root`.
3
+ Build a market on Hookr's live root hooks with a custom rule configuration, quote and swap
4
+ through Hookr's router, read markets, and claim payouts, on Robinhood Chain (id 4663).
5
+
6
+ Two roots are live. A pool names exactly one of them in `PoolKey.hooks`:
7
+
8
+ | Root | Export | What it runs |
9
+ | --- | --- | --- |
10
+ | `HookrModularHookV6` | `ADDRESSES.root`, `ROOTS.default` | the five native rules |
11
+ | `HookrModularHookV6WthV5` | `ADDRESSES.recaptureRoot`, `ROOTS.recapture` | the same five rules plus the arbitrage-recapture lane |
12
+
13
+ Earlier recapture roots still hold trading pools. Their pools read back like any other, so resolve
14
+ a root from the pool's own `PoolKey.hooks` or from the registry rather than from a list.
6
15
 
7
16
  ## Install
8
17
 
@@ -10,6 +19,21 @@ through Hookr's router, read markets, and claim payouts. There is exactly one ro
10
19
  npm install hookr-sdk viem
11
20
  ```
12
21
 
22
+ ## RPC
23
+
24
+ `RPC_URL` is the chain's public, keyless endpoint. It is fine for a few reads. Under load it rate
25
+ limits, and a wide `eth_getLogs` can come back as an empty array instead of an error. `listMarkets`
26
+ scans in windows of `MARKET_SCAN_CHUNK_BLOCKS` (3,000,000 blocks, the width the app's own scanners
27
+ use; pass `chunkBlocks` to lower it) so no single answer is wide, but anything that must not miss
28
+ data should still run on a provider endpoint you own:
29
+
30
+ ```ts
31
+ import { http } from "viem";
32
+ import { RPC_URL } from "hookr-sdk";
33
+
34
+ const transport = http(process.env.HOOKR_RPC_URL ?? RPC_URL);
35
+ ```
36
+
13
37
  ## List and validate rules
14
38
 
15
39
  ```ts
@@ -23,9 +47,15 @@ const { ok, errors } = validateStack(stack);
23
47
 
24
48
  ## Build and send a create-market call
25
49
 
50
+ A market opens on the default root unless `root: ROOTS.recapture` is passed. On the recapture
51
+ root the builder also freezes the five correction fields of the stack (`RECAPTURE_CORRECTION`
52
+ plus the creator), without which the pool would carry that root's name and never correct. The
53
+ registry admits that lane on native-ETH-quoted pools only, so the builder throws on any other
54
+ quote, and it needs `creator` (the launching wallet) to freeze as the correction creator.
55
+
26
56
  ```ts
27
57
  import { createWalletClient, http } from "viem";
28
- import { CHAIN_ID, RPC_URL, buildExistingTokenMarketCall } from "hookr-sdk";
58
+ import { CHAIN_ID, NATIVE_ADDRESS, ROOTS, buildExistingTokenMarketCall } from "hookr-sdk";
29
59
 
30
60
  const call = buildExistingTokenMarketCall({
31
61
  subject: token, quote: usdcAddress, tickSpacing: 60, sqrtPriceX96,
@@ -33,6 +63,14 @@ const call = buildExistingTokenMarketCall({
33
63
  protocolRecipient, protocolShareBps, caps, // read from the coordinator and native block first
34
64
  });
35
65
  const hash = await walletClient.writeContract(call);
66
+
67
+ // The same market on the recapture root: native ETH quote, and the creator wallet.
68
+ const recaptureCall = buildExistingTokenMarketCall({
69
+ subject: token, quote: NATIVE_ADDRESS, tickSpacing: 60, sqrtPriceX96,
70
+ rules: { baseFeePips: 3_000, blocks: stack },
71
+ protocolRecipient, protocolShareBps, caps,
72
+ root: ROOTS.recapture, creator: account.address,
73
+ });
36
74
  ```
37
75
 
38
76
  `caps` comes from `nativeMechanicsBlock.validateStack(...)`; `protocolShareBps` from
@@ -41,17 +79,49 @@ package computes and what it takes as an already-resolved input.
41
79
 
42
80
  ## Read a market, quote, and swap
43
81
 
82
+ `poolKeyFor` defaults `hooks` to the default root. Pass `hooks: ADDRESSES.recaptureRoot` for a
83
+ pool on the recapture root, or use the `kernel` a `readMarket` row carries.
84
+
44
85
  ```ts
45
86
  import { createPublicClient, http } from "viem";
46
- import { readMarket, poolKeyFor, quoteExactInput, buildExactInputSwapCall } from "hookr-sdk";
87
+ import { ADDRESSES, readMarket, poolKeyFor, quoteExactInput, buildExactInputSwapCall } from "hookr-sdk";
47
88
 
48
- const client = createPublicClient({ transport: http(RPC_URL) });
89
+ const client = createPublicClient({ transport });
49
90
  const market = await readMarket(client, poolId);
50
- const key = poolKeyFor({ subject: market.subject, quote: market.quote, tickSpacing: market.tickSpacing });
91
+ const key = poolKeyFor({
92
+ subject: market.subject, quote: market.quote, tickSpacing: market.tickSpacing,
93
+ hooks: market.kernel, // ADDRESSES.root or ADDRESSES.recaptureRoot
94
+ });
51
95
  const { amountOut } = await quoteExactInput(client, { key, user, currencyIn: market.quote, amountIn });
52
96
  const swapCall = buildExactInputSwapCall({ key, currencyIn: market.quote, amountIn, amountOutMinimum, recipient: user, deadline });
53
97
  ```
54
98
 
99
+ A swap on a recapture pool needs more gas than an estimate reports, because the correction is
100
+ fail-open: an estimate converges on the cheapest swap that succeeds, which is the one that skipped
101
+ the correction. The swap builders put `gas: CORRECTION_SWAP_GAS_FLOOR` (1,100,000, the app's
102
+ floor) on the call whenever the pool's root is the current recapture root, so pass the call to
103
+ `writeContract` as it is; on the default root the call carries no `gas` and the wallet estimates
104
+ as usual. A pool opened on a superseded recapture root keeps its executor and keeps correcting;
105
+ read `poolCorrects(await readPoolRules(client, poolId))` and pass it as `correcting` to the swap
106
+ builders to put the same floor on that call (since 0.2.1).
107
+
108
+ ## Roots
109
+
110
+ `ROOTS` names the two roots Hookr opens markets on today. The registry itself never retires a
111
+ root, so `listRoots` shows every kernel it has registered, current or not, with its status and
112
+ whether its profile is sealed (the coordinator opens markets on sealed profiles only):
113
+
114
+ ```ts
115
+ import { listRoots } from "hookr-sdk";
116
+
117
+ for (const root of await listRoots(client)) {
118
+ console.log(root.hook, root.status, root.sealed ? "sealed" : "unsealed");
119
+ }
120
+ ```
121
+
122
+ Pools on any of those roots read, quote and swap through this package when their root's address is
123
+ passed as `hooks`; only market creation is limited to `ROOTS`.
124
+
55
125
  ## Claim
56
126
 
57
127
  ```ts
@@ -65,6 +135,19 @@ if (owed > 0n) await walletClient.writeContract(claimCall(quoteAddress));
65
135
 
66
136
  `RULES` and `PARAMS` are this package's own machine-readable list of the five public rules and
67
137
  their bounds, and the same list is served live at https://hookr.fun/api/hooks/catalog together
68
- with the root and contract addresses, the fee model, the composition limits and prefilled
138
+ with the roots and contract addresses, the fee model, the composition limits and prefilled
69
139
  builder links for example configurations. Both come from the same source in the Hookr app, and a
70
140
  parity test in that repository keeps the package and the app in step.
141
+
142
+ ## Changes
143
+
144
+ - 0.2.1: the swap builders take `correcting` (default `isCorrectingRoot(key.hooks)`, the current
145
+ recapture root), and `poolCorrects(rules)` reads from a pool's frozen rules whether it names a
146
+ correction executor, so a pool on a superseded recapture root can carry the gas floor too.
147
+ - 0.2.0: names both live roots (`ADDRESSES.recaptureRoot`, `RECAPTURE_KERNEL_ID`, `ROOTS`); the
148
+ market builders take `root` and freeze the recapture correction fields (`RECAPTURE_CORRECTION`);
149
+ swap calls carry `gas: CORRECTION_SWAP_GAS_FLOOR` on a correcting root; `listMarkets` scans in
150
+ windows (`MARKET_SCAN_CHUNK_BLOCKS`, `chunkBlocks`); `listRoots` reads every registered root
151
+ from the registry; documents the public RPC's limits. The README on npm for 0.1.0 had drifted
152
+ from the repository.
153
+ - 0.1.0: first release, default root only.
package/dist/chain.d.ts CHANGED
@@ -1,25 +1,36 @@
1
1
  import type { Address, Hex } from "viem";
2
2
  /**
3
- * Robinhood Chain, and Hookr's one live root hook on it.
3
+ * Robinhood Chain, and Hookr's two live root hooks on it.
4
4
  *
5
5
  * Every value here is a literal copy of `src/lib/v2-deployments.ts` (the addresses, read from
6
6
  * the app's own deployment evidence) and `src/lib/chain.ts` (the RPC and explorer origin) in the
7
7
  * Hookr app. `src/test/hookr-sdk-parity.test.ts` in that app asserts this file agrees with both,
8
8
  * so a redeploy or an RPC change that is not mirrored here fails that suite.
9
9
  *
10
- * There is exactly one root today: `HookrModularHookV6`, deployed as `ADDRESSES.root`. Every V2
11
- * pool's `PoolKey.hooks` is that address.
10
+ * Two roots take new markets today, and a pool names exactly one of them in `PoolKey.hooks`:
11
+ * `HookrModularHookV6` (`ADDRESSES.root`, the default root: the five native rules) and
12
+ * `HookrModularHookV6WthV5` (`ADDRESSES.recaptureRoot`: the same five rules plus the arbitrage
13
+ * recapture lane, whose correction executor is run by WTH). `ROOTS` pairs each with the kernel
14
+ * id the coordinator opens it under. Earlier recapture roots still hold trading pools; resolve
15
+ * any other root from the registry rather than from a list.
12
16
  */
13
17
  /** Robinhood Chain's id. */
14
18
  export declare const CHAIN_ID = 4663;
15
- /** The chain's public, keyless RPC endpoint. */
19
+ /**
20
+ * The chain's public, keyless RPC endpoint. Use it for light reads only: under load it rate
21
+ * limits, and an over-wide `eth_getLogs` can come back as an empty array instead of an error.
22
+ * Anything that scans logs or must not miss data should run on a provider endpoint you own and
23
+ * check for failed ranges; see the README.
24
+ */
16
25
  export declare const RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
17
26
  /** Origin of the chain's block explorer (Blockscout). No trailing slash. */
18
27
  export declare const EXPLORER_URL = "https://robinhoodchain.blockscout.com";
19
28
  /** The V2 contracts and ids a third-party integration calls or reads. */
20
29
  export declare const ADDRESSES: Readonly<{
21
- /** `HookrModularHookV6` — `hooks` in every V2 `PoolKey`. The one live root. */
30
+ /** `HookrModularHookV6` — the default root, `hooks` in every pool opened on it. */
22
31
  root: Address;
32
+ /** `HookrModularHookV6WthV5` — the arbitrage-recapture root, `hooks` in every pool opened on it. */
33
+ recaptureRoot: Address;
23
34
  /** `HookrMarketCoordinatorV5` — opens markets, collects founding fees, resolves the share tier. */
24
35
  coordinator: Address;
25
36
  /** `HookrKernelRouterV3` — the swap router that pays into and out of the pot. */
@@ -37,8 +48,39 @@ export declare const ADDRESSES: Readonly<{
37
48
  }>;
38
49
  /** The native-mechanics module id every V2 pool's one module is keyed by in the registry. */
39
50
  export declare const MODULE_ID: Hex;
40
- /** The coordinator's kernel id for the root hook, carried in `MarketParams.kernelId`. */
51
+ /** The coordinator's kernel id for the default root, carried in `MarketParams.kernelId`. */
41
52
  export declare const KERNEL_ID: Hex;
53
+ /** The coordinator's kernel id for the recapture root. */
54
+ export declare const RECAPTURE_KERNEL_ID: Hex;
55
+ /** The two live roots, each with the kernel id a market opens under on it. */
56
+ export declare const ROOTS: Readonly<{
57
+ default: Readonly<{
58
+ hook: `0x${string}`;
59
+ kernelId: `0x${string}`;
60
+ }>;
61
+ recapture: Readonly<{
62
+ hook: `0x${string}`;
63
+ kernelId: `0x${string}`;
64
+ }>;
65
+ }>;
66
+ /**
67
+ * The frozen `StackLimits` correction fields a market on the recapture root must carry for its
68
+ * correction lane to exist at all (the app's `V2_RECAPTURE_CORRECTION`). The kernel returns before
69
+ * correcting when a pool's frozen `correctionExecutor` is zero, and the fields are part of the stack
70
+ * hash, so they are set at creation or never. `executor` is the adapter the current recapture root's
71
+ * profile admits, bound immutably to WTH's executor; `feePolicyId` is the id that adapter answers
72
+ * with; `maxVolumeBps` and `minProfitQuote` are the registry's permissive ceiling and its smallest
73
+ * legal floor, frozen but not enforced (WTH sizes its own corrections). The registry admits a
74
+ * correction executor only on a native-ETH-quoted pool, and the pool's creator is frozen beside them.
75
+ */
76
+ export declare const RECAPTURE_CORRECTION: Readonly<{
77
+ executor: Address;
78
+ feePolicyId: Hex;
79
+ maxVolumeBps: 5000;
80
+ minProfitQuote: 1n;
81
+ }>;
82
+ /** True when `hooks` is the recapture root, the one root whose pools carry a correction lane. */
83
+ export declare function isRecaptureRoot(hooks: Address): boolean;
42
84
  /** Native ETH as a v4 currency: the zero address. Always `currency0` when it is one side of a pool. */
43
85
  export declare const NATIVE_ADDRESS: Address;
44
86
  export declare function explorerAddressUrl(address: Address): string;
package/dist/chain.js CHANGED
@@ -1,24 +1,35 @@
1
1
  /**
2
- * Robinhood Chain, and Hookr's one live root hook on it.
2
+ * Robinhood Chain, and Hookr's two live root hooks on it.
3
3
  *
4
4
  * Every value here is a literal copy of `src/lib/v2-deployments.ts` (the addresses, read from
5
5
  * the app's own deployment evidence) and `src/lib/chain.ts` (the RPC and explorer origin) in the
6
6
  * Hookr app. `src/test/hookr-sdk-parity.test.ts` in that app asserts this file agrees with both,
7
7
  * so a redeploy or an RPC change that is not mirrored here fails that suite.
8
8
  *
9
- * There is exactly one root today: `HookrModularHookV6`, deployed as `ADDRESSES.root`. Every V2
10
- * pool's `PoolKey.hooks` is that address.
9
+ * Two roots take new markets today, and a pool names exactly one of them in `PoolKey.hooks`:
10
+ * `HookrModularHookV6` (`ADDRESSES.root`, the default root: the five native rules) and
11
+ * `HookrModularHookV6WthV5` (`ADDRESSES.recaptureRoot`: the same five rules plus the arbitrage
12
+ * recapture lane, whose correction executor is run by WTH). `ROOTS` pairs each with the kernel
13
+ * id the coordinator opens it under. Earlier recapture roots still hold trading pools; resolve
14
+ * any other root from the registry rather than from a list.
11
15
  */
12
16
  /** Robinhood Chain's id. */
13
17
  export const CHAIN_ID = 4663;
14
- /** The chain's public, keyless RPC endpoint. */
18
+ /**
19
+ * The chain's public, keyless RPC endpoint. Use it for light reads only: under load it rate
20
+ * limits, and an over-wide `eth_getLogs` can come back as an empty array instead of an error.
21
+ * Anything that scans logs or must not miss data should run on a provider endpoint you own and
22
+ * check for failed ranges; see the README.
23
+ */
15
24
  export const RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
16
25
  /** Origin of the chain's block explorer (Blockscout). No trailing slash. */
17
26
  export const EXPLORER_URL = "https://robinhoodchain.blockscout.com";
18
27
  /** The V2 contracts and ids a third-party integration calls or reads. */
19
28
  export const ADDRESSES = Object.freeze({
20
- /** `HookrModularHookV6` — `hooks` in every V2 `PoolKey`. The one live root. */
29
+ /** `HookrModularHookV6` — the default root, `hooks` in every pool opened on it. */
21
30
  root: "0xb3cA29cF721380CEe8b8e4755F3865Ebc68Fe8cC",
31
+ /** `HookrModularHookV6WthV5` — the arbitrage-recapture root, `hooks` in every pool opened on it. */
32
+ recaptureRoot: "0xb914f955294799de4b891bd2EA8AF628Fa1c68CC",
22
33
  /** `HookrMarketCoordinatorV5` — opens markets, collects founding fees, resolves the share tier. */
23
34
  coordinator: "0x53A192A3fCeE94Da77916B461E0cCa2Dd6402442",
24
35
  /** `HookrKernelRouterV3` — the swap router that pays into and out of the pot. */
@@ -36,8 +47,35 @@ export const ADDRESSES = Object.freeze({
36
47
  });
37
48
  /** The native-mechanics module id every V2 pool's one module is keyed by in the registry. */
38
49
  export const MODULE_ID = "0x961091565aaf4eaa4996296fda9772957e61d18d90b965111a59ded2bbb5e774";
39
- /** The coordinator's kernel id for the root hook, carried in `MarketParams.kernelId`. */
50
+ /** The coordinator's kernel id for the default root, carried in `MarketParams.kernelId`. */
40
51
  export const KERNEL_ID = "0x1be0c118b1c6520d97de31ee9f0c33069f0e715ffcdcb16c87a343752bb5be14";
52
+ /** The coordinator's kernel id for the recapture root. */
53
+ export const RECAPTURE_KERNEL_ID = "0xd8b6c165b82efc3b7498081f071ea4f2476e2fb9c61b2e614aba2a016f94555a";
54
+ /** The two live roots, each with the kernel id a market opens under on it. */
55
+ export const ROOTS = Object.freeze({
56
+ default: Object.freeze({ hook: ADDRESSES.root, kernelId: KERNEL_ID }),
57
+ recapture: Object.freeze({ hook: ADDRESSES.recaptureRoot, kernelId: RECAPTURE_KERNEL_ID }),
58
+ });
59
+ /**
60
+ * The frozen `StackLimits` correction fields a market on the recapture root must carry for its
61
+ * correction lane to exist at all (the app's `V2_RECAPTURE_CORRECTION`). The kernel returns before
62
+ * correcting when a pool's frozen `correctionExecutor` is zero, and the fields are part of the stack
63
+ * hash, so they are set at creation or never. `executor` is the adapter the current recapture root's
64
+ * profile admits, bound immutably to WTH's executor; `feePolicyId` is the id that adapter answers
65
+ * with; `maxVolumeBps` and `minProfitQuote` are the registry's permissive ceiling and its smallest
66
+ * legal floor, frozen but not enforced (WTH sizes its own corrections). The registry admits a
67
+ * correction executor only on a native-ETH-quoted pool, and the pool's creator is frozen beside them.
68
+ */
69
+ export const RECAPTURE_CORRECTION = Object.freeze({
70
+ executor: "0x28AF7A3645080e926a3101461e0Ec0594D42D806",
71
+ feePolicyId: "0xd2653e091cb7002585fd8b1192b58f11b9c951061dc797123e37d5e2ccb45cef",
72
+ maxVolumeBps: 5_000,
73
+ minProfitQuote: 1n,
74
+ });
75
+ /** True when `hooks` is the recapture root, the one root whose pools carry a correction lane. */
76
+ export function isRecaptureRoot(hooks) {
77
+ return hooks.toLowerCase() === ADDRESSES.recaptureRoot.toLowerCase();
78
+ }
41
79
  /** Native ETH as a v4 currency: the zero address. Always `currency0` when it is one side of a pool. */
42
80
  export const NATIVE_ADDRESS = "0x0000000000000000000000000000000000000000";
43
81
  export function explorerAddressUrl(address) {
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
- * `@hookr/sdk` — build a market on Hookr's one live root hook with a custom rule configuration,
2
+ * `hookr-sdk` — build a market on Hookr's live root hooks with a custom rule configuration,
3
3
  * quote and swap through Hookr's router, read markets, and claim payouts.
4
4
  *
5
- * There is exactly one root today: `HookrModularHookV6` on Robinhood Chain (id 4663),
6
- * `ADDRESSES.root` in `chain.ts`. See the README for a quickstart.
5
+ * Two roots are live on Robinhood Chain (id 4663): the default root `HookrModularHookV6`
6
+ * (`ADDRESSES.root`) and the arbitrage-recapture root `HookrModularHookV6WthV5`
7
+ * (`ADDRESSES.recaptureRoot`); `ROOTS` in `chain.ts` pairs each with its kernel id. See the
8
+ * README for a quickstart.
7
9
  */
8
10
  export * from "./chain.js";
9
11
  export * from "./abi.js";
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
- * `@hookr/sdk` — build a market on Hookr's one live root hook with a custom rule configuration,
2
+ * `hookr-sdk` — build a market on Hookr's live root hooks with a custom rule configuration,
3
3
  * quote and swap through Hookr's router, read markets, and claim payouts.
4
4
  *
5
- * There is exactly one root today: `HookrModularHookV6` on Robinhood Chain (id 4663),
6
- * `ADDRESSES.root` in `chain.ts`. See the README for a quickstart.
5
+ * Two roots are live on Robinhood Chain (id 4663): the default root `HookrModularHookV6`
6
+ * (`ADDRESSES.root`) and the arbitrage-recapture root `HookrModularHookV6WthV5`
7
+ * (`ADDRESSES.recaptureRoot`); `ROOTS` in `chain.ts` pairs each with its kernel id. See the
8
+ * README for a quickstart.
7
9
  */
8
10
  export * from "./chain.js";
9
11
  export * from "./abi.js";
package/dist/market.d.ts CHANGED
@@ -50,7 +50,8 @@ export type PoolKey = Readonly<{
50
50
  export declare function subjectIsCurrency0(subject: Address, quote: Address): boolean;
51
51
  /**
52
52
  * `HookrMarketCoordinatorV5._poolKey`: currencies sorted numerically, `fee = DYNAMIC_FEE`,
53
- * `hooks` the root hook (defaults to `ADDRESSES.root`, the one live root).
53
+ * `hooks` the root hook: `ADDRESSES.root` (the default root) unless a pool on another root is
54
+ * being addressed, in which case pass that root's address (a market row's `kernel`).
54
55
  */
55
56
  export declare function poolKeyFor(input: {
56
57
  subject: Address;
@@ -93,6 +94,15 @@ export declare const MODULE_GAS = 2000000;
93
94
  export declare const ZERO_BYTES32: Hex;
94
95
  /** What `HookrNativeMechanicsBlockV2.validateStack` returns for a config: the surcharge and
95
96
  * take-bps ceilings this creator's stack is admitted at. A live chain read; see the module doc. */
97
+ /**
98
+ * The root a market opens on: its hook address (`PoolKey.hooks`) and the kernel id the coordinator
99
+ * opens it under. `ROOTS.default` and `ROOTS.recapture` are the two current roots; the registry
100
+ * accepts any active kernel, so this is a product choice, not a chain constraint.
101
+ */
102
+ export type RootChoice = Readonly<{
103
+ hook: Address;
104
+ kernelId: Hex;
105
+ }>;
96
106
  export type StackCaps = Readonly<{
97
107
  configHash: Hex;
98
108
  maxLpFeeSurchargePips: number;
@@ -115,13 +125,27 @@ export type StackLimits = Readonly<{
115
125
  correctionMinProfitQuote: bigint;
116
126
  correctionFeePolicyId: Hex;
117
127
  }>;
128
+ /** The correction lane frozen into a recapture-root pool's limits: `RECAPTURE_CORRECTION` plus the creator. */
129
+ export type CorrectionWiring = Readonly<{
130
+ executor: Address;
131
+ creator: Address;
132
+ maxVolumeBps: number;
133
+ minProfitQuote: bigint;
134
+ feePolicyId: Hex;
135
+ }>;
136
+ /**
137
+ * The correction wiring a market on `root` must freeze, or null on a root that does not correct.
138
+ * The registry admits a correction executor only on a native-ETH-quoted pool and requires a
139
+ * non-zero creator, so both are checked here rather than failing on chain.
140
+ */
141
+ export declare function correctionFor(root: RootChoice, quote: Address, creator: Address | undefined): CorrectionWiring | null;
118
142
  /**
119
143
  * The tightest `StackLimits` the registry admits: equal to the block's own caps, exactly as the
120
- * app's `v2StackLimits` sets them. `trustedRouter`/`trustedQuoter` are always `ADDRESSES.router`
144
+ * app's `v2StackLimits` sets them, with the correction lane frozen in when `correction` is given. `trustedRouter`/`trustedQuoter` are always `ADDRESSES.router`
121
145
  * / `ADDRESSES.quoter` today — the app's own equivalent takes them as a parameter only so its
122
146
  * tests can fake them; a real launch always wires the same two addresses this package does.
123
147
  */
124
- export declare function stackLimitsFor(config: NativeConfig, caps: StackCaps): StackLimits;
148
+ export declare function stackLimitsFor(config: NativeConfig, caps: StackCaps, correction?: CorrectionWiring | null): StackLimits;
125
149
  export type ModuleSelection = Readonly<{
126
150
  moduleId: Hex;
127
151
  config: Hex;
@@ -213,6 +237,8 @@ export declare function buildNewTokenMarketCall(input: {
213
237
  deadline: bigint;
214
238
  moduleData?: Hex;
215
239
  }>;
240
+ /** The root to open the market on: `ROOTS.default` when omitted, or `ROOTS.recapture`. */
241
+ root?: RootChoice;
216
242
  }): CoordinatorCall<readonly [NewTokenArgs, Hex, Address]>;
217
243
  /**
218
244
  * Build the exact `openExistingTokenMarket` call. The existing-asset lane carries no guard, no
@@ -229,6 +255,10 @@ export declare function buildExistingTokenMarketCall(input: {
229
255
  protocolRecipient: Address;
230
256
  protocolShareBps: number;
231
257
  caps: StackCaps;
258
+ /** The root to open the market on: `ROOTS.default` when omitted, or `ROOTS.recapture`. */
259
+ root?: RootChoice;
260
+ /** The launching wallet; required on the recapture root, where it is frozen as the correction creator. */
261
+ creator?: Address;
232
262
  }): CoordinatorCall<readonly [ExistingTokenArgs]>;
233
263
  /** The calldata `encodeFunctionData` would produce for a built call, for sending with viem directly. */
234
264
  export declare function encodeMarketCall(call: CoordinatorCall<readonly unknown[]>): Hex;
package/dist/market.js CHANGED
@@ -36,7 +36,7 @@
36
36
  */
37
37
  import { encodeAbiParameters, encodeFunctionData, keccak256 } from "viem";
38
38
  import { hookrMarketCoordinatorV5Abi } from "./abi.js";
39
- import { ADDRESSES, KERNEL_ID, MODULE_ID, NATIVE_ADDRESS } from "./chain.js";
39
+ import { ADDRESSES, MODULE_ID, NATIVE_ADDRESS, RECAPTURE_CORRECTION, ROOTS, isRecaptureRoot } from "./chain.js";
40
40
  import { compileRuleBlocks } from "./rules.js";
41
41
  // -----------------------------------------------------------------------------------------------
42
42
  // Pool key and pool id
@@ -61,7 +61,8 @@ export function subjectIsCurrency0(subject, quote) {
61
61
  }
62
62
  /**
63
63
  * `HookrMarketCoordinatorV5._poolKey`: currencies sorted numerically, `fee = DYNAMIC_FEE`,
64
- * `hooks` the root hook (defaults to `ADDRESSES.root`, the one live root).
64
+ * `hooks` the root hook: `ADDRESSES.root` (the default root) unless a pool on another root is
65
+ * being addressed, in which case pass that root's address (a market row's `kernel`).
65
66
  */
66
67
  export function poolKeyFor(input) {
67
68
  const subjectFirst = subjectIsCurrency0(input.subject, input.quote);
@@ -122,13 +123,29 @@ export function guardEndBlockFor(contractBlock, guardBlocks) {
122
123
  /** The native block's registered callback gas; `limits.maxTotalModuleGas` in every fixture. */
123
124
  export const MODULE_GAS = 2_000_000;
124
125
  export const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
126
+ /**
127
+ * The correction wiring a market on `root` must freeze, or null on a root that does not correct.
128
+ * The registry admits a correction executor only on a native-ETH-quoted pool and requires a
129
+ * non-zero creator, so both are checked here rather than failing on chain.
130
+ */
131
+ export function correctionFor(root, quote, creator) {
132
+ if (!isRecaptureRoot(root.hook))
133
+ return null;
134
+ if (quote.toLowerCase() !== NATIVE_ADDRESS.toLowerCase()) {
135
+ throw new Error("The recapture root corrects native-ETH-quoted pools only; the registry rejects any other quote.");
136
+ }
137
+ if (!creator || creator.toLowerCase() === NATIVE_ADDRESS.toLowerCase()) {
138
+ throw new Error("Opening a market on the recapture root needs the creator wallet; it is frozen as the correction creator.");
139
+ }
140
+ return Object.freeze({ ...RECAPTURE_CORRECTION, creator });
141
+ }
125
142
  /**
126
143
  * The tightest `StackLimits` the registry admits: equal to the block's own caps, exactly as the
127
- * app's `v2StackLimits` sets them. `trustedRouter`/`trustedQuoter` are always `ADDRESSES.router`
144
+ * app's `v2StackLimits` sets them, with the correction lane frozen in when `correction` is given. `trustedRouter`/`trustedQuoter` are always `ADDRESSES.router`
128
145
  * / `ADDRESSES.quoter` today — the app's own equivalent takes them as a parameter only so its
129
146
  * tests can fake them; a real launch always wires the same two addresses this package does.
130
147
  */
131
- export function stackLimitsFor(config, caps) {
148
+ export function stackLimitsFor(config, caps, correction = null) {
132
149
  return Object.freeze({
133
150
  baseLpFeePips: config.baseFeePips,
134
151
  maxLpFeePips: config.baseFeePips + caps.maxLpFeeSurchargePips,
@@ -138,11 +155,11 @@ export function stackLimitsFor(config, caps) {
138
155
  maxTotalModuleGas: MODULE_GAS,
139
156
  trustedRouter: ADDRESSES.router,
140
157
  trustedQuoter: ADDRESSES.quoter,
141
- correctionExecutor: NATIVE_ADDRESS,
142
- correctionCreator: NATIVE_ADDRESS,
143
- correctionMaxVolumeBps: 0,
144
- correctionMinProfitQuote: 0n,
145
- correctionFeePolicyId: ZERO_BYTES32,
158
+ correctionExecutor: correction?.executor ?? NATIVE_ADDRESS,
159
+ correctionCreator: correction?.creator ?? NATIVE_ADDRESS,
160
+ correctionMaxVolumeBps: correction?.maxVolumeBps ?? 0,
161
+ correctionMinProfitQuote: correction?.minProfitQuote ?? 0n,
162
+ correctionFeePolicyId: correction?.feePolicyId ?? ZERO_BYTES32,
146
163
  });
147
164
  }
148
165
  const NO_INITIAL_BUY = Object.freeze({
@@ -157,7 +174,7 @@ function buildNativeConfig(input) {
157
174
  const guardOn = lane === "new" && (rules.guardBlocks ?? 0) > 0;
158
175
  return Object.freeze({
159
176
  poolId: input.poolId,
160
- kernel: ADDRESSES.root,
177
+ kernel: input.kernel ?? ADDRESSES.root,
161
178
  subject: input.subject,
162
179
  quote: input.quote,
163
180
  lockedLiquidityProvider: guardOn ? ADDRESSES.coordinator : NATIVE_ADDRESS,
@@ -186,10 +203,12 @@ function buildNativeConfig(input) {
186
203
  * an allowance, exactly like the app).
187
204
  */
188
205
  export function buildNewTokenMarketCall(input) {
206
+ const root = input.root ?? ROOTS.default;
189
207
  const subject = input.expectedToken;
190
- const poolId = poolIdFor(poolKeyFor({ subject, quote: input.quote, tickSpacing: input.tickSpacing }));
208
+ const poolId = poolIdFor(poolKeyFor({ subject, quote: input.quote, tickSpacing: input.tickSpacing, hooks: root.hook }));
191
209
  const config = buildNativeConfig({
192
210
  lane: "new",
211
+ kernel: root.hook,
193
212
  rules: input.rules,
194
213
  poolId,
195
214
  subject,
@@ -199,7 +218,7 @@ export function buildNewTokenMarketCall(input) {
199
218
  protocolShareBps: input.protocolShareBps,
200
219
  });
201
220
  const encodedConfig = encodeNativeBlockConfig(config);
202
- const limits = stackLimitsFor(config, input.caps);
221
+ const limits = stackLimitsFor(config, input.caps, correctionFor(root, input.quote, input.creator));
203
222
  const market = {
204
223
  quote: input.quote,
205
224
  subjectAmount: input.totalSupply,
@@ -207,7 +226,7 @@ export function buildNewTokenMarketCall(input) {
207
226
  lpFeeRecipient: input.lpFeeRecipient ?? input.creator,
208
227
  tickSpacing: input.tickSpacing,
209
228
  sqrtPriceX96: input.sqrtPriceX96,
210
- kernelId: KERNEL_ID,
229
+ kernelId: root.kernelId,
211
230
  modules: [{ moduleId: MODULE_ID, config: encodedConfig }],
212
231
  limits,
213
232
  };
@@ -246,9 +265,11 @@ export function buildNewTokenMarketCall(input) {
246
265
  * refuses a guard on this lane rather than silently applying it.
247
266
  */
248
267
  export function buildExistingTokenMarketCall(input) {
249
- const poolId = poolIdFor(poolKeyFor({ subject: input.subject, quote: input.quote, tickSpacing: input.tickSpacing }));
268
+ const root = input.root ?? ROOTS.default;
269
+ const poolId = poolIdFor(poolKeyFor({ subject: input.subject, quote: input.quote, tickSpacing: input.tickSpacing, hooks: root.hook }));
250
270
  const config = buildNativeConfig({
251
271
  lane: "existing",
272
+ kernel: root.hook,
252
273
  rules: input.rules,
253
274
  poolId,
254
275
  subject: input.subject,
@@ -258,7 +279,7 @@ export function buildExistingTokenMarketCall(input) {
258
279
  protocolShareBps: input.protocolShareBps,
259
280
  });
260
281
  const encodedConfig = encodeNativeBlockConfig(config);
261
- const limits = stackLimitsFor(config, input.caps);
282
+ const limits = stackLimitsFor(config, input.caps, correctionFor(root, input.quote, input.creator));
262
283
  const market = {
263
284
  quote: input.quote,
264
285
  subjectAmount: 0n,
@@ -266,7 +287,7 @@ export function buildExistingTokenMarketCall(input) {
266
287
  lpFeeRecipient: NATIVE_ADDRESS,
267
288
  tickSpacing: input.tickSpacing,
268
289
  sqrtPriceX96: input.sqrtPriceX96,
269
- kernelId: KERNEL_ID,
290
+ kernelId: root.kernelId,
270
291
  modules: [{ moduleId: MODULE_ID, config: encodedConfig }],
271
292
  limits,
272
293
  };
package/dist/reads.d.ts CHANGED
@@ -45,6 +45,13 @@ export type Market = Readonly<{
45
45
  export declare function readMarket(client: PublicClient, poolId: Hex): Promise<Market>;
46
46
  /** The coordinator's deploy block on chain 4663: no `MarketCreated` log can sit before it. */
47
47
  export declare const MARKET_SCAN_FROM_BLOCK = 57229676n;
48
+ /**
49
+ * Blocks per `eth_getLogs` window in `listMarkets`, the same width the app's own scanners use.
50
+ * The chain's public node answers an over-wide window with an empty array rather than an error,
51
+ * so one request over the whole history is a silent miss waiting to happen; a bounded window
52
+ * keeps each answer small. Lower it further on a provider with a tighter range limit.
53
+ */
54
+ export declare const MARKET_SCAN_CHUNK_BLOCKS = 3000000n;
48
55
  export declare const MARKET_CREATED_TOPIC: Hex;
49
56
  /** One decoded `MarketCreated` log: every field the coordinator emitted, nothing derived. */
50
57
  export type MarketRow = Readonly<{
@@ -61,17 +68,19 @@ export type MarketRow = Readonly<{
61
68
  logIndex: number;
62
69
  }>;
63
70
  /**
64
- * Every `MarketCreated` log the coordinator emitted in `[fromBlock, toBlock]`, oldest first.
71
+ * Every `MarketCreated` log the coordinator emitted in `[fromBlock, toBlock]`, oldest first,
72
+ * fetched in windows of `chunkBlocks` (`MARKET_SCAN_CHUNK_BLOCKS` by default).
65
73
  *
66
74
  * `root`, when given, is checked against every row's `kernel` rather than passed to the log
67
- * filter: `kernel` is not an indexed topic, and there is exactly one live root today
68
- * (`ADDRESSES.root`), so every row already carries it. A future second root would need a
69
- * client-side filter here, which is exactly what this parameter is for.
75
+ * filter, because `kernel` is not an indexed topic. Pass `ADDRESSES.root` for markets on the
76
+ * default root, `ADDRESSES.recaptureRoot` for markets on the recapture root, or nothing for
77
+ * every market the coordinator opened, on any root.
70
78
  */
71
79
  export declare function listMarkets(client: PublicClient, options?: Readonly<{
72
80
  root?: Address;
73
81
  fromBlock?: bigint;
74
82
  toBlock?: bigint;
83
+ chunkBlocks?: bigint;
75
84
  }>): Promise<readonly MarketRow[]>;
76
85
  /** The inverse of `market.ts`'s `encodeNativeBlockConfig`. */
77
86
  export declare function decodeNativeBlockConfig(data: Hex): NativeConfig;
@@ -89,3 +98,39 @@ export type PoolRules = Readonly<{
89
98
  * by this package.
90
99
  */
91
100
  export declare function readPoolRules(client: PublicClient, poolId: Hex): Promise<PoolRules>;
101
+ /**
102
+ * True when the pool's frozen rules name a correction executor, so the root's kernel dispatches a
103
+ * correction around every swap and the swap needs `CORRECTION_SWAP_GAS_FLOOR`. This is the pool's
104
+ * own record rather than the root's identity: a pool on a superseded recapture root keeps its
105
+ * executor and keeps correcting, which `isCorrectingRoot` (current root only) does not see. Pass
106
+ * the result as `correcting` to the swap builders. It is also true for a pool whose lane cannot
107
+ * run on ordinary gas (the first recapture root); the floor is then unused headroom, not a loss.
108
+ */
109
+ export declare function poolCorrects(rules: PoolRules): boolean;
110
+ /** `HookrStackRegistryV1.KernelStatus`. */
111
+ export type RootStatus = "unset" | "active" | "retired";
112
+ /** One registered root (kernel) as the stack registry records it. */
113
+ export type RootRow = Readonly<{
114
+ kernelId: Hex;
115
+ /** The root hook: `PoolKey.hooks` for every pool opened on it. */
116
+ hook: Address;
117
+ version: number;
118
+ codeHash: Hex;
119
+ hookFlags: bigint;
120
+ status: RootStatus;
121
+ /** True when the registry holds a sealed root profile for this kernel; the coordinator only
122
+ * opens markets on a sealed profile. */
123
+ sealed: boolean;
124
+ profileId: Hex | null;
125
+ moduleCount: number | null;
126
+ }>;
127
+ /**
128
+ * Every kernel the stack registry has registered, in registration order: `kernelCount`, then
129
+ * `kernelIdAt`, `kernel`, `kernelStatus` and `rootProfile` for each. Nothing here comes from a
130
+ * list in this package, so a root this package has never heard of still appears. `rootProfile`
131
+ * reverts for a kernel whose profile was never sealed; that reads as `sealed: false`.
132
+ *
133
+ * The registry never retires a root on its own, so an active, sealed root is not the same thing
134
+ * as a current one: `ROOTS` names the two roots Hookr opens markets on today.
135
+ */
136
+ export declare function listRoots(client: PublicClient): Promise<readonly RootRow[]>;
package/dist/reads.js CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { decodeAbiParameters, getAbiItem, toEventSelector } from "viem";
13
13
  import { hookrMarketCoordinatorV5Abi, hookrStackRegistryV2Abi } from "./abi.js";
14
- import { ADDRESSES, MODULE_ID } from "./chain.js";
14
+ import { ADDRESSES, MODULE_ID, NATIVE_ADDRESS } from "./chain.js";
15
15
  function marketOrigin(value) {
16
16
  if (value === 1)
17
17
  return "new-token";
@@ -34,26 +34,41 @@ export async function readMarket(client, poolId) {
34
34
  // -----------------------------------------------------------------------------------------------
35
35
  /** The coordinator's deploy block on chain 4663: no `MarketCreated` log can sit before it. */
36
36
  export const MARKET_SCAN_FROM_BLOCK = 57229676n;
37
+ /**
38
+ * Blocks per `eth_getLogs` window in `listMarkets`, the same width the app's own scanners use.
39
+ * The chain's public node answers an over-wide window with an empty array rather than an error,
40
+ * so one request over the whole history is a silent miss waiting to happen; a bounded window
41
+ * keeps each answer small. Lower it further on a provider with a tighter range limit.
42
+ */
43
+ export const MARKET_SCAN_CHUNK_BLOCKS = 3000000n;
37
44
  const MARKET_CREATED_EVENT = getAbiItem({ abi: hookrMarketCoordinatorV5Abi, name: "MarketCreated" });
38
45
  export const MARKET_CREATED_TOPIC = toEventSelector(MARKET_CREATED_EVENT);
39
46
  /**
40
- * Every `MarketCreated` log the coordinator emitted in `[fromBlock, toBlock]`, oldest first.
47
+ * Every `MarketCreated` log the coordinator emitted in `[fromBlock, toBlock]`, oldest first,
48
+ * fetched in windows of `chunkBlocks` (`MARKET_SCAN_CHUNK_BLOCKS` by default).
41
49
  *
42
50
  * `root`, when given, is checked against every row's `kernel` rather than passed to the log
43
- * filter: `kernel` is not an indexed topic, and there is exactly one live root today
44
- * (`ADDRESSES.root`), so every row already carries it. A future second root would need a
45
- * client-side filter here, which is exactly what this parameter is for.
51
+ * filter, because `kernel` is not an indexed topic. Pass `ADDRESSES.root` for markets on the
52
+ * default root, `ADDRESSES.recaptureRoot` for markets on the recapture root, or nothing for
53
+ * every market the coordinator opened, on any root.
46
54
  */
47
55
  export async function listMarkets(client, options = {}) {
48
56
  const fromBlock = options.fromBlock ?? MARKET_SCAN_FROM_BLOCK;
49
57
  const toBlock = options.toBlock ?? (await client.getBlockNumber());
50
- const logs = await client.getLogs({
51
- address: ADDRESSES.coordinator,
52
- event: MARKET_CREATED_EVENT,
53
- fromBlock,
54
- toBlock,
55
- strict: true,
56
- });
58
+ const chunk = options.chunkBlocks ?? MARKET_SCAN_CHUNK_BLOCKS;
59
+ if (chunk <= 0n)
60
+ throw new Error("chunkBlocks must be positive.");
61
+ const logs = [];
62
+ for (let from = fromBlock; from <= toBlock; from += chunk) {
63
+ const to = from + chunk - 1n < toBlock ? from + chunk - 1n : toBlock;
64
+ logs.push(...(await client.getLogs({
65
+ address: ADDRESSES.coordinator,
66
+ event: MARKET_CREATED_EVENT,
67
+ fromBlock: from,
68
+ toBlock: to,
69
+ strict: true,
70
+ })));
71
+ }
57
72
  const rows = [];
58
73
  for (const log of logs) {
59
74
  const args = log.args;
@@ -138,3 +153,67 @@ export async function readPoolRules(client, poolId) {
138
153
  }
139
154
  return Object.freeze({ configured: true, limits: core.limits, nativeConfig });
140
155
  }
156
+ /**
157
+ * True when the pool's frozen rules name a correction executor, so the root's kernel dispatches a
158
+ * correction around every swap and the swap needs `CORRECTION_SWAP_GAS_FLOOR`. This is the pool's
159
+ * own record rather than the root's identity: a pool on a superseded recapture root keeps its
160
+ * executor and keeps correcting, which `isCorrectingRoot` (current root only) does not see. Pass
161
+ * the result as `correcting` to the swap builders. It is also true for a pool whose lane cannot
162
+ * run on ordinary gas (the first recapture root); the floor is then unused headroom, not a loss.
163
+ */
164
+ export function poolCorrects(rules) {
165
+ const executor = rules.limits?.correctionExecutor;
166
+ return executor !== undefined && executor.toLowerCase() !== NATIVE_ADDRESS.toLowerCase();
167
+ }
168
+ function rootStatus(value) {
169
+ if (value === 0)
170
+ return "unset";
171
+ if (value === 1)
172
+ return "active";
173
+ if (value === 2)
174
+ return "retired";
175
+ throw new Error(`Unknown KernelStatus ${value}.`);
176
+ }
177
+ /**
178
+ * Every kernel the stack registry has registered, in registration order: `kernelCount`, then
179
+ * `kernelIdAt`, `kernel`, `kernelStatus` and `rootProfile` for each. Nothing here comes from a
180
+ * list in this package, so a root this package has never heard of still appears. `rootProfile`
181
+ * reverts for a kernel whose profile was never sealed; that reads as `sealed: false`.
182
+ *
183
+ * The registry never retires a root on its own, so an active, sealed root is not the same thing
184
+ * as a current one: `ROOTS` names the two roots Hookr opens markets on today.
185
+ */
186
+ export async function listRoots(client) {
187
+ const registry = { address: ADDRESSES.registry, abi: hookrStackRegistryV2Abi };
188
+ const count = (await client.readContract({ ...registry, functionName: "kernelCount" }));
189
+ const rows = [];
190
+ for (let i = 0n; i < count; i++) {
191
+ const kernelId = (await client.readContract({ ...registry, functionName: "kernelIdAt", args: [i] }));
192
+ const record = (await client.readContract({ ...registry, functionName: "kernel", args: [kernelId] }));
193
+ const status = (await client.readContract({ ...registry, functionName: "kernelStatus", args: [kernelId] }));
194
+ let sealed = false;
195
+ let profileId = null;
196
+ let moduleCount = null;
197
+ try {
198
+ const profile = (await client.readContract({ ...registry, functionName: "rootProfile", args: [kernelId] }));
199
+ sealed = profile.isSealed;
200
+ profileId = profile.profileId;
201
+ moduleCount = profile.moduleCount;
202
+ }
203
+ catch {
204
+ // Unsealed: the registry reverts rather than returning an empty profile.
205
+ }
206
+ rows.push(Object.freeze({
207
+ kernelId,
208
+ hook: record.implementation,
209
+ version: record.version,
210
+ codeHash: record.implementationCodeHash,
211
+ hookFlags: record.hookFlags,
212
+ status: rootStatus(status),
213
+ sealed,
214
+ profileId,
215
+ moduleCount,
216
+ }));
217
+ }
218
+ return rows;
219
+ }
package/dist/swap.d.ts CHANGED
@@ -10,6 +10,28 @@
10
10
  import type { Address, ContractFunctionArgs, Hex, PublicClient } from "viem";
11
11
  import { hookrKernelRouterV3Abi } from "./abi.js";
12
12
  import type { PoolKey } from "./market.js";
13
+ /**
14
+ * The gas limit a swap on a correcting root must carry (the app's `CORRECTION_SWAP_GAS_FLOOR` in
15
+ * `src/lib/gas-headroom.ts`). The recapture root runs its correction in both swap phases, once in
16
+ * `beforeSwap` and again in `afterSwap` (`HookrSwapKernelV5Wth.sol`, `_tryCorrection` at lines 214
17
+ * and 250), each inside a bounded call that fails open: a wallet's gas estimate covers the swap
18
+ * alone, so an estimated transaction succeeds with the correction skipped and nothing recaptured.
19
+ * The swap builders below put this floor on the call whenever `key.hooks` is the current recapture
20
+ * root, or whenever the caller passes `correcting: true`; pass it through to `writeContract` (viem
21
+ * sends `gas` as the limit) instead of estimating.
22
+ */
23
+ export declare const CORRECTION_SWAP_GAS_FLOOR = 1100000n;
24
+ /**
25
+ * True when `hooks` is the current recapture root, which corrects in both swap phases. This is the
26
+ * root's identity, not the pool's: a pool opened on a superseded recapture root keeps the executor
27
+ * frozen into its rules and keeps correcting, and this returns false for it. Read that from the
28
+ * pool itself with `poolCorrects(await readPoolRules(client, poolId))` and pass the answer as
29
+ * `correcting` to the swap builders.
30
+ */
31
+ export declare function isCorrectingRoot(hooks: Address): boolean;
32
+ /** The gas limit to send with a swap on `key`: the floor when the pool corrects (`correcting`,
33
+ * defaulting to `isCorrectingRoot(key.hooks)`), else `undefined` (let the wallet estimate). */
34
+ export declare function swapGasLimit(key: PoolKey, correcting?: boolean): bigint | undefined;
13
35
  /** Router / block `MIN_SQRT_PRICE_LIMIT`: the full-fill limit for a `zeroForOne` swap. */
14
36
  export declare const MIN_SQRT_PRICE_LIMIT = 4295128740n;
15
37
  /** Router / block `MAX_SQRT_PRICE_LIMIT`: the full-fill limit for a `oneForZero` swap. */
@@ -31,10 +53,14 @@ export type RouterCall<F extends "exactInput" | "exactOutput"> = Readonly<{
31
53
  functionName: F;
32
54
  args: ContractFunctionArgs<typeof hookrKernelRouterV3Abi, "payable", F>;
33
55
  value: bigint;
56
+ /** Set to `CORRECTION_SWAP_GAS_FLOOR` when the pool corrects; absent otherwise. */
57
+ gas?: bigint;
34
58
  }>;
35
59
  /**
36
60
  * `exactInput` through the Hookr router. `value` is `amountIn` when `currencyIn` is native ETH,
37
- * else 0 (the router pulls the ERC-20 by `transferFrom`; approve it first).
61
+ * else 0 (the router pulls the ERC-20 by `transferFrom`; approve it first). `gas` carries the
62
+ * correction floor when the pool corrects; send it as the gas limit. `correcting` defaults to
63
+ * `isCorrectingRoot(key.hooks)`; pass `poolCorrects(rules)` for a pool on a superseded root.
38
64
  */
39
65
  export declare function buildExactInputSwapCall(input: {
40
66
  key: PoolKey;
@@ -43,10 +69,12 @@ export declare function buildExactInputSwapCall(input: {
43
69
  amountOutMinimum: bigint;
44
70
  recipient: Address;
45
71
  deadline: bigint;
72
+ correcting?: boolean;
46
73
  }): RouterCall<"exactInput">;
47
74
  /**
48
75
  * `exactOutput` through the Hookr router. `value` is `amountInMaximum` when `currencyIn` is
49
- * native ETH — the router refunds what the swap does not use.
76
+ * native ETH — the router refunds what the swap does not use. `correcting` works as on
77
+ * `buildExactInputSwapCall`.
50
78
  */
51
79
  export declare function buildExactOutputSwapCall(input: {
52
80
  key: PoolKey;
@@ -55,6 +83,7 @@ export declare function buildExactOutputSwapCall(input: {
55
83
  amountInMaximum: bigint;
56
84
  recipient: Address;
57
85
  deadline: bigint;
86
+ correcting?: boolean;
58
87
  }): RouterCall<"exactOutput">;
59
88
  export type Quote = Readonly<{
60
89
  amountIn: bigint;
package/dist/swap.js CHANGED
@@ -1,5 +1,31 @@
1
1
  import { hookrKernelQuoterV1Abi, hookrKernelRouterV3Abi } from "./abi.js";
2
2
  import { ADDRESSES, NATIVE_ADDRESS } from "./chain.js";
3
+ /**
4
+ * The gas limit a swap on a correcting root must carry (the app's `CORRECTION_SWAP_GAS_FLOOR` in
5
+ * `src/lib/gas-headroom.ts`). The recapture root runs its correction in both swap phases, once in
6
+ * `beforeSwap` and again in `afterSwap` (`HookrSwapKernelV5Wth.sol`, `_tryCorrection` at lines 214
7
+ * and 250), each inside a bounded call that fails open: a wallet's gas estimate covers the swap
8
+ * alone, so an estimated transaction succeeds with the correction skipped and nothing recaptured.
9
+ * The swap builders below put this floor on the call whenever `key.hooks` is the current recapture
10
+ * root, or whenever the caller passes `correcting: true`; pass it through to `writeContract` (viem
11
+ * sends `gas` as the limit) instead of estimating.
12
+ */
13
+ export const CORRECTION_SWAP_GAS_FLOOR = 1100000n;
14
+ /**
15
+ * True when `hooks` is the current recapture root, which corrects in both swap phases. This is the
16
+ * root's identity, not the pool's: a pool opened on a superseded recapture root keeps the executor
17
+ * frozen into its rules and keeps correcting, and this returns false for it. Read that from the
18
+ * pool itself with `poolCorrects(await readPoolRules(client, poolId))` and pass the answer as
19
+ * `correcting` to the swap builders.
20
+ */
21
+ export function isCorrectingRoot(hooks) {
22
+ return hooks.toLowerCase() === ADDRESSES.recaptureRoot.toLowerCase();
23
+ }
24
+ /** The gas limit to send with a swap on `key`: the floor when the pool corrects (`correcting`,
25
+ * defaulting to `isCorrectingRoot(key.hooks)`), else `undefined` (let the wallet estimate). */
26
+ export function swapGasLimit(key, correcting) {
27
+ return (correcting ?? isCorrectingRoot(key.hooks)) ? CORRECTION_SWAP_GAS_FLOOR : undefined;
28
+ }
3
29
  /** Router / block `MIN_SQRT_PRICE_LIMIT`: the full-fill limit for a `zeroForOne` swap. */
4
30
  export const MIN_SQRT_PRICE_LIMIT = 4295128740n;
5
31
  /** Router / block `MAX_SQRT_PRICE_LIMIT`: the full-fill limit for a `oneForZero` swap. */
@@ -26,11 +52,14 @@ export function swapDirection(key, currencyIn) {
26
52
  }
27
53
  /**
28
54
  * `exactInput` through the Hookr router. `value` is `amountIn` when `currencyIn` is native ETH,
29
- * else 0 (the router pulls the ERC-20 by `transferFrom`; approve it first).
55
+ * else 0 (the router pulls the ERC-20 by `transferFrom`; approve it first). `gas` carries the
56
+ * correction floor when the pool corrects; send it as the gas limit. `correcting` defaults to
57
+ * `isCorrectingRoot(key.hooks)`; pass `poolCorrects(rules)` for a pool on a superseded root.
30
58
  */
31
59
  export function buildExactInputSwapCall(input) {
32
60
  const zeroForOne = swapDirection(input.key, input.currencyIn);
33
61
  const native = input.currencyIn.toLowerCase() === NATIVE_ADDRESS.toLowerCase();
62
+ const gas = swapGasLimit(input.key, input.correcting);
34
63
  return Object.freeze({
35
64
  address: ADDRESSES.router,
36
65
  abi: hookrKernelRouterV3Abi,
@@ -48,15 +77,18 @@ export function buildExactInputSwapCall(input) {
48
77
  EMPTY_MODULE_DATA,
49
78
  ],
50
79
  value: native ? input.amountIn : 0n,
80
+ ...(gas === undefined ? {} : { gas }),
51
81
  });
52
82
  }
53
83
  /**
54
84
  * `exactOutput` through the Hookr router. `value` is `amountInMaximum` when `currencyIn` is
55
- * native ETH — the router refunds what the swap does not use.
85
+ * native ETH — the router refunds what the swap does not use. `correcting` works as on
86
+ * `buildExactInputSwapCall`.
56
87
  */
57
88
  export function buildExactOutputSwapCall(input) {
58
89
  const zeroForOne = swapDirection(input.key, input.currencyIn);
59
90
  const native = input.currencyIn.toLowerCase() === NATIVE_ADDRESS.toLowerCase();
91
+ const gas = swapGasLimit(input.key, input.correcting);
60
92
  return Object.freeze({
61
93
  address: ADDRESSES.router,
62
94
  abi: hookrKernelRouterV3Abi,
@@ -74,6 +106,7 @@ export function buildExactOutputSwapCall(input) {
74
106
  EMPTY_MODULE_DATA,
75
107
  ],
76
108
  value: native ? input.amountInMaximum : 0n,
109
+ ...(gas === undefined ? {} : { gas }),
77
110
  });
78
111
  }
79
112
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hookr-sdk",
3
- "version": "0.1.0",
4
- "description": "Build a market on Hookr's live root hook, quote and swap through its router, read markets, and claim payouts.",
3
+ "version": "0.2.1",
4
+ "description": "Build a market on Hookr's live root hooks, quote and swap through its router, read markets, and claim payouts.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {