hookr-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Pool key/id, the native block's config encoding, stack limits, and the two coordinator calls
3
+ * that open a market.
4
+ *
5
+ * `poolKeyFor`, `poolIdFor`, the config encoder and `stackLimitsFor` are pure, faithful copies
6
+ * of `src/lib/v2-encode.ts` in the Hookr app (`v2PoolKey`, `v2PoolId`, `encodeV2Config`,
7
+ * `v2StackLimits`) — nothing here reads a chain, so `src/test/hookr-sdk-parity.test.ts` proves
8
+ * them by calling both on the same fixed inputs and asserting byte-for-byte agreement.
9
+ *
10
+ * `buildNewTokenMarketCall` and `buildExistingTokenMarketCall` assemble the exact
11
+ * `openNewTokenMarket` / `openExistingTokenMarket` calldata `src/lib/v2-launch-execute.ts`
12
+ * produces from a reviewed plan (`v2ExecuteTransactions`), field for field with
13
+ * `src/lib/v2-launch-plan.ts`'s `buildV2LaunchPlan`. Parity reached, with one deliberate scope
14
+ * cut, stated here rather than left implicit:
15
+ *
16
+ * - `caps` (the native block's `validateStack` answer — its surcharge and take-bps ceilings)
17
+ * and `protocolShareBps` / `protocolRecipient` (the coordinator's and block's own reads for
18
+ * this creator) are NOT re-derived here: they are live chain reads in the app
19
+ * (`buildV2LaunchPlan` calls `validateStack`, `protocolShareBps`, and reads the block's
20
+ * `protocolRecipient()`), and this package does not carry a client of its own. Pass them in
21
+ * — read with viem's `readContract` against `abi.ts`'s ABIs — exactly as the app's plan
22
+ * builder does before it ever encodes a call.
23
+ * - The opening `sqrtPriceX96` (and, on the new-token lane, the founding band) is a caller
24
+ * input too. The app derives it from a chosen price via tick math
25
+ * (`v2SqrtPriceForOpeningPrice`, `v2FoundingBand`) that depends on Uniswap's `TickMath`
26
+ * library; reimplementing and re-verifying that library was out of this pass's time box, so
27
+ * it is not duplicated here. Compute it with the app's own `v2-encode.ts` (or an equivalent),
28
+ * or read it back from an already-opened pool with `reads.ts`.
29
+ * - The dev buy (`initialBuy`) is accepted as an already-priced input, not computed: sizing one
30
+ * against the founding band (`src/lib/v2-dev-buy.ts`) is a further, separate pricing
31
+ * concern this pass did not reach. Omit it for a plain launch with no creator buy.
32
+ *
33
+ * Everything downstream of those three reads — the pool id, the config bytes, the stack limits,
34
+ * and the final struct-for-struct call — is exact, proven, and the parity test's fixed-input
35
+ * cases exercise the full assembly with a representative `caps`/`sqrtPriceX96` pair.
36
+ */
37
+ import { type Address, type Hex } from "viem";
38
+ import { hookrMarketCoordinatorV5Abi } from "./abi.js";
39
+ import { type RuleBlock } from "./rules.js";
40
+ /** `HookrMarketCoordinatorV5.DYNAMIC_FEE_FLAG`: the Uniswap v4 dynamic-fee sentinel in every V2 PoolKey. */
41
+ export declare const DYNAMIC_FEE = 8388608;
42
+ export type PoolKey = Readonly<{
43
+ currency0: Address;
44
+ currency1: Address;
45
+ fee: number;
46
+ tickSpacing: number;
47
+ hooks: Address;
48
+ }>;
49
+ /** `subject < quote` by numeric value; ETH (the zero address) is therefore never `currency1`. */
50
+ export declare function subjectIsCurrency0(subject: Address, quote: Address): boolean;
51
+ /**
52
+ * `HookrMarketCoordinatorV5._poolKey`: currencies sorted numerically, `fee = DYNAMIC_FEE`,
53
+ * `hooks` the root hook (defaults to `ADDRESSES.root`, the one live root).
54
+ */
55
+ export declare function poolKeyFor(input: {
56
+ subject: Address;
57
+ quote: Address;
58
+ tickSpacing: number;
59
+ hooks?: Address;
60
+ }): PoolKey;
61
+ /** `PoolId = keccak256(abi.encode(key))`. */
62
+ export declare function poolIdFor(key: PoolKey): Hex;
63
+ export type NativeConfig = Readonly<{
64
+ poolId: Hex;
65
+ kernel: Address;
66
+ subject: Address;
67
+ quote: Address;
68
+ lockedLiquidityProvider: Address;
69
+ guardEndBlock: bigint;
70
+ baseFeePips: number;
71
+ maxFeePips: number;
72
+ snipeTaxPips: number;
73
+ surgeSens: number;
74
+ burnBps: number;
75
+ lpBps: number;
76
+ potBps: number;
77
+ royaltyBps: number;
78
+ potEveryNBuys: number;
79
+ maxBuyQuoteAmount: bigint;
80
+ potMinBuyWei: bigint;
81
+ royaltyTo: Address;
82
+ protocolRecipient: Address;
83
+ protocolShareBps: number;
84
+ }>;
85
+ /** `abi.encode(Config)`: one static tuple, exactly 640 bytes. */
86
+ export declare function encodeNativeBlockConfig(config: NativeConfig): Hex;
87
+ /** `HookrNativeMechanicsBlockV2.MAX_GUARD_BLOCKS`. */
88
+ export declare const MAX_GUARD_BLOCKS = 100000;
89
+ /** `guardEndBlock` for a guard of `guardBlocks` blocks from `contractBlock`; `0n` (off) at or below zero. */
90
+ export declare function guardEndBlockFor(contractBlock: bigint, guardBlocks: number): bigint;
91
+ /** The native block's registered callback gas; `limits.maxTotalModuleGas` in every fixture. */
92
+ export declare const MODULE_GAS = 2000000;
93
+ export declare const ZERO_BYTES32: Hex;
94
+ /** What `HookrNativeMechanicsBlockV2.validateStack` returns for a config: the surcharge and
95
+ * take-bps ceilings this creator's stack is admitted at. A live chain read; see the module doc. */
96
+ export type StackCaps = Readonly<{
97
+ configHash: Hex;
98
+ maxLpFeeSurchargePips: number;
99
+ maxSpecifiedQuoteTakeBps: number;
100
+ maxUnspecifiedQuoteTakeBps: number;
101
+ maxSubjectTakeBps: number;
102
+ }>;
103
+ export type StackLimits = Readonly<{
104
+ baseLpFeePips: number;
105
+ maxLpFeePips: number;
106
+ maxSpecifiedQuoteTakeBps: number;
107
+ maxUnspecifiedQuoteTakeBps: number;
108
+ maxSubjectTakeBps: number;
109
+ maxTotalModuleGas: number;
110
+ trustedRouter: Address;
111
+ trustedQuoter: Address;
112
+ correctionExecutor: Address;
113
+ correctionCreator: Address;
114
+ correctionMaxVolumeBps: number;
115
+ correctionMinProfitQuote: bigint;
116
+ correctionFeePolicyId: Hex;
117
+ }>;
118
+ /**
119
+ * 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`
121
+ * / `ADDRESSES.quoter` today — the app's own equivalent takes them as a parameter only so its
122
+ * tests can fake them; a real launch always wires the same two addresses this package does.
123
+ */
124
+ export declare function stackLimitsFor(config: NativeConfig, caps: StackCaps): StackLimits;
125
+ export type ModuleSelection = Readonly<{
126
+ moduleId: Hex;
127
+ config: Hex;
128
+ }>;
129
+ export type MarketParams = Readonly<{
130
+ quote: Address;
131
+ subjectAmount: bigint;
132
+ quoteAmount: bigint;
133
+ lpFeeRecipient: Address;
134
+ tickSpacing: number;
135
+ sqrtPriceX96: bigint;
136
+ kernelId: Hex;
137
+ modules: readonly ModuleSelection[];
138
+ limits: StackLimits;
139
+ }>;
140
+ export type InitialBuyParams = Readonly<{
141
+ quoteAmountIn: bigint;
142
+ subjectAmountOutMinimum: bigint;
143
+ deadline: bigint;
144
+ moduleData: Hex;
145
+ }>;
146
+ export type NewTokenArgs = Readonly<{
147
+ name: string;
148
+ symbol: string;
149
+ tagline: string;
150
+ logoURI: string;
151
+ expectedCreator: Address;
152
+ totalSupply: bigint;
153
+ deploymentSalt: Hex;
154
+ market: MarketParams;
155
+ initialBuy: InitialBuyParams;
156
+ }>;
157
+ export type ExistingTokenArgs = Readonly<{
158
+ subject: Address;
159
+ market: MarketParams;
160
+ }>;
161
+ /** The rule numbers a market's stack carries, as the builders take them. */
162
+ export type MarketRulesInput = Readonly<{
163
+ /** The stack's Base LP fee, in pips. */
164
+ baseFeePips: number;
165
+ blocks: readonly RuleBlock[];
166
+ /** Guard length in contract blocks; 0 (default) turns the guard off. New-token lane only. */
167
+ guardBlocks?: number;
168
+ /** Per-block max buy in quote raw units while the guard is up. New-token lane only. */
169
+ maxBuyQuoteAmount?: bigint;
170
+ royaltyBps?: number;
171
+ /** Required exactly when `royaltyBps` is nonzero. */
172
+ royaltyTo?: Address | null;
173
+ }>;
174
+ export type CoordinatorCall<Args extends readonly unknown[]> = Readonly<{
175
+ address: Address;
176
+ abi: typeof hookrMarketCoordinatorV5Abi;
177
+ functionName: "openNewTokenMarket" | "openExistingTokenMarket";
178
+ args: Args;
179
+ value: bigint;
180
+ }>;
181
+ /**
182
+ * Build the exact `openNewTokenMarket` call: pool key/id from `subject` (the previewed token
183
+ * address — `marketCoordinator.previewNewTokenAddress`) and `quote`, the native config from
184
+ * `rules`, the one native-mechanics module, and the stack limits from `caps`. `value` is the
185
+ * dev buy's quote amount for a native-ETH quote, else 0 (the router pulls an ERC-20 dev buy from
186
+ * an allowance, exactly like the app).
187
+ */
188
+ export declare function buildNewTokenMarketCall(input: {
189
+ creator: Address;
190
+ name: string;
191
+ symbol: string;
192
+ tagline: string;
193
+ logoURI: string;
194
+ totalSupply: bigint;
195
+ deploymentSalt: Hex;
196
+ intentId: Hex;
197
+ /** The coordinator's previewed token address for this intent; also the market's `subject`. */
198
+ expectedToken: Address;
199
+ quote: Address;
200
+ tickSpacing: number;
201
+ sqrtPriceX96: bigint;
202
+ /** Founding-position fee recipient; defaults to `creator`. */
203
+ lpFeeRecipient?: Address;
204
+ rules: MarketRulesInput;
205
+ /** The contract clock's block at build time (`context.contractBlock` in the app); anchors the guard. */
206
+ contractBlock: bigint;
207
+ protocolRecipient: Address;
208
+ protocolShareBps: number;
209
+ caps: StackCaps;
210
+ initialBuy?: Readonly<{
211
+ quoteAmountIn: bigint;
212
+ subjectAmountOutMinimum: bigint;
213
+ deadline: bigint;
214
+ moduleData?: Hex;
215
+ }>;
216
+ }): CoordinatorCall<readonly [NewTokenArgs, Hex, Address]>;
217
+ /**
218
+ * Build the exact `openExistingTokenMarket` call. The existing-asset lane carries no guard, no
219
+ * founding position and no dev buy: `value` is always 0, `lpFeeRecipient` is always the zero
220
+ * address, and any guard field in `rules` is ignored (zeroed), exactly as the app's plan builder
221
+ * refuses a guard on this lane rather than silently applying it.
222
+ */
223
+ export declare function buildExistingTokenMarketCall(input: {
224
+ subject: Address;
225
+ quote: Address;
226
+ tickSpacing: number;
227
+ sqrtPriceX96: bigint;
228
+ rules: MarketRulesInput;
229
+ protocolRecipient: Address;
230
+ protocolShareBps: number;
231
+ caps: StackCaps;
232
+ }): CoordinatorCall<readonly [ExistingTokenArgs]>;
233
+ /** The calldata `encodeFunctionData` would produce for a built call, for sending with viem directly. */
234
+ export declare function encodeMarketCall(call: CoordinatorCall<readonly unknown[]>): Hex;
package/dist/market.js ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Pool key/id, the native block's config encoding, stack limits, and the two coordinator calls
3
+ * that open a market.
4
+ *
5
+ * `poolKeyFor`, `poolIdFor`, the config encoder and `stackLimitsFor` are pure, faithful copies
6
+ * of `src/lib/v2-encode.ts` in the Hookr app (`v2PoolKey`, `v2PoolId`, `encodeV2Config`,
7
+ * `v2StackLimits`) — nothing here reads a chain, so `src/test/hookr-sdk-parity.test.ts` proves
8
+ * them by calling both on the same fixed inputs and asserting byte-for-byte agreement.
9
+ *
10
+ * `buildNewTokenMarketCall` and `buildExistingTokenMarketCall` assemble the exact
11
+ * `openNewTokenMarket` / `openExistingTokenMarket` calldata `src/lib/v2-launch-execute.ts`
12
+ * produces from a reviewed plan (`v2ExecuteTransactions`), field for field with
13
+ * `src/lib/v2-launch-plan.ts`'s `buildV2LaunchPlan`. Parity reached, with one deliberate scope
14
+ * cut, stated here rather than left implicit:
15
+ *
16
+ * - `caps` (the native block's `validateStack` answer — its surcharge and take-bps ceilings)
17
+ * and `protocolShareBps` / `protocolRecipient` (the coordinator's and block's own reads for
18
+ * this creator) are NOT re-derived here: they are live chain reads in the app
19
+ * (`buildV2LaunchPlan` calls `validateStack`, `protocolShareBps`, and reads the block's
20
+ * `protocolRecipient()`), and this package does not carry a client of its own. Pass them in
21
+ * — read with viem's `readContract` against `abi.ts`'s ABIs — exactly as the app's plan
22
+ * builder does before it ever encodes a call.
23
+ * - The opening `sqrtPriceX96` (and, on the new-token lane, the founding band) is a caller
24
+ * input too. The app derives it from a chosen price via tick math
25
+ * (`v2SqrtPriceForOpeningPrice`, `v2FoundingBand`) that depends on Uniswap's `TickMath`
26
+ * library; reimplementing and re-verifying that library was out of this pass's time box, so
27
+ * it is not duplicated here. Compute it with the app's own `v2-encode.ts` (or an equivalent),
28
+ * or read it back from an already-opened pool with `reads.ts`.
29
+ * - The dev buy (`initialBuy`) is accepted as an already-priced input, not computed: sizing one
30
+ * against the founding band (`src/lib/v2-dev-buy.ts`) is a further, separate pricing
31
+ * concern this pass did not reach. Omit it for a plain launch with no creator buy.
32
+ *
33
+ * Everything downstream of those three reads — the pool id, the config bytes, the stack limits,
34
+ * and the final struct-for-struct call — is exact, proven, and the parity test's fixed-input
35
+ * cases exercise the full assembly with a representative `caps`/`sqrtPriceX96` pair.
36
+ */
37
+ import { encodeAbiParameters, encodeFunctionData, keccak256 } from "viem";
38
+ import { hookrMarketCoordinatorV5Abi } from "./abi.js";
39
+ import { ADDRESSES, KERNEL_ID, MODULE_ID, NATIVE_ADDRESS } from "./chain.js";
40
+ import { compileRuleBlocks } from "./rules.js";
41
+ // -----------------------------------------------------------------------------------------------
42
+ // Pool key and pool id
43
+ // -----------------------------------------------------------------------------------------------
44
+ /** `HookrMarketCoordinatorV5.DYNAMIC_FEE_FLAG`: the Uniswap v4 dynamic-fee sentinel in every V2 PoolKey. */
45
+ export const DYNAMIC_FEE = 0x800000;
46
+ const POOL_KEY_COMPONENTS = [
47
+ { name: "currency0", type: "address" },
48
+ { name: "currency1", type: "address" },
49
+ { name: "fee", type: "uint24" },
50
+ { name: "tickSpacing", type: "int24" },
51
+ { name: "hooks", type: "address" },
52
+ ];
53
+ const POOL_KEY_ABI = [{ type: "tuple", components: POOL_KEY_COMPONENTS }];
54
+ /** `subject < quote` by numeric value; ETH (the zero address) is therefore never `currency1`. */
55
+ export function subjectIsCurrency0(subject, quote) {
56
+ const s = BigInt(subject);
57
+ const q = BigInt(quote);
58
+ if (s === q)
59
+ throw new Error("The subject and the quote are the same currency.");
60
+ return s < q;
61
+ }
62
+ /**
63
+ * `HookrMarketCoordinatorV5._poolKey`: currencies sorted numerically, `fee = DYNAMIC_FEE`,
64
+ * `hooks` the root hook (defaults to `ADDRESSES.root`, the one live root).
65
+ */
66
+ export function poolKeyFor(input) {
67
+ const subjectFirst = subjectIsCurrency0(input.subject, input.quote);
68
+ return Object.freeze({
69
+ currency0: subjectFirst ? input.subject : input.quote,
70
+ currency1: subjectFirst ? input.quote : input.subject,
71
+ fee: DYNAMIC_FEE,
72
+ tickSpacing: input.tickSpacing,
73
+ hooks: input.hooks ?? ADDRESSES.root,
74
+ });
75
+ }
76
+ /** `PoolId = keccak256(abi.encode(key))`. */
77
+ export function poolIdFor(key) {
78
+ return keccak256(encodeAbiParameters(POOL_KEY_ABI, [key]));
79
+ }
80
+ // -----------------------------------------------------------------------------------------------
81
+ // HookrNativeMechanicsBlockV2.Config
82
+ // -----------------------------------------------------------------------------------------------
83
+ /** The 20 fields of `HookrNativeMechanicsBlockV2.Config`, in the struct's own order. */
84
+ const CONFIG_COMPONENTS = [
85
+ { name: "poolId", type: "bytes32" },
86
+ { name: "kernel", type: "address" },
87
+ { name: "subject", type: "address" },
88
+ { name: "quote", type: "address" },
89
+ { name: "lockedLiquidityProvider", type: "address" },
90
+ { name: "guardEndBlock", type: "uint40" },
91
+ { name: "baseFeePips", type: "uint24" },
92
+ { name: "maxFeePips", type: "uint24" },
93
+ { name: "snipeTaxPips", type: "uint24" },
94
+ { name: "surgeSens", type: "uint16" },
95
+ { name: "burnBps", type: "uint16" },
96
+ { name: "lpBps", type: "uint16" },
97
+ { name: "potBps", type: "uint16" },
98
+ { name: "royaltyBps", type: "uint16" },
99
+ { name: "potEveryNBuys", type: "uint32" },
100
+ { name: "maxBuyQuoteAmount", type: "uint96" },
101
+ { name: "potMinBuyWei", type: "uint96" },
102
+ { name: "royaltyTo", type: "address" },
103
+ { name: "protocolRecipient", type: "address" },
104
+ { name: "protocolShareBps", type: "uint24" },
105
+ ];
106
+ const CONFIG_ABI = [{ type: "tuple", components: CONFIG_COMPONENTS }];
107
+ /** `abi.encode(Config)`: one static tuple, exactly 640 bytes. */
108
+ export function encodeNativeBlockConfig(config) {
109
+ return encodeAbiParameters(CONFIG_ABI, [{ ...config, guardEndBlock: Number(config.guardEndBlock) }]);
110
+ }
111
+ /** `HookrNativeMechanicsBlockV2.MAX_GUARD_BLOCKS`. */
112
+ export const MAX_GUARD_BLOCKS = 100_000;
113
+ /** `guardEndBlock` for a guard of `guardBlocks` blocks from `contractBlock`; `0n` (off) at or below zero. */
114
+ export function guardEndBlockFor(contractBlock, guardBlocks) {
115
+ if (guardBlocks <= 0)
116
+ return 0n;
117
+ return contractBlock + BigInt(Math.min(Math.round(guardBlocks), MAX_GUARD_BLOCKS));
118
+ }
119
+ // -----------------------------------------------------------------------------------------------
120
+ // Stack limits
121
+ // -----------------------------------------------------------------------------------------------
122
+ /** The native block's registered callback gas; `limits.maxTotalModuleGas` in every fixture. */
123
+ export const MODULE_GAS = 2_000_000;
124
+ export const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
125
+ /**
126
+ * 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`
128
+ * / `ADDRESSES.quoter` today — the app's own equivalent takes them as a parameter only so its
129
+ * tests can fake them; a real launch always wires the same two addresses this package does.
130
+ */
131
+ export function stackLimitsFor(config, caps) {
132
+ return Object.freeze({
133
+ baseLpFeePips: config.baseFeePips,
134
+ maxLpFeePips: config.baseFeePips + caps.maxLpFeeSurchargePips,
135
+ maxSpecifiedQuoteTakeBps: caps.maxSpecifiedQuoteTakeBps,
136
+ maxUnspecifiedQuoteTakeBps: caps.maxUnspecifiedQuoteTakeBps,
137
+ maxSubjectTakeBps: caps.maxSubjectTakeBps,
138
+ maxTotalModuleGas: MODULE_GAS,
139
+ trustedRouter: ADDRESSES.router,
140
+ trustedQuoter: ADDRESSES.quoter,
141
+ correctionExecutor: NATIVE_ADDRESS,
142
+ correctionCreator: NATIVE_ADDRESS,
143
+ correctionMaxVolumeBps: 0,
144
+ correctionMinProfitQuote: 0n,
145
+ correctionFeePolicyId: ZERO_BYTES32,
146
+ });
147
+ }
148
+ const NO_INITIAL_BUY = Object.freeze({
149
+ quoteAmountIn: 0n,
150
+ subjectAmountOutMinimum: 0n,
151
+ deadline: 0n,
152
+ moduleData: "0x",
153
+ });
154
+ function buildNativeConfig(input) {
155
+ const { lane, rules } = input;
156
+ const compiled = compileRuleBlocks(rules.blocks, rules.baseFeePips);
157
+ const guardOn = lane === "new" && (rules.guardBlocks ?? 0) > 0;
158
+ return Object.freeze({
159
+ poolId: input.poolId,
160
+ kernel: ADDRESSES.root,
161
+ subject: input.subject,
162
+ quote: input.quote,
163
+ lockedLiquidityProvider: guardOn ? ADDRESSES.coordinator : NATIVE_ADDRESS,
164
+ guardEndBlock: guardOn ? guardEndBlockFor(input.contractBlock, compiled.guardBlocks) : 0n,
165
+ baseFeePips: compiled.baseFeePips,
166
+ maxFeePips: compiled.maxFeePips,
167
+ snipeTaxPips: lane === "new" ? compiled.snipeTaxPips : 0,
168
+ surgeSens: compiled.surgeSens,
169
+ burnBps: compiled.burnBps,
170
+ lpBps: compiled.lpBps,
171
+ potBps: compiled.potBps,
172
+ royaltyBps: rules.royaltyBps ?? 0,
173
+ potEveryNBuys: compiled.potEveryNBuys,
174
+ maxBuyQuoteAmount: lane === "new" ? rules.maxBuyQuoteAmount ?? 0n : 0n,
175
+ potMinBuyWei: compiled.potMinBuyWei,
176
+ royaltyTo: rules.royaltyTo ?? NATIVE_ADDRESS,
177
+ protocolRecipient: input.protocolRecipient,
178
+ protocolShareBps: input.protocolShareBps,
179
+ });
180
+ }
181
+ /**
182
+ * Build the exact `openNewTokenMarket` call: pool key/id from `subject` (the previewed token
183
+ * address — `marketCoordinator.previewNewTokenAddress`) and `quote`, the native config from
184
+ * `rules`, the one native-mechanics module, and the stack limits from `caps`. `value` is the
185
+ * dev buy's quote amount for a native-ETH quote, else 0 (the router pulls an ERC-20 dev buy from
186
+ * an allowance, exactly like the app).
187
+ */
188
+ export function buildNewTokenMarketCall(input) {
189
+ const subject = input.expectedToken;
190
+ const poolId = poolIdFor(poolKeyFor({ subject, quote: input.quote, tickSpacing: input.tickSpacing }));
191
+ const config = buildNativeConfig({
192
+ lane: "new",
193
+ rules: input.rules,
194
+ poolId,
195
+ subject,
196
+ quote: input.quote,
197
+ contractBlock: input.contractBlock,
198
+ protocolRecipient: input.protocolRecipient,
199
+ protocolShareBps: input.protocolShareBps,
200
+ });
201
+ const encodedConfig = encodeNativeBlockConfig(config);
202
+ const limits = stackLimitsFor(config, input.caps);
203
+ const market = {
204
+ quote: input.quote,
205
+ subjectAmount: input.totalSupply,
206
+ quoteAmount: 0n,
207
+ lpFeeRecipient: input.lpFeeRecipient ?? input.creator,
208
+ tickSpacing: input.tickSpacing,
209
+ sqrtPriceX96: input.sqrtPriceX96,
210
+ kernelId: KERNEL_ID,
211
+ modules: [{ moduleId: MODULE_ID, config: encodedConfig }],
212
+ limits,
213
+ };
214
+ const initialBuy = input.initialBuy
215
+ ? {
216
+ quoteAmountIn: input.initialBuy.quoteAmountIn,
217
+ subjectAmountOutMinimum: input.initialBuy.subjectAmountOutMinimum,
218
+ deadline: input.initialBuy.deadline,
219
+ moduleData: input.initialBuy.moduleData ?? "0x",
220
+ }
221
+ : NO_INITIAL_BUY;
222
+ const args = {
223
+ name: input.name,
224
+ symbol: input.symbol,
225
+ tagline: input.tagline,
226
+ logoURI: input.logoURI,
227
+ expectedCreator: input.creator,
228
+ totalSupply: input.totalSupply,
229
+ deploymentSalt: input.deploymentSalt,
230
+ market,
231
+ initialBuy,
232
+ };
233
+ const value = input.quote === NATIVE_ADDRESS ? initialBuy.quoteAmountIn : 0n;
234
+ return Object.freeze({
235
+ address: ADDRESSES.coordinator,
236
+ abi: hookrMarketCoordinatorV5Abi,
237
+ functionName: "openNewTokenMarket",
238
+ args: [args, input.intentId, input.expectedToken],
239
+ value,
240
+ });
241
+ }
242
+ /**
243
+ * Build the exact `openExistingTokenMarket` call. The existing-asset lane carries no guard, no
244
+ * founding position and no dev buy: `value` is always 0, `lpFeeRecipient` is always the zero
245
+ * address, and any guard field in `rules` is ignored (zeroed), exactly as the app's plan builder
246
+ * refuses a guard on this lane rather than silently applying it.
247
+ */
248
+ export function buildExistingTokenMarketCall(input) {
249
+ const poolId = poolIdFor(poolKeyFor({ subject: input.subject, quote: input.quote, tickSpacing: input.tickSpacing }));
250
+ const config = buildNativeConfig({
251
+ lane: "existing",
252
+ rules: input.rules,
253
+ poolId,
254
+ subject: input.subject,
255
+ quote: input.quote,
256
+ contractBlock: 0n,
257
+ protocolRecipient: input.protocolRecipient,
258
+ protocolShareBps: input.protocolShareBps,
259
+ });
260
+ const encodedConfig = encodeNativeBlockConfig(config);
261
+ const limits = stackLimitsFor(config, input.caps);
262
+ const market = {
263
+ quote: input.quote,
264
+ subjectAmount: 0n,
265
+ quoteAmount: 0n,
266
+ lpFeeRecipient: NATIVE_ADDRESS,
267
+ tickSpacing: input.tickSpacing,
268
+ sqrtPriceX96: input.sqrtPriceX96,
269
+ kernelId: KERNEL_ID,
270
+ modules: [{ moduleId: MODULE_ID, config: encodedConfig }],
271
+ limits,
272
+ };
273
+ const args = { subject: input.subject, market };
274
+ return Object.freeze({
275
+ address: ADDRESSES.coordinator,
276
+ abi: hookrMarketCoordinatorV5Abi,
277
+ functionName: "openExistingTokenMarket",
278
+ args: [args],
279
+ value: 0n,
280
+ });
281
+ }
282
+ /** The calldata `encodeFunctionData` would produce for a built call, for sending with viem directly. */
283
+ export function encodeMarketCall(call) {
284
+ return encodeFunctionData({
285
+ abi: call.abi,
286
+ functionName: call.functionName,
287
+ args: call.args,
288
+ });
289
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Read a market from the coordinator, list markets from `MarketCreated` logs, and read a pool's
3
+ * frozen rules from the stack registry.
4
+ *
5
+ * `readMarket` mirrors `HookrMarketCoordinatorV5.getMarket`; `listMarkets` mirrors the log shape
6
+ * and scan-from block in the app's `src/lib/v2-discover-server.ts` (`v2MarketRowFromLog`,
7
+ * `V2_MARKET_SCAN_FROM_BLOCK`); `readPoolRules` mirrors the registry read the app's agent skill
8
+ * (`src/lib/agent-skill.ts`, "Read a pool's rules") describes in prose: `stack(poolId)` for the
9
+ * module count and limits, then `moduleAt(poolId, i)` for each module, decoding the
10
+ * native-mechanics module's `config` with `market.ts`'s `NativeConfig` tuple.
11
+ */
12
+ import { type Address, type Hex, type PublicClient } from "viem";
13
+ import type { NativeConfig, StackLimits } from "./market.js";
14
+ /** `HookrMarketCoordinatorV5.MarketOrigin`: 1 is a token the coordinator deployed, 2 an existing one. */
15
+ export type MarketOrigin = "new-token" | "existing-token";
16
+ export type Market = Readonly<{
17
+ live: boolean;
18
+ origin: MarketOrigin;
19
+ subject: Address;
20
+ quote: Address;
21
+ creator: Address;
22
+ kernel: Address;
23
+ lpFeeRecipient: Address;
24
+ kernelId: Hex;
25
+ stackHash: Hex;
26
+ poolId: Hex;
27
+ sqrtPriceX96: bigint;
28
+ tickSpacing: number;
29
+ subjectSupplied: bigint;
30
+ quoteSupplied: bigint;
31
+ subjectUsed: bigint;
32
+ quoteUsed: bigint;
33
+ cumulativeFee0: bigint;
34
+ cumulativeFee1: bigint;
35
+ openedAtBlock: bigint;
36
+ launchIntentId: Hex;
37
+ initialBuyRouter: Address;
38
+ creatorAllocation: bigint;
39
+ initialBuyQuoteIn: bigint;
40
+ initialBuySubjectOut: bigint;
41
+ initialBuySubjectOutMinimum: bigint;
42
+ initialBuyModuleDataHash: Hex;
43
+ }>;
44
+ /** `marketCoordinator.getMarket(poolId)`. `live` is false for a `poolId` the coordinator never opened. */
45
+ export declare function readMarket(client: PublicClient, poolId: Hex): Promise<Market>;
46
+ /** The coordinator's deploy block on chain 4663: no `MarketCreated` log can sit before it. */
47
+ export declare const MARKET_SCAN_FROM_BLOCK = 57229676n;
48
+ export declare const MARKET_CREATED_TOPIC: Hex;
49
+ /** One decoded `MarketCreated` log: every field the coordinator emitted, nothing derived. */
50
+ export type MarketRow = Readonly<{
51
+ poolId: Hex;
52
+ subject: Address;
53
+ quote: Address;
54
+ creator: Address;
55
+ origin: MarketOrigin;
56
+ kernel: Address;
57
+ sqrtPriceX96: bigint;
58
+ tickSpacing: number;
59
+ blockNumber: bigint;
60
+ txHash: Hex;
61
+ logIndex: number;
62
+ }>;
63
+ /**
64
+ * Every `MarketCreated` log the coordinator emitted in `[fromBlock, toBlock]`, oldest first.
65
+ *
66
+ * `root`, when given, is checked against every row's `kernel` rather than passed to the log
67
+ * filter: `kernel` is not an indexed topic, and there is exactly one live root today
68
+ * (`ADDRESSES.root`), so every row already carries it. A future second root would need a
69
+ * client-side filter here, which is exactly what this parameter is for.
70
+ */
71
+ export declare function listMarkets(client: PublicClient, options?: Readonly<{
72
+ root?: Address;
73
+ fromBlock?: bigint;
74
+ toBlock?: bigint;
75
+ }>): Promise<readonly MarketRow[]>;
76
+ /** The inverse of `market.ts`'s `encodeNativeBlockConfig`. */
77
+ export declare function decodeNativeBlockConfig(data: Hex): NativeConfig;
78
+ export type PoolRules = Readonly<{
79
+ /** False when the registry has no stack for this `poolId` at all. */
80
+ configured: boolean;
81
+ limits: StackLimits | null;
82
+ /** The native-mechanics module's decoded config; null when the pool carries no such module. */
83
+ nativeConfig: NativeConfig | null;
84
+ }>;
85
+ /**
86
+ * `stackRegistry.stack(poolId)` for `moduleCount`/`limits`, then `stackRegistry.moduleAt` for
87
+ * each module, decoding the native-mechanics module's `config` bytes. Every other module (the
88
+ * directional tax block, when a pool carries one) is skipped: its config schema is not modeled
89
+ * by this package.
90
+ */
91
+ export declare function readPoolRules(client: PublicClient, poolId: Hex): Promise<PoolRules>;