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 +91 -8
- package/dist/chain.d.ts +48 -6
- package/dist/chain.js +44 -6
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/market.d.ts +33 -3
- package/dist/market.js +37 -16
- package/dist/reads.d.ts +49 -4
- package/dist/reads.js +91 -12
- package/dist/swap.d.ts +31 -2
- package/dist/swap.js +35 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# hookr-sdk
|
|
2
2
|
|
|
3
|
-
Build a market on Hookr's live root
|
|
4
|
-
through Hookr's router, read markets, and claim payouts
|
|
5
|
-
|
|
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,
|
|
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
|
|
89
|
+
const client = createPublicClient({ transport });
|
|
49
90
|
const market = await readMarket(client, poolId);
|
|
50
|
-
const key = poolKeyFor({
|
|
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
|
|
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
6
|
-
* `ADDRESSES.root`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
6
|
-
* `ADDRESSES.root`
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
4
|
-
"description": "Build a market on Hookr's live root
|
|
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": {
|