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,45 @@
1
+ import type { Address, Hex } from "viem";
2
+ /**
3
+ * Robinhood Chain, and Hookr's one live root hook on it.
4
+ *
5
+ * Every value here is a literal copy of `src/lib/v2-deployments.ts` (the addresses, read from
6
+ * the app's own deployment evidence) and `src/lib/chain.ts` (the RPC and explorer origin) in the
7
+ * Hookr app. `src/test/hookr-sdk-parity.test.ts` in that app asserts this file agrees with both,
8
+ * so a redeploy or an RPC change that is not mirrored here fails that suite.
9
+ *
10
+ * There is exactly one root today: `HookrModularHookV6`, deployed as `ADDRESSES.root`. Every V2
11
+ * pool's `PoolKey.hooks` is that address.
12
+ */
13
+ /** Robinhood Chain's id. */
14
+ export declare const CHAIN_ID = 4663;
15
+ /** The chain's public, keyless RPC endpoint. */
16
+ export declare const RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
17
+ /** Origin of the chain's block explorer (Blockscout). No trailing slash. */
18
+ export declare const EXPLORER_URL = "https://robinhoodchain.blockscout.com";
19
+ /** The V2 contracts and ids a third-party integration calls or reads. */
20
+ export declare const ADDRESSES: Readonly<{
21
+ /** `HookrModularHookV6` — `hooks` in every V2 `PoolKey`. The one live root. */
22
+ root: Address;
23
+ /** `HookrMarketCoordinatorV5` — opens markets, collects founding fees, resolves the share tier. */
24
+ coordinator: Address;
25
+ /** `HookrKernelRouterV3` — the swap router that pays into and out of the pot. */
26
+ router: Address;
27
+ /** `HookrKernelQuoterV1` — the quoter that runs the full module loop. */
28
+ quoter: Address;
29
+ /** `HookrNativeMechanicsBlockV2` — the one native block every V2 stack carries; also where claims are paid. */
30
+ nativeBlock: Address;
31
+ /** `HookrStackRegistryV2` — read-only, a pool's frozen config and module list. */
32
+ registry: Address;
33
+ /** `HookrModuleCatalogV1` — the on-chain catalog of admitted modules. */
34
+ catalog: Address;
35
+ /** `HookrReleaseCreate2FactoryV1` — the CREATE2 factory the release was deployed through. */
36
+ factory: Address;
37
+ }>;
38
+ /** The native-mechanics module id every V2 pool's one module is keyed by in the registry. */
39
+ export declare const MODULE_ID: Hex;
40
+ /** The coordinator's kernel id for the root hook, carried in `MarketParams.kernelId`. */
41
+ export declare const KERNEL_ID: Hex;
42
+ /** Native ETH as a v4 currency: the zero address. Always `currency0` when it is one side of a pool. */
43
+ export declare const NATIVE_ADDRESS: Address;
44
+ export declare function explorerAddressUrl(address: Address): string;
45
+ export declare function explorerTxUrl(hash: Hex): string;
package/dist/chain.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Robinhood Chain, and Hookr's one live root hook on it.
3
+ *
4
+ * Every value here is a literal copy of `src/lib/v2-deployments.ts` (the addresses, read from
5
+ * the app's own deployment evidence) and `src/lib/chain.ts` (the RPC and explorer origin) in the
6
+ * Hookr app. `src/test/hookr-sdk-parity.test.ts` in that app asserts this file agrees with both,
7
+ * so a redeploy or an RPC change that is not mirrored here fails that suite.
8
+ *
9
+ * There is exactly one root today: `HookrModularHookV6`, deployed as `ADDRESSES.root`. Every V2
10
+ * pool's `PoolKey.hooks` is that address.
11
+ */
12
+ /** Robinhood Chain's id. */
13
+ export const CHAIN_ID = 4663;
14
+ /** The chain's public, keyless RPC endpoint. */
15
+ export const RPC_URL = "https://rpc.mainnet.chain.robinhood.com";
16
+ /** Origin of the chain's block explorer (Blockscout). No trailing slash. */
17
+ export const EXPLORER_URL = "https://robinhoodchain.blockscout.com";
18
+ /** The V2 contracts and ids a third-party integration calls or reads. */
19
+ export const ADDRESSES = Object.freeze({
20
+ /** `HookrModularHookV6` — `hooks` in every V2 `PoolKey`. The one live root. */
21
+ root: "0xb3cA29cF721380CEe8b8e4755F3865Ebc68Fe8cC",
22
+ /** `HookrMarketCoordinatorV5` — opens markets, collects founding fees, resolves the share tier. */
23
+ coordinator: "0x53A192A3fCeE94Da77916B461E0cCa2Dd6402442",
24
+ /** `HookrKernelRouterV3` — the swap router that pays into and out of the pot. */
25
+ router: "0xf0E528c39f33F565876cbaa7e0DFaCa38Df966E9",
26
+ /** `HookrKernelQuoterV1` — the quoter that runs the full module loop. */
27
+ quoter: "0x5Ba8FBbB4aB20Ff6Daf0ecDEBe3784f869BcB1B5",
28
+ /** `HookrNativeMechanicsBlockV2` — the one native block every V2 stack carries; also where claims are paid. */
29
+ nativeBlock: "0xD700492b504ba5A72D7de28dDe11Cd7985a7F1ae",
30
+ /** `HookrStackRegistryV2` — read-only, a pool's frozen config and module list. */
31
+ registry: "0x5b7f1A117A83aaac15B0698Aa4eB9D53fE5f5BA3",
32
+ /** `HookrModuleCatalogV1` — the on-chain catalog of admitted modules. */
33
+ catalog: "0x6a3561d5B6305E2229F30EDF66702e8Bc4fA19D1",
34
+ /** `HookrReleaseCreate2FactoryV1` — the CREATE2 factory the release was deployed through. */
35
+ factory: "0xBc54e888C1A5B71744B035DfeC5611B4DC69ccDe",
36
+ });
37
+ /** The native-mechanics module id every V2 pool's one module is keyed by in the registry. */
38
+ export const MODULE_ID = "0x961091565aaf4eaa4996296fda9772957e61d18d90b965111a59ded2bbb5e774";
39
+ /** The coordinator's kernel id for the root hook, carried in `MarketParams.kernelId`. */
40
+ export const KERNEL_ID = "0x1be0c118b1c6520d97de31ee9f0c33069f0e715ffcdcb16c87a343752bb5be14";
41
+ /** Native ETH as a v4 currency: the zero address. Always `currency0` when it is one side of a pool. */
42
+ export const NATIVE_ADDRESS = "0x0000000000000000000000000000000000000000";
43
+ export function explorerAddressUrl(address) {
44
+ return `${EXPLORER_URL}/address/${address}`;
45
+ }
46
+ export function explorerTxUrl(hash) {
47
+ return `${EXPLORER_URL}/tx/${hash}`;
48
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Claim what the native block owes: pot winnings and royalties, pooled per quote currency
3
+ * across every pool. Copied from the app's `src/lib/agent-skill.ts` ("Claim"):
4
+ *
5
+ * nativeMechanicsBlock.claimable(quote, account) view returns (uint256)
6
+ * nativeMechanicsBlock.claim(quote) // pays msg.sender
7
+ * nativeMechanicsBlock.claimTo(quote, to) // pays to
8
+ *
9
+ * `claim`/`claimTo` pay pot winnings and royalties; read `claimable` first, since a zero balance
10
+ * reverts. `quote` is the zero address for a claim in native ETH.
11
+ */
12
+ import type { Address } from "viem";
13
+ import { hookrNativeMechanicsBlockV2Abi } from "./abi.js";
14
+ export type ReadCall<F extends string> = Readonly<{
15
+ address: Address;
16
+ abi: typeof hookrNativeMechanicsBlockV2Abi;
17
+ functionName: F;
18
+ args: readonly unknown[];
19
+ }>;
20
+ export type WriteCall<F extends string> = Readonly<{
21
+ address: Address;
22
+ abi: typeof hookrNativeMechanicsBlockV2Abi;
23
+ functionName: F;
24
+ args: readonly unknown[];
25
+ value: bigint;
26
+ }>;
27
+ /** `nativeMechanicsBlock.claimable(quote, account)`. Read with viem's `readContract`. */
28
+ export declare function claimableCall(quote: Address, account: Address): ReadCall<"claimable">;
29
+ /** `nativeMechanicsBlock.claim(quote)`: pays `msg.sender`. Reverts on a zero balance. */
30
+ export declare function claimCall(quote: Address): WriteCall<"claim">;
31
+ /** `nativeMechanicsBlock.claimTo(quote, to)`: pays `to`. Reverts on a zero balance. */
32
+ export declare function claimToCall(quote: Address, to: Address): WriteCall<"claimTo">;
package/dist/claims.js ADDED
@@ -0,0 +1,31 @@
1
+ import { hookrNativeMechanicsBlockV2Abi } from "./abi.js";
2
+ import { ADDRESSES } from "./chain.js";
3
+ /** `nativeMechanicsBlock.claimable(quote, account)`. Read with viem's `readContract`. */
4
+ export function claimableCall(quote, account) {
5
+ return Object.freeze({
6
+ address: ADDRESSES.nativeBlock,
7
+ abi: hookrNativeMechanicsBlockV2Abi,
8
+ functionName: "claimable",
9
+ args: [quote, account],
10
+ });
11
+ }
12
+ /** `nativeMechanicsBlock.claim(quote)`: pays `msg.sender`. Reverts on a zero balance. */
13
+ export function claimCall(quote) {
14
+ return Object.freeze({
15
+ address: ADDRESSES.nativeBlock,
16
+ abi: hookrNativeMechanicsBlockV2Abi,
17
+ functionName: "claim",
18
+ args: [quote],
19
+ value: 0n,
20
+ });
21
+ }
22
+ /** `nativeMechanicsBlock.claimTo(quote, to)`: pays `to`. Reverts on a zero balance. */
23
+ export function claimToCall(quote, to) {
24
+ return Object.freeze({
25
+ address: ADDRESSES.nativeBlock,
26
+ abi: hookrNativeMechanicsBlockV2Abi,
27
+ functionName: "claimTo",
28
+ args: [quote, to],
29
+ value: 0n,
30
+ });
31
+ }
package/dist/fees.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The V2 fee model: base fee bounds, and the per-stream recipient/protocol split.
3
+ *
4
+ * Bounds are copied from the app's `src/lib/v2-fee-tiers.ts` and `src/lib/v2-combinations.ts`.
5
+ * `feeModel` walks a stack exactly the way `src/lib/v2-fee-streams.ts`'s `buildV2FeeStreams`
6
+ * does — same streams, same order, same split arithmetic — restated in wei terms at a supplied
7
+ * sample amount (default one whole unit, 1e18) rather than the app's formatted, labelled rows.
8
+ * `src/test/hookr-sdk-parity.test.ts` calls both on the same three configurations and the same
9
+ * sample amount and asserts the wei splits agree.
10
+ */
11
+ import { type RuleBlock } from "./rules.js";
12
+ /** `HookrNativeMechanicsBlockV2.MAX_TOTAL_FEE_PIPS`: 50% of a swap, base + surge + snipe. */
13
+ export declare const MAX_TOTAL_FEE_PIPS = 500000;
14
+ /** Smallest base fee accepted. One pip is a millionth; 100 pips is 0.01%. */
15
+ export declare const MIN_BASE_FEE_PIPS = 100;
16
+ /** The base fee's admissible range, in pips. */
17
+ export declare const baseFeeBounds: Readonly<{
18
+ min: number;
19
+ max: number;
20
+ }>;
21
+ /** `HookrMarketCoordinatorV5.defaultProtocolShareBps`: the protocol's default share of each add-on. */
22
+ export declare const DEFAULT_PROTOCOL_SHARE_BPS = 2000;
23
+ /** `HookrNativeMechanicsBlockV2.MAX_PROTOCOL_SHARE_BPS`: the hard ceiling on that share. */
24
+ export declare const MAX_PROTOCOL_SHARE_BPS = 5000;
25
+ export type FeeStreamKind = "baseFee" | "surge" | "snipeTax" | "lpReward" | "pot" | "burn";
26
+ export type FeeStreamSplit = Readonly<{
27
+ stream: FeeStreamKind;
28
+ /** The rule that controls this stream; null for the base fee, which is not a placeable rule. */
29
+ rule: "snipe" | "fee" | "burn" | "lp" | "pot" | null;
30
+ /** This stream's total, in wei of the sample amount. For `burn`, the protocol's quote-side
31
+ * slice only: the burned share itself is a rate on the token OUTPUT (`burnNetBps`), which is
32
+ * not known until the pool prices the swap. */
33
+ totalWei: bigint;
34
+ recipientWei: bigint;
35
+ protocolWei: bigint;
36
+ /** Set only on the `burn` row: the share of token output burned, net of the protocol's slice. */
37
+ burnNetBps: number | null;
38
+ }>;
39
+ export type FeeModelResult = Readonly<{
40
+ amountWei: bigint;
41
+ protocolShareBps: number;
42
+ streams: readonly FeeStreamSplit[];
43
+ /** Every stream's protocol slice, summed. */
44
+ totalProtocolWei: bigint;
45
+ }>;
46
+ /**
47
+ * Walk a stack of rule blocks through the fee model for one sample amount, exactly as
48
+ * `buildV2FeeStreams` does: the base fee is always shown; surge, the snipe tax, the LP-reward
49
+ * and pot cuts, and the burn cut each appear only while their rule is on the stack with a
50
+ * nonzero rate. The snipe-tax row is shown as if the guard were open, because that is the only
51
+ * state in which it is ever nonzero — the app's row says so too.
52
+ *
53
+ * `baseFeePips` is the stack's Base LP fee (see `compileRuleBlocks`); `amountWei` defaults to
54
+ * one whole unit (1e18) of the pool's quote currency.
55
+ */
56
+ export declare function feeModel(baseFeePips: number, blocks: readonly RuleBlock[], opts?: Readonly<{
57
+ amountWei?: bigint;
58
+ protocolShareBps?: number;
59
+ }>): FeeModelResult;
package/dist/fees.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The V2 fee model: base fee bounds, and the per-stream recipient/protocol split.
3
+ *
4
+ * Bounds are copied from the app's `src/lib/v2-fee-tiers.ts` and `src/lib/v2-combinations.ts`.
5
+ * `feeModel` walks a stack exactly the way `src/lib/v2-fee-streams.ts`'s `buildV2FeeStreams`
6
+ * does — same streams, same order, same split arithmetic — restated in wei terms at a supplied
7
+ * sample amount (default one whole unit, 1e18) rather than the app's formatted, labelled rows.
8
+ * `src/test/hookr-sdk-parity.test.ts` calls both on the same three configurations and the same
9
+ * sample amount and asserts the wei splits agree.
10
+ */
11
+ import { compileRuleBlocks } from "./rules.js";
12
+ /** `HookrNativeMechanicsBlockV2.MAX_TOTAL_FEE_PIPS`: 50% of a swap, base + surge + snipe. */
13
+ export const MAX_TOTAL_FEE_PIPS = 500_000;
14
+ /** Smallest base fee accepted. One pip is a millionth; 100 pips is 0.01%. */
15
+ export const MIN_BASE_FEE_PIPS = 100;
16
+ /** The base fee's admissible range, in pips. */
17
+ export const baseFeeBounds = Object.freeze({
18
+ min: MIN_BASE_FEE_PIPS,
19
+ max: MAX_TOTAL_FEE_PIPS,
20
+ });
21
+ /** `HookrMarketCoordinatorV5.defaultProtocolShareBps`: the protocol's default share of each add-on. */
22
+ export const DEFAULT_PROTOCOL_SHARE_BPS = 2_000;
23
+ /** `HookrNativeMechanicsBlockV2.MAX_PROTOCOL_SHARE_BPS`: the hard ceiling on that share. */
24
+ export const MAX_PROTOCOL_SHARE_BPS = 5_000;
25
+ const BPS = 10000n;
26
+ const PIPS = 1000000n;
27
+ function pipsOf(pips, baseWei) {
28
+ return (baseWei * BigInt(Math.max(0, Math.round(pips)))) / PIPS;
29
+ }
30
+ function bpsOf(bps, baseWei) {
31
+ return (baseWei * BigInt(Math.max(0, Math.round(bps)))) / BPS;
32
+ }
33
+ /** A stream's total split into the recipient's share and the protocol's, per the fee model: the
34
+ * protocol's share is always a floor of the total, never above the frozen rate. */
35
+ function splitByShare(totalWei, protocolShareBps) {
36
+ const bounded = Math.max(0, Math.min(10_000, Math.round(protocolShareBps)));
37
+ const protocol = (totalWei * BigInt(bounded)) / BPS;
38
+ return { recipient: totalWei - protocol, protocol };
39
+ }
40
+ /**
41
+ * Walk a stack of rule blocks through the fee model for one sample amount, exactly as
42
+ * `buildV2FeeStreams` does: the base fee is always shown; surge, the snipe tax, the LP-reward
43
+ * and pot cuts, and the burn cut each appear only while their rule is on the stack with a
44
+ * nonzero rate. The snipe-tax row is shown as if the guard were open, because that is the only
45
+ * state in which it is ever nonzero — the app's row says so too.
46
+ *
47
+ * `baseFeePips` is the stack's Base LP fee (see `compileRuleBlocks`); `amountWei` defaults to
48
+ * one whole unit (1e18) of the pool's quote currency.
49
+ */
50
+ export function feeModel(baseFeePips, blocks, opts = {}) {
51
+ const amountWei = opts.amountWei ?? 10n ** 18n;
52
+ const protocolShareBps = opts.protocolShareBps ?? DEFAULT_PROTOCOL_SHARE_BPS;
53
+ const compiled = compileRuleBlocks(blocks, baseFeePips);
54
+ const { maxFeePips, snipeTaxPips, lpBps, potBps, burnBps } = compiled;
55
+ const base = Math.max(0, compiled.baseFeePips);
56
+ // The gross LP-reward-plus-pot cut, exactly as the app's `buildBuyWaterfall` computes it with
57
+ // royaltyBps pinned to 0 (the same pinning `buildV2FeeStreams` uses): one bigint division of
58
+ // the sample amount by the combined cut, then split between the two rules by their own share
59
+ // of that combined cut — never re-derived from the amount separately, so the same rounding
60
+ // dust lands in the same place (the pot) that the app's walk does.
61
+ const totalCutBps = lpBps + potBps;
62
+ const grossCutWei = totalCutBps > 0 ? (amountWei * BigInt(totalCutBps)) / BPS : 0n;
63
+ const lpRewardTotalWei = totalCutBps > 0 ? (grossCutWei * BigInt(lpBps)) / BigInt(totalCutBps) : 0n;
64
+ const potTotalWei = grossCutWei - lpRewardTotalWei;
65
+ const baseWei = pipsOf(base, amountWei);
66
+ const surgeTotalWei = pipsOf(Math.max(0, maxFeePips - base), amountWei);
67
+ const snipeTotalWei = pipsOf(snipeTaxPips, amountWei);
68
+ const surgeSplit = splitByShare(surgeTotalWei, protocolShareBps);
69
+ const snipeSplit = splitByShare(snipeTotalWei, protocolShareBps);
70
+ const lpRewardSplit = splitByShare(lpRewardTotalWei, protocolShareBps);
71
+ const potSplit = splitByShare(potTotalWei, protocolShareBps);
72
+ const burnProtocolBps = Math.floor((burnBps * protocolShareBps) / 10_000);
73
+ const burnNetBps = burnBps - burnProtocolBps;
74
+ const burnProtocolWei = bpsOf(burnProtocolBps, amountWei);
75
+ const streams = [
76
+ {
77
+ stream: "baseFee",
78
+ rule: null,
79
+ totalWei: baseWei,
80
+ recipientWei: baseWei,
81
+ protocolWei: 0n,
82
+ burnNetBps: null,
83
+ },
84
+ ];
85
+ if (maxFeePips > base) {
86
+ streams.push({
87
+ stream: "surge",
88
+ rule: "fee",
89
+ totalWei: surgeTotalWei,
90
+ recipientWei: surgeSplit.recipient,
91
+ protocolWei: surgeSplit.protocol,
92
+ burnNetBps: null,
93
+ });
94
+ }
95
+ if (snipeTaxPips > 0) {
96
+ streams.push({
97
+ stream: "snipeTax",
98
+ rule: "snipe",
99
+ totalWei: snipeTotalWei,
100
+ recipientWei: snipeSplit.recipient,
101
+ protocolWei: snipeSplit.protocol,
102
+ burnNetBps: null,
103
+ });
104
+ }
105
+ if (lpBps > 0) {
106
+ streams.push({
107
+ stream: "lpReward",
108
+ rule: "lp",
109
+ totalWei: lpRewardTotalWei,
110
+ recipientWei: lpRewardSplit.recipient,
111
+ protocolWei: lpRewardSplit.protocol,
112
+ burnNetBps: null,
113
+ });
114
+ }
115
+ if (potBps > 0) {
116
+ streams.push({
117
+ stream: "pot",
118
+ rule: "pot",
119
+ totalWei: potTotalWei,
120
+ recipientWei: potSplit.recipient,
121
+ protocolWei: potSplit.protocol,
122
+ burnNetBps: null,
123
+ });
124
+ }
125
+ if (burnBps > 0) {
126
+ streams.push({
127
+ stream: "burn",
128
+ rule: "burn",
129
+ totalWei: burnProtocolWei,
130
+ recipientWei: 0n,
131
+ protocolWei: burnProtocolWei,
132
+ burnNetBps,
133
+ });
134
+ }
135
+ const totalProtocolWei = surgeSplit.protocol + snipeSplit.protocol + lpRewardSplit.protocol + potSplit.protocol + burnProtocolWei;
136
+ return Object.freeze({
137
+ amountWei,
138
+ protocolShareBps,
139
+ streams: Object.freeze(streams),
140
+ totalProtocolWei,
141
+ });
142
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@hookr/sdk` — build a market on Hookr's one live root hook with a custom rule configuration,
3
+ * quote and swap through Hookr's router, read markets, and claim payouts.
4
+ *
5
+ * There is exactly one root today: `HookrModularHookV6` on Robinhood Chain (id 4663),
6
+ * `ADDRESSES.root` in `chain.ts`. See the README for a quickstart.
7
+ */
8
+ export * from "./chain.js";
9
+ export * from "./abi.js";
10
+ export * from "./rules.js";
11
+ export * from "./fees.js";
12
+ export * from "./market.js";
13
+ export * from "./swap.js";
14
+ export * from "./reads.js";
15
+ export * from "./claims.js";
16
+ export * from "./links.js";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@hookr/sdk` — build a market on Hookr's one live root hook with a custom rule configuration,
3
+ * quote and swap through Hookr's router, read markets, and claim payouts.
4
+ *
5
+ * There is exactly one root today: `HookrModularHookV6` on Robinhood Chain (id 4663),
6
+ * `ADDRESSES.root` in `chain.ts`. See the README for a quickstart.
7
+ */
8
+ export * from "./chain.js";
9
+ export * from "./abi.js";
10
+ export * from "./rules.js";
11
+ export * from "./fees.js";
12
+ export * from "./market.js";
13
+ export * from "./swap.js";
14
+ export * from "./reads.js";
15
+ export * from "./claims.js";
16
+ export * from "./links.js";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Deep links into Hookr's builder, in the app's "remix" wire format.
3
+ *
4
+ * Copied from `src/lib/remix.ts` in the app (`encodeStack`, `decodeStack`,
5
+ * `decodeStackBaseFeePips`): one segment per rule, values in `PARAMS[type]` key order, plus an
6
+ * optional trailing `base:<percent>` segment for the stack's Base LP fee — written only when it
7
+ * differs from the launchpad's immutable default, so a link encoded before that segment existed
8
+ * keeps decoding exactly as it always did. `src/test/hookr-sdk-parity.test.ts` asserts this
9
+ * module's `encodeStack`/`decodeStack` round-trip through the app's own functions.
10
+ *
11
+ * The app hydrates a `?stack=`/`?from=` link on its `/builder` route only (`decodeRemix`, read
12
+ * there); there is no separate "ready to launch" page that accepts one yet. `launchLink` and
13
+ * `builderLink` both resolve to that one route today for that reason — kept as two names so a
14
+ * caller can use whichever fits its own UI, and so this package does not need to change if the
15
+ * app ever does split them.
16
+ */
17
+ import type { Address } from "viem";
18
+ import { type RuleBlock } from "./rules.js";
19
+ /** `hooks-model.ts`'s `DEFAULT_BASE_FEE_PIPS`: 0.3%, the launchpad's immutable default. */
20
+ export declare const DEFAULT_BASE_FEE_PIPS = 3000;
21
+ export declare const DEFAULT_ORIGIN = "https://hookr.fun";
22
+ /** `stack:` value for a set of rule blocks and an optional stack-level Base LP fee (pips). */
23
+ export declare function encodeStack(blocks: readonly RuleBlock[], baseFeePips?: number): string;
24
+ /**
25
+ * Parse a `stack:` value. Null when nothing usable is present. Every parameter is clamped to
26
+ * its rule's bounds — a remix is a starting point, not a replica.
27
+ */
28
+ export declare function decodeStack(value: string | null | undefined): RuleBlock[] | null;
29
+ /** The stack-level Base LP fee out of a `stack:` value, in pips. `DEFAULT_BASE_FEE_PIPS` when absent. */
30
+ export declare function decodeStackBaseFeePips(value: string | null | undefined): number;
31
+ export type LinkConfig = Readonly<{
32
+ blocks: readonly RuleBlock[];
33
+ /** The stack's Base LP fee, in pips. Omit to leave it at the launchpad default. */
34
+ baseFeePips?: number;
35
+ }>;
36
+ export type LinkOptions = Readonly<{
37
+ /** Defaults to `https://hookr.fun`. */
38
+ origin?: string;
39
+ /** The token this configuration was copied from, for the "from" query param. */
40
+ from?: Address;
41
+ }>;
42
+ /** A link into Hookr's builder with this configuration pre-loaded, for exploring or tweaking it. */
43
+ export declare function builderLink(config: LinkConfig, opts?: LinkOptions): string;
44
+ /**
45
+ * A link into Hookr to open a market with this configuration. Resolves to the same `/builder`
46
+ * route as `builderLink` today — see the module doc.
47
+ */
48
+ export declare function launchLink(config: LinkConfig, opts?: LinkOptions): string;
package/dist/links.js ADDED
@@ -0,0 +1,93 @@
1
+ import { MAX_TOTAL_FEE_PIPS, MIN_BASE_FEE_PIPS } from "./fees.js";
2
+ import { PARAMS, RULE_ORDER } from "./rules.js";
3
+ /** `hooks-model.ts`'s `DEFAULT_BASE_FEE_PIPS`: 0.3%, the launchpad's immutable default. */
4
+ export const DEFAULT_BASE_FEE_PIPS = 3_000;
5
+ export const DEFAULT_ORIGIN = "https://hookr.fun";
6
+ const BASE_FEE_SEGMENT = "base";
7
+ function trimNumber(value) {
8
+ if (!Number.isFinite(value))
9
+ return "0";
10
+ return String(Number(value.toPrecision(12)));
11
+ }
12
+ function isRuleKey(value) {
13
+ return RULE_ORDER.includes(value);
14
+ }
15
+ /** `stack:` value for a set of rule blocks and an optional stack-level Base LP fee (pips). */
16
+ export function encodeStack(blocks, baseFeePips) {
17
+ const blockSegments = blocks.map((block) => {
18
+ const values = PARAMS[block.type].map((spec) => trimNumber(block.params[spec.key] ?? spec.def));
19
+ return `${block.type}:${values.join(",")}`;
20
+ });
21
+ const segments = baseFeePips !== undefined && Math.round(baseFeePips) !== DEFAULT_BASE_FEE_PIPS
22
+ ? [...blockSegments, `${BASE_FEE_SEGMENT}:${trimNumber(baseFeePips / 10_000)}`]
23
+ : blockSegments;
24
+ return segments.join("|");
25
+ }
26
+ /**
27
+ * Parse a `stack:` value. Null when nothing usable is present. Every parameter is clamped to
28
+ * its rule's bounds — a remix is a starting point, not a replica.
29
+ */
30
+ export function decodeStack(value) {
31
+ if (!value)
32
+ return null;
33
+ const blocks = [];
34
+ const seen = new Set();
35
+ let sawBaseFeeSegment = false;
36
+ for (const segment of value.split("|")) {
37
+ const [type, list] = segment.split(":");
38
+ if (type === BASE_FEE_SEGMENT) {
39
+ sawBaseFeeSegment = true;
40
+ continue;
41
+ }
42
+ if (!type || !isRuleKey(type) || seen.has(type))
43
+ continue;
44
+ const specs = PARAMS[type];
45
+ const parts = (list ?? "").split(",");
46
+ const params = {};
47
+ specs.forEach((spec, i) => {
48
+ const parsed = Number(parts[i]);
49
+ const v = Number.isFinite(parsed) ? parsed : spec.def;
50
+ params[spec.key] = Math.min(spec.max, Math.max(spec.min, v));
51
+ });
52
+ seen.add(type);
53
+ blocks.push({ type, params: Object.freeze(params) });
54
+ }
55
+ return blocks.length > 0 || sawBaseFeeSegment ? blocks : null;
56
+ }
57
+ /** The stack-level Base LP fee out of a `stack:` value, in pips. `DEFAULT_BASE_FEE_PIPS` when absent. */
58
+ export function decodeStackBaseFeePips(value) {
59
+ if (!value)
60
+ return DEFAULT_BASE_FEE_PIPS;
61
+ for (const segment of value.split("|")) {
62
+ const [type, list] = segment.split(":");
63
+ if (type !== BASE_FEE_SEGMENT)
64
+ continue;
65
+ const parsed = Number(list);
66
+ if (!Number.isFinite(parsed))
67
+ continue;
68
+ const pips = Math.round(parsed * 10_000);
69
+ return Math.min(MAX_TOTAL_FEE_PIPS, Math.max(MIN_BASE_FEE_PIPS, pips));
70
+ }
71
+ return DEFAULT_BASE_FEE_PIPS;
72
+ }
73
+ function stackHref(path, config, opts = {}) {
74
+ const stack = encodeStack(config.blocks, config.baseFeePips);
75
+ const query = new URLSearchParams();
76
+ if (stack)
77
+ query.set("stack", stack);
78
+ if (opts.from)
79
+ query.set("from", opts.from);
80
+ const qs = query.toString();
81
+ return `${opts.origin ?? DEFAULT_ORIGIN}${path}${qs ? `?${qs}` : ""}`;
82
+ }
83
+ /** A link into Hookr's builder with this configuration pre-loaded, for exploring or tweaking it. */
84
+ export function builderLink(config, opts) {
85
+ return stackHref("/builder", config, opts);
86
+ }
87
+ /**
88
+ * A link into Hookr to open a market with this configuration. Resolves to the same `/builder`
89
+ * route as `builderLink` today — see the module doc.
90
+ */
91
+ export function launchLink(config, opts) {
92
+ return stackHref("/builder", config, opts);
93
+ }