hookr-sdk 1.0.0 → 1.1.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/CHANGELOG.md +40 -4
- package/MIGRATING.md +6 -6
- package/README.md +87 -65
- package/dist/index.d.ts +38 -2
- package/dist/index.js +36 -2
- package/dist/releases.d.ts +56 -9
- package/dist/releases.js +107 -10
- package/dist/wallet-cap.d.ts +159 -0
- package/dist/wallet-cap.js +234 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
README only, no code changes. 1.1.0's README said the SDK's builders always open a pool with the pot off. `defaultRulesLaunch` does, and `checkRulesConfig` refuses a pot, but `buildLaunch` encodes the Rules config you pass it, pot included.
|
|
6
|
+
|
|
7
|
+
## 1.1.0
|
|
8
|
+
|
|
9
|
+
The Rules modules `HookrRoot` admits beside the release's own, and Wallet Cap, which runs on one of them.
|
|
10
|
+
|
|
11
|
+
### What is new
|
|
12
|
+
|
|
13
|
+
- `HOOKR_1.rulesModules`: `standard`, the release's HookrRules; `whole-pot`, where King of the Pool's winner takes the whole pot (`0x10f684361b35C8411a2c07F68A6fB1F4B7e555F7`); `wallet-cap`, HookrRulesWalletCap (`0xb67b73E28C522a3AAC438bE970bf94976Deee52a`); and `round-trip`, HookrRulesRoundTrip (`0x3d7e31746127FD9a14d3C7fC12E73611D2e8cb34`), which keeps a record of who traded in each block for Buy/Sell Block's round-trip check. These are the four Rules the registry admits on `HookrRoot`. Each comes with the HookrRecapture its constructor created and both runtime code hashes. `rulesModule(release, name)` names one and `otherRulesModules(release)` lists those beside the release's Rules. `releaseManifest` pins each as one more `rules` pin, so the builders that name a Rules take it, and `readReleaseManifest` reads their code and refuses any other.
|
|
14
|
+
- Wallet Cap: while a launch's guard lasts, no wallet buys more than the pool's cap of the token. `walletCapAmount` turns a share of the supply into the cap. `checkWalletCapRulesConfig` and `walletCapPoolTerms` check a pool on the Wallet Cap Rules and give its caps, gas and rules data with the cap. `checkWalletCapLaunch` holds a launch's pools to one cap and to arb recapture on every pool or none. `readWalletCap` reads a pool's launch, guard end and cap, and a wallet's count and room, through the Rules' `extsload` (`walletCapPoolSlot`, `walletCapBoughtSlot`, `walletCapLaunch`, `RULES_SLOT`). `walletCapRefusal` and `walletCapRevert` find `GuardWalletLimit` inside the root's, the PoolManager's and the quoter's wrappers. `WALLET_CAP_ABI` is `extsload` and `GuardWalletLimit`, what the Wallet Cap Rules add to `rulesAbi`.
|
|
15
|
+
- `readParentBlockNumber`: the block number the contracts read as `block.number`, Ethereum's, from the latest block's `l1BlockNumber`. Anti-Snipe's launch guard counts these blocks.
|
|
16
|
+
- `kingOfThePoolPrizeBound`: `kingOfTheHillPrizeBound` under King of the Pool's name.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- The README's launch example counted the launch guard from `getBlockNumber()`, the chain's own block number, so the guard the defaults turn on ended far past the 100,000 parent blocks the Rules accept, and the launch would revert. It reads `readParentBlockNumber` now.
|
|
21
|
+
- The README's claim example read the release's HookrRules, so it showed nothing owed on a pool of another Rules module. It reads the pool's own Rules, `config.rules`, now.
|
|
22
|
+
- The README, MIGRATING.md and `laneGasFloor`'s comment said a fixed gas limit of 7.5 million is enough on a pool with arb recapture on. A buy on a live lane pool estimates at about 7.5 million, so they say at least 8.8 million now, the estimate plus a sixth.
|
|
23
|
+
|
|
24
|
+
### Deprecated
|
|
25
|
+
|
|
26
|
+
- `buildExcludeQuoteClass`, `buildStopOpenQuotes`, `encodeQuoteStateCall`, `decodeQuoteState`, `encodeCuratedQuoteCall` and `decodeCuratedQuote`, and the `QuoteState` type: they build, encode or decode calls to functions HookrRegistry does not have, so they never worked on chain.
|
|
27
|
+
- `registryHasQuoteAdmission` and `registryHasOperation`: they only check an ABI offline, and return false for the deployed registry's.
|
|
28
|
+
- `QUOTE_ADMISSION_ABI` and `QUOTE_ADMISSION_OPERATIONS`: they describe calls HookrRegistry does not have.
|
|
29
|
+
- `kingOfTheHillPrizeBound`: use `kingOfThePoolPrizeBound`, the same function.
|
|
30
|
+
|
|
31
|
+
The quote-admission exports still resolve, and go in 2.0. `quoteAdmission` is not deprecated: its `admitted` is the registry's `isQuote`. Its `curated` is always null.
|
|
32
|
+
|
|
33
|
+
### Also changed
|
|
34
|
+
|
|
35
|
+
- `releaseManifest(HOOKR_1)` pins three more contracts than 1.0.0 did, all `rules`, and `readReleaseManifest(client, HOOKR_1)` makes three more `getCode` reads. `HookrRelease` has a new field, `rulesModules`.
|
|
36
|
+
- `CANARY_HOOKR_1_A` is marked internal. It is not for users.
|
|
37
|
+
- `src/core` is unchanged, the release SDK of `412960b37` byte for byte, and the package exports all of it. The core's `checkRulesConfig` and `rulesPoolTerms` still refuse a nonzero `potMinBuyQuote`, as every Rules but Wallet Cap's does.
|
|
38
|
+
|
|
3
39
|
## 1.0.0
|
|
4
40
|
|
|
5
|
-
|
|
41
|
+
The package now builds on `hookr-1`, Hookr's production contracts on Robinhood Chain: one root, `HookrRoot`, whose pools each freeze their own Hook Blocks and, when they want it, arb recapture.
|
|
6
42
|
|
|
7
43
|
### What is new
|
|
8
44
|
|
|
@@ -21,17 +57,17 @@ Hookr 1. The package now builds on the `hookr-1` release on Robinhood Chain: one
|
|
|
21
57
|
|
|
22
58
|
Of the 0.2.x exports only these carry over: `CHAIN_ID`, `RPC_URL`, `EXPLORER_URL`, `explorerAddressUrl`, `explorerTxUrl`, the `PoolKey` type, `DYNAMIC_FEE`, `MIN_SQRT_PRICE_LIMIT`, `MAX_SQRT_PRICE_LIMIT`, `DEFAULT_PROTOCOL_SHARE_BPS` (2,000), `MAX_PROTOCOL_SHARE_BPS` (5,000) and `MAX_GUARD_BLOCKS` (100,000). `EXPLORER_URL` is now RobinScan, `https://robin.etherscan.io`, where every contract's source is verified, so `explorerAddressUrl` and `explorerTxUrl` open RobinScan. `RootStatus` keeps its name with a new shape: the registry's `rootStatus(root)`, `{ advisory, verified }`. The builders return `{ chainId, to, data, value }` for `sendTransaction`, not `writeContract` arguments, and take the release manifest. Node 18.16 or newer. [MIGRATING.md](https://unpkg.com/hookr-sdk@1/MIGRATING.md) maps the exports in common use. Every other 0.2.x export is removed.
|
|
23
59
|
|
|
24
|
-
0.2.x keeps working for pools on
|
|
60
|
+
0.2.x keeps working for pools on `HookrModularHookV6` and `HookrModularHookV6WthV5`.
|
|
25
61
|
|
|
26
62
|
## 0.2.1 (2026-09-22)
|
|
27
63
|
|
|
28
|
-
- The swap builders take `correcting` (default `isCorrectingRoot(key.hooks)`, the
|
|
64
|
+
- The swap builders take `correcting` (default `isCorrectingRoot(key.hooks)`, the recapture root), and `poolCorrects(rules)` reads from a pool's frozen rules whether it names an arb recapture executor, so a pool on a retired recapture root can carry the gas floor too.
|
|
29
65
|
|
|
30
66
|
A later 0.2.x change, `listMarkets` reading in windows of at most 5,000 blocks, was never published.
|
|
31
67
|
|
|
32
68
|
## 0.2.0 (2026-09-21)
|
|
33
69
|
|
|
34
|
-
- Names both live roots (`ADDRESSES.recaptureRoot`, `RECAPTURE_KERNEL_ID`, `ROOTS`). The market builders take `root` and freeze the recapture
|
|
70
|
+
- Names both live roots (`ADDRESSES.recaptureRoot`, `RECAPTURE_KERNEL_ID`, `ROOTS`). The market builders take `root` and freeze the arb recapture fields (`RECAPTURE_CORRECTION`). Swap calls carry `gas: CORRECTION_SWAP_GAS_FLOOR` on a correcting root. `listMarkets` scans in windows. `listRoots` reads every registered root from the registry.
|
|
35
71
|
|
|
36
72
|
## 0.1.0 (2026-09-10)
|
|
37
73
|
|
package/MIGRATING.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Moving from 0.2.x to 1.0
|
|
2
2
|
|
|
3
|
-
1.
|
|
3
|
+
1.x builds on `hookr-1`, Hookr's production contracts. 0.2.x builds on `HookrModularHookV6` and `HookrModularHookV6WthV5`. Pools of both sets live on Robinhood Chain, and each package only talks to its own.
|
|
4
4
|
|
|
5
|
-
## Keep 0.2.x for the
|
|
5
|
+
## Keep 0.2.x for pools on the V6 roots
|
|
6
6
|
|
|
7
7
|
Pools on `HookrModularHookV6` (`0xb3cA29cF721380CEe8b8e4755F3865Ebc68Fe8cC`) and `HookrModularHookV6WthV5` (`0xb914f955294799de4b891bd2EA8AF628Fa1c68CC`) keep trading. 0.2.x stays on npm for them:
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ An app that serves both sets of pools can install both under two names:
|
|
|
16
16
|
npm install hookr-sdk hookr-sdk-v0@npm:hookr-sdk@0.2
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Pick by the pool's `PoolKey.hooks`: `HOOKR_1.contracts.HookrRoot` is 1.
|
|
19
|
+
Pick by the pool's `PoolKey.hooks`: `HOOKR_1.contracts.HookrRoot` is 1.x's, the two roots above are 0.2.x's.
|
|
20
20
|
|
|
21
21
|
## What changes
|
|
22
22
|
|
|
@@ -70,7 +70,7 @@ await walletClient.sendTransaction({ to: call.to, data: call.data, value: call.v
|
|
|
70
70
|
| `readMarket`, `readPoolRules` | `poolConfig(id)` on `HookrRoot` (`rootAbi`), `config(id)` on the pool's HookrRules (`rulesAbi`), `laneOf(id)` (`laneRootAbi`) |
|
|
71
71
|
| `listMarkets`, `listRoots`, `MARKET_CREATED_TOPIC`, `MARKET_SCAN_FROM_BLOCK`, `MARKET_SCAN_CHUNK_BLOCKS`, `MARKET_SCAN_CONCURRENCY` | none: read `PoolOpened` (`rootAbi`) and `MemberLaunched` (`launcherAbi`) with your own log reader |
|
|
72
72
|
| `MAX_LOG_BLOCK_RANGE`, `logBlockRange`, `logBlockRanges`, `capLogSpan` | none: ask for at most 5,000 blocks a request |
|
|
73
|
-
| `claimableCall`, `claimCall`, `claimToCall` | `claimable`, `claim` and `claimTo`
|
|
73
|
+
| `claimableCall`, `claimCall`, `claimToCall` | `claimable`, `claim` and `claimTo` (`rulesAbi`) on the pool's own Rules, `poolConfig(id).rules` |
|
|
74
74
|
| `ReadCall`, `WriteCall`, `RouterCall`, `CoordinatorCall` | `Call`: `{ chainId, to, data, value }` |
|
|
75
75
|
| `builderLink`, `launchLink`, `encodeStack`, `decodeStack`, `decodeStackBaseFeePips`, `DEFAULT_ORIGIN`, `DEFAULT_BASE_FEE_PIPS` | none |
|
|
76
76
|
| `hookrKernelRouterV3Abi`, `hookrKernelQuoterV1Abi`, `hookrMarketCoordinatorV5Abi`, `hookrNativeMechanicsBlockV2Abi`, `hookrStackRegistryV2Abi` | `routerAbi`, `quoterAbi`, `launcherAbi`, `rulesAbi`, `registryAbi`, and the full ABIs in `ARTIFACT_ABIS` |
|
|
@@ -80,9 +80,9 @@ await walletClient.sendTransaction({ to: call.to, data: call.data, value: call.v
|
|
|
80
80
|
|
|
81
81
|
## Fees and arb recapture
|
|
82
82
|
|
|
83
|
-
On the
|
|
83
|
+
On the 0.2.x recapture roots Hookr and What The Hook each took 10% of an arb recapture's profit. On `HookrRoot` each takes 25%: Hookr's `RECAPTURE_PROTOCOL_BPS` is 2,500 and What The Hook keeps at most `MAX_LANE_PARTNER_BPS`, 2,500. The rest is the pool's, as it froze it at launch.
|
|
84
84
|
|
|
85
|
-
A swap on a
|
|
85
|
+
A swap on a `HookrRoot` pool with arb recapture on carries the lane's gas floor, 6,802,619 at the lane's 3,000,000 gas cap, where 0.2.x put 1,100,000 on a correcting root. The swap's own gas comes on top, so estimate and add a sixth, or use a fixed limit of at least 8.8 million. See [Gas](https://www.npmjs.com/package/hookr-sdk#gas) in the README.
|
|
86
86
|
|
|
87
87
|
## Requirements
|
|
88
88
|
|
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# hookr-sdk
|
|
2
2
|
|
|
3
|
-
Build on Hookr
|
|
3
|
+
Build on Hookr, the Uniswap v4 hook on Robinhood Chain: launch tokens with Hook Blocks, open pools, quote and swap.
|
|
4
4
|
|
|
5
|
-
The package carries
|
|
5
|
+
The package carries Hookr's contracts, release id `hookr-1`: their addresses, deployment and ABIs, the 22-quote catalog, the bounds of every setting of the core Hook Blocks and advisories, and the builders hookr.fun itself uses. It holds no wallet and signs nothing. Each builder returns `{ chainId, to, data, value }` for your wallet to send.
|
|
6
6
|
|
|
7
7
|
- [Install](#install)
|
|
8
8
|
- [Quick start](#quick-start)
|
|
9
9
|
- [Addresses](#addresses)
|
|
10
|
+
- [Wallet Cap](#wallet-cap)
|
|
10
11
|
- [Arb recapture](#arb-recapture)
|
|
11
12
|
- [Gas](#gas)
|
|
12
13
|
- [Quotes](#quotes)
|
|
@@ -44,7 +45,7 @@ const manifest = await readReleaseManifest(client, HOOKR_1);
|
|
|
44
45
|
|
|
45
46
|
### Read a pool
|
|
46
47
|
|
|
47
|
-
A Hookr
|
|
48
|
+
A pool on Hookr's shared hook is a Uniswap v4 pool whose `PoolKey.hooks` is `HookrRoot`. A hook you deploy through the owned-root factory is an unmodified copy of it at an address of its own, and its pools name that address instead. Either way the pool's fee is `DYNAMIC_FEE`: the hook sets the LP fee on every swap.
|
|
48
49
|
|
|
49
50
|
```ts
|
|
50
51
|
import { DYNAMIC_FEE, HOOKR_1, ZERO, laneRootAbi, poolId, rootAbi, rulesAbi, type PoolKey } from "hookr-sdk";
|
|
@@ -68,7 +69,7 @@ const [executor, codeHash, partnerBps, family, laneOn, gasCap, gasFloor] = await
|
|
|
68
69
|
});
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
`config` is the pool's `PoolConfig`: its quote, base LP fee and caps, and the HookrRules contract that holds its Hook Blocks. `blocks` is those Hook Blocks as frozen at launch: Anti-Snipe, the dynamic fee, Auto Burn, LP Rewards, the royalty and the protocol share. `laneOn` says whether arb recapture runs on the pool, and `gasFloor` is the least gas a swap must carry while it does
|
|
72
|
+
`config` is the pool's `PoolConfig`: its quote, base LP fee and caps, and the HookrRules contract that holds its Hook Blocks, one of the release's [Rules modules](#rules-modules). `blocks` is those Hook Blocks as frozen at launch: Anti-Snipe, the dynamic fee, Auto Burn, LP Rewards, the royalty and the protocol share. `laneOn` says whether arb recapture runs on the pool, and `gasFloor` is the least gas a swap must carry while it does ([Gas](#gas)). On the Wallet Cap Rules, `blocks.potMinBuyQuote` is the pool's [Wallet Cap](#wallet-cap).
|
|
72
73
|
|
|
73
74
|
### Quote
|
|
74
75
|
|
|
@@ -104,7 +105,6 @@ import { buildSwap } from "hookr-sdk";
|
|
|
104
105
|
const minOut = (receipt.outputAmount * 995n) / 1000n; // 0.5% slippage
|
|
105
106
|
const swapCall = buildSwap(manifest, { ...swap, amountBound: minOut });
|
|
106
107
|
|
|
107
|
-
// With arb recapture on, the estimate includes the lane's gas floor, so it runs above 7.4 million.
|
|
108
108
|
const estimate = await client.estimateGas({ account, to: swapCall.to, data: swapCall.data, value: swapCall.value });
|
|
109
109
|
const hash = await wallet.sendTransaction({ account, to: swapCall.to, data: swapCall.data, value: swapCall.value, gas: estimate + estimate / 6n });
|
|
110
110
|
```
|
|
@@ -115,6 +115,8 @@ When the estimate fails, the swap would revert as built. Check the quote, the al
|
|
|
115
115
|
|
|
116
116
|
One launch creates the token and opens its pools, each with its own Hook Blocks, frozen when the pool opens. `defaultRulesLaunch` sets every block to its default. Change what you want, and `rulesPoolTerms` checks every value against the contracts' bounds and returns the pool's caps and gas.
|
|
117
117
|
|
|
118
|
+
Anti-Snipe's launch guard, on by default, counts the blocks the contracts read as `block.number`: Ethereum's, which each Robinhood Chain block carries as `l1BlockNumber`. `readParentBlockNumber` reads it. `getBlockNumber()` is the chain's own, far larger number, and a guard counted from it is one the Rules refuse.
|
|
119
|
+
|
|
118
120
|
```ts
|
|
119
121
|
import { zeroHash } from "viem";
|
|
120
122
|
import {
|
|
@@ -125,13 +127,14 @@ import {
|
|
|
125
127
|
buildLaunch,
|
|
126
128
|
defaultRulesLaunch,
|
|
127
129
|
launchFeeAbi,
|
|
130
|
+
readParentBlockNumber,
|
|
128
131
|
rulesAbi,
|
|
129
132
|
rulesPoolTerms,
|
|
130
133
|
type Admission,
|
|
131
134
|
} from "hookr-sdk";
|
|
132
135
|
|
|
133
136
|
const { HookrRegistry, HookrRoot, HookrRules, HookrLauncher } = HOOKR_1.contracts;
|
|
134
|
-
const blockNumber = await client
|
|
137
|
+
const blockNumber = await readParentBlockNumber(client);
|
|
135
138
|
const [open, admission, minProtocolShareBps, [launchFee]] = await Promise.all([
|
|
136
139
|
client.readContract({ address: HookrRegistry, abi: ARTIFACT_ABIS.HookrRegistry, functionName: "rootOpenFor", args: [HookrRoot, account] }),
|
|
137
140
|
client.readContract({ address: HookrRegistry, abi: ARTIFACT_ABIS.HookrRegistry, functionName: "admission", args: [HookrRoot, HookrRules] }),
|
|
@@ -186,7 +189,7 @@ await wallet.sendTransaction({ account, to: launchCall.to, data: launchCall.data
|
|
|
186
189
|
|
|
187
190
|
One launch opens one to eight pools of the same token, each on its own quote: pass more members. `buildLaunchAdvised` binds an advisory per pool, and `buildLaunchFamily` adds a dev buy, supply weights and an opening price check. `HookrLauncher.predictToken(creator, token)` gives the token's address before you send.
|
|
188
191
|
|
|
189
|
-
A member with arb recapture on takes the release's default `RecaptureConfig` in place of `RECAPTURE_OFF
|
|
192
|
+
A member with arb recapture on takes the release's default `RecaptureConfig` in place of `RECAPTURE_OFF`. Read it with `recaptureAbi` at the HookrRules address: `recaptureDefaults()` belongs to HookrRecapture, which HookrRules reaches for every call it does not handle itself.
|
|
190
193
|
|
|
191
194
|
```ts
|
|
192
195
|
import { recaptureAbi } from "hookr-sdk";
|
|
@@ -215,21 +218,21 @@ const openCall = buildLaunch(manifest, existing, HookrRoot, [member], seed.amoun
|
|
|
215
218
|
|
|
216
219
|
### Claim
|
|
217
220
|
|
|
218
|
-
Royalties, integrator fees and a trader's arb recapture rebate are held in `
|
|
221
|
+
Royalties, integrator fees and a trader's arb recapture rebate are held per currency in each pool's own Rules, the `config.rules` its `poolConfig` names (see [Read a pool](#read-a-pool)), until claimed.
|
|
219
222
|
|
|
220
223
|
```ts
|
|
221
|
-
import {
|
|
224
|
+
import { rulesAbi } from "hookr-sdk";
|
|
222
225
|
|
|
223
|
-
const owed = await client.readContract({ address:
|
|
226
|
+
const owed = await client.readContract({ address: config.rules, abi: rulesAbi, functionName: "claimable", args: [currency, account] });
|
|
224
227
|
if (owed > 0n)
|
|
225
|
-
await wallet.writeContract({ account, address:
|
|
228
|
+
await wallet.writeContract({ account, address: config.rules, abi: rulesAbi, functionName: "claim", args: [currency] });
|
|
226
229
|
```
|
|
227
230
|
|
|
228
231
|
`claimTo` sends the claim to another address. `claimAsClaims` takes it as Uniswap v4 ERC-6909 claims.
|
|
229
232
|
|
|
230
233
|
## Addresses
|
|
231
234
|
|
|
232
|
-
Robinhood Chain, chain id 4663.
|
|
235
|
+
Robinhood Chain, chain id 4663. `HOOKR_1.contracts` holds every address. `HOOKR_1.deployment` holds the deployment: the block the manifest starts from and its hash, each contract's runtime code hash, and the hash of the release's source verification on Blockscout. Each address below opens on RobinScan, where its source is verified.
|
|
233
236
|
|
|
234
237
|
| Contract | `hookr-1` | What it does |
|
|
235
238
|
| --- | --- | --- |
|
|
@@ -243,39 +246,57 @@ Robinhood Chain, chain id 4663. Every address is fixed by CREATE3 from the relea
|
|
|
243
246
|
| HookrForwarder | [`0xe7863CC0E2Fb783b44c279aB159bcD812c2ceA1c`](https://robin.etherscan.io/address/0xe7863CC0E2Fb783b44c279aB159bcD812c2ceA1c) | Runs a signed swap for a relayer |
|
|
244
247
|
| PoolManager | [`0x8366a39CC670B4001A1121B8F6A443A643e40951`](https://robin.etherscan.io/address/0x8366a39CC670B4001A1121B8F6A443A643e40951) | Uniswap v4's PoolManager, `POOL_MANAGER` |
|
|
245
248
|
|
|
246
|
-
|
|
249
|
+
Every other contract is in `HOOKR_1.contracts`.
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
251
|
+
### Rules modules
|
|
252
|
+
|
|
253
|
+
A pool binds one HookrRules, its `PoolConfig.rules`, and keeps it. Beside the release's own, the registry admits three more on `HookrRoot` on the same terms, each with the HookrRecapture its constructor created. `HOOKR_1.rulesModules` holds all four and `rulesModule(HOOKR_1, name)` names one. The manifest pins each by its runtime code hash, so `readReleaseManifest` refuses one that runs other code.
|
|
254
|
+
|
|
255
|
+
| Name | Rules | HookrRecapture | What a pool on them runs |
|
|
256
|
+
| --- | --- | --- | --- |
|
|
257
|
+
| `standard` | [`0x4a71c270b1520F94064268102AebD744b0DE7942`](https://robin.etherscan.io/address/0x4a71c270b1520F94064268102AebD744b0DE7942) | [`0xF83569E564d70daebc35950f66a73f2e2B915583`](https://robin.etherscan.io/address/0xF83569E564d70daebc35950f66a73f2e2B915583) | The release's HookrRules |
|
|
258
|
+
| `whole-pot` | [`0x10f684361b35C8411a2c07F68A6fB1F4B7e555F7`](https://robin.etherscan.io/address/0x10f684361b35C8411a2c07F68A6fB1F4B7e555F7) | [`0x60Efc4a41498e9433be2155b6cc1677AEC31D649`](https://robin.etherscan.io/address/0x60Efc4a41498e9433be2155b6cc1677AEC31D649) | The same Rules, where King of the Pool's winner takes the whole pot |
|
|
259
|
+
| `wallet-cap` | [`0xb67b73E28C522a3AAC438bE970bf94976Deee52a`](https://robin.etherscan.io/address/0xb67b73E28C522a3AAC438bE970bf94976Deee52a) | [`0xbbC43DC2f01F5E7882caCd78EF094A44eF0E4d23`](https://robin.etherscan.io/address/0xbbC43DC2f01F5E7882caCd78EF094A44eF0E4d23) | The whole-pot Rules plus [Wallet Cap](#wallet-cap) |
|
|
260
|
+
| `round-trip` | [`0x3d7e31746127FD9a14d3C7fC12E73611D2e8cb34`](https://robin.etherscan.io/address/0x3d7e31746127FD9a14d3C7fC12E73611D2e8cb34) | [`0xddFF6326E5CB9AA98b88e9064d9A9570f777f8A3`](https://robin.etherscan.io/address/0xddFF6326E5CB9AA98b88e9064d9A9570f777f8A3) | Rules that keep a record of who traded in each block, for Buy/Sell Block's round-trip check |
|
|
261
|
+
|
|
262
|
+
## Wallet Cap
|
|
263
|
+
|
|
264
|
+
While a launch's guard lasts, no wallet can buy more than the pool's cap of the token, its `rules.potMinBuyQuote`. A pool takes it at launch by binding the `wallet-cap` Rules.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { checkWalletCapLaunch, readWalletCap, rulesModule, walletCapAmount, walletCapPoolTerms, walletCapRevert } from "hookr-sdk";
|
|
268
|
+
|
|
269
|
+
const walletCap = rulesModule(HOOKR_1, "wallet-cap");
|
|
270
|
+
const capAdmission = await client.readContract({ address: HookrRegistry, abi: ARTIFACT_ABIS.HookrRegistry, functionName: "admission", args: [HookrRoot, walletCap.rules] });
|
|
271
|
+
|
|
272
|
+
// The defaults keep the launch guard on: 50 parent blocks, about 10 minutes. Until it ends, no wallet buys more than 2% of the supply.
|
|
273
|
+
const capped = defaultRulesLaunch(ctx);
|
|
274
|
+
capped.rules.potMinBuyQuote = walletCapAmount(token.supply, 200);
|
|
275
|
+
const cappedTerms = walletCapPoolTerms(capped, ctx, capAdmission as Admission);
|
|
276
|
+
const cappedMember = {
|
|
277
|
+
...member,
|
|
278
|
+
config: { ...member.config, rules: walletCap.rules, caps: cappedTerms.caps, rulesGasLimit: cappedTerms.rulesGasLimit },
|
|
279
|
+
rules: capped.rules,
|
|
280
|
+
};
|
|
281
|
+
checkWalletCapLaunch([cappedMember]);
|
|
282
|
+
|
|
283
|
+
// A pool binds only while its guard has yet to end, so the launch must land well inside it.
|
|
284
|
+
const cappedDeadline = BigInt(Math.floor(Date.now() / 1000) + 300);
|
|
285
|
+
const cappedCall = buildLaunch(manifest, token, HookrRoot, [cappedMember], seed.amount0Max, cappedDeadline, launchFee);
|
|
286
|
+
|
|
287
|
+
// Once it trades: what a wallet may still buy, until the guard ends at a parent block (readParentBlockNumber).
|
|
288
|
+
const cap = await readWalletCap(client, { poolId: id, wallet: account, subject: tokenAddress });
|
|
289
|
+
if (cap) console.log(`${cap.room} of ${cap.cap} left until parent block ${cap.guardEndBlock}`);
|
|
290
|
+
|
|
291
|
+
// A buy past the cap fails its quote and its gas estimate with GuardWalletLimit, however the contracts wrapped it.
|
|
292
|
+
try {
|
|
293
|
+
await client.estimateGas({ account, to: swapCall.to, data: swapCall.data, value: swapCall.value });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.log(walletCapRevert(error) ?? error);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`readWalletCap` reads the pool through the Rules' `extsload`, one storage word at a time, and gives null for a pool on any other Rules. `walletCapPoolSlot` and `walletCapBoughtSlot` give the slots for reads of your own, and `WALLET_CAP_ABI` is what the Wallet Cap Rules add to `rulesAbi`.
|
|
279
300
|
|
|
280
301
|
## Arb recapture
|
|
281
302
|
|
|
@@ -285,15 +306,17 @@ A pool with arb recapture on lets What The Hook's executor close the price gap b
|
|
|
285
306
|
| --- | --- |
|
|
286
307
|
| Hookr | 25% (`RECAPTURE_PROTOCOL_BPS`, 2,500) |
|
|
287
308
|
| What The Hook | up to 25% (`MAX_LANE_PARTNER_BPS`, 2,500), what its executor keeps |
|
|
288
|
-
| The pool | the rest: the trader's rebate (0 to 50% of it, 25% by default), the King of the Pool pot (
|
|
309
|
+
| The pool | the rest: the trader's rebate (0 to 50% of it, 25% by default), the King of the Pool pot (off by default; up to 50% of it on the standard and round-trip Rules, up to all of it on the whole-pot and Wallet Cap Rules) and the LPs |
|
|
310
|
+
|
|
311
|
+
The pool's split is frozen at launch. `recaptureSplit` and `recaptureProfit` work it out to the wei; `recaptureSplit` takes a pot of up to 50%. `defaultRulesLaunch` leaves the pot off and `checkRulesConfig` refuses one, but `buildLaunch` encodes the Rules config you pass it, pot included. The LPs' part is released over `releaseBlocks` L2 blocks: 1,200 to 864,000, 36,000 by default. The release counts at most 10 of them a second, so 36,000 blocks take at least an hour.
|
|
289
312
|
|
|
290
|
-
The
|
|
313
|
+
The lane's switch is not frozen: the registry's owner, or its guardian once one is appointed, can switch the lane off, or lower its gas cap, for every pool that froze it, at once, and the swaps then skip or shrink arb recapture. Switching it back on, or raising the cap, waits in the registry's 30-minute timelock. The switch touches nothing else of the pool.
|
|
291
314
|
|
|
292
315
|
## Gas
|
|
293
316
|
|
|
294
|
-
A swap on a pool with arb recapture on must carry the lane's gas floor, or it reverts `InsufficientLaneGas`. The floor is `2 × gasCap + gasCap / 63 + 755,000`: `laneGasFloor(gasCap)` here, `gasFloor` from `laneOf(id)` on chain. At the lane's 3,000,000 gas cap it is 6,802,619. The swap's own gas comes on top:
|
|
317
|
+
A swap on a pool with arb recapture on must carry the lane's gas floor, or it reverts `InsufficientLaneGas`. The floor is `2 × gasCap + gasCap / 63 + 755,000`: `laneGasFloor(gasCap)` here, `gasFloor` from `laneOf(id)` on chain. At the lane's 3,000,000 gas cap it is 6,802,619. The swap's own gas comes on top: a buy through `HookrRouter` on a live lane pool estimates at about 7.5 million.
|
|
295
318
|
|
|
296
|
-
Estimate and add a sixth, as the [Swap](#swap) example does. The estimate already includes the floor, which is why it is high. If your wallet sets a fixed limit, use at least
|
|
319
|
+
Estimate and add a sixth, as the [Swap](#swap) example does. The estimate already includes the floor, which is why it is high. If your wallet sets a fixed limit, use at least 8.8 million on such a pool. A wallet pays for the gas used, not the limit. A pool without arb recapture has no floor.
|
|
297
320
|
|
|
298
321
|
## Quotes
|
|
299
322
|
|
|
@@ -301,24 +324,23 @@ A pool may quote in native ETH, any of the 22 tokens on the genesis catalog, any
|
|
|
301
324
|
|
|
302
325
|
## What is in the package
|
|
303
326
|
|
|
304
|
-
|
|
|
327
|
+
| Source file | What it does |
|
|
305
328
|
| --- | --- |
|
|
306
|
-
| `releases` | `HOOKR_1
|
|
307
|
-
| `wallet` |
|
|
308
|
-
| `
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
311
|
-
| `
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
`releaseManifest` and `readReleaseManifest` build the manifest the builders read. It is not a Hookr service's manifest: give `HookrClient` the release id and manifest hash the service publishes.
|
|
329
|
+
| `releases.ts` | `HOOKR_1`: addresses, deployment, the Rules modules, the manifest, `ROBINHOOD_CHAIN`, explorer links, the lane gas floor and the parent block the launch guard counts |
|
|
330
|
+
| `wallet-cap.ts` | Wallet Cap: the cap from a share of supply, a pool's terms and a launch's checks, the reads through `extsload`, and its refusal in plain words |
|
|
331
|
+
| `wallet.ts` | The launcher, router and quoter calls, each checked against its bounds |
|
|
332
|
+
| `launch.ts` | Multi-pool launch, the dev buy and exactly what the launcher pulls |
|
|
333
|
+
| `params.ts` | Every setting of the core Hook Blocks and advisories, with its range and default, and the checks that refuse a value outside them |
|
|
334
|
+
| `rules.ts` | Encodes and checks a pool's Hook Blocks, and prices the Snipe tax, the dynamic fee and the arb recapture split |
|
|
335
|
+
| `quotes.ts` | The quote catalog, quote classes and amounts in each quote's own decimals |
|
|
336
|
+
| `registry.ts` | Typed registry operations and the admission checks |
|
|
337
|
+
| `advisories.ts` | The managed fee and market-hours advisory configs |
|
|
338
|
+
| `abi.ts` | The call ABIs for the root and its lane, the router, quoter, launcher, HookrRules, HookrRecapture, the registry, treasury, programs, referrals and forwarder |
|
|
339
|
+
| `generated.ts` | `ARTIFACT_ABIS`, the full ABIs of the registry, HookrRules, the treasury, the root factory and pair root, the fee and session advisories, the liquidity vault, the family contracts, LP Boost and the owned-root contracts, and the constants the encoders use |
|
|
340
|
+
| `vault.ts`, `referrals.ts`, `proofs.ts` | Liquidity vault and treasury calls, referral authorizations and reward proofs |
|
|
341
|
+
| `client.ts` | `HookrClient`, for a Hookr service at a URL you give it. Hookr runs no public service. |
|
|
342
|
+
|
|
343
|
+
Everything but `releases.ts`, `deployments.ts` and `wallet-cap.ts` is the release SDK hookr.fun runs, copied byte for byte. Its quote-admission exports in `registry.ts` are deprecated: HookrRegistry does not have their calls. `provenance.json` names the release's source commit and the hash of every file. Every contract of the release has its source verified at its address on RobinScan and Sourcify.
|
|
322
344
|
|
|
323
345
|
Before you send a call, check the wallet's chain, the payer, the token approvals, the deadline and the slippage yourself. A quote simulates the pool's accounting. It does not prove approvals, balances or gas. Treat token names and links as untrusted display text.
|
|
324
346
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hookr-sdk: build on Hookr
|
|
2
|
+
* hookr-sdk: build on Hookr, the Uniswap v4 hook on Robinhood Chain.
|
|
3
3
|
*
|
|
4
4
|
* `./core` is the release SDK of the release commit, copied byte for byte (`provenance.json` names the commit and
|
|
5
5
|
* every file's hash). `./releases` adds the addresses and deployment of the production release and its canary, the
|
|
6
|
-
* chain for viem, and builds the DeploymentManifest the core
|
|
6
|
+
* Rules modules its root admits beside the release's own, the chain for viem, and builds the DeploymentManifest the core
|
|
7
|
+
* builders take. `./wallet-cap` adds Wallet Cap: its pool terms, its storage reads and its refusal.
|
|
8
|
+
*
|
|
9
|
+
* Below, the core's quote-admission exports are exported again, marked deprecated: they name functions HookrRegistry
|
|
10
|
+
* does not have on chain.
|
|
7
11
|
*/
|
|
12
|
+
import * as registry from "./core/registry.js";
|
|
13
|
+
import { kingOfTheHillPrizeBound as prizeBound } from "./core/rules.js";
|
|
8
14
|
export * from "./core/index.js";
|
|
9
15
|
export * from "./releases.js";
|
|
16
|
+
export * from "./wallet-cap.js";
|
|
17
|
+
/**
|
|
18
|
+
* The largest King of the Pool `maxPrizeBps` a recapture pool binds with: HookrRecapture's `prizeBoundFor(lpBps,
|
|
19
|
+
* burnBps, protocolShareBps, integratorBps)`, worked out offline.
|
|
20
|
+
*/
|
|
21
|
+
export declare const kingOfThePoolPrizeBound: typeof prizeBound;
|
|
22
|
+
/** @deprecated The same function as `kingOfThePoolPrizeBound`, under King of the Pool's earlier name. */
|
|
23
|
+
export declare const kingOfTheHillPrizeBound: typeof prizeBound;
|
|
24
|
+
/** @deprecated Describes calls HookrRegistry does not have on chain. Removed in 2.0. */
|
|
25
|
+
export declare const QUOTE_ADMISSION_ABI: typeof registry.QUOTE_ADMISSION_ABI;
|
|
26
|
+
/** @deprecated Names functions HookrRegistry does not have on chain. Removed in 2.0. */
|
|
27
|
+
export declare const QUOTE_ADMISSION_OPERATIONS: typeof registry.QUOTE_ADMISSION_OPERATIONS;
|
|
28
|
+
/** @deprecated Checks an ABI offline; against the deployed HookrRegistry's ABI it returns false. Removed in 2.0. */
|
|
29
|
+
export declare const registryHasQuoteAdmission: typeof registry.registryHasQuoteAdmission;
|
|
30
|
+
/** @deprecated Checks an ABI offline; against the deployed HookrRegistry's ABI it returns false. Removed in 2.0. */
|
|
31
|
+
export declare const registryHasOperation: typeof registry.registryHasOperation;
|
|
32
|
+
/** @deprecated Calls `excludeQuoteClassNow`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
33
|
+
export declare const buildExcludeQuoteClass: typeof registry.buildExcludeQuoteClass;
|
|
34
|
+
/** @deprecated Calls `stopOpenQuotesNow`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
35
|
+
export declare const buildStopOpenQuotes: typeof registry.buildStopOpenQuotes;
|
|
36
|
+
/** @deprecated Encodes `quoteState()`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
37
|
+
export declare const encodeQuoteStateCall: typeof registry.encodeQuoteStateCall;
|
|
38
|
+
/** @deprecated Decodes `quoteState()`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
39
|
+
export declare const decodeQuoteState: typeof registry.decodeQuoteState;
|
|
40
|
+
/** @deprecated The result of `quoteState()`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
41
|
+
export type QuoteState = registry.QuoteState;
|
|
42
|
+
/** @deprecated Encodes `curatedQuote(asset)`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
43
|
+
export declare const encodeCuratedQuoteCall: typeof registry.encodeCuratedQuoteCall;
|
|
44
|
+
/** @deprecated Decodes `curatedQuote(asset)`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
45
|
+
export declare const decodeCuratedQuote: typeof registry.decodeCuratedQuote;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hookr-sdk: build on Hookr
|
|
2
|
+
* hookr-sdk: build on Hookr, the Uniswap v4 hook on Robinhood Chain.
|
|
3
3
|
*
|
|
4
4
|
* `./core` is the release SDK of the release commit, copied byte for byte (`provenance.json` names the commit and
|
|
5
5
|
* every file's hash). `./releases` adds the addresses and deployment of the production release and its canary, the
|
|
6
|
-
* chain for viem, and builds the DeploymentManifest the core
|
|
6
|
+
* Rules modules its root admits beside the release's own, the chain for viem, and builds the DeploymentManifest the core
|
|
7
|
+
* builders take. `./wallet-cap` adds Wallet Cap: its pool terms, its storage reads and its refusal.
|
|
8
|
+
*
|
|
9
|
+
* Below, the core's quote-admission exports are exported again, marked deprecated: they name functions HookrRegistry
|
|
10
|
+
* does not have on chain.
|
|
7
11
|
*/
|
|
12
|
+
import * as registry from "./core/registry.js";
|
|
13
|
+
import { kingOfTheHillPrizeBound as prizeBound } from "./core/rules.js";
|
|
8
14
|
export * from "./core/index.js";
|
|
9
15
|
export * from "./releases.js";
|
|
16
|
+
export * from "./wallet-cap.js";
|
|
17
|
+
/**
|
|
18
|
+
* The largest King of the Pool `maxPrizeBps` a recapture pool binds with: HookrRecapture's `prizeBoundFor(lpBps,
|
|
19
|
+
* burnBps, protocolShareBps, integratorBps)`, worked out offline.
|
|
20
|
+
*/
|
|
21
|
+
export const kingOfThePoolPrizeBound = prizeBound;
|
|
22
|
+
/** @deprecated The same function as `kingOfThePoolPrizeBound`, under King of the Pool's earlier name. */
|
|
23
|
+
export const kingOfTheHillPrizeBound = prizeBound;
|
|
24
|
+
/** @deprecated Describes calls HookrRegistry does not have on chain. Removed in 2.0. */
|
|
25
|
+
export const QUOTE_ADMISSION_ABI = registry.QUOTE_ADMISSION_ABI;
|
|
26
|
+
/** @deprecated Names functions HookrRegistry does not have on chain. Removed in 2.0. */
|
|
27
|
+
export const QUOTE_ADMISSION_OPERATIONS = registry.QUOTE_ADMISSION_OPERATIONS;
|
|
28
|
+
/** @deprecated Checks an ABI offline; against the deployed HookrRegistry's ABI it returns false. Removed in 2.0. */
|
|
29
|
+
export const registryHasQuoteAdmission = registry.registryHasQuoteAdmission;
|
|
30
|
+
/** @deprecated Checks an ABI offline; against the deployed HookrRegistry's ABI it returns false. Removed in 2.0. */
|
|
31
|
+
export const registryHasOperation = registry.registryHasOperation;
|
|
32
|
+
/** @deprecated Calls `excludeQuoteClassNow`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
33
|
+
export const buildExcludeQuoteClass = registry.buildExcludeQuoteClass;
|
|
34
|
+
/** @deprecated Calls `stopOpenQuotesNow`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
35
|
+
export const buildStopOpenQuotes = registry.buildStopOpenQuotes;
|
|
36
|
+
/** @deprecated Encodes `quoteState()`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
37
|
+
export const encodeQuoteStateCall = registry.encodeQuoteStateCall;
|
|
38
|
+
/** @deprecated Decodes `quoteState()`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
39
|
+
export const decodeQuoteState = registry.decodeQuoteState;
|
|
40
|
+
/** @deprecated Encodes `curatedQuote(asset)`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
41
|
+
export const encodeCuratedQuoteCall = registry.encodeCuratedQuoteCall;
|
|
42
|
+
/** @deprecated Decodes `curatedQuote(asset)`, which HookrRegistry does not have on chain. Removed in 2.0. */
|
|
43
|
+
export const decodeCuratedQuote = registry.decodeCuratedQuote;
|
package/dist/releases.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hookr
|
|
3
|
-
* deployment, and the deployment manifest the builders take.
|
|
2
|
+
* Hookr on Robinhood Chain: the production release `hookr-1`, its canary `canary-hookr-1-a`, each release's
|
|
3
|
+
* deployment, the Rules modules its root admits beside the release's own, and the deployment manifest the builders take.
|
|
4
4
|
*
|
|
5
5
|
* Every address is fixed by CREATE3 from the release deployer, the release id and the contract's name, so it is known
|
|
6
6
|
* before the contract is deployed and is the same on every machine. `test/releases.test.ts` derives each one again
|
|
@@ -17,7 +17,7 @@ export declare const CHAIN_ID = 4663;
|
|
|
17
17
|
* endpoint of its own.
|
|
18
18
|
*/
|
|
19
19
|
export declare const RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
|
|
20
|
-
/** The block explorer people open: RobinScan
|
|
20
|
+
/** The block explorer people open: RobinScan. No trailing slash. */
|
|
21
21
|
export declare const EXPLORER_URL = "https://robin.etherscan.io";
|
|
22
22
|
/**
|
|
23
23
|
* Robinhood Chain for viem, on any viem 2, with RobinScan as its explorer. viem 2.55 and later also ship it as
|
|
@@ -102,10 +102,38 @@ export interface HookrRelease {
|
|
|
102
102
|
* before the release was deployed and verified. `releaseManifest` and `readReleaseManifest` use it by default.
|
|
103
103
|
*/
|
|
104
104
|
deployment: Readonly<ReleaseDeployment> | null;
|
|
105
|
+
/**
|
|
106
|
+
* The Rules modules a pool on this release's root can bind, by name: `standard`, the release's own HookrRules, and
|
|
107
|
+
* on `hookr-1` also `whole-pot`, where King of the Pool pays the round's winner the whole pot, `wallet-cap`, the
|
|
108
|
+
* whole-pot Rules with a per-wallet buy cap while the launch guard lasts (`wallet-cap.ts`), and `round-trip`, the
|
|
109
|
+
* Rules that record who traded in each block for Buy/Sell Block's round-trip check. The registry admits each on the
|
|
110
|
+
* root with the release Rules' own terms. `rulesModule` names one.
|
|
111
|
+
*/
|
|
112
|
+
rulesModules: Readonly<Partial<Record<RulesModuleName, Readonly<RulesModule>>>>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The name of a Rules module: the release's own Rules, or beside them the whole-pot King of the Pool Rules, the Wallet
|
|
116
|
+
* Cap Rules or the round-trip Rules.
|
|
117
|
+
*/
|
|
118
|
+
export type RulesModuleName = "standard" | "whole-pot" | "wallet-cap" | "round-trip";
|
|
119
|
+
/**
|
|
120
|
+
* A HookrRules a pool can bind on the release's root (its `PoolConfig.rules`), and the HookrRecapture it created in its
|
|
121
|
+
* constructor. The Rules reach that module for every King of the Pool call, so a pool's round, pot, prize and claims
|
|
122
|
+
* live at the Rules address. Both runtime hashes are the code read back from chain.
|
|
123
|
+
*/
|
|
124
|
+
export interface RulesModule {
|
|
125
|
+
name: RulesModuleName;
|
|
126
|
+
rules: Address;
|
|
127
|
+
rulesRuntimeHash: Hash;
|
|
128
|
+
recapture: Address;
|
|
129
|
+
recaptureRuntimeHash: Hash;
|
|
105
130
|
}
|
|
106
131
|
/** The production release. Build on this one. */
|
|
107
132
|
export declare const HOOKR_1: HookrRelease;
|
|
108
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* The canary: the same contracts at their own addresses, deployed and proven before production. Not for users.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
109
137
|
export declare const CANARY_HOOKR_1_A: HookrRelease;
|
|
110
138
|
/** Both releases by release id. */
|
|
111
139
|
export declare const RELEASES: Readonly<Record<HookrReleaseId, HookrRelease>>;
|
|
@@ -129,13 +157,18 @@ export interface ReleaseDeployment {
|
|
|
129
157
|
}
|
|
130
158
|
/**
|
|
131
159
|
* The DeploymentManifest the builders take for a release: every role at its fixed address with its runtime hash, from
|
|
132
|
-
* the release's own deployment unless one is given
|
|
133
|
-
*
|
|
160
|
+
* the release's own deployment unless one is given, then each further Rules module (`otherRulesModules`) as one more
|
|
161
|
+
* `rules` pin with its own runtime hash, so the builders that name a Rules accept it. It makes no reads. Refuses a role
|
|
162
|
+
* without a runtime hash, and anything `deployment()` refuses.
|
|
134
163
|
*
|
|
135
164
|
* It is the builders' manifest. Its release id is the release's, `hookr-1`, so it is not the manifest a Hookr service
|
|
136
165
|
* publishes: `HookrClient` takes the release id and manifest hash the service names.
|
|
137
166
|
*/
|
|
138
167
|
export declare function releaseManifest(release: HookrRelease, d?: Readonly<ReleaseDeployment> | null): DeploymentManifest;
|
|
168
|
+
/** The release's Rules modules other than its own HookrRules: on `hookr-1`, the whole-pot, Wallet Cap and round-trip Rules. */
|
|
169
|
+
export declare function otherRulesModules(release: HookrRelease): Readonly<RulesModule>[];
|
|
170
|
+
/** The release's Rules module `name`, or an error for a release without it (the canary has only `standard`). */
|
|
171
|
+
export declare function rulesModule(release: HookrRelease, name: RulesModuleName): Readonly<RulesModule>;
|
|
139
172
|
/** The two reads `readReleaseManifest` makes. A viem PublicClient has both. */
|
|
140
173
|
export interface ReleaseReader {
|
|
141
174
|
getCode(args: {
|
|
@@ -160,13 +193,27 @@ export interface ReadReleaseOptions {
|
|
|
160
193
|
/**
|
|
161
194
|
* `releaseManifest` with each role's runtime hash and the start block's hash read from chain. Refuses a role with no
|
|
162
195
|
* code: the release is not deployed, or not on this chain. Where the release carries its deployment, it also refuses
|
|
163
|
-
* a role whose code is not the release's, and a start block whose hash is not the release's.
|
|
196
|
+
* a role whose code is not the release's, and a start block whose hash is not the release's. It always refuses a
|
|
197
|
+
* further Rules module whose code is not the one the release pins for it.
|
|
164
198
|
*/
|
|
165
199
|
export declare function readReleaseManifest(client: ReleaseReader, release: HookrRelease, options?: ReadReleaseOptions): Promise<DeploymentManifest>;
|
|
166
200
|
/**
|
|
167
201
|
* The gas floor a swap on an arb recapture pool must carry while its lane is on: `2 × gasCap + gasCap / 63 + 755,000`
|
|
168
202
|
* (HookrLane), the `gasFloor` `laneOf(id)` returns. At the 3,000,000 cap the lane opens with it is 6,802,619. A swap
|
|
169
|
-
* sent with less reverts `InsufficientLaneGas`. The swap's own gas comes on top:
|
|
170
|
-
*
|
|
203
|
+
* sent with less reverts `InsufficientLaneGas`. The swap's own gas comes on top: a buy on a live lane pool estimates at
|
|
204
|
+
* about 7.5 million, so a fixed limit on such a pool should be at least 8,800,000, the estimate plus a sixth.
|
|
171
205
|
*/
|
|
172
206
|
export declare function laneGasFloor(gasCap: number | bigint): bigint;
|
|
207
|
+
/** The read `readParentBlockNumber` makes. A viem PublicClient has it. */
|
|
208
|
+
export interface ParentBlockReader {
|
|
209
|
+
getBlock(args: {
|
|
210
|
+
blockTag: "latest";
|
|
211
|
+
}): Promise<unknown>;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The block number the contracts read as `block.number`: on Robinhood Chain, an Arbitrum Orbit chain, it is the parent
|
|
215
|
+
* chain's (Ethereum's), which each block carries as `l1BlockNumber`, about one every 12 seconds. Anti-Snipe's launch
|
|
216
|
+
* guard (`guardEndBlock`), and Wallet Cap with it, counts these blocks, so pass this as a launch's `blockNumber`.
|
|
217
|
+
* `getBlockNumber()` is the chain's own, far larger number: a guard counted from it is one the Rules refuse at bind.
|
|
218
|
+
*/
|
|
219
|
+
export declare function readParentBlockNumber(client: ParentBlockReader): Promise<bigint>;
|
package/dist/releases.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hookr
|
|
3
|
-
* deployment, and the deployment manifest the builders take.
|
|
2
|
+
* Hookr on Robinhood Chain: the production release `hookr-1`, its canary `canary-hookr-1-a`, each release's
|
|
3
|
+
* deployment, the Rules modules its root admits beside the release's own, and the deployment manifest the builders take.
|
|
4
4
|
*
|
|
5
5
|
* Every address is fixed by CREATE3 from the release deployer, the release id and the contract's name, so it is known
|
|
6
6
|
* before the contract is deployed and is the same on every machine. `test/releases.test.ts` derives each one again
|
|
@@ -18,7 +18,7 @@ export const CHAIN_ID = 4663;
|
|
|
18
18
|
* endpoint of its own.
|
|
19
19
|
*/
|
|
20
20
|
export const RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
|
|
21
|
-
/** The block explorer people open: RobinScan
|
|
21
|
+
/** The block explorer people open: RobinScan. No trailing slash. */
|
|
22
22
|
export const EXPLORER_URL = "https://robin.etherscan.io";
|
|
23
23
|
/**
|
|
24
24
|
* Robinhood Chain for viem, on any viem 2, with RobinScan as its explorer. viem 2.55 and later also ship it as
|
|
@@ -66,6 +66,53 @@ export const HOOKR_1 = Object.freeze({
|
|
|
66
66
|
canary: false,
|
|
67
67
|
chainId: CHAIN_ID,
|
|
68
68
|
deployment: frozen(DEPLOYMENTS["hookr-1"]),
|
|
69
|
+
rulesModules: Object.freeze({
|
|
70
|
+
standard: Object.freeze({
|
|
71
|
+
name: "standard",
|
|
72
|
+
rules: "0x4a71c270b1520F94064268102AebD744b0DE7942",
|
|
73
|
+
rulesRuntimeHash: "0x4b6d50b40981b8082f3ee5a7ebf57e48489f71c94c2a3b94032a1d1a382b515b",
|
|
74
|
+
recapture: "0xF83569E564d70daebc35950f66a73f2e2B915583",
|
|
75
|
+
recaptureRuntimeHash: "0xb78c68d64388c9c0c4d20faa3d429309a6a025024c3eefc579831bb10de145ed",
|
|
76
|
+
}),
|
|
77
|
+
/**
|
|
78
|
+
* Deployed 2026-10-07 beside the release (CREATE3 from the release deployer, release id `hookr-1-whole-pot`), with
|
|
79
|
+
* the release's constructor arguments. Its HookrRecapture pays a round's winner the whole pot and lets a pool put
|
|
80
|
+
* up to all of its arb recapture share in the pot. Same ABI as the release's Rules.
|
|
81
|
+
*/
|
|
82
|
+
"whole-pot": Object.freeze({
|
|
83
|
+
name: "whole-pot",
|
|
84
|
+
rules: "0x10f684361b35C8411a2c07F68A6fB1F4B7e555F7",
|
|
85
|
+
rulesRuntimeHash: "0x3ffac05e248f81ad3843949052af1c0c6343179f0ec591bae3e271e249c7d879",
|
|
86
|
+
recapture: "0x60Efc4a41498e9433be2155b6cc1677AEC31D649",
|
|
87
|
+
recaptureRuntimeHash: "0xdc89f3eb02ff5bfbc07ac078e4f1d1de215ec253bc7ba843962e0b6cd11445c4",
|
|
88
|
+
}),
|
|
89
|
+
/**
|
|
90
|
+
* HookrRulesWalletCap v2, deployed 2026-10-12 beside the release (CREATE3 from the release deployer, release id
|
|
91
|
+
* `hookr-1-wallet-cap-v2`) with the release's constructor arguments, and admitted on HookrRoot with the release
|
|
92
|
+
* Rules' terms. The whole-pot Rules plus a per-wallet buy cap while a pool's launch guard lasts: it binds only a
|
|
93
|
+
* pool with a cap and a guard, and answers `extsload` (`wallet-cap.ts`). Its HookrRecapture is the whole-pot one's
|
|
94
|
+
* source, so its runtime, which keeps its own address, is its own. Same ABI as the release's Rules otherwise.
|
|
95
|
+
*/
|
|
96
|
+
"wallet-cap": Object.freeze({
|
|
97
|
+
name: "wallet-cap",
|
|
98
|
+
rules: "0xb67b73E28C522a3AAC438bE970bf94976Deee52a",
|
|
99
|
+
rulesRuntimeHash: "0x0fed58ec91368b16ba4166acc65ddd9252529fb860834f81fdc5dab3179a6247",
|
|
100
|
+
recapture: "0xbbC43DC2f01F5E7882caCd78EF094A44eF0E4d23",
|
|
101
|
+
recaptureRuntimeHash: "0x04001c31836049074c926cf6519ecf999c18267229fe6a05df26685caadb0984",
|
|
102
|
+
}),
|
|
103
|
+
/**
|
|
104
|
+
* HookrRulesRoundTrip, deployed beside the release (CREATE3 from the release deployer, release id `hookr-1`) and
|
|
105
|
+
* admitted on HookrRoot with the release Rules' terms at block 82,372,631. Rules that keep a record of who traded
|
|
106
|
+
* in each block, which Buy/Sell Block's round-trip check reads. Same ABI as the release's Rules otherwise.
|
|
107
|
+
*/
|
|
108
|
+
"round-trip": Object.freeze({
|
|
109
|
+
name: "round-trip",
|
|
110
|
+
rules: "0x3d7e31746127FD9a14d3C7fC12E73611D2e8cb34",
|
|
111
|
+
rulesRuntimeHash: "0x925797125cde5723bfa57ac04238e847ac0098a2821e6703fd4d5e20881a322b",
|
|
112
|
+
recapture: "0xddFF6326E5CB9AA98b88e9064d9A9570f777f8A3",
|
|
113
|
+
recaptureRuntimeHash: "0x7e8e752507e29441d26bec6646cd57598e63825ab888cf166da27cf9a5a31e5e",
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
69
116
|
contracts: Object.freeze({
|
|
70
117
|
HookrRegistryChecks: "0xB9F5c54E5Cc7ac6c7a96335E5A334Cf4eA496E98",
|
|
71
118
|
HookrRegistryAdmin: "0x675ac2102E8c69E2467F64656E3c235AF3901Ac1",
|
|
@@ -104,12 +151,24 @@ export const HOOKR_1 = Object.freeze({
|
|
|
104
151
|
HookrLpBoost: "0x52536F5F4E5BC7cEA2197c8F1BB49bf2B133E7f1",
|
|
105
152
|
}),
|
|
106
153
|
});
|
|
107
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* The canary: the same contracts at their own addresses, deployed and proven before production. Not for users.
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
108
158
|
export const CANARY_HOOKR_1_A = Object.freeze({
|
|
109
159
|
releaseId: "canary-hookr-1-a",
|
|
110
160
|
canary: true,
|
|
111
161
|
chainId: CHAIN_ID,
|
|
112
162
|
deployment: frozen(DEPLOYMENTS["canary-hookr-1-a"]),
|
|
163
|
+
rulesModules: Object.freeze({
|
|
164
|
+
standard: Object.freeze({
|
|
165
|
+
name: "standard",
|
|
166
|
+
rules: "0x8Ef8869CD47d9928d563001F1d38D65c3c3e01Ab",
|
|
167
|
+
rulesRuntimeHash: "0x7e51ac228eafdaa7f2b3c45f2fec6ec9c872f81ce0258c1a2da95090883b30a5",
|
|
168
|
+
recapture: "0xe7035c0d8523a5eE9Fc5Ba5Ec97a4f1D771a6F87",
|
|
169
|
+
recaptureRuntimeHash: "0x54aa061f121e798029af86b9b700433fec566cdb570578e4e57a69865d6095a6",
|
|
170
|
+
}),
|
|
171
|
+
}),
|
|
113
172
|
contracts: Object.freeze({
|
|
114
173
|
HookrRegistryChecks: "0x4598d64B6c5730813a469e4B36cf379DbfB24118",
|
|
115
174
|
HookrRegistryAdmin: "0x57252DB7C9ed51213a324A2160934Ee2f840d6E8",
|
|
@@ -185,8 +244,9 @@ function noDeployment(release) {
|
|
|
185
244
|
}
|
|
186
245
|
/**
|
|
187
246
|
* The DeploymentManifest the builders take for a release: every role at its fixed address with its runtime hash, from
|
|
188
|
-
* the release's own deployment unless one is given
|
|
189
|
-
*
|
|
247
|
+
* the release's own deployment unless one is given, then each further Rules module (`otherRulesModules`) as one more
|
|
248
|
+
* `rules` pin with its own runtime hash, so the builders that name a Rules accept it. It makes no reads. Refuses a role
|
|
249
|
+
* without a runtime hash, and anything `deployment()` refuses.
|
|
190
250
|
*
|
|
191
251
|
* It is the builders' manifest. Its release id is the release's, `hookr-1`, so it is not the manifest a Hookr service
|
|
192
252
|
* publishes: `HookrClient` takes the release id and manifest hash the service names.
|
|
@@ -205,6 +265,8 @@ export function releaseManifest(release, d = release.deployment) {
|
|
|
205
265
|
startBlock: d.startBlock,
|
|
206
266
|
};
|
|
207
267
|
});
|
|
268
|
+
for (const m of otherRulesModules(release))
|
|
269
|
+
contracts.push({ role: "rules", address: m.rules.toLowerCase(), runtimeHash: m.rulesRuntimeHash, startBlock: d.startBlock });
|
|
208
270
|
return deployment({
|
|
209
271
|
schema: "hookr.engagement.sources.v1",
|
|
210
272
|
releaseId: release.releaseId,
|
|
@@ -215,10 +277,22 @@ export function releaseManifest(release, d = release.deployment) {
|
|
|
215
277
|
qualification: { status: "verified", evidenceHash: d.evidenceHash, sourceCommit: RELEASE_SOURCE_COMMIT },
|
|
216
278
|
});
|
|
217
279
|
}
|
|
280
|
+
/** The release's Rules modules other than its own HookrRules: on `hookr-1`, the whole-pot, Wallet Cap and round-trip Rules. */
|
|
281
|
+
export function otherRulesModules(release) {
|
|
282
|
+
return Object.values(release.rulesModules).filter((m) => m !== undefined && m.rules.toLowerCase() !== release.contracts.HookrRules.toLowerCase());
|
|
283
|
+
}
|
|
284
|
+
/** The release's Rules module `name`, or an error for a release without it (the canary has only `standard`). */
|
|
285
|
+
export function rulesModule(release, name) {
|
|
286
|
+
const m = release.rulesModules[name];
|
|
287
|
+
if (!m)
|
|
288
|
+
throw new Error(`${release.releaseId} has no ${name} Rules`);
|
|
289
|
+
return m;
|
|
290
|
+
}
|
|
218
291
|
/**
|
|
219
292
|
* `releaseManifest` with each role's runtime hash and the start block's hash read from chain. Refuses a role with no
|
|
220
293
|
* code: the release is not deployed, or not on this chain. Where the release carries its deployment, it also refuses
|
|
221
|
-
* a role whose code is not the release's, and a start block whose hash is not the release's.
|
|
294
|
+
* a role whose code is not the release's, and a start block whose hash is not the release's. It always refuses a
|
|
295
|
+
* further Rules module whose code is not the one the release pins for it.
|
|
222
296
|
*/
|
|
223
297
|
export async function readReleaseManifest(client, release, options = {}) {
|
|
224
298
|
const pinned = release.deployment;
|
|
@@ -227,9 +301,11 @@ export async function readReleaseManifest(client, release, options = {}) {
|
|
|
227
301
|
if (startBlock === undefined || evidenceHash === undefined)
|
|
228
302
|
throw noDeployment(release);
|
|
229
303
|
const roles = Object.keys(MANIFEST_ROLES);
|
|
230
|
-
const
|
|
304
|
+
const modules = otherRulesModules(release);
|
|
305
|
+
const [block, codes, moduleCodes] = await Promise.all([
|
|
231
306
|
client.getBlock({ blockNumber: startBlock }),
|
|
232
307
|
Promise.all(roles.map((role) => client.getCode({ address: releaseAddress(release, role), blockNumber: options.blockNumber }))),
|
|
308
|
+
Promise.all(modules.map((m) => client.getCode({ address: m.rules, blockNumber: options.blockNumber }))),
|
|
233
309
|
]);
|
|
234
310
|
if (!block.hash)
|
|
235
311
|
throw new Error(`Block ${startBlock} has no hash yet`);
|
|
@@ -247,6 +323,13 @@ export async function readReleaseManifest(client, release, options = {}) {
|
|
|
247
323
|
throw new Error(`${role} ${releaseAddress(release, role)} does not run ${release.releaseId}'s code`);
|
|
248
324
|
runtimeHashes[role] = runtimeHash;
|
|
249
325
|
});
|
|
326
|
+
modules.forEach((m, i) => {
|
|
327
|
+
const code = moduleCodes[i];
|
|
328
|
+
if (!code || code === "0x")
|
|
329
|
+
throw new Error(`No code at the ${m.name} Rules ${m.rules}`);
|
|
330
|
+
if (keccak256(code) !== m.rulesRuntimeHash.toLowerCase())
|
|
331
|
+
throw new Error(`The ${m.name} Rules ${m.rules} do not run the code ${release.releaseId} pins for them`);
|
|
332
|
+
});
|
|
250
333
|
return releaseManifest(release, {
|
|
251
334
|
startBlock: startBlock.toString(),
|
|
252
335
|
parentHash: block.hash,
|
|
@@ -257,8 +340,8 @@ export async function readReleaseManifest(client, release, options = {}) {
|
|
|
257
340
|
/**
|
|
258
341
|
* The gas floor a swap on an arb recapture pool must carry while its lane is on: `2 × gasCap + gasCap / 63 + 755,000`
|
|
259
342
|
* (HookrLane), the `gasFloor` `laneOf(id)` returns. At the 3,000,000 cap the lane opens with it is 6,802,619. A swap
|
|
260
|
-
* sent with less reverts `InsufficientLaneGas`. The swap's own gas comes on top:
|
|
261
|
-
*
|
|
343
|
+
* sent with less reverts `InsufficientLaneGas`. The swap's own gas comes on top: a buy on a live lane pool estimates at
|
|
344
|
+
* about 7.5 million, so a fixed limit on such a pool should be at least 8,800,000, the estimate plus a sixth.
|
|
262
345
|
*/
|
|
263
346
|
export function laneGasFloor(gasCap) {
|
|
264
347
|
const cap = BigInt(gasCap);
|
|
@@ -266,3 +349,17 @@ export function laneGasFloor(gasCap) {
|
|
|
266
349
|
return 0n;
|
|
267
350
|
return 2n * cap + cap / 63n + 755000n;
|
|
268
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The block number the contracts read as `block.number`: on Robinhood Chain, an Arbitrum Orbit chain, it is the parent
|
|
354
|
+
* chain's (Ethereum's), which each block carries as `l1BlockNumber`, about one every 12 seconds. Anti-Snipe's launch
|
|
355
|
+
* guard (`guardEndBlock`), and Wallet Cap with it, counts these blocks, so pass this as a launch's `blockNumber`.
|
|
356
|
+
* `getBlockNumber()` is the chain's own, far larger number: a guard counted from it is one the Rules refuse at bind.
|
|
357
|
+
*/
|
|
358
|
+
export async function readParentBlockNumber(client) {
|
|
359
|
+
const { l1BlockNumber } = ((await client.getBlock({ blockTag: "latest" })) ?? {});
|
|
360
|
+
if (typeof l1BlockNumber === "bigint" && l1BlockNumber > 0n)
|
|
361
|
+
return l1BlockNumber;
|
|
362
|
+
if (typeof l1BlockNumber === "string" && /^0x0*[1-9a-fA-F][0-9a-fA-F]*$/.test(l1BlockNumber))
|
|
363
|
+
return BigInt(l1BlockNumber);
|
|
364
|
+
throw new Error("The latest block carries no l1BlockNumber: read it from Robinhood Chain's own RPC");
|
|
365
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Cap: HookrRulesWalletCap (`rulesModule(HOOKR_1, "wallet-cap")`), the whole-pot Rules with a per-wallet buy
|
|
3
|
+
* cap on each launch while its pools' launch guards last.
|
|
4
|
+
*
|
|
5
|
+
* - The cap rides in `RulesConfig.potMinBuyQuote`, in the token's raw units: the most of it one wallet may buy while the
|
|
6
|
+
* pool's guard lasts. These Rules bind only a pool with a cap and a launch guard (`guardEndBlock` within the next
|
|
7
|
+
* 100,000 parent blocks), and `potEveryNBuys` stays zero. Every other Rules refuses a nonzero `potMinBuyQuote`, as the
|
|
8
|
+
* core's `checkRulesConfig` does.
|
|
9
|
+
* - Each guarded buy counts what it delivers (the pool's output less Auto Burn) against its wallet: the payer
|
|
10
|
+
* HookrRouter, the forwarder, the family router or the curated Uniswap router names, or for a swap through a router
|
|
11
|
+
* that names none, the transaction's signer. Counts are per token and per launch: the pools of one launch share
|
|
12
|
+
* `Bound.launchedAt`, the second they bound, and a later launch of the token counts from zero, so every pool of a
|
|
13
|
+
* launch takes the same cap, and arb recapture on every pool or none (`checkWalletCapLaunch`). A sell frees no room.
|
|
14
|
+
* The creator's dev buy and the pool's own arb recapture executor are not counted. A buy past the cap reverts
|
|
15
|
+
* `GuardWalletLimit(wallet, actual, limit)`. After the guard nothing is counted or capped.
|
|
16
|
+
* - The Rules answer `extsload(bytes32)`, one word of their storage, and `readWalletCap` reads through it.
|
|
17
|
+
*/
|
|
18
|
+
import { type Hex } from "viem";
|
|
19
|
+
import { type Address, type Admission, type Caps, type Hash, type ParamContext, type PoolTerms, type RootAdvisoryKind, type RulesConfig, type RulesContext, type RulesLaunch } from "./core/index.js";
|
|
20
|
+
import { type HookrRelease } from "./releases.js";
|
|
21
|
+
/**
|
|
22
|
+
* What HookrRulesWalletCap adds to the release Rules' ABI (`rulesAbi`): `extsload`, one word of its storage as Uniswap
|
|
23
|
+
* v4's Extsload reads it, and the refusal of a buy that would take a wallet past its pool's cap.
|
|
24
|
+
*/
|
|
25
|
+
export declare const WALLET_CAP_ABI: readonly [{
|
|
26
|
+
readonly type: "function";
|
|
27
|
+
readonly name: "extsload";
|
|
28
|
+
readonly stateMutability: "view";
|
|
29
|
+
readonly inputs: readonly [{
|
|
30
|
+
readonly name: "slot";
|
|
31
|
+
readonly type: "bytes32";
|
|
32
|
+
readonly internalType: "bytes32";
|
|
33
|
+
}];
|
|
34
|
+
readonly outputs: readonly [{
|
|
35
|
+
readonly name: "value";
|
|
36
|
+
readonly type: "bytes32";
|
|
37
|
+
readonly internalType: "bytes32";
|
|
38
|
+
}];
|
|
39
|
+
}, {
|
|
40
|
+
readonly type: "error";
|
|
41
|
+
readonly name: "GuardWalletLimit";
|
|
42
|
+
readonly inputs: readonly [{
|
|
43
|
+
readonly name: "wallet";
|
|
44
|
+
readonly type: "address";
|
|
45
|
+
readonly internalType: "address";
|
|
46
|
+
}, {
|
|
47
|
+
readonly name: "actual";
|
|
48
|
+
readonly type: "uint256";
|
|
49
|
+
readonly internalType: "uint256";
|
|
50
|
+
}, {
|
|
51
|
+
readonly name: "limit";
|
|
52
|
+
readonly type: "uint256";
|
|
53
|
+
readonly internalType: "uint256";
|
|
54
|
+
}];
|
|
55
|
+
}];
|
|
56
|
+
/** erc7201:hookr.rules, every HookrRules' State: keccak256(abi.encode(uint256(keccak256("hookr.rules")) - 1)) & ~0xff. */
|
|
57
|
+
export declare const RULES_SLOT = 53445494162334802764660739626191472437579742535980357254313114366502610401536n;
|
|
58
|
+
/**
|
|
59
|
+
* The slot of a pool's first Bound word, `State.pools[poolId]`: keccak256(abi.encode(poolId, RULES_SLOT)). Its launch
|
|
60
|
+
* guard's end is in the next slot (`uint40` of the word) and its cap two slots on (the top 128 bits).
|
|
61
|
+
*/
|
|
62
|
+
export declare function walletCapPoolSlot(poolId: Hash): Hash;
|
|
63
|
+
/** The slot of `wallet`'s count of `subject` on launch `launch`, `State.bought[subject][launch][wallet]`. */
|
|
64
|
+
export declare function walletCapBoughtSlot(subject: Address, launch: number | bigint, wallet: Address): Hash;
|
|
65
|
+
/**
|
|
66
|
+
* The launch in a pool's first Bound word: its top 32 bits, `Bound.launchedAt`. The word packs, low to high,
|
|
67
|
+
* baseFeePips (24 bits), tickSpacing (16), bound (8), quoteIsCurrency0 (8), terms (8), liquidityOwner (160) and
|
|
68
|
+
* launchedAt (32). Null for anything but 32 bytes of hex with the bound flag set: a pool these Rules never bound.
|
|
69
|
+
*/
|
|
70
|
+
export declare function walletCapLaunch(value: Hex): number | null;
|
|
71
|
+
/**
|
|
72
|
+
* A pool's cap, in the token's raw units, for a share of `supply` (raw units) in basis points: the floor of
|
|
73
|
+
* supply × bps / 10,000, so 200 bps of a supply of 10^27 is 2 × 10^25. It goes in the pool's `rules.potMinBuyQuote`.
|
|
74
|
+
* Refuses a share outside 1 to 10,000 bps and a cap that rounds to zero. hookr.fun offers 0.1% to 10%, 2% by default.
|
|
75
|
+
*/
|
|
76
|
+
export declare function walletCapAmount(supply: bigint, bps: number | bigint): bigint;
|
|
77
|
+
/** What a wallet may still buy while the guard lasts: the cap less what it bought, never below zero. */
|
|
78
|
+
export declare function walletCapRoom(cap: bigint, bought: bigint): bigint;
|
|
79
|
+
/**
|
|
80
|
+
* Refuses a RulesConfig HookrRulesWalletCap's bind would refuse and returns its fee ceilings: `checkRulesConfig` with
|
|
81
|
+
* the cap set aside, plus the Wallet Cap Rules' own terms: a cap above zero in `potMinBuyQuote` (below 2^128), and a
|
|
82
|
+
* launch guard, with its liquidity owner, ending within 100,000 parent blocks after `ctx.blockNumber`, which is required
|
|
83
|
+
* here. `potEveryNBuys` stays zero. Every other Rules refuses a cap: check those with `checkRulesConfig`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function checkWalletCapRulesConfig(c: RulesConfig, ctx: RulesContext): Caps;
|
|
86
|
+
/**
|
|
87
|
+
* `rulesPoolTerms` for a launch member on the Wallet Cap Rules: every check `checkWalletCapRulesConfig` makes, the
|
|
88
|
+
* pool's caps and gas limits from the Rules' admission (`admission(HookrRoot, rulesModule(HOOKR_1, "wallet-cap").rules)`
|
|
89
|
+
* on the registry), and its `rulesData` with the cap. Refuses the admission of any other Rules. The member binds these
|
|
90
|
+
* Rules as `config.rules` and `l.rules`, cap included, as `rules`; every pool of the launch takes the same cap.
|
|
91
|
+
*/
|
|
92
|
+
export declare function walletCapPoolTerms(l: RulesLaunch, ctx: ParamContext & {
|
|
93
|
+
liquidityOwner?: Address;
|
|
94
|
+
}, rulesAdmission: Admission, advisory?: {
|
|
95
|
+
admission: Admission;
|
|
96
|
+
kind: RootAdvisoryKind;
|
|
97
|
+
phases?: number;
|
|
98
|
+
}, release?: HookrRelease): PoolTerms;
|
|
99
|
+
/**
|
|
100
|
+
* Refuses a launch whose members (`buildLaunch`'s) Wallet Cap would leave a way round, or turn against traders, though
|
|
101
|
+
* the Rules bind each pool alone: some pools on the Wallet Cap Rules and some not, capped pools with different caps (a
|
|
102
|
+
* wallet could then buy the largest), or capped pools some with arb recapture and some without (on a capped pool
|
|
103
|
+
* without the lane, the executor's legs count against the trader whose swap set them off). Passes a launch without it.
|
|
104
|
+
*/
|
|
105
|
+
export declare function checkWalletCapLaunch(members: readonly Record<string, unknown>[], release?: HookrRelease): void;
|
|
106
|
+
/** The read `readWalletCap` makes: an `eth_call`. A viem PublicClient has it. */
|
|
107
|
+
export interface WalletCapReader {
|
|
108
|
+
call(args: {
|
|
109
|
+
to: Address;
|
|
110
|
+
data: Hex;
|
|
111
|
+
blockNumber?: bigint;
|
|
112
|
+
}): Promise<{
|
|
113
|
+
data?: Hex;
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
116
|
+
/** Which pool, and optionally which wallet, `readWalletCap` reads. */
|
|
117
|
+
export interface WalletCapQuery {
|
|
118
|
+
/** The pool's PoolId (`poolId(key)`). */
|
|
119
|
+
poolId: Hash;
|
|
120
|
+
/** The wallet whose count and room to read. */
|
|
121
|
+
wallet?: Address;
|
|
122
|
+
/** The pool's token, with `wallet`: counts are per token. */
|
|
123
|
+
subject?: Address;
|
|
124
|
+
/** The block to read at. Defaults to the latest. */
|
|
125
|
+
blockNumber?: bigint;
|
|
126
|
+
}
|
|
127
|
+
/** A pool's wallet cap as its Rules hold it, and a wallet's count against it. */
|
|
128
|
+
export interface WalletCap {
|
|
129
|
+
/** The pool's launch: `Bound.launchedAt`, the second its launch bound it, which the launch's pools share. */
|
|
130
|
+
launch: number;
|
|
131
|
+
/** The parent block the pool's launch guard, and the cap with it, ends at (`readParentBlockNumber` reads the clock). */
|
|
132
|
+
guardEndBlock: bigint;
|
|
133
|
+
/** The most of the token, in raw units, one wallet may buy across the launch's pools while the guard lasts. */
|
|
134
|
+
cap: bigint;
|
|
135
|
+
/** What the wallet bought across the launch's pools while their guards lasted, or null without a wallet. */
|
|
136
|
+
bought: bigint | null;
|
|
137
|
+
/** What the wallet may still buy before the guard ends (`walletCapRoom`), or null without a wallet. */
|
|
138
|
+
room: bigint | null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Reads a pool on the Wallet Cap Rules through their `extsload`: its launch, guard end and cap from its Bound
|
|
142
|
+
* (`walletCapPoolSlot` and the two slots after it), then, with a wallet, that wallet's count on the launch
|
|
143
|
+
* (`walletCapBoughtSlot`) and its room. Null for a pool these Rules have not bound: a pool on any other Rules has no
|
|
144
|
+
* wallet cap. A count and its room hold while the guard lasts; after it nothing is counted or capped.
|
|
145
|
+
*/
|
|
146
|
+
export declare function readWalletCap(client: WalletCapReader, query: WalletCapQuery, release?: HookrRelease): Promise<WalletCap | null>;
|
|
147
|
+
/** A buy Wallet Cap refused: the wallet it counted, the total the buy would have brought it to, and the cap. */
|
|
148
|
+
export interface WalletCapRefusal {
|
|
149
|
+
wallet: Address;
|
|
150
|
+
actual: bigint;
|
|
151
|
+
limit: bigint;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The GuardWalletLimit inside a failed quote, swap or launch, however the root, the lane, the PoolManager and the quoter
|
|
155
|
+
* wrapped it, or null. Takes the revert data, or the viem error that carries it.
|
|
156
|
+
*/
|
|
157
|
+
export declare function walletCapRefusal(error: unknown): WalletCapRefusal | null;
|
|
158
|
+
/** Names Wallet Cap's refusal in `error` (revert data, or the viem error that carries it), or null. */
|
|
159
|
+
export declare function walletCapRevert(error: unknown): string | null;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Cap: HookrRulesWalletCap (`rulesModule(HOOKR_1, "wallet-cap")`), the whole-pot Rules with a per-wallet buy
|
|
3
|
+
* cap on each launch while its pools' launch guards last.
|
|
4
|
+
*
|
|
5
|
+
* - The cap rides in `RulesConfig.potMinBuyQuote`, in the token's raw units: the most of it one wallet may buy while the
|
|
6
|
+
* pool's guard lasts. These Rules bind only a pool with a cap and a launch guard (`guardEndBlock` within the next
|
|
7
|
+
* 100,000 parent blocks), and `potEveryNBuys` stays zero. Every other Rules refuses a nonzero `potMinBuyQuote`, as the
|
|
8
|
+
* core's `checkRulesConfig` does.
|
|
9
|
+
* - Each guarded buy counts what it delivers (the pool's output less Auto Burn) against its wallet: the payer
|
|
10
|
+
* HookrRouter, the forwarder, the family router or the curated Uniswap router names, or for a swap through a router
|
|
11
|
+
* that names none, the transaction's signer. Counts are per token and per launch: the pools of one launch share
|
|
12
|
+
* `Bound.launchedAt`, the second they bound, and a later launch of the token counts from zero, so every pool of a
|
|
13
|
+
* launch takes the same cap, and arb recapture on every pool or none (`checkWalletCapLaunch`). A sell frees no room.
|
|
14
|
+
* The creator's dev buy and the pool's own arb recapture executor are not counted. A buy past the cap reverts
|
|
15
|
+
* `GuardWalletLimit(wallet, actual, limit)`. After the guard nothing is counted or capped.
|
|
16
|
+
* - The Rules answer `extsload(bytes32)`, one word of their storage, and `readWalletCap` reads through it.
|
|
17
|
+
*/
|
|
18
|
+
import { BaseError, decodeErrorResult, encodeAbiParameters, encodeFunctionData, getAddress, keccak256, numberToHex, parseAbi, } from "viem";
|
|
19
|
+
import { address, assert, checkRulesConfig, encodeRulesData, hash, rulesKnobsOf, rulesPoolTerms, uint, } from "./core/index.js";
|
|
20
|
+
import { HOOKR_1, rulesModule } from "./releases.js";
|
|
21
|
+
/**
|
|
22
|
+
* What HookrRulesWalletCap adds to the release Rules' ABI (`rulesAbi`): `extsload`, one word of its storage as Uniswap
|
|
23
|
+
* v4's Extsload reads it, and the refusal of a buy that would take a wallet past its pool's cap.
|
|
24
|
+
*/
|
|
25
|
+
export const WALLET_CAP_ABI = [
|
|
26
|
+
{
|
|
27
|
+
type: "function",
|
|
28
|
+
name: "extsload",
|
|
29
|
+
stateMutability: "view",
|
|
30
|
+
inputs: [{ name: "slot", type: "bytes32", internalType: "bytes32" }],
|
|
31
|
+
outputs: [{ name: "value", type: "bytes32", internalType: "bytes32" }],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "error",
|
|
35
|
+
name: "GuardWalletLimit",
|
|
36
|
+
inputs: [
|
|
37
|
+
{ name: "wallet", type: "address", internalType: "address" },
|
|
38
|
+
{ name: "actual", type: "uint256", internalType: "uint256" },
|
|
39
|
+
{ name: "limit", type: "uint256", internalType: "uint256" },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
/** erc7201:hookr.rules, every HookrRules' State: keccak256(abi.encode(uint256(keccak256("hookr.rules")) - 1)) & ~0xff. */
|
|
44
|
+
export const RULES_SLOT = 0x762913e4bc0f59f7b08c82f07586fa48932548ab5fab99e6db0b09765b4ff900n;
|
|
45
|
+
/** `State.bought`, the State's fourteenth field: what each wallet bought, per subject and launch. */
|
|
46
|
+
const BOUGHT = RULES_SLOT + 13n;
|
|
47
|
+
/** A slot as `extsload` takes it: 32 bytes, wrapping as the EVM's slot arithmetic does. */
|
|
48
|
+
const word = (n) => numberToHex(n & ((1n << 256n) - 1n), { size: 32 });
|
|
49
|
+
/** The slot of `key` in the mapping at `slot`: keccak256(abi.encode(key, slot)). */
|
|
50
|
+
const mapping = (keyType, key, slot) => BigInt(keccak256(encodeAbiParameters([{ type: keyType }, { type: "uint256" }], [key, slot])));
|
|
51
|
+
/**
|
|
52
|
+
* The slot of a pool's first Bound word, `State.pools[poolId]`: keccak256(abi.encode(poolId, RULES_SLOT)). Its launch
|
|
53
|
+
* guard's end is in the next slot (`uint40` of the word) and its cap two slots on (the top 128 bits).
|
|
54
|
+
*/
|
|
55
|
+
export function walletCapPoolSlot(poolId) {
|
|
56
|
+
return word(mapping("bytes32", hash(poolId), RULES_SLOT));
|
|
57
|
+
}
|
|
58
|
+
/** The slot of `wallet`'s count of `subject` on launch `launch`, `State.bought[subject][launch][wallet]`. */
|
|
59
|
+
export function walletCapBoughtSlot(subject, launch, wallet) {
|
|
60
|
+
const bySubject = mapping("address", address(subject), BOUGHT);
|
|
61
|
+
return word(mapping("address", address(wallet), mapping("uint256", uint(BigInt(launch), 32), bySubject)));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The launch in a pool's first Bound word: its top 32 bits, `Bound.launchedAt`. The word packs, low to high,
|
|
65
|
+
* baseFeePips (24 bits), tickSpacing (16), bound (8), quoteIsCurrency0 (8), terms (8), liquidityOwner (160) and
|
|
66
|
+
* launchedAt (32). Null for anything but 32 bytes of hex with the bound flag set: a pool these Rules never bound.
|
|
67
|
+
*/
|
|
68
|
+
export function walletCapLaunch(value) {
|
|
69
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value))
|
|
70
|
+
return null;
|
|
71
|
+
const w = BigInt(value);
|
|
72
|
+
return ((w >> 40n) & 0xffn) === 1n ? Number(w >> 224n) : null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A pool's cap, in the token's raw units, for a share of `supply` (raw units) in basis points: the floor of
|
|
76
|
+
* supply × bps / 10,000, so 200 bps of a supply of 10^27 is 2 × 10^25. It goes in the pool's `rules.potMinBuyQuote`.
|
|
77
|
+
* Refuses a share outside 1 to 10,000 bps and a cap that rounds to zero. hookr.fun offers 0.1% to 10%, 2% by default.
|
|
78
|
+
*/
|
|
79
|
+
export function walletCapAmount(supply, bps) {
|
|
80
|
+
assert(typeof supply === "bigint" && supply > 0n, "Wallet Cap needs the token's supply in raw units");
|
|
81
|
+
const share = BigInt(bps);
|
|
82
|
+
assert(share >= 1n && share <= 10000n, `Wallet Cap share ${share} bps outside 1 to 10,000`);
|
|
83
|
+
const cap = (supply * share) / 10000n;
|
|
84
|
+
assert(cap > 0n, "Wallet Cap's share of this supply rounds to zero tokens");
|
|
85
|
+
return uint(cap, 128);
|
|
86
|
+
}
|
|
87
|
+
/** What a wallet may still buy while the guard lasts: the cap less what it bought, never below zero. */
|
|
88
|
+
export function walletCapRoom(cap, bought) {
|
|
89
|
+
return bought >= cap ? 0n : cap - bought;
|
|
90
|
+
}
|
|
91
|
+
/** The terms only the Wallet Cap Rules hold a pool to, and the parent block to check its guard against. */
|
|
92
|
+
function walletCapTerms(c, blockNumber) {
|
|
93
|
+
assert(typeof c.potMinBuyQuote === "bigint" && c.potMinBuyQuote > 0n, "Wallet Cap needs its cap in potMinBuyQuote: the most of the token, in raw units, one wallet may buy during the guard");
|
|
94
|
+
uint(c.potMinBuyQuote, 128);
|
|
95
|
+
assert(c.guardEndBlock !== 0, "Wallet Cap lasts as long as the launch guard: set guardEndBlock");
|
|
96
|
+
assert(blockNumber !== undefined, "Wallet Cap needs blockNumber, the parent block the launch lands in (readParentBlockNumber), to check its guard");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Refuses a RulesConfig HookrRulesWalletCap's bind would refuse and returns its fee ceilings: `checkRulesConfig` with
|
|
100
|
+
* the cap set aside, plus the Wallet Cap Rules' own terms: a cap above zero in `potMinBuyQuote` (below 2^128), and a
|
|
101
|
+
* launch guard, with its liquidity owner, ending within 100,000 parent blocks after `ctx.blockNumber`, which is required
|
|
102
|
+
* here. `potEveryNBuys` stays zero. Every other Rules refuses a cap: check those with `checkRulesConfig`.
|
|
103
|
+
*/
|
|
104
|
+
export function checkWalletCapRulesConfig(c, ctx) {
|
|
105
|
+
walletCapTerms(c, ctx.blockNumber);
|
|
106
|
+
return checkRulesConfig({ ...c, potMinBuyQuote: 0n }, ctx);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `rulesPoolTerms` for a launch member on the Wallet Cap Rules: every check `checkWalletCapRulesConfig` makes, the
|
|
110
|
+
* pool's caps and gas limits from the Rules' admission (`admission(HookrRoot, rulesModule(HOOKR_1, "wallet-cap").rules)`
|
|
111
|
+
* on the registry), and its `rulesData` with the cap. Refuses the admission of any other Rules. The member binds these
|
|
112
|
+
* Rules as `config.rules` and `l.rules`, cap included, as `rules`; every pool of the launch takes the same cap.
|
|
113
|
+
*/
|
|
114
|
+
export function walletCapPoolTerms(l, ctx, rulesAdmission, advisory, release = HOOKR_1) {
|
|
115
|
+
const m = rulesModule(release, "wallet-cap");
|
|
116
|
+
assert(address(rulesAdmission.implementation, true) === address(m.rules) && hash(rulesAdmission.codeHash) === hash(m.rulesRuntimeHash), `Not the admission of ${release.releaseId}'s Wallet Cap Rules ${m.rules}`);
|
|
117
|
+
walletCapTerms(l.rules, ctx.blockNumber);
|
|
118
|
+
const terms = rulesPoolTerms({ ...l, rules: { ...l.rules, potMinBuyQuote: 0n } }, ctx, rulesAdmission, advisory);
|
|
119
|
+
return { ...terms, rulesData: encodeRulesData(l.rules, rulesKnobsOf(l)) };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Refuses a launch whose members (`buildLaunch`'s) Wallet Cap would leave a way round, or turn against traders, though
|
|
123
|
+
* the Rules bind each pool alone: some pools on the Wallet Cap Rules and some not, capped pools with different caps (a
|
|
124
|
+
* wallet could then buy the largest), or capped pools some with arb recapture and some without (on a capped pool
|
|
125
|
+
* without the lane, the executor's legs count against the trader whose swap set them off). Passes a launch without it.
|
|
126
|
+
*/
|
|
127
|
+
export function checkWalletCapLaunch(members, release = HOOKR_1) {
|
|
128
|
+
const pin = release.rulesModules["wallet-cap"];
|
|
129
|
+
const field = (member, part, key) => member[part]?.[key];
|
|
130
|
+
const capped = (member) => {
|
|
131
|
+
const rules = field(member, "config", "rules");
|
|
132
|
+
return pin !== undefined && typeof rules === "string" && rules.toLowerCase() === pin.rules.toLowerCase();
|
|
133
|
+
};
|
|
134
|
+
if (!members.some(capped))
|
|
135
|
+
return;
|
|
136
|
+
assert(members.every(capped), "Wallet Cap counts a wallet's buys across the launch's pools: every pool binds its Rules, or none does");
|
|
137
|
+
const caps = new Set(members.map((m) => field(m, "rules", "potMinBuyQuote")));
|
|
138
|
+
const [cap] = caps;
|
|
139
|
+
assert(caps.size === 1 && typeof cap === "bigint" && cap > 0n, "Every pool of a Wallet Cap launch takes the same cap, above zero");
|
|
140
|
+
assert(new Set(members.map((m) => field(m, "recapture", "on") === true)).size === 1, "In a Wallet Cap launch every pool takes arb recapture, or none does");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Reads a pool on the Wallet Cap Rules through their `extsload`: its launch, guard end and cap from its Bound
|
|
144
|
+
* (`walletCapPoolSlot` and the two slots after it), then, with a wallet, that wallet's count on the launch
|
|
145
|
+
* (`walletCapBoughtSlot`) and its room. Null for a pool these Rules have not bound: a pool on any other Rules has no
|
|
146
|
+
* wallet cap. A count and its room hold while the guard lasts; after it nothing is counted or capped.
|
|
147
|
+
*/
|
|
148
|
+
export async function readWalletCap(client, query, release = HOOKR_1) {
|
|
149
|
+
assert((query.wallet === undefined) === (query.subject === undefined), "A wallet's count needs the pool's subject");
|
|
150
|
+
const rules = rulesModule(release, "wallet-cap").rules;
|
|
151
|
+
const at = query.blockNumber === undefined ? {} : { blockNumber: query.blockNumber };
|
|
152
|
+
const read = async (slot) => {
|
|
153
|
+
const data = encodeFunctionData({ abi: WALLET_CAP_ABI, functionName: "extsload", args: [word(slot)] });
|
|
154
|
+
const answer = (await client.call({ to: rules, data, ...at })).data;
|
|
155
|
+
assert(typeof answer === "string" && /^0x[0-9a-fA-F]{64}$/.test(answer), `The Wallet Cap Rules ${rules} answered extsload with ${answer}`);
|
|
156
|
+
return answer;
|
|
157
|
+
};
|
|
158
|
+
const pool = BigInt(walletCapPoolSlot(query.poolId));
|
|
159
|
+
const [first, second, third] = await Promise.all([read(pool), read(pool + 1n), read(pool + 2n)]);
|
|
160
|
+
const launch = walletCapLaunch(first);
|
|
161
|
+
if (launch === null)
|
|
162
|
+
return null;
|
|
163
|
+
const cap = BigInt(third) >> 128n;
|
|
164
|
+
assert(cap > 0n, `The Wallet Cap Rules ${rules} hold no cap for pool ${query.poolId}`);
|
|
165
|
+
const guardEndBlock = BigInt(second) & ((1n << 40n) - 1n);
|
|
166
|
+
if (query.wallet === undefined)
|
|
167
|
+
return { launch, guardEndBlock, cap, bought: null, room: null };
|
|
168
|
+
const bought = BigInt(await read(BigInt(walletCapBoughtSlot(query.subject, launch, query.wallet))));
|
|
169
|
+
return { launch, guardEndBlock, cap, bought, room: walletCapRoom(cap, bought) };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The errors a refusal can reach a caller inside: the root's and the lane's (a module's own revert), the PoolManager's
|
|
173
|
+
* (a hook's revert in a swap) and the quoter's (a quoted swap's revert).
|
|
174
|
+
*/
|
|
175
|
+
const REFUSAL_ABI = [
|
|
176
|
+
...WALLET_CAP_ABI,
|
|
177
|
+
...parseAbi([
|
|
178
|
+
"error ModuleCallReverted(address module, bytes4 callSelector, bytes reason)",
|
|
179
|
+
"error WrappedError(address target, bytes4 selector, bytes reason, bytes details)",
|
|
180
|
+
"error SimulationFailed(bytes reason)",
|
|
181
|
+
]),
|
|
182
|
+
];
|
|
183
|
+
/** The most wrappers stripped from one revert, so a malformed one cannot recurse without end. */
|
|
184
|
+
const MAX_WRAPS = 4;
|
|
185
|
+
const hex = (value) => typeof value === "string" && /^0x([0-9a-fA-F]{2})+$/.test(value) ? value : null;
|
|
186
|
+
/** Revert data: a hex string as given, or the data a viem error carries anywhere in its cause chain. */
|
|
187
|
+
function revertData(error) {
|
|
188
|
+
if (typeof error === "string")
|
|
189
|
+
return hex(error);
|
|
190
|
+
for (let e = error, depth = 0; e !== null && typeof e === "object" && depth < 16; depth++) {
|
|
191
|
+
const { raw, data, cause } = e;
|
|
192
|
+
const found = hex(raw) ?? hex(data) ?? hex(data?.data);
|
|
193
|
+
if (found)
|
|
194
|
+
return found;
|
|
195
|
+
e = cause;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The GuardWalletLimit inside a failed quote, swap or launch, however the root, the lane, the PoolManager and the quoter
|
|
201
|
+
* wrapped it, or null. Takes the revert data, or the viem error that carries it.
|
|
202
|
+
*/
|
|
203
|
+
export function walletCapRefusal(error) {
|
|
204
|
+
let data = revertData(error);
|
|
205
|
+
for (let wraps = 0; data !== null && wraps <= MAX_WRAPS; wraps++) {
|
|
206
|
+
let e;
|
|
207
|
+
try {
|
|
208
|
+
e = decodeErrorResult({ abi: REFUSAL_ABI, data });
|
|
209
|
+
}
|
|
210
|
+
catch (caught) {
|
|
211
|
+
if (!(caught instanceof BaseError))
|
|
212
|
+
throw caught;
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (e.errorName === "GuardWalletLimit") {
|
|
216
|
+
const [wallet, actual, limit] = e.args;
|
|
217
|
+
return { wallet: getAddress(wallet), actual, limit };
|
|
218
|
+
}
|
|
219
|
+
if (e.errorName === "ModuleCallReverted" || e.errorName === "WrappedError")
|
|
220
|
+
data = hex(e.args[2]);
|
|
221
|
+
else if (e.errorName === "SimulationFailed")
|
|
222
|
+
data = hex(e.args[0]);
|
|
223
|
+
else
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
/** Names Wallet Cap's refusal in `error` (revert data, or the viem error that carries it), or null. */
|
|
229
|
+
export function walletCapRevert(error) {
|
|
230
|
+
const r = walletCapRefusal(error);
|
|
231
|
+
return r
|
|
232
|
+
? `The buy would bring ${r.wallet}'s buys of the subject during the launch guard to ${r.actual}, above the pool's wallet cap ${r.limit}`
|
|
233
|
+
: null;
|
|
234
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hookr-sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Build on Hookr
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Build on Hookr, the Uniswap v4 hook on Robinhood Chain: launch tokens with Hook Blocks, open pools, quote and swap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://hookr.fun/docs",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"v4-hooks",
|
|
13
13
|
"hooks",
|
|
14
14
|
"hook-blocks",
|
|
15
|
-
"
|
|
15
|
+
"token-launch",
|
|
16
16
|
"robinhood-chain",
|
|
17
17
|
"viem",
|
|
18
18
|
"sdk"
|