@pickhooks/sdk 0.0.0-stage → 0.2.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -3
  3. package/dist/abis/any3.d.ts +2123 -0
  4. package/dist/abis/any3.js +3 -0
  5. package/dist/abis/anytax.d.ts +1786 -0
  6. package/dist/abis/anytax.js +3 -0
  7. package/dist/abis/calls.d.ts +2515 -0
  8. package/dist/abis/calls.js +4 -0
  9. package/dist/abis/common.d.ts +449 -0
  10. package/dist/abis/common.js +91 -0
  11. package/dist/abis/dips.d.ts +897 -0
  12. package/dist/abis/dips.js +2 -0
  13. package/dist/abis/index.d.ts +12 -0
  14. package/dist/abis/index.js +12 -0
  15. package/dist/abis/infra.d.ts +701 -0
  16. package/dist/abis/infra.js +4 -0
  17. package/dist/abis/lever.d.ts +1710 -0
  18. package/dist/abis/lever.js +5 -0
  19. package/dist/abis/lever2.d.ts +2710 -0
  20. package/dist/abis/lever2.js +5 -0
  21. package/dist/abis/pools.d.ts +1898 -0
  22. package/dist/abis/pools.js +4 -0
  23. package/dist/abis/sticky.d.ts +953 -0
  24. package/dist/abis/sticky.js +4 -0
  25. package/dist/abis/v1.d.ts +1627 -0
  26. package/dist/abis/v1.js +5 -0
  27. package/dist/abis/v2.d.ts +3264 -0
  28. package/dist/abis/v2.js +8 -0
  29. package/dist/addresses.d.ts +87 -0
  30. package/dist/addresses.js +62 -0
  31. package/dist/any.d.ts +447 -0
  32. package/dist/any.js +729 -0
  33. package/dist/api.d.ts +62 -0
  34. package/dist/api.js +30 -0
  35. package/dist/blocks.d.ts +107 -0
  36. package/dist/blocks.js +126 -0
  37. package/dist/coin-rules-config.d.ts +8 -0
  38. package/dist/coin-rules-config.js +13 -0
  39. package/dist/coin-rules.d.ts +383 -0
  40. package/dist/coin-rules.js +878 -0
  41. package/dist/coins.d.ts +217 -0
  42. package/dist/coins.js +520 -0
  43. package/dist/dips.d.ts +133 -0
  44. package/dist/dips.js +148 -0
  45. package/dist/errors.d.ts +35 -0
  46. package/dist/errors.js +347 -0
  47. package/dist/index.d.ts +73 -0
  48. package/dist/index.js +58 -0
  49. package/dist/infinity.d.ts +63 -0
  50. package/dist/infinity.js +96 -0
  51. package/dist/infra.d.ts +112 -0
  52. package/dist/infra.js +114 -0
  53. package/dist/lever.d.ts +391 -0
  54. package/dist/lever.js +419 -0
  55. package/dist/pools.d.ts +274 -0
  56. package/dist/pools.js +405 -0
  57. package/dist/sticky-rules.d.ts +179 -0
  58. package/dist/sticky-rules.js +580 -0
  59. package/dist/sticky.d.ts +216 -0
  60. package/dist/sticky.js +292 -0
  61. package/dist/tx.d.ts +70 -0
  62. package/dist/tx.js +69 -0
  63. package/dist/v3.d.ts +42 -0
  64. package/dist/v3.js +98 -0
  65. package/package.json +58 -4
@@ -0,0 +1,274 @@
1
+ import { type Address, type Hex } from "viem";
2
+ import { type Picked, type R } from "./coin-rules.js";
3
+ import type { CoinRule } from "./coins.js";
4
+ import { type PoolKey } from "./infinity.js";
5
+ import { type Ctx, type SendOptions, type Sent, type TxRequest } from "./tx.js";
6
+ /**
7
+ * Coins with their rules in a PancakeSwap Infinity pool hook: the coin is a plain ERC20, the hook checks the rules on every
8
+ * swap of its pool and takes the fee from the BNB side. Two products share this shape:
9
+ * - pools (factory 0x3F97…241B): rules in the pool
10
+ * - curve calls (factory 0x98D4…8301): the curve is held by the calls desk as 24 steps above the price that anyone can
11
+ * rent for 10 minutes to 24 hours (a call), keeping every BNB above the step when it closes
12
+ * Trades go through PancakeSwap's UniversalRouter like any Infinity pool; the quoter runs the hook, so a refusing rule
13
+ * comes back named before anyone signs.
14
+ */
15
+ export type HookCoin = {
16
+ token: Address;
17
+ name: string;
18
+ symbol: string;
19
+ metadata: string;
20
+ creator: Address;
21
+ start: number;
22
+ /** the Infinity pool id and key */
23
+ id: Hex;
24
+ key: PoolKey;
25
+ rules: CoinRule[];
26
+ raw: R;
27
+ scanner: "green" | "yellow" | "red";
28
+ /** the creator's share of the hook's fee, and the part of it that feeds the jackpot */
29
+ creatorBps: number;
30
+ potBps: number;
31
+ jackpotPot: bigint;
32
+ jackpotLast: Address;
33
+ jackpotTime: number;
34
+ rewardsPot: bigint;
35
+ buyers: number;
36
+ buys: number;
37
+ sells: number;
38
+ /** share of the curve sold, basis points */
39
+ soldBps: number;
40
+ priceBnb: number;
41
+ mcapBnb: number;
42
+ chainNow: number;
43
+ };
44
+ export type HookLaunch = {
45
+ name: string;
46
+ symbol: string;
47
+ metadata: string;
48
+ /** rules of the catalog that work in a pool (the factory's KNOWN mask); none for a plain calls coin */
49
+ rules?: Picked[];
50
+ /** the creator's first buy, BNB wei */
51
+ devBuy?: bigint;
52
+ salt?: Hex;
53
+ };
54
+ /** pools: rules in a PancakeSwap Infinity pool hook. */
55
+ export declare function poolsModule(ctx: Ctx): {
56
+ factory: `0x${string}`;
57
+ hook: () => Promise<Address>;
58
+ key: (token: Address) => Promise<PoolKey>;
59
+ count: () => Promise<number>;
60
+ coins: (o?: {
61
+ limit?: number;
62
+ offset?: number;
63
+ }) => Promise<HookCoin[]>;
64
+ coin: (token: Address) => Promise<HookCoin | null>;
65
+ isCoin: (token: Address) => Promise<boolean>;
66
+ knownKinds: () => Promise<number[]>;
67
+ quote: (q: {
68
+ token: Address;
69
+ side: "buy" | "sell";
70
+ amount: bigint;
71
+ from?: Address;
72
+ }) => Promise<{
73
+ amountOut: bigint;
74
+ }>;
75
+ check: (c: {
76
+ token: Address;
77
+ side: "buy" | "sell";
78
+ wallet: Address;
79
+ amount: bigint;
80
+ }) => Promise<{
81
+ ok: true;
82
+ } | {
83
+ ok: false;
84
+ kind: number;
85
+ name: string;
86
+ text: string;
87
+ }>;
88
+ sellApprovals: (owner: Address, token: Address, amount: bigint) => Promise<TxRequest[]>;
89
+ build: {
90
+ buy: (k: PoolKey, bnbIn: bigint, minOut: bigint, deadline?: bigint) => TxRequest;
91
+ sell: (k: PoolKey, amount: bigint, minOut: bigint, deadline?: bigint) => TxRequest;
92
+ /** jackpot wins, rewards and the creator's share, for every coin of the hook */
93
+ claim: () => Promise<TxRequest>;
94
+ settleJackpot: (token: Address) => Promise<TxRequest>;
95
+ fundPot: (token: Address, pot: "jackpot" | "rewards", wei: bigint) => Promise<TxRequest>;
96
+ withdrawRewards: (token: Address, wei: bigint) => Promise<TxRequest>;
97
+ };
98
+ buy: (b: {
99
+ token: Address;
100
+ bnbIn: bigint;
101
+ slippageBps?: number;
102
+ minOut?: bigint;
103
+ deadline?: bigint;
104
+ }, s?: SendOptions) => Promise<Sent & {
105
+ quoted: bigint;
106
+ minOut: bigint;
107
+ }>;
108
+ sell: (x: {
109
+ token: Address;
110
+ amount: bigint;
111
+ slippageBps?: number;
112
+ minOut?: bigint;
113
+ deadline?: bigint;
114
+ }, s?: SendOptions) => Promise<Sent & {
115
+ quoted: bigint;
116
+ minOut: bigint;
117
+ }>;
118
+ launch: (l: HookLaunch, s?: SendOptions) => Promise<Sent & {
119
+ token: Address;
120
+ id: Hex;
121
+ }>;
122
+ /** BNB the hook owes a wallet (jackpot wins, rewards, a creator's share) */
123
+ owed: (who: Address) => Promise<bigint>;
124
+ claim: (s?: SendOptions) => Promise<Sent>;
125
+ settleJackpot: (token: Address, s?: SendOptions) => Promise<Sent>;
126
+ fundPot: (token: Address, pot: "jackpot" | "rewards", wei: bigint, s?: SendOptions) => Promise<Sent>;
127
+ withdrawRewards: (token: Address, wei: bigint, s?: SendOptions) => Promise<Sent>;
128
+ };
129
+ export type CallStep = {
130
+ step: number;
131
+ /** the step's ticks (the pool's price is coins per BNB: the coin is dearer at lower ticks) */
132
+ lo: number;
133
+ hi: number;
134
+ /** how far the coin's BNB price must rise to reach the step and to cross it, from the price the desk uses now (percent) */
135
+ fromPct: number;
136
+ toPct: number;
137
+ lentCoins: bigint;
138
+ capCoins: bigint;
139
+ /** a call can be rented on it right now */
140
+ open: boolean;
141
+ };
142
+ export type CallRow = {
143
+ id: bigint;
144
+ owner: Address;
145
+ token: Address;
146
+ step: number;
147
+ coins: bigint;
148
+ premium: bigint;
149
+ opened: number;
150
+ expiry: number;
151
+ liquidity: bigint;
152
+ };
153
+ /** curve calls: the pools-shaped coin plus the desk that rents out its curve. */
154
+ export declare function callsModule(ctx: Ctx): {
155
+ desk: () => Promise<`0x${string}`>;
156
+ deskTerms: () => Promise<{
157
+ bond: bigint;
158
+ minDuration: number;
159
+ maxDuration: number;
160
+ steps: number;
161
+ maxAhead: number;
162
+ openAfter: number;
163
+ stepCapBps: number;
164
+ }>;
165
+ steps: (token: Address) => Promise<CallStep[]>;
166
+ quoteCall: (token: Address, step: number, coins: bigint, seconds: number) => Promise<{
167
+ premium: bigint;
168
+ liquidity: bigint;
169
+ }>;
170
+ calls: (token: Address, o?: {
171
+ scan?: number;
172
+ }) => Promise<CallRow[]>;
173
+ payout: (id: bigint, as: Address) => Promise<bigint>;
174
+ pending: (who: Address) => Promise<bigint>;
175
+ build: {
176
+ /** value = the premium (at most maxPremium) plus the desk's bond, which comes back with the close */
177
+ openCall: (token: Address, step: number, coins: bigint, seconds: number, maxPremium: bigint) => Promise<TxRequest>;
178
+ closeCall: (id: bigint, minPayout: bigint) => Promise<TxRequest>;
179
+ /** BNB the desk could not send (a wallet that refused it) */
180
+ withdraw: () => Promise<TxRequest>;
181
+ buy: (k: PoolKey, bnbIn: bigint, minOut: bigint, deadline?: bigint) => TxRequest;
182
+ sell: (k: PoolKey, amount: bigint, minOut: bigint, deadline?: bigint) => TxRequest;
183
+ /** jackpot wins, rewards and the creator's share, for every coin of the hook */
184
+ claim: () => Promise<TxRequest>;
185
+ settleJackpot: (token: Address) => Promise<TxRequest>;
186
+ fundPot: (token: Address, pot: "jackpot" | "rewards", wei: bigint) => Promise<TxRequest>;
187
+ withdrawRewards: (token: Address, wei: bigint) => Promise<TxRequest>;
188
+ };
189
+ /** rents `coins` of a step for `seconds`; the premium is capped at the quote plus slippageBps (default 100) */
190
+ openCall: (c: {
191
+ token: Address;
192
+ step: number;
193
+ coins: bigint;
194
+ seconds: number;
195
+ maxPremium?: bigint;
196
+ slippageBps?: number;
197
+ }, s?: SendOptions) => Promise<Sent & {
198
+ id: bigint;
199
+ premium: bigint;
200
+ }>;
201
+ /** closes a call for at least the payout of a fresh simulation less slippageBps (default 100) */
202
+ closeCall: (id: bigint, o?: {
203
+ minPayout?: bigint;
204
+ slippageBps?: number;
205
+ }, s?: SendOptions) => Promise<Sent & {
206
+ payout: bigint;
207
+ }>;
208
+ withdraw: (s?: SendOptions) => Promise<Sent>;
209
+ factory: `0x${string}`;
210
+ hook: () => Promise<Address>;
211
+ key: (token: Address) => Promise<PoolKey>;
212
+ count: () => Promise<number>;
213
+ coins: (o?: {
214
+ limit?: number;
215
+ offset?: number;
216
+ }) => Promise<HookCoin[]>;
217
+ coin: (token: Address) => Promise<HookCoin | null>;
218
+ isCoin: (token: Address) => Promise<boolean>;
219
+ knownKinds: () => Promise<number[]>;
220
+ quote: (q: {
221
+ token: Address;
222
+ side: "buy" | "sell";
223
+ amount: bigint;
224
+ from?: Address;
225
+ }) => Promise<{
226
+ amountOut: bigint;
227
+ }>;
228
+ check: (c: {
229
+ token: Address;
230
+ side: "buy" | "sell";
231
+ wallet: Address;
232
+ amount: bigint;
233
+ }) => Promise<{
234
+ ok: true;
235
+ } | {
236
+ ok: false;
237
+ kind: number;
238
+ name: string;
239
+ text: string;
240
+ }>;
241
+ sellApprovals: (owner: Address, token: Address, amount: bigint) => Promise<TxRequest[]>;
242
+ buy: (b: {
243
+ token: Address;
244
+ bnbIn: bigint;
245
+ slippageBps?: number;
246
+ minOut?: bigint;
247
+ deadline?: bigint;
248
+ }, s?: SendOptions) => Promise<Sent & {
249
+ quoted: bigint;
250
+ minOut: bigint;
251
+ }>;
252
+ sell: (x: {
253
+ token: Address;
254
+ amount: bigint;
255
+ slippageBps?: number;
256
+ minOut?: bigint;
257
+ deadline?: bigint;
258
+ }, s?: SendOptions) => Promise<Sent & {
259
+ quoted: bigint;
260
+ minOut: bigint;
261
+ }>;
262
+ launch: (l: HookLaunch, s?: SendOptions) => Promise<Sent & {
263
+ token: Address;
264
+ id: Hex;
265
+ }>;
266
+ /** BNB the hook owes a wallet (jackpot wins, rewards, a creator's share) */
267
+ owed: (who: Address) => Promise<bigint>;
268
+ claim: (s?: SendOptions) => Promise<Sent>;
269
+ settleJackpot: (token: Address, s?: SendOptions) => Promise<Sent>;
270
+ fundPot: (token: Address, pot: "jackpot" | "rewards", wei: bigint, s?: SendOptions) => Promise<Sent>;
271
+ withdrawRewards: (token: Address, wei: bigint, s?: SendOptions) => Promise<Sent>;
272
+ };
273
+ export type PoolsModule = ReturnType<typeof poolsModule>;
274
+ export type CallsModule = ReturnType<typeof callsModule>;
package/dist/pools.js ADDED
@@ -0,0 +1,405 @@
1
+ import { encodeFunctionData, isAddressEqual, parseEventLogs } from "viem";
2
+ import { callsDeskAbi, callsFactoryAbi, callsHookAbi } from "./abis/calls.js";
3
+ import { clPoolManagerAbi } from "./abis/common.js";
4
+ import { poolsFactoryAbi, poolsHookAbi } from "./abis/pools.js";
5
+ import { RULE_BY_ID, RULE_BY_KIND, buildRules, kindsOf, selectionProblem, worstScanner } from "./coin-rules.js";
6
+ import { ruleText } from "./coins.js";
7
+ import { PickhooksError, explain, toPickhooksError } from "./errors.js";
8
+ import { blockedKind, buildInfiSwap, infiPriceBnb, infiQuote, permit2Approvals, toKey } from "./infinity.js";
9
+ import { minusBps, sendAll, sendTx, signer } from "./tx.js";
10
+ const randomSalt = () => {
11
+ const b = new Uint8Array(32);
12
+ globalThis.crypto.getRandomValues(b);
13
+ return `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
14
+ };
15
+ function hookCoinModule(ctx, factory, fAbi, hAbi, product) {
16
+ const { pub, a } = ctx;
17
+ let hookAddr = null;
18
+ const keys = new Map();
19
+ const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
20
+ async function hook() {
21
+ hookAddr ??= await call(factory, fAbi, "hook");
22
+ return hookAddr;
23
+ }
24
+ async function key(token) {
25
+ const hit = keys.get(token.toLowerCase());
26
+ if (hit)
27
+ return hit;
28
+ const k = toKey(await call(factory, fAbi, "poolKeyOf", [token]));
29
+ if (/^0x0{40}$/i.test(k.hooks))
30
+ throw new PickhooksError(`This is not a ${product} coin.`, { code: "input" });
31
+ keys.set(token.toLowerCase(), k);
32
+ return k;
33
+ }
34
+ const count = async () => Number(await call(factory, fAbi, "coinCount"));
35
+ const isCoin = async (token) => (await call(factory, fAbi, "isCoin", [token]).catch(() => false)) === true;
36
+ /** Coins newest first. */
37
+ async function coins(o = {}) {
38
+ const n = await count();
39
+ const end = Math.max(0, n - (o.offset ?? 0));
40
+ const from = Math.max(0, end - (o.limit ?? 50));
41
+ if (end <= from)
42
+ return [];
43
+ const tokens = (await pub.multicall({
44
+ contracts: Array.from({ length: end - from }, (_, i) => ({ address: factory, abi: fAbi, functionName: "coins", args: [BigInt(end - 1 - i)] })),
45
+ allowFailure: false,
46
+ }));
47
+ return summaries(tokens);
48
+ }
49
+ /** One coin, or null. */
50
+ async function coin(token) {
51
+ if (!(await isCoin(token)))
52
+ return null;
53
+ return (await summaries([token]))[0] ?? null;
54
+ }
55
+ async function summaries(tokens) {
56
+ if (!tokens.length)
57
+ return [];
58
+ const h = await hook();
59
+ const per = 5;
60
+ const [res, head] = await Promise.all([
61
+ pub.multicall({
62
+ contracts: tokens.flatMap((t) => [
63
+ { address: h, abi: hAbi, functionName: "coinOf", args: [t] },
64
+ { address: h, abi: hAbi, functionName: "rulesOf", args: [t] },
65
+ { address: h, abi: hAbi, functionName: "statsOf", args: [t] },
66
+ { address: h, abi: hAbi, functionName: "poolOf", args: [t] },
67
+ { address: factory, abi: fAbi, functionName: "poolKeyOf", args: [t] },
68
+ ]),
69
+ allowFailure: true,
70
+ }),
71
+ pub.getBlock({ blockTag: "latest" }),
72
+ ]);
73
+ // name, symbol and metadata are the token's own (the hook ABIs do not carry them): read them as an ERC20
74
+ const tokenMeta = await pub.multicall({
75
+ contracts: tokens.flatMap((t) => ["name", "symbol", "metadata"].map((functionName) => ({ address: t, abi: PLAIN, functionName }))),
76
+ allowFailure: true,
77
+ });
78
+ const ids = tokens.map((_, i) => res[i * per + 3].result);
79
+ const slots = await pub.multicall({
80
+ contracts: ids.map((id) => ({ address: a.clPoolManager, abi: clPoolManagerAbi, functionName: "getSlot0", args: [id ?? `0x${"0".repeat(64)}`] })),
81
+ allowFailure: true,
82
+ });
83
+ const now = Number(head.timestamp);
84
+ const untilRes = await pub.multicall({
85
+ contracts: tokens.flatMap((_, i) => {
86
+ const r = res[i * per + 1].result;
87
+ return r && ids[i] ? kindsOf(r.mask).map((k) => ({ address: h, abi: hAbi, functionName: "until", args: [ids[i], k] })) : [];
88
+ }),
89
+ allowFailure: true,
90
+ });
91
+ let u = 0;
92
+ const out = [];
93
+ tokens.forEach((token, i) => {
94
+ const g = (k) => res[i * per + k];
95
+ const rules = g(1).result;
96
+ const kinds = rules && ids[i] ? kindsOf(rules.mask) : [];
97
+ const untils = kinds.map(() => untilRes[u++]);
98
+ if (g(0).status !== "success" || !rules || !ids[i] || g(4).status !== "success")
99
+ return;
100
+ const c = g(0).result;
101
+ const st = g(2).result;
102
+ const k = toKey(g(4).result);
103
+ keys.set(token.toLowerCase(), k);
104
+ const sl = slots[i];
105
+ const priceBnb = sl.status === "success" ? infiPriceBnb(sl.result[0]) : 0;
106
+ const m = (j) => tokenMeta[i * 3 + j];
107
+ out.push({
108
+ token,
109
+ name: m(0).status === "success" ? String(m(0).result) : "",
110
+ symbol: m(1).status === "success" ? String(m(1).result) : "",
111
+ metadata: m(2).status === "success" ? String(m(2).result) : "",
112
+ creator: c[0],
113
+ start: Number(c[1]),
114
+ id: ids[i],
115
+ key: k,
116
+ rules: kinds.map((kind, j) => {
117
+ const def = RULE_BY_KIND.get(kind);
118
+ const until = untils[j]?.status === "success" ? Number(untils[j].result) : 0;
119
+ let text = def?.line ?? "";
120
+ try {
121
+ if (def)
122
+ text = def.describe(rules);
123
+ }
124
+ catch {
125
+ /* keep the line */
126
+ }
127
+ return { kind, name: def?.name ?? `Rule ${kind}`, text, scanner: def?.scanner.level ?? "green", until, ended: until !== 0 && now >= until };
128
+ }),
129
+ raw: rules,
130
+ scanner: worstScanner(kinds),
131
+ creatorBps: Number(c[3]),
132
+ potBps: Number(c[4]),
133
+ jackpotPot: c[5],
134
+ jackpotLast: c[6],
135
+ jackpotTime: Number(c[7]),
136
+ rewardsPot: c[8],
137
+ buyers: Number(st.buyers),
138
+ buys: Number(st.buys),
139
+ sells: Number(st.sells),
140
+ soldBps: c[2] ? Number((st.net * 10000n) / c[2]) : 0,
141
+ priceBnb,
142
+ mcapBnb: priceBnb * 1e9,
143
+ chainNow: now,
144
+ });
145
+ });
146
+ return out;
147
+ }
148
+ /** a refusal of the hook as a sentence that names the rule */
149
+ async function refusalOf(token, e, fallback) {
150
+ const kind = blockedKind(e);
151
+ if (kind === null)
152
+ return toPickhooksError(e, fallback);
153
+ const rules = await call(await hook(), hAbi, "rulesOf", [token]).catch(() => null);
154
+ return new PickhooksError(ruleText(kind, rules).text, { code: "Blocked", args: [kind], cause: e });
155
+ }
156
+ /** What a trade gets, from PancakeSwap's quoter running the swap with the hook; a refusing rule is named. `from` is the trader. */
157
+ async function quote(q) {
158
+ try {
159
+ const r = await infiQuote(ctx, { key: await key(q.token), buy: q.side === "buy", amount: q.amount, from: q.from });
160
+ return { amountOut: r.amountOut };
161
+ }
162
+ catch (e) {
163
+ throw await refusalOf(q.token, e, "The quoter refused this trade.");
164
+ }
165
+ }
166
+ /** Would this trade go through for this wallet: the quote runs the hook's rules as the trader. */
167
+ async function check(c) {
168
+ try {
169
+ await infiQuote(ctx, { key: await key(c.token), buy: c.side === "buy", amount: c.amount, from: c.wallet });
170
+ return { ok: true };
171
+ }
172
+ catch (e) {
173
+ const kind = blockedKind(e);
174
+ if (kind === null)
175
+ return { ok: false, kind: 0, name: "", text: explain(e, "The pool refused this trade.").text };
176
+ const rules = await call(await hook(), hAbi, "rulesOf", [c.token]).catch(() => null);
177
+ return { ok: false, kind, ...ruleText(kind, rules) };
178
+ }
179
+ }
180
+ const buildBuy = (k, bnbIn, minOut, deadline) => buildInfiSwap(a, { key: k, buy: true, legs: [bnbIn], minOut, deadline, gasPct: 150 });
181
+ const buildSell = (k, amount, minOut, deadline) => buildInfiSwap(a, { key: k, buy: false, legs: [amount], minOut, deadline, gasPct: 150 });
182
+ /** Quotes as the trader (a refusing rule stops it before anything is signed), then buys through the UniversalRouter. */
183
+ async function buy(b, s = {}) {
184
+ const { address } = await signer(ctx, s.wallet);
185
+ const q = await quote({ token: b.token, side: "buy", amount: b.bnbIn, from: address });
186
+ const minOut = b.minOut ?? minusBps(q.amountOut, b.slippageBps ?? 100);
187
+ try {
188
+ return { ...(await sendTx(ctx, buildBuy(await key(b.token), b.bnbIn, minOut, b.deadline), s)), quoted: q.amountOut, minOut };
189
+ }
190
+ catch (e) {
191
+ throw await refusalOf(b.token, e, "The buy would fail.");
192
+ }
193
+ }
194
+ /** Approves Permit2 if needed, quotes as the trader, sells through the UniversalRouter. */
195
+ async function sell(x, s = {}) {
196
+ const { address } = await signer(ctx, s.wallet);
197
+ const q = await quote({ token: x.token, side: "sell", amount: x.amount, from: address });
198
+ await sendAll(ctx, await permit2Approvals(ctx, address, x.token, x.amount), s);
199
+ const minOut = x.minOut ?? minusBps(q.amountOut, x.slippageBps ?? 100);
200
+ try {
201
+ return { ...(await sendTx(ctx, buildSell(await key(x.token), x.amount, minOut, x.deadline), s)), quoted: q.amountOut, minOut };
202
+ }
203
+ catch (e) {
204
+ throw await refusalOf(x.token, e, "The sell would fail.");
205
+ }
206
+ }
207
+ /** The factory's rule kinds that work in a pool (its KNOWN mask). */
208
+ const knownKinds = async () => kindsOf(await call(factory, fAbi, "KNOWN"));
209
+ /** Checks the rules (catalog, the factory's known kinds, the factory's own validate), launches, returns the coin. */
210
+ async function launch(l, s = {}) {
211
+ const picked = l.rules ?? [];
212
+ const problem = selectionProblem(picked);
213
+ if (problem)
214
+ throw new PickhooksError(problem, { code: "input" });
215
+ const known = await knownKinds();
216
+ for (const p of picked) {
217
+ const def = RULE_BY_ID.get(p.id);
218
+ if (def && !known.includes(def.kind))
219
+ throw new PickhooksError(`${def.name} does not work in a pool: it needs the coin itself.`, { code: "input" });
220
+ }
221
+ const now = Number((await pub.getBlock({ blockTag: "latest" })).timestamp);
222
+ const built = buildRules(picked, now);
223
+ try {
224
+ await call(factory, fAbi, "validate", [built.rules, built.untilIds, built.untilTimes]);
225
+ }
226
+ catch (e) {
227
+ throw toPickhooksError(e, "The factory refused these rules.");
228
+ }
229
+ const tx = {
230
+ to: factory,
231
+ value: l.devBuy ?? 0n,
232
+ data: encodeFunctionData({ abi: fAbi, functionName: "launch", args: [l.name, l.symbol, l.metadata, built.rules, built.untilIds, built.untilTimes, l.salt ?? randomSalt()] }),
233
+ gasPct: 125,
234
+ label: "The launch",
235
+ };
236
+ const sent = await sendTx(ctx, tx, s);
237
+ const ev = parseEventLogs({ abi: fAbi, logs: sent.receipt.logs.filter((x) => isAddressEqual(x.address, factory)) }).find((e) => e.eventName === "Launched");
238
+ if (!ev)
239
+ throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
240
+ return { ...sent, token: ev.args.token, id: ev.args.poolId };
241
+ }
242
+ const hookTx = async (fn, args = [], value = 0n, label) => ({
243
+ to: await hook(),
244
+ value,
245
+ data: encodeFunctionData({ abi: hAbi, functionName: fn, args }),
246
+ label,
247
+ });
248
+ const build = {
249
+ buy: (k, bnbIn, minOut, deadline) => buildBuy(k, bnbIn, minOut, deadline),
250
+ sell: (k, amount, minOut, deadline) => buildSell(k, amount, minOut, deadline),
251
+ /** jackpot wins, rewards and the creator's share, for every coin of the hook */
252
+ claim: () => hookTx("claim", [], 0n, "The claim"),
253
+ settleJackpot: (token) => hookTx("settleJackpot", [token], 0n, "Settling the jackpot"),
254
+ fundPot: (token, pot, wei) => hookTx(pot === "rewards" ? "fundRewards" : "fundJackpot", [token], wei, `Funding the ${pot}`),
255
+ withdrawRewards: (token, wei) => hookTx("withdrawRewards", [token, wei], 0n, "Taking rewards back"),
256
+ };
257
+ return {
258
+ factory,
259
+ hook,
260
+ key,
261
+ count,
262
+ coins,
263
+ coin,
264
+ isCoin,
265
+ knownKinds,
266
+ quote,
267
+ check,
268
+ sellApprovals: (owner, token, amount) => permit2Approvals(ctx, owner, token, amount),
269
+ build,
270
+ buy,
271
+ sell,
272
+ launch,
273
+ /** BNB the hook owes a wallet (jackpot wins, rewards, a creator's share) */
274
+ owed: async (who) => call(await hook(), hAbi, "owed", [who]),
275
+ claim: async (s = {}) => sendTx(ctx, await build.claim(), s),
276
+ settleJackpot: async (token, s = {}) => sendTx(ctx, await build.settleJackpot(token), s),
277
+ fundPot: async (token, pot, wei, s = {}) => sendTx(ctx, await build.fundPot(token, pot, wei), s),
278
+ withdrawRewards: async (token, wei, s = {}) => sendTx(ctx, await build.withdrawRewards(token, wei), s),
279
+ };
280
+ }
281
+ const PLAIN = [
282
+ { type: "function", name: "name", inputs: [], outputs: [{ name: "", type: "string" }], stateMutability: "view" },
283
+ { type: "function", name: "symbol", inputs: [], outputs: [{ name: "", type: "string" }], stateMutability: "view" },
284
+ { type: "function", name: "metadata", inputs: [], outputs: [{ name: "", type: "string" }], stateMutability: "view" },
285
+ ];
286
+ /** pools: rules in a PancakeSwap Infinity pool hook. */
287
+ export function poolsModule(ctx) {
288
+ return hookCoinModule(ctx, ctx.a.poolsFactory, poolsFactoryAbi, poolsHookAbi, "pools");
289
+ }
290
+ const LN_TICK = Math.log(1.0001);
291
+ const priceAt = (tick) => Math.exp(-tick * LN_TICK);
292
+ /** curve calls: the pools-shaped coin plus the desk that rents out its curve. */
293
+ export function callsModule(ctx) {
294
+ const base = hookCoinModule(ctx, ctx.a.callsFactory, callsFactoryAbi, callsHookAbi, "curve calls");
295
+ const { pub } = ctx;
296
+ let deskAddr = null;
297
+ const desk = async () => (deskAddr ??= (await pub.readContract({ address: ctx.a.callsFactory, abi: callsFactoryAbi, functionName: "desk" })));
298
+ const D = callsDeskAbi;
299
+ const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
300
+ /** The desk's rules: bond, durations, steps, how far ahead, when calls open after a launch. */
301
+ async function deskTerms() {
302
+ const d = await desk();
303
+ const fns = ["BOND", "MIN_DURATION", "MAX_DURATION", "STEPS", "MAX_AHEAD", "OPEN_AFTER", "STEP_CAP_BPS"];
304
+ const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: d, abi: D, functionName })), allowFailure: false });
305
+ return { bond: r[0], minDuration: Number(r[1]), maxDuration: Number(r[2]), steps: Number(r[3]), maxAhead: Number(r[4]), openAfter: Number(r[5]), stepCapBps: Number(r[6]) };
306
+ }
307
+ /** The steps of a coin's curve a call can rent, with how far the price must rise to reach each one. */
308
+ async function steps(token) {
309
+ const [d, t, h] = await Promise.all([desk(), deskTerms(), base.hook()]);
310
+ const [coin, oracle, block] = await Promise.all([
311
+ call(h, callsHookAbi, "coinOf", [token]),
312
+ call(h, callsHookAbi, "oracleOf", [token]),
313
+ pub.getBlock({ blockTag: "latest" }),
314
+ ]);
315
+ const now = Number(block.timestamp);
316
+ const at = Math.min(Number(oracle[0]), Number(oracle[1]));
317
+ const r = await pub.multicall({ contracts: Array.from({ length: t.steps }, (_, k) => ({ address: d, abi: D, functionName: "stepOf", args: [token, k] })), allowFailure: false });
318
+ return r.map((x, k) => {
319
+ const [lo, hi, , , lentCoins, capCoins] = x;
320
+ const open = Number(hi) <= at && at - Number(hi) <= t.maxAhead * (Number(hi) - Number(lo)) && now >= Number(coin[1]) + t.openAfter;
321
+ return { step: k, lo: Number(lo), hi: Number(hi), fromPct: (priceAt(Number(hi)) / priceAt(at) - 1) * 100, toPct: (priceAt(Number(lo)) / priceAt(at) - 1) * 100, lentCoins, capCoins, open };
322
+ });
323
+ }
324
+ /** The premium for renting `coins` of a step for `seconds`, priced by the desk. */
325
+ async function quoteCall(token, step, coins, seconds) {
326
+ try {
327
+ const [premium, liquidity] = await call(await desk(), D, "quote", [token, step, coins, seconds]);
328
+ return { premium, liquidity };
329
+ }
330
+ catch (e) {
331
+ throw toPickhooksError(e, "The desk refused this call.");
332
+ }
333
+ }
334
+ /** Open calls of a coin (the newest `scan` calls of the desk are read, default 400). */
335
+ async function calls(token, o = {}) {
336
+ const d = await desk();
337
+ const n = Number(await call(d, D, "callCount"));
338
+ if (!n)
339
+ return [];
340
+ const from = Math.max(1, n - (o.scan ?? 400) + 1);
341
+ const res = await pub.multicall({ contracts: Array.from({ length: n - from + 1 }, (_, i) => ({ address: d, abi: D, functionName: "callOf", args: [BigInt(from + i)] })), allowFailure: true });
342
+ const out = [];
343
+ res.forEach((r, i) => {
344
+ if (r.status !== "success")
345
+ return;
346
+ const c = r.result;
347
+ if (/^0x0{40}$/i.test(c.owner) || !isAddressEqual(c.token, token))
348
+ return;
349
+ out.push({ id: BigInt(from + i), owner: c.owner, token: c.token, step: Number(c.step), coins: c.coins, premium: c.premium, opened: Number(c.opened), expiry: Number(c.expiry), liquidity: c.liquidity });
350
+ });
351
+ return out;
352
+ }
353
+ /** What closing a call pays now, simulated as `as` (its owner before the end, anyone after). */
354
+ async function payout(id, as) {
355
+ try {
356
+ const r = await pub.simulateContract({ address: await desk(), abi: D, functionName: "close", args: [id, 0n], account: as });
357
+ return r.result;
358
+ }
359
+ catch (e) {
360
+ throw toPickhooksError(e, "This call cannot be closed now.");
361
+ }
362
+ }
363
+ const deskTx = async (fn, args = [], value = 0n, label) => ({
364
+ to: await desk(),
365
+ value,
366
+ data: encodeFunctionData({ abi: D, functionName: fn, args }),
367
+ label,
368
+ });
369
+ const callBuild = {
370
+ ...base.build,
371
+ /** value = the premium (at most maxPremium) plus the desk's bond, which comes back with the close */
372
+ openCall: async (token, step, coins, seconds, maxPremium) => deskTx("open", [token, step, coins, seconds, maxPremium], maxPremium + (await deskTerms()).bond, "Opening the call"),
373
+ closeCall: (id, minPayout) => deskTx("close", [id, minPayout], 0n, "Closing the call"),
374
+ /** BNB the desk could not send (a wallet that refused it) */
375
+ withdraw: () => deskTx("withdraw", [], 0n, "The withdrawal"),
376
+ };
377
+ return {
378
+ ...base,
379
+ desk,
380
+ deskTerms,
381
+ steps,
382
+ quoteCall,
383
+ calls,
384
+ payout,
385
+ pending: async (who) => call(await desk(), D, "pending", [who]),
386
+ build: callBuild,
387
+ /** rents `coins` of a step for `seconds`; the premium is capped at the quote plus slippageBps (default 100) */
388
+ openCall: async (c, s = {}) => {
389
+ const q = await quoteCall(c.token, c.step, c.coins, c.seconds);
390
+ const max = c.maxPremium ?? (q.premium * BigInt(10_000 + (c.slippageBps ?? 100))) / 10000n;
391
+ const sent = await sendTx(ctx, await callBuild.openCall(c.token, c.step, c.coins, c.seconds, max), s);
392
+ const ev = parseEventLogs({ abi: D, logs: sent.receipt.logs }).find((e) => e.eventName === "CallOpened");
393
+ return { ...sent, id: ev?.args.id ?? 0n, premium: ev?.args.premium ?? q.premium };
394
+ },
395
+ /** closes a call for at least the payout of a fresh simulation less slippageBps (default 100) */
396
+ closeCall: async (id, o = {}, s = {}) => {
397
+ const { address } = await signer(ctx, s.wallet);
398
+ const now = await payout(id, address);
399
+ const sent = await sendTx(ctx, await callBuild.closeCall(id, o.minPayout ?? minusBps(now, o.slippageBps ?? 100)), s);
400
+ const ev = parseEventLogs({ abi: D, logs: sent.receipt.logs }).find((e) => e.eventName === "CallClosed");
401
+ return { ...sent, payout: ev?.args.payout ?? now };
402
+ },
403
+ withdraw: async (s = {}) => sendTx(ctx, await callBuild.withdraw(), s),
404
+ };
405
+ }