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.
- package/README.md +70 -0
- package/dist/abi.d.ts +6404 -0
- package/dist/abi.js +8300 -0
- package/dist/chain.d.ts +45 -0
- package/dist/chain.js +48 -0
- package/dist/claims.d.ts +32 -0
- package/dist/claims.js +31 -0
- package/dist/fees.d.ts +59 -0
- package/dist/fees.js +142 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/links.d.ts +48 -0
- package/dist/links.js +93 -0
- package/dist/market.d.ts +234 -0
- package/dist/market.js +289 -0
- package/dist/reads.d.ts +91 -0
- package/dist/reads.js +140 -0
- package/dist/rules.d.ts +84 -0
- package/dist/rules.js +296 -0
- package/dist/swap.d.ts +79 -0
- package/dist/swap.js +111 -0
- package/package.json +29 -0
package/dist/reads.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
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 { decodeAbiParameters, getAbiItem, toEventSelector } from "viem";
|
|
13
|
+
import { hookrMarketCoordinatorV5Abi, hookrStackRegistryV2Abi } from "./abi.js";
|
|
14
|
+
import { ADDRESSES, MODULE_ID } from "./chain.js";
|
|
15
|
+
function marketOrigin(value) {
|
|
16
|
+
if (value === 1)
|
|
17
|
+
return "new-token";
|
|
18
|
+
if (value === 2)
|
|
19
|
+
return "existing-token";
|
|
20
|
+
throw new Error(`Unknown MarketOrigin ${value}.`);
|
|
21
|
+
}
|
|
22
|
+
/** `marketCoordinator.getMarket(poolId)`. `live` is false for a `poolId` the coordinator never opened. */
|
|
23
|
+
export async function readMarket(client, poolId) {
|
|
24
|
+
const m = (await client.readContract({
|
|
25
|
+
address: ADDRESSES.coordinator,
|
|
26
|
+
abi: hookrMarketCoordinatorV5Abi,
|
|
27
|
+
functionName: "getMarket",
|
|
28
|
+
args: [poolId],
|
|
29
|
+
}));
|
|
30
|
+
return Object.freeze({ ...m, origin: marketOrigin(m.origin) });
|
|
31
|
+
}
|
|
32
|
+
// -----------------------------------------------------------------------------------------------
|
|
33
|
+
// listMarkets
|
|
34
|
+
// -----------------------------------------------------------------------------------------------
|
|
35
|
+
/** The coordinator's deploy block on chain 4663: no `MarketCreated` log can sit before it. */
|
|
36
|
+
export const MARKET_SCAN_FROM_BLOCK = 57229676n;
|
|
37
|
+
const MARKET_CREATED_EVENT = getAbiItem({ abi: hookrMarketCoordinatorV5Abi, name: "MarketCreated" });
|
|
38
|
+
export const MARKET_CREATED_TOPIC = toEventSelector(MARKET_CREATED_EVENT);
|
|
39
|
+
/**
|
|
40
|
+
* Every `MarketCreated` log the coordinator emitted in `[fromBlock, toBlock]`, oldest first.
|
|
41
|
+
*
|
|
42
|
+
* `root`, when given, is checked against every row's `kernel` rather than passed to the log
|
|
43
|
+
* filter: `kernel` is not an indexed topic, and there is exactly one live root today
|
|
44
|
+
* (`ADDRESSES.root`), so every row already carries it. A future second root would need a
|
|
45
|
+
* client-side filter here, which is exactly what this parameter is for.
|
|
46
|
+
*/
|
|
47
|
+
export async function listMarkets(client, options = {}) {
|
|
48
|
+
const fromBlock = options.fromBlock ?? MARKET_SCAN_FROM_BLOCK;
|
|
49
|
+
const toBlock = options.toBlock ?? (await client.getBlockNumber());
|
|
50
|
+
const logs = await client.getLogs({
|
|
51
|
+
address: ADDRESSES.coordinator,
|
|
52
|
+
event: MARKET_CREATED_EVENT,
|
|
53
|
+
fromBlock,
|
|
54
|
+
toBlock,
|
|
55
|
+
strict: true,
|
|
56
|
+
});
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const log of logs) {
|
|
59
|
+
const args = log.args;
|
|
60
|
+
const row = Object.freeze({
|
|
61
|
+
poolId: args.poolId,
|
|
62
|
+
subject: args.subject,
|
|
63
|
+
quote: args.quote,
|
|
64
|
+
creator: args.creator,
|
|
65
|
+
origin: marketOrigin(args.origin),
|
|
66
|
+
kernel: args.kernel,
|
|
67
|
+
sqrtPriceX96: args.sqrtPriceX96,
|
|
68
|
+
tickSpacing: args.tickSpacing,
|
|
69
|
+
blockNumber: log.blockNumber,
|
|
70
|
+
txHash: log.transactionHash,
|
|
71
|
+
logIndex: log.logIndex ?? 0,
|
|
72
|
+
});
|
|
73
|
+
if (options.root && row.kernel.toLowerCase() !== options.root.toLowerCase())
|
|
74
|
+
continue;
|
|
75
|
+
rows.push(row);
|
|
76
|
+
}
|
|
77
|
+
rows.sort((a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1));
|
|
78
|
+
return Object.freeze(rows);
|
|
79
|
+
}
|
|
80
|
+
// -----------------------------------------------------------------------------------------------
|
|
81
|
+
// readPoolRules
|
|
82
|
+
// -----------------------------------------------------------------------------------------------
|
|
83
|
+
const CONFIG_COMPONENTS = [
|
|
84
|
+
{ name: "poolId", type: "bytes32" },
|
|
85
|
+
{ name: "kernel", type: "address" },
|
|
86
|
+
{ name: "subject", type: "address" },
|
|
87
|
+
{ name: "quote", type: "address" },
|
|
88
|
+
{ name: "lockedLiquidityProvider", type: "address" },
|
|
89
|
+
{ name: "guardEndBlock", type: "uint40" },
|
|
90
|
+
{ name: "baseFeePips", type: "uint24" },
|
|
91
|
+
{ name: "maxFeePips", type: "uint24" },
|
|
92
|
+
{ name: "snipeTaxPips", type: "uint24" },
|
|
93
|
+
{ name: "surgeSens", type: "uint16" },
|
|
94
|
+
{ name: "burnBps", type: "uint16" },
|
|
95
|
+
{ name: "lpBps", type: "uint16" },
|
|
96
|
+
{ name: "potBps", type: "uint16" },
|
|
97
|
+
{ name: "royaltyBps", type: "uint16" },
|
|
98
|
+
{ name: "potEveryNBuys", type: "uint32" },
|
|
99
|
+
{ name: "maxBuyQuoteAmount", type: "uint96" },
|
|
100
|
+
{ name: "potMinBuyWei", type: "uint96" },
|
|
101
|
+
{ name: "royaltyTo", type: "address" },
|
|
102
|
+
{ name: "protocolRecipient", type: "address" },
|
|
103
|
+
{ name: "protocolShareBps", type: "uint24" },
|
|
104
|
+
];
|
|
105
|
+
const CONFIG_ABI = [{ type: "tuple", components: CONFIG_COMPONENTS }];
|
|
106
|
+
/** The inverse of `market.ts`'s `encodeNativeBlockConfig`. */
|
|
107
|
+
export function decodeNativeBlockConfig(data) {
|
|
108
|
+
const raw = decodeAbiParameters(CONFIG_ABI, data)[0];
|
|
109
|
+
return Object.freeze({ ...raw, guardEndBlock: BigInt(raw.guardEndBlock) });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `stackRegistry.stack(poolId)` for `moduleCount`/`limits`, then `stackRegistry.moduleAt` for
|
|
113
|
+
* each module, decoding the native-mechanics module's `config` bytes. Every other module (the
|
|
114
|
+
* directional tax block, when a pool carries one) is skipped: its config schema is not modeled
|
|
115
|
+
* by this package.
|
|
116
|
+
*/
|
|
117
|
+
export async function readPoolRules(client, poolId) {
|
|
118
|
+
const core = (await client.readContract({
|
|
119
|
+
address: ADDRESSES.registry,
|
|
120
|
+
abi: hookrStackRegistryV2Abi,
|
|
121
|
+
functionName: "stack",
|
|
122
|
+
args: [poolId],
|
|
123
|
+
}));
|
|
124
|
+
if (!core.configured)
|
|
125
|
+
return Object.freeze({ configured: false, limits: null, nativeConfig: null });
|
|
126
|
+
let nativeConfig = null;
|
|
127
|
+
for (let i = 0; i < core.moduleCount; i++) {
|
|
128
|
+
const [snapshot, config] = (await client.readContract({
|
|
129
|
+
address: ADDRESSES.registry,
|
|
130
|
+
abi: hookrStackRegistryV2Abi,
|
|
131
|
+
functionName: "moduleAt",
|
|
132
|
+
args: [poolId, BigInt(i)],
|
|
133
|
+
}));
|
|
134
|
+
if (snapshot.moduleId.toLowerCase() === MODULE_ID.toLowerCase()) {
|
|
135
|
+
nativeConfig = decodeNativeBlockConfig(config);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({ configured: true, limits: core.limits, nativeConfig });
|
|
140
|
+
}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public rules a market's stack can carry, and their bounds.
|
|
3
|
+
*
|
|
4
|
+
* Copied from the Hookr app's `src/lib/hooks-model.ts`: `RULES` mirrors `HOOKS` reduced to
|
|
5
|
+
* `{ key, name, summary, explanation }`, and `PARAMS` mirrors the bounds/step/default of the
|
|
6
|
+
* app's `PARAMS[type]` (its display-only `fmt` function is not carried here; `unit` is a short,
|
|
7
|
+
* human label instead — see the module doc below). Both are restricted to the app's
|
|
8
|
+
* `PUBLIC_BUILDER_HOOK_ORDER`, which today is the five deployed blocks (`HOOK_ORDER`): the
|
|
9
|
+
* directional tax module (`DIRECTIONAL_TAX_UI` in the app) is off and is not offered here either.
|
|
10
|
+
* `src/test/hookr-sdk-parity.test.ts` in the app asserts the key order and every bound match.
|
|
11
|
+
*
|
|
12
|
+
* A rule compiles into the native block's `Config` (see `fees.ts` and `market.ts`); it is not a
|
|
13
|
+
* separate contract call. At most five rules on a stack today, one per key, because there are
|
|
14
|
+
* only five public rule keys — `validateStack` still checks the count explicitly so a future
|
|
15
|
+
* sixth key does not silently raise the ceiling.
|
|
16
|
+
*/
|
|
17
|
+
export type RuleKey = "snipe" | "fee" | "burn" | "lp" | "pot";
|
|
18
|
+
/** The public rule keys, in the order the app's builder palette offers them. */
|
|
19
|
+
export declare const RULE_ORDER: readonly RuleKey[];
|
|
20
|
+
export type Rule = Readonly<{
|
|
21
|
+
key: RuleKey;
|
|
22
|
+
name: string;
|
|
23
|
+
/** Compact catalogue tagline. */
|
|
24
|
+
summary: string;
|
|
25
|
+
/** One complete, plain-language sentence. */
|
|
26
|
+
explanation: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare const RULES: readonly Rule[];
|
|
29
|
+
export declare function ruleFor(key: RuleKey): Rule;
|
|
30
|
+
export type Param = Readonly<{
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
min: number;
|
|
34
|
+
max: number;
|
|
35
|
+
step: number;
|
|
36
|
+
def: number;
|
|
37
|
+
/** Short display unit, e.g. "%", "BLK", "ETH". Display-only; not asserted byte-for-byte
|
|
38
|
+
* against the app's `fmt` function, which the app keeps as a closure, not exported data. */
|
|
39
|
+
unit: string;
|
|
40
|
+
hint: string;
|
|
41
|
+
}>;
|
|
42
|
+
export declare const PARAMS: Readonly<Record<RuleKey, readonly Param[]>>;
|
|
43
|
+
export type RuleBlock = Readonly<{
|
|
44
|
+
type: RuleKey;
|
|
45
|
+
params: Readonly<Record<string, number>>;
|
|
46
|
+
}>;
|
|
47
|
+
/** The native block's `Config` rate fields a stack of rule blocks compiles into. */
|
|
48
|
+
export type CompiledRules = Readonly<{
|
|
49
|
+
guardBlocks: number;
|
|
50
|
+
maxBuyBps: number;
|
|
51
|
+
snipeTaxPips: number;
|
|
52
|
+
baseFeePips: number;
|
|
53
|
+
maxFeePips: number;
|
|
54
|
+
surgeSens: number;
|
|
55
|
+
burnBps: number;
|
|
56
|
+
lpBps: number;
|
|
57
|
+
potBps: number;
|
|
58
|
+
potEveryNBuys: number;
|
|
59
|
+
potMinBuyWei: bigint;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Compile a stack of public rule blocks into the exact rate fields the native block's `Config`
|
|
63
|
+
* carries, mirroring `compileStack` in the app's `src/lib/hooks-model.ts`.
|
|
64
|
+
*
|
|
65
|
+
* `baseFeePipsOverride` is the stack-level Base LP fee, chosen independently of whether a
|
|
66
|
+
* Surge Fees rule is present — it always wins over the Surge block's own "Base fee" slider,
|
|
67
|
+
* exactly like the app's builder. The block still supplies the surge ceiling and sensitivity.
|
|
68
|
+
*/
|
|
69
|
+
export declare function compileRuleBlocks(blocks: readonly RuleBlock[], baseFeePipsOverride: number): CompiledRules;
|
|
70
|
+
/** The default single block for one rule: every param at its `def`. */
|
|
71
|
+
export declare function defaultBlock(key: RuleKey): RuleBlock;
|
|
72
|
+
/** The starting stack a new configuration opens with: no rules, base fee only. */
|
|
73
|
+
export declare function defaultStack(): readonly RuleBlock[];
|
|
74
|
+
export declare const MAX_STACK_RULES = 5;
|
|
75
|
+
export type ValidationResult = Readonly<{
|
|
76
|
+
ok: boolean;
|
|
77
|
+
errors: readonly string[];
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Validate a stack of rule blocks: unknown rule keys, duplicate rules, more than
|
|
81
|
+
* `MAX_STACK_RULES` rules, an unknown parameter, or a parameter outside its bounds or off its
|
|
82
|
+
* step grid. Every problem is reported, not just the first.
|
|
83
|
+
*/
|
|
84
|
+
export declare function validateStack(blocks: readonly RuleBlock[]): ValidationResult;
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public rules a market's stack can carry, and their bounds.
|
|
3
|
+
*
|
|
4
|
+
* Copied from the Hookr app's `src/lib/hooks-model.ts`: `RULES` mirrors `HOOKS` reduced to
|
|
5
|
+
* `{ key, name, summary, explanation }`, and `PARAMS` mirrors the bounds/step/default of the
|
|
6
|
+
* app's `PARAMS[type]` (its display-only `fmt` function is not carried here; `unit` is a short,
|
|
7
|
+
* human label instead — see the module doc below). Both are restricted to the app's
|
|
8
|
+
* `PUBLIC_BUILDER_HOOK_ORDER`, which today is the five deployed blocks (`HOOK_ORDER`): the
|
|
9
|
+
* directional tax module (`DIRECTIONAL_TAX_UI` in the app) is off and is not offered here either.
|
|
10
|
+
* `src/test/hookr-sdk-parity.test.ts` in the app asserts the key order and every bound match.
|
|
11
|
+
*
|
|
12
|
+
* A rule compiles into the native block's `Config` (see `fees.ts` and `market.ts`); it is not a
|
|
13
|
+
* separate contract call. At most five rules on a stack today, one per key, because there are
|
|
14
|
+
* only five public rule keys — `validateStack` still checks the count explicitly so a future
|
|
15
|
+
* sixth key does not silently raise the ceiling.
|
|
16
|
+
*/
|
|
17
|
+
/** The public rule keys, in the order the app's builder palette offers them. */
|
|
18
|
+
export const RULE_ORDER = ["snipe", "fee", "burn", "lp", "pot"];
|
|
19
|
+
export const RULES = Object.freeze([
|
|
20
|
+
Object.freeze({
|
|
21
|
+
key: "snipe",
|
|
22
|
+
name: "Anti-Snipe",
|
|
23
|
+
summary: "Caps early buys and adds an LP fee in the first blocks",
|
|
24
|
+
explanation: "It caps total ETH bought per pool block and adds an extra LP fee during a fixed opening window.",
|
|
25
|
+
}),
|
|
26
|
+
Object.freeze({
|
|
27
|
+
key: "fee",
|
|
28
|
+
name: "Surge Fees",
|
|
29
|
+
summary: "Fee scales with trade size",
|
|
30
|
+
explanation: "It raises the LP fee from a base rate toward a ceiling as trade size grows relative to in-range depth.",
|
|
31
|
+
}),
|
|
32
|
+
Object.freeze({
|
|
33
|
+
key: "burn",
|
|
34
|
+
name: "Auto Burn",
|
|
35
|
+
summary: "Burns a share of actual buy output",
|
|
36
|
+
explanation: "It sends a chosen share of each exact-input buy's token output directly to the dead address.",
|
|
37
|
+
}),
|
|
38
|
+
Object.freeze({
|
|
39
|
+
key: "lp",
|
|
40
|
+
name: "LP Rewards",
|
|
41
|
+
summary: "Fee share streams to LPs",
|
|
42
|
+
explanation: "It donates an ETH-side share of each exact-input buy to liquidity positions that are in range.",
|
|
43
|
+
}),
|
|
44
|
+
Object.freeze({
|
|
45
|
+
key: "pot",
|
|
46
|
+
name: "Nth-buy Pot",
|
|
47
|
+
summary: "A public counter pays every Nth buy",
|
|
48
|
+
explanation: "It fills a deterministic pot from exact-input buys and pays it to the recipient of every Nth qualifying buy.",
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
const RULES_BY_KEY = Object.freeze(Object.fromEntries(RULES.map((rule) => [rule.key, rule])));
|
|
52
|
+
export function ruleFor(key) {
|
|
53
|
+
return RULES_BY_KEY[key];
|
|
54
|
+
}
|
|
55
|
+
export const PARAMS = Object.freeze({
|
|
56
|
+
snipe: Object.freeze([
|
|
57
|
+
Object.freeze({
|
|
58
|
+
key: "guard",
|
|
59
|
+
label: "Guard window",
|
|
60
|
+
min: 5,
|
|
61
|
+
max: 500,
|
|
62
|
+
step: 5,
|
|
63
|
+
def: 100,
|
|
64
|
+
unit: "BLK",
|
|
65
|
+
hint: "Blocks after the pool opens with sniper protection active. Counted in the parent-chain blocks the contracts see (~12 s each), not this chain's ~0.1 s blocks, so 100 is roughly 20 minutes.",
|
|
66
|
+
}),
|
|
67
|
+
Object.freeze({
|
|
68
|
+
key: "maxbuy",
|
|
69
|
+
label: "Max buy",
|
|
70
|
+
min: 0.1,
|
|
71
|
+
max: 5,
|
|
72
|
+
step: 0.1,
|
|
73
|
+
def: 0.5,
|
|
74
|
+
unit: "%",
|
|
75
|
+
hint: "Cumulative cap on all buys in one pool block during the guard window, derived from a share of the launch's supply basis. Splitting one order does not create more room in that block.",
|
|
76
|
+
}),
|
|
77
|
+
Object.freeze({
|
|
78
|
+
key: "tax",
|
|
79
|
+
label: "Snipe tax",
|
|
80
|
+
min: 0,
|
|
81
|
+
max: 45,
|
|
82
|
+
step: 5,
|
|
83
|
+
def: 40,
|
|
84
|
+
unit: "%",
|
|
85
|
+
hint: "Extra LP fee on swaps inside the guard window. Goes to LPs.",
|
|
86
|
+
}),
|
|
87
|
+
]),
|
|
88
|
+
fee: Object.freeze([
|
|
89
|
+
Object.freeze({
|
|
90
|
+
key: "base",
|
|
91
|
+
label: "Base fee",
|
|
92
|
+
min: 0.05,
|
|
93
|
+
max: 1,
|
|
94
|
+
step: 0.05,
|
|
95
|
+
def: 0.3,
|
|
96
|
+
unit: "%",
|
|
97
|
+
hint: "Swap fee when the pool is calm.",
|
|
98
|
+
}),
|
|
99
|
+
Object.freeze({
|
|
100
|
+
key: "max",
|
|
101
|
+
label: "Max fee",
|
|
102
|
+
min: 1,
|
|
103
|
+
max: 10,
|
|
104
|
+
step: 0.5,
|
|
105
|
+
def: 3,
|
|
106
|
+
unit: "%",
|
|
107
|
+
hint: "Ceiling for large trades relative to pool depth.",
|
|
108
|
+
}),
|
|
109
|
+
Object.freeze({
|
|
110
|
+
key: "sens",
|
|
111
|
+
label: "Size sensitivity",
|
|
112
|
+
min: 1,
|
|
113
|
+
max: 10,
|
|
114
|
+
step: 1,
|
|
115
|
+
def: 5,
|
|
116
|
+
unit: "X",
|
|
117
|
+
hint: "How fast the fee ramps as a trade eats the pool.",
|
|
118
|
+
}),
|
|
119
|
+
]),
|
|
120
|
+
burn: Object.freeze([
|
|
121
|
+
Object.freeze({
|
|
122
|
+
key: "cut",
|
|
123
|
+
label: "Output burn",
|
|
124
|
+
min: 0.1,
|
|
125
|
+
max: 5,
|
|
126
|
+
step: 0.1,
|
|
127
|
+
def: 1,
|
|
128
|
+
unit: "% OUT",
|
|
129
|
+
hint: "Share of the actual token output sent directly to the dead address on each exact-input buy. No ETH vault or market buy.",
|
|
130
|
+
}),
|
|
131
|
+
]),
|
|
132
|
+
lp: Object.freeze([
|
|
133
|
+
Object.freeze({
|
|
134
|
+
key: "share",
|
|
135
|
+
label: "LP fee share",
|
|
136
|
+
min: 0.05,
|
|
137
|
+
max: 1,
|
|
138
|
+
step: 0.05,
|
|
139
|
+
def: 0.25,
|
|
140
|
+
unit: "%",
|
|
141
|
+
hint: "Slice of each buy donated to in-range LPs.",
|
|
142
|
+
}),
|
|
143
|
+
]),
|
|
144
|
+
pot: Object.freeze([
|
|
145
|
+
Object.freeze({
|
|
146
|
+
key: "cut",
|
|
147
|
+
label: "Pot share",
|
|
148
|
+
min: 0.1,
|
|
149
|
+
max: 2,
|
|
150
|
+
step: 0.1,
|
|
151
|
+
def: 0.5,
|
|
152
|
+
unit: "% SWAP",
|
|
153
|
+
hint: "Share of every exact-input buy added to the deterministic pot.",
|
|
154
|
+
}),
|
|
155
|
+
Object.freeze({
|
|
156
|
+
key: "every",
|
|
157
|
+
label: "Pays every N buys",
|
|
158
|
+
min: 100,
|
|
159
|
+
max: 5000,
|
|
160
|
+
step: 100,
|
|
161
|
+
def: 500,
|
|
162
|
+
unit: "buys",
|
|
163
|
+
hint: "The public counter advances at most once per pool per block. The Nth qualifying buy wins; this is not random.",
|
|
164
|
+
}),
|
|
165
|
+
Object.freeze({
|
|
166
|
+
key: "minbuy",
|
|
167
|
+
label: "Min qualifying buy",
|
|
168
|
+
min: 0.001,
|
|
169
|
+
max: 1,
|
|
170
|
+
step: 0.001,
|
|
171
|
+
def: 0.01,
|
|
172
|
+
unit: "ETH",
|
|
173
|
+
hint: "Buys below this cannot advance the public pot counter.",
|
|
174
|
+
}),
|
|
175
|
+
]),
|
|
176
|
+
});
|
|
177
|
+
/** `eth` -> wei at fixed 9-decimal precision, matching the app's `toWei` (avoids float drift). */
|
|
178
|
+
function toWei(eth) {
|
|
179
|
+
return BigInt(Math.round(eth * 1e9)) * 10n ** 9n;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Compile a stack of public rule blocks into the exact rate fields the native block's `Config`
|
|
183
|
+
* carries, mirroring `compileStack` in the app's `src/lib/hooks-model.ts`.
|
|
184
|
+
*
|
|
185
|
+
* `baseFeePipsOverride` is the stack-level Base LP fee, chosen independently of whether a
|
|
186
|
+
* Surge Fees rule is present — it always wins over the Surge block's own "Base fee" slider,
|
|
187
|
+
* exactly like the app's builder. The block still supplies the surge ceiling and sensitivity.
|
|
188
|
+
*/
|
|
189
|
+
export function compileRuleBlocks(blocks, baseFeePipsOverride) {
|
|
190
|
+
const baseFeePips = Math.round(baseFeePipsOverride);
|
|
191
|
+
const out = {
|
|
192
|
+
guardBlocks: 0,
|
|
193
|
+
maxBuyBps: 0,
|
|
194
|
+
snipeTaxPips: 0,
|
|
195
|
+
baseFeePips,
|
|
196
|
+
maxFeePips: baseFeePips,
|
|
197
|
+
surgeSens: 0,
|
|
198
|
+
burnBps: 0,
|
|
199
|
+
lpBps: 0,
|
|
200
|
+
potBps: 0,
|
|
201
|
+
potEveryNBuys: 0,
|
|
202
|
+
potMinBuyWei: 0n,
|
|
203
|
+
};
|
|
204
|
+
for (const block of blocks) {
|
|
205
|
+
const p = block.params;
|
|
206
|
+
if (block.type === "snipe") {
|
|
207
|
+
out.guardBlocks = Math.round(p.guard);
|
|
208
|
+
out.maxBuyBps = Math.round(p.maxbuy * 100);
|
|
209
|
+
out.snipeTaxPips = Math.round(p.tax * 10_000);
|
|
210
|
+
}
|
|
211
|
+
if (block.type === "fee") {
|
|
212
|
+
out.maxFeePips = Math.round(p.max * 10_000);
|
|
213
|
+
out.surgeSens = Math.round(p.sens);
|
|
214
|
+
}
|
|
215
|
+
if (block.type === "burn") {
|
|
216
|
+
out.burnBps = Math.round(p.cut * 100);
|
|
217
|
+
}
|
|
218
|
+
if (block.type === "lp") {
|
|
219
|
+
out.lpBps = Math.round(p.share * 100);
|
|
220
|
+
}
|
|
221
|
+
if (block.type === "pot") {
|
|
222
|
+
out.potBps = Math.round(p.cut * 100);
|
|
223
|
+
out.potEveryNBuys = Math.round(p.every);
|
|
224
|
+
out.potMinBuyWei = toWei(p.minbuy);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (out.maxFeePips < out.baseFeePips)
|
|
228
|
+
out.maxFeePips = out.baseFeePips;
|
|
229
|
+
return Object.freeze(out);
|
|
230
|
+
}
|
|
231
|
+
/** The default single block for one rule: every param at its `def`. */
|
|
232
|
+
export function defaultBlock(key) {
|
|
233
|
+
const params = {};
|
|
234
|
+
for (const spec of PARAMS[key])
|
|
235
|
+
params[spec.key] = spec.def;
|
|
236
|
+
return Object.freeze({ type: key, params: Object.freeze(params) });
|
|
237
|
+
}
|
|
238
|
+
/** The starting stack a new configuration opens with: no rules, base fee only. */
|
|
239
|
+
export function defaultStack() {
|
|
240
|
+
return Object.freeze([]);
|
|
241
|
+
}
|
|
242
|
+
export const MAX_STACK_RULES = 5;
|
|
243
|
+
/** Whether `value` sits on `spec`'s step grid, within floating-point rounding. */
|
|
244
|
+
function onStep(value, spec) {
|
|
245
|
+
const steps = (value - spec.min) / spec.step;
|
|
246
|
+
return Math.abs(steps - Math.round(steps)) < 1e-9;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Validate a stack of rule blocks: unknown rule keys, duplicate rules, more than
|
|
250
|
+
* `MAX_STACK_RULES` rules, an unknown parameter, or a parameter outside its bounds or off its
|
|
251
|
+
* step grid. Every problem is reported, not just the first.
|
|
252
|
+
*/
|
|
253
|
+
export function validateStack(blocks) {
|
|
254
|
+
const errors = [];
|
|
255
|
+
if (blocks.length > MAX_STACK_RULES) {
|
|
256
|
+
errors.push(`A stack may carry at most ${MAX_STACK_RULES} rules; this one carries ${blocks.length}.`);
|
|
257
|
+
}
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
for (const block of blocks) {
|
|
260
|
+
if (seen.has(block.type)) {
|
|
261
|
+
errors.push(`Rule "${block.type}" appears more than once in the stack.`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
seen.add(block.type);
|
|
265
|
+
const specs = PARAMS[block.type];
|
|
266
|
+
if (!specs) {
|
|
267
|
+
errors.push(`Unknown rule "${block.type}".`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const knownKeys = new Set(specs.map((spec) => spec.key));
|
|
271
|
+
for (const paramKey of Object.keys(block.params)) {
|
|
272
|
+
if (!knownKeys.has(paramKey)) {
|
|
273
|
+
errors.push(`Rule "${block.type}" has no parameter "${paramKey}".`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const spec of specs) {
|
|
277
|
+
const value = block.params[spec.key];
|
|
278
|
+
if (value === undefined) {
|
|
279
|
+
errors.push(`Rule "${block.type}" is missing its "${spec.key}" parameter.`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!Number.isFinite(value)) {
|
|
283
|
+
errors.push(`Rule "${block.type}"'s "${spec.key}" must be a finite number.`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (value < spec.min || value > spec.max) {
|
|
287
|
+
errors.push(`Rule "${block.type}"'s "${spec.key}" (${value}) must be between ${spec.min} and ${spec.max}.`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!onStep(value, spec)) {
|
|
291
|
+
errors.push(`Rule "${block.type}"'s "${spec.key}" (${value}) must be a multiple of ${spec.step} from ${spec.min}.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return Object.freeze({ ok: errors.length === 0, errors: Object.freeze(errors) });
|
|
296
|
+
}
|
package/dist/swap.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote and swap through Hookr's kernel router and quoter (chain 4663).
|
|
3
|
+
*
|
|
4
|
+
* Copied from the app's `src/lib/v2-swap.ts` (`v2ExactInputCall`, `v2ExactOutputCall`,
|
|
5
|
+
* `v2QuoteCall`): empty module data, the full-fill price limit in both directions, and native
|
|
6
|
+
* ETH sent as `value` exactly when the input currency is the zero address. A pool that carries
|
|
7
|
+
* any input cut (surge, guard, burn, LP reward or pot) rejects any other limit outright — see
|
|
8
|
+
* `market.ts`'s module doc for the fee model these cuts come from.
|
|
9
|
+
*/
|
|
10
|
+
import type { Address, ContractFunctionArgs, Hex, PublicClient } from "viem";
|
|
11
|
+
import { hookrKernelRouterV3Abi } from "./abi.js";
|
|
12
|
+
import type { PoolKey } from "./market.js";
|
|
13
|
+
/** Router / block `MIN_SQRT_PRICE_LIMIT`: the full-fill limit for a `zeroForOne` swap. */
|
|
14
|
+
export declare const MIN_SQRT_PRICE_LIMIT = 4295128740n;
|
|
15
|
+
/** Router / block `MAX_SQRT_PRICE_LIMIT`: the full-fill limit for a `oneForZero` swap. */
|
|
16
|
+
export declare const MAX_SQRT_PRICE_LIMIT = 1461446703485210103287273052203988822378723970341n;
|
|
17
|
+
/** `type(int128).max`: the router's and the quoter's ceiling on every amount and bound. */
|
|
18
|
+
export declare const INT128_MAX: bigint;
|
|
19
|
+
/** The bound an exact-input quote carries: the smallest value the quoter admits. */
|
|
20
|
+
export declare const QUOTE_MIN_OUT_BOUND = 1n;
|
|
21
|
+
/** The router's `moduleData` for a plain swap: the native block reads none. */
|
|
22
|
+
export declare const EMPTY_MODULE_DATA: Hex;
|
|
23
|
+
/** The full-fill `sqrtPriceLimitX96`: `MIN` when selling currency0, `MAX` otherwise. Any pool
|
|
24
|
+
* with an input cut rejects every other limit on an exact-input buy. */
|
|
25
|
+
export declare function fullFillLimit(zeroForOne: boolean): bigint;
|
|
26
|
+
/** `zeroForOne = currencyIn == key.currency0`. Throws when `currencyIn` is not in the pool. */
|
|
27
|
+
export declare function swapDirection(key: PoolKey, currencyIn: Address): boolean;
|
|
28
|
+
export type RouterCall<F extends "exactInput" | "exactOutput"> = Readonly<{
|
|
29
|
+
address: Address;
|
|
30
|
+
abi: typeof hookrKernelRouterV3Abi;
|
|
31
|
+
functionName: F;
|
|
32
|
+
args: ContractFunctionArgs<typeof hookrKernelRouterV3Abi, "payable", F>;
|
|
33
|
+
value: bigint;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* `exactInput` through the Hookr router. `value` is `amountIn` when `currencyIn` is native ETH,
|
|
37
|
+
* else 0 (the router pulls the ERC-20 by `transferFrom`; approve it first).
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildExactInputSwapCall(input: {
|
|
40
|
+
key: PoolKey;
|
|
41
|
+
currencyIn: Address;
|
|
42
|
+
amountIn: bigint;
|
|
43
|
+
amountOutMinimum: bigint;
|
|
44
|
+
recipient: Address;
|
|
45
|
+
deadline: bigint;
|
|
46
|
+
}): RouterCall<"exactInput">;
|
|
47
|
+
/**
|
|
48
|
+
* `exactOutput` through the Hookr router. `value` is `amountInMaximum` when `currencyIn` is
|
|
49
|
+
* native ETH — the router refunds what the swap does not use.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildExactOutputSwapCall(input: {
|
|
52
|
+
key: PoolKey;
|
|
53
|
+
currencyIn: Address;
|
|
54
|
+
amountOut: bigint;
|
|
55
|
+
amountInMaximum: bigint;
|
|
56
|
+
recipient: Address;
|
|
57
|
+
deadline: bigint;
|
|
58
|
+
}): RouterCall<"exactOutput">;
|
|
59
|
+
export type Quote = Readonly<{
|
|
60
|
+
amountIn: bigint;
|
|
61
|
+
amountOut: bigint;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Quote through `kernelQuoter.quote`. Run as a simulated call from `account` rather than an
|
|
65
|
+
* ordinary read: the function is declared `nonpayable` (it runs the full module loop, including
|
|
66
|
+
* the pot leg, which is recipient-specific), which is also why the app's own equivalent
|
|
67
|
+
* (`quoteV2` in `src/lib/v2-swap.ts`) calls `simulateContract` rather than `readContract` — the
|
|
68
|
+
* same `eth_call` either way, just the viem entry point whose types match a nonpayable function.
|
|
69
|
+
* `amountSpecified` is negative for an exact-input quote, bound at 1 wei/unit — the least the
|
|
70
|
+
* quoter admits; pass `amountBound` to override it (an exact-output quote wants `INT128_MAX` or
|
|
71
|
+
* your own maximum).
|
|
72
|
+
*/
|
|
73
|
+
export declare function quoteExactInput(client: PublicClient, input: {
|
|
74
|
+
key: PoolKey;
|
|
75
|
+
user: Address;
|
|
76
|
+
currencyIn: Address;
|
|
77
|
+
amountIn: bigint;
|
|
78
|
+
amountBound?: bigint;
|
|
79
|
+
}): Promise<Quote>;
|