@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
package/dist/any.js ADDED
@@ -0,0 +1,729 @@
1
+ import { concatHex, encodeAbiParameters, encodeFunctionData, isAddressEqual, maxUint256, parseAbi, parseEventLogs, } from "viem";
2
+ import { any3FactoryAbi, any3HookAbi } from "./abis/any3.js";
3
+ import { anyTaxFactoryAbi, anyTaxHookAbi } from "./abis/anytax.js";
4
+ import { SWAP_EXACT_IN_SINGLE, clPoolManagerAbi, erc20Abi, pancakeV2FactoryAbi, universalRouterAbi } from "./abis/common.js";
5
+ import { LIMITS, NATIVE, ZERO_ADDRESS } from "./addresses.js";
6
+ import { CHECK_TEXT, PickhooksError, toPickhooksError } from "./errors.js";
7
+ import { buildInfiSwap, infiQuote, permit2Approvals, poolIdOf, toKey } from "./infinity.js";
8
+ import { deadlineIn, minusBps, sendAll, sendTx, senderAddress, signer } from "./tx.js";
9
+ export { poolIdOf } from "./infinity.js";
10
+ // ---------------------------------------------------------------- pure helpers
11
+ const Q96 = 2 ** 96;
12
+ const LOG_BASE = Math.log(1.0001);
13
+ export const MIN_TICK = -887272;
14
+ export const MAX_TICK = 887272;
15
+ /** flap's anti-farmer state: every pool transfer is taxed, the Infinity vault's too */
16
+ const ANTI_FARMER = 2;
17
+ /** AnyTaxHook.QUOTE = keccak256("pickhooks.any.tax.quote"): a sell quote carries it to look past the pay-first guard */
18
+ export const TAX_QUOTE_MARK = "0xef4a4762058b7d8f677fa48345d3b8a6bfcfe2e6867a31c94f0749a6b8079c54";
19
+ /** The tick of a sqrt price (Q64.96), rounded down. */
20
+ export function tickOf(sqrtPrice) {
21
+ const p = (Number(sqrtPrice) / Q96) ** 2;
22
+ return Math.floor(Math.log(p) / LOG_BASE);
23
+ }
24
+ const sqrtAt = (tick) => Math.sqrt(1.0001 ** tick);
25
+ const floorTo = (t, s) => Math.floor(t / s) * s;
26
+ const ceilTo = (t, s) => Math.ceil(t / s) * s;
27
+ /**
28
+ * The ticks of a range for a slot. The pool's price is coin per BNB, so the coin gets dearer as the tick falls.
29
+ * ladder: coins only, from the price up with no top; ladder10: coins only, up to 10x; around: half to twice the price.
30
+ */
31
+ export function rangeFor(kind, sqrtPrice, slot) {
32
+ const s = LIMITS.tickBase + slot;
33
+ const t = tickOf(sqrtPrice);
34
+ const minUsable = ceilTo(MIN_TICK, s);
35
+ const maxUsable = floorTo(MAX_TICK, s);
36
+ if (kind === "around") {
37
+ const w = Math.round(Math.log(2) / LOG_BASE);
38
+ return { lower: Math.max(minUsable, floorTo(t - w, s)), upper: Math.min(maxUsable, ceilTo(t + w, s)) };
39
+ }
40
+ // one spacing of margin below the price, so a small move before the open never needs BNB
41
+ const upper = floorTo(t, s) - s;
42
+ if (kind === "ladder10")
43
+ return { lower: Math.max(minUsable, floorTo(upper - Math.round(Math.log(10) / LOG_BASE), s)), upper };
44
+ return { lower: minUsable, upper };
45
+ }
46
+ /**
47
+ * About how many coins (raw) and how much BNB a range needs so the new pool is as deep for buys as the coin's main pool
48
+ * (the factory refuses a shallower one with TooShallow), with a 3 % margin. Depth is liquidity / sqrt price, the number a
49
+ * V2 pool's BNB reserve means. A tax-line coin arrives less its sell tax: pass `sellTaxBps` (CoinCheck.tax.poolSellBps).
50
+ */
51
+ export function depthNeeds(check, range, slot, sellTaxBps = 0) {
52
+ const { lower, upper } = rangeFor(range, check.sqrtPrice, slot);
53
+ const sp = Number(check.sqrtPrice) / Q96;
54
+ const sl = sqrtAt(lower);
55
+ const su = sqrtAt(upper);
56
+ const d = Number(check.depth) * 1.03;
57
+ const gross = (c) => (sellTaxBps > 0 ? (c * 10_000) / (10_000 - sellTaxBps) : c);
58
+ if (range === "around") {
59
+ const l = d * sp;
60
+ return { coins: BigInt(Math.ceil(gross(l * (sp - sl)))), bnb: BigInt(Math.ceil((l * (su - sp)) / (sp * su))) };
61
+ }
62
+ // coins only, the price above the range: the depth counts at the range's top
63
+ return { coins: BigInt(Math.ceil(gross(d * su * (su - sl)))), bnb: 0n };
64
+ }
65
+ /**
66
+ * The widest coins-only ladder right above the price that `coins` can make at least as deep as the coin's main pool
67
+ * (plus marginBps, default 500): the way to open with few coins (depth sits at the price, the ladder ends sooner).
68
+ * A tax-line coin arrives less its sell tax. Null when the coins cannot fill even one tick spacing.
69
+ */
70
+ export function fitLadder(check, coins, slot, sellTaxBps = 0, marginBps = 500) {
71
+ const s = LIMITS.tickBase + slot;
72
+ const upper = floorTo(tickOf(check.sqrtPrice), s) - s;
73
+ const arrive = (Number(coins) * (10_000 - sellTaxBps)) / 10_000;
74
+ const need = (Number(check.depth) * (10_000 + marginBps)) / 10_000;
75
+ const su = sqrtAt(upper);
76
+ let width = 0;
77
+ for (let w = s; w <= 2000 * s && upper - w >= ceilTo(MIN_TICK, s); w += s) {
78
+ // liquidity of the coins over [upper - w, upper], and its depth at the top (the price sits just above the range)
79
+ const l = arrive / (su - sqrtAt(upper - w));
80
+ if (l / su < need)
81
+ break;
82
+ width = w;
83
+ }
84
+ return width ? { tickLower: upper - width, tickUpper: upper } : null;
85
+ }
86
+ /** The site's link that opens a pool with this partner named (the open form reads ?partner=). */
87
+ export function partnerLink(partner, site = "https://pickhooks.com") {
88
+ return `${site.replace(/\/$/, "")}/any?partner=${partner}`;
89
+ }
90
+ /** what arrives of `amount` after a tax of `bps` (flap floors the tax) */
91
+ export const afterTax = (amount, bps) => amount - (amount * BigInt(bps)) / 10000n;
92
+ const flapAbi = parseAbi([
93
+ "function getPoolStateData() view returns (uint8 state, uint16 buyTax, uint16 sellTax, uint256 threshold, uint256 taxExpiry, uint256 antiFarmerExpiry)",
94
+ "function pools(address) view returns (bool)",
95
+ "function quoteToken() view returns (address)",
96
+ ]);
97
+ export function anyModule(ctx) {
98
+ const { pub, a } = ctx;
99
+ const TAX_ON = !/^0x0{40}$/i.test(a.anyTaxFactory);
100
+ const LINES = {
101
+ main: { name: "main", on: true, factory: a.anyFactory, fAbi: any3FactoryAbi, hAbi: any3HookAbi },
102
+ tax: { name: "tax", on: TAX_ON, factory: a.anyTaxFactory, fAbi: anyTaxFactoryAbi, hAbi: anyTaxHookAbi },
103
+ };
104
+ const hooks = new Map([["main", a.anyHook]]);
105
+ /** where each pool id lives, learned from reads */
106
+ const where = new Map();
107
+ const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
108
+ async function hookOf(line) {
109
+ const known = hooks.get(line);
110
+ if (known)
111
+ return known;
112
+ const h = await call(LINES[line].factory, LINES[line].fAbi, "hook");
113
+ hooks.set(line, h);
114
+ return h;
115
+ }
116
+ const lineOfFactory = (f) => (TAX_ON && isAddressEqual(f, a.anyTaxFactory) ? "tax" : "main");
117
+ /** a pool's line and key: from the ref, a read before, or the factories (main first) */
118
+ async function locate(ref) {
119
+ if (typeof ref !== "string" && "id" in ref)
120
+ return { id: ref.id, line: ref.line ?? "main", key: ref.key };
121
+ if (typeof ref !== "string") {
122
+ const id = poolIdOf(ref);
123
+ return { id, line: isAddressEqual(ref.hooks, a.anyHook) ? "main" : TAX_ON && isAddressEqual(ref.hooks, await hookOf("tax")) ? "tax" : "main", key: ref };
124
+ }
125
+ const hit = where.get(ref.toLowerCase());
126
+ if (hit)
127
+ return { id: ref, ...hit };
128
+ for (const line of ["main", "tax"]) {
129
+ if (!LINES[line].on)
130
+ continue;
131
+ const raw = await call(LINES[line].factory, LINES[line].fAbi, "keyOf", [ref]);
132
+ if (/^0x0{40}$/i.test(raw.hooks))
133
+ continue;
134
+ const key = toKey(raw);
135
+ where.set(ref.toLowerCase(), { line, key });
136
+ return { id: ref, line, key };
137
+ }
138
+ throw new PickhooksError("There is no pickhooks pool with this id.", { code: "UnknownPool" });
139
+ }
140
+ const key = async (ref) => (await locate(ref)).key;
141
+ // ------------------------------------------------ reads
142
+ /** The main factory's terms for pools opened now. */
143
+ async function terms() {
144
+ const fns = ["owner", "platformWallet", "openerBps", "traderBps", "hook"];
145
+ const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: a.anyFactory, abi: LINES.main.fAbi, functionName })), allowFailure: false });
146
+ return { owner: r[0], platformWallet: r[1], openerBps: Number(r[2]), traderBps: Number(r[3]), hook: r[4] };
147
+ }
148
+ /** A flap tax coin's tax now, and what of it applies in a tax-line pool. */
149
+ async function taxOf(token) {
150
+ const r = await pub.multicall({
151
+ contracts: [
152
+ { address: token, abi: flapAbi, functionName: "getPoolStateData" },
153
+ { address: token, abi: flapAbi, functionName: "pools", args: [a.vault] },
154
+ { address: token, abi: flapAbi, functionName: "quoteToken" },
155
+ ],
156
+ allowFailure: true,
157
+ });
158
+ const st = r[0].status === "success" ? r[0].result : null;
159
+ // a coin that does not answer counts as taxed, like the hook does
160
+ const state = st ? Number(st[0]) : ANTI_FARMER;
161
+ const buyBps = st ? Number(st[1]) : 0;
162
+ const sellBps = st ? Number(st[2]) : 0;
163
+ const vaultTaxed = r[1].status === "success" ? Boolean(r[1].result) : true;
164
+ const live = vaultTaxed && state === ANTI_FARMER;
165
+ return {
166
+ state,
167
+ buyBps,
168
+ sellBps,
169
+ antiFarmerEnd: st ? Number(st[5]) : 0,
170
+ vaultTaxed,
171
+ poolBuyBps: live ? buyBps : 0,
172
+ poolSellBps: live ? sellBps : 0,
173
+ payFirst: live && sellBps > 0,
174
+ quote: r[2].status === "success" ? r[2].result : ZERO_ADDRESS,
175
+ };
176
+ }
177
+ /**
178
+ * Whether a coin can get a pool, at what price, how deep it must be, and its first free slot. The main factory decides
179
+ * first; a coin it refuses as taxed (4) or for its dividends (5) goes to the tax line when that is on.
180
+ */
181
+ async function check(token) {
182
+ let line = "main";
183
+ let code = Number(await call(a.anyFactory, LINES.main.fAbi, "check", [token]));
184
+ if (TAX_ON && (code === 4 || code === 5)) {
185
+ const taxCode = Number(await call(a.anyTaxFactory, LINES.tax.fAbi, "check", [token]));
186
+ // 7: not a flap tax coin, the main factory's reason stands
187
+ if (taxCode !== 7) {
188
+ line = "tax";
189
+ code = taxCode;
190
+ }
191
+ }
192
+ const L = LINES[line];
193
+ const [meta, ref, slots, pair, tax] = await Promise.all([
194
+ pub.multicall({
195
+ contracts: ["name", "symbol", "decimals", "owner"].map((functionName) => ({ address: token, abi: erc20Abi, functionName })),
196
+ allowFailure: true,
197
+ }),
198
+ call(L.factory, L.fAbi, "referencePrice", [token]).catch(() => [0n, ZERO_ADDRESS, 0n]),
199
+ call(L.factory, L.fAbi, "slots", [token]).catch(() => []),
200
+ call(a.v2Factory, pancakeV2FactoryAbi, "getPair", [token, a.wbnb]).catch(() => ZERO_ADDRESS),
201
+ line === "tax" ? taxOf(token).catch(() => null) : Promise.resolve(null),
202
+ ]);
203
+ const [sqrtPrice, priceSource, depth] = ref;
204
+ const decimals = meta[2].status === "success" ? Number(meta[2].result) : 18;
205
+ const owner = meta[3].status === "success" ? meta[3].result : null;
206
+ const used = slots.map(Number);
207
+ const free = Array.from({ length: LIMITS.maxSlots }, (_, i) => i).find((i) => !used.includes(i));
208
+ let text = CHECK_TEXT[code] ?? "This coin cannot get a pool.";
209
+ if (line === "tax" && code === 0)
210
+ text = "This flap tax coin can get a pickhooks pool. Its tax stays: buyers and sellers here pay it like on its main pool.";
211
+ if (line === "tax" && code === 6)
212
+ text = "No PancakeSwap pool of this coin holds at least 3 BNB of value, against BNB or against its quote token, so there is no fair price to open at.";
213
+ return {
214
+ token,
215
+ line,
216
+ factory: L.factory,
217
+ code,
218
+ ok: code === 0,
219
+ text,
220
+ name: meta[0].status === "success" ? String(meta[0].result) : "?",
221
+ symbol: meta[1].status === "success" ? String(meta[1].result) : "?",
222
+ decimals,
223
+ sqrtPrice,
224
+ priceSource,
225
+ depth,
226
+ price: sqrtPrice > 0n ? (Number(sqrtPrice) / Q96) ** 2 * 10 ** (18 - decimals) : 0,
227
+ // a flap tax coin's owner (flap's portal, or nobody) can only run its migration, it cannot pause or blacklist
228
+ owner: owner && !/^0x0{40}$/i.test(owner) && line === "main" ? owner : null,
229
+ slotsUsed: used,
230
+ freeSlot: free ?? null,
231
+ pair,
232
+ tax,
233
+ };
234
+ }
235
+ /** How many pools a line's factory has opened (main by default). */
236
+ const count = async (line = "main") => (LINES[line].on ? Number(await call(LINES[line].factory, LINES[line].fAbi, "poolCount")) : 0);
237
+ async function idsOf(line, limit, offset) {
238
+ const L = LINES[line];
239
+ const n = await count(line);
240
+ const end = Math.max(0, n - offset);
241
+ const from = Math.max(0, end - limit);
242
+ if (end <= from)
243
+ return [];
244
+ return (await pub.multicall({
245
+ contracts: Array.from({ length: end - from }, (_, i) => ({ address: L.factory, abi: L.fAbi, functionName: "allPools", args: [BigInt(from + i)] })),
246
+ allowFailure: false,
247
+ }));
248
+ }
249
+ /** Pools newest first: `limit` of each line, skipping the newest `offset` (both lines merged by opening time). */
250
+ async function pools(o = {}) {
251
+ const lines = (o.line ? [o.line] : ["main", "tax"]).filter((l) => LINES[l].on);
252
+ const lists = await Promise.all(lines.map(async (l) => read(l, await idsOf(l, o.limit ?? 50, o.offset ?? 0))));
253
+ return lists.flat().sort((x, y) => y.start - x.start || (x.line === y.line ? 0 : x.line === "main" ? -1 : 1));
254
+ }
255
+ /** One pool of either line, or null. */
256
+ async function pool(ref) {
257
+ try {
258
+ const p = await locate(ref);
259
+ return (await read(p.line, [p.id]))[0] ?? null;
260
+ }
261
+ catch (e) {
262
+ if (e instanceof PickhooksError && e.code === "UnknownPool")
263
+ return null;
264
+ throw e;
265
+ }
266
+ }
267
+ /** Every pool of a coin on both lines (one per used slot). */
268
+ async function poolsOf(token) {
269
+ const out = [];
270
+ for (const line of ["main", "tax"]) {
271
+ const L = LINES[line];
272
+ if (!L.on)
273
+ continue;
274
+ const slots = (await call(L.factory, L.fAbi, "slots", [token])).map(Number);
275
+ if (!slots.length)
276
+ continue;
277
+ const ks = (await pub.multicall({
278
+ contracts: slots.map((s) => ({ address: L.factory, abi: L.fAbi, functionName: "poolKey", args: [token, s] })),
279
+ allowFailure: false,
280
+ }));
281
+ out.push(...(await read(line, ks.map((k) => poolIdOf(toKey(k))))));
282
+ }
283
+ return out;
284
+ }
285
+ async function read(line, ids) {
286
+ if (!ids.length)
287
+ return [];
288
+ const L = LINES[line];
289
+ const hook = await hookOf(line);
290
+ const many = (address, abi, functionName) => pub.multicall({ contracts: ids.map((id) => ({ address, abi, functionName, args: [id] })), allowFailure: true });
291
+ const [raws, rawKeys, slot0s, positions, head] = await Promise.all([
292
+ many(hook, L.hAbi, "poolOf"),
293
+ many(L.factory, L.fAbi, "keyOf"),
294
+ many(a.clPoolManager, clPoolManagerAbi, "getSlot0"),
295
+ many(L.factory, L.fAbi, "positions"),
296
+ pub.getBlock({ blockTag: "latest" }),
297
+ ]);
298
+ const tokens = [...new Set(raws.filter((r) => r.status === "success" && r.result && !/^0x0{40}$/i.test(r.result.token)).map((r) => r.result.token.toLowerCase()))];
299
+ const meta = await pub.multicall({
300
+ contracts: tokens.flatMap((t) => [
301
+ { address: t, abi: erc20Abi, functionName: "name" },
302
+ { address: t, abi: erc20Abi, functionName: "symbol" },
303
+ { address: t, abi: erc20Abi, functionName: "decimals" },
304
+ { address: hook, abi: L.hAbi, functionName: "chest", args: [t] },
305
+ ]),
306
+ allowFailure: true,
307
+ });
308
+ const info = new Map();
309
+ const taxes = line === "tax" ? await Promise.all(tokens.map((t) => taxOf(t).catch(() => null))) : [];
310
+ tokens.forEach((t, i) => {
311
+ const r = (k) => meta[i * 4 + k];
312
+ info.set(t, {
313
+ name: r(0).status === "success" ? String(r(0).result) : "?",
314
+ symbol: r(1).status === "success" ? String(r(1).result) : "?",
315
+ decimals: r(2).status === "success" ? Number(r(2).result) : 18,
316
+ chest: r(3).status === "success" ? r(3).result : 0n,
317
+ tax: taxes[i] ?? null,
318
+ });
319
+ });
320
+ const out = [];
321
+ ids.forEach((id, i) => {
322
+ const p = raws[i].status === "success" ? raws[i].result : undefined;
323
+ const k = rawKeys[i].status === "success" ? rawKeys[i].result : undefined;
324
+ if (!p || !k || /^0x0{40}$/i.test(p.token) || /^0x0{40}$/i.test(k.hooks))
325
+ return;
326
+ const keyN = toKey(k);
327
+ where.set(id.toLowerCase(), { line, key: keyN });
328
+ const m = info.get(p.token.toLowerCase());
329
+ const s0 = slot0s[i].status === "success" ? slot0s[i].result : undefined;
330
+ const sp = s0 ? s0[0] : 0n;
331
+ const spacing = Number((BigInt(k.parameters) >> 16n) & 0xffffffn);
332
+ const c = p.cfg;
333
+ let tax = m.tax;
334
+ // the pool's own tax listing is fixed at opening: the vault was on the coin's list then, or not
335
+ if (tax && !c.vaultTaxed)
336
+ tax = { ...tax, poolBuyBps: 0, poolSellBps: 0, payFirst: false };
337
+ out.push({
338
+ id,
339
+ line,
340
+ factory: L.factory,
341
+ hook,
342
+ key: keyN,
343
+ token: p.token,
344
+ name: m.name,
345
+ symbol: m.symbol,
346
+ decimals: m.decimals,
347
+ slot: spacing - LIMITS.tickBase,
348
+ opener: p.opener,
349
+ start: Number(p.start),
350
+ config: {
351
+ feeBps: Number(c.feeBps),
352
+ holderFeeBps: Number(c.holderFeeBps),
353
+ holderMin: c.holderMin,
354
+ openerBps: Number(c.openerBps),
355
+ potBps: Number(c.potBps),
356
+ chestBps: Number(c.chestBps),
357
+ jackpotShareBps: Number(c.jackpotShareBps),
358
+ jackpotSilence: Number(c.jackpotSilence),
359
+ jackpotMinBuy: c.jackpotMinBuy,
360
+ bonusBps: Number(c.bonusBps),
361
+ bonusLock: Number(c.bonusLock),
362
+ bonusMinBuy: c.bonusMinBuy,
363
+ bonusCap: c.bonusCap,
364
+ pair: c.pair ?? ZERO_ADDRESS,
365
+ traderBps: Number(c.traderBps ?? 0),
366
+ partner: c.partner ?? ZERO_ADDRESS,
367
+ partnerBps: Number(c.partnerBps ?? 0),
368
+ vaultTaxed: Boolean(c.vaultTaxed),
369
+ },
370
+ buys: Number(p.buys),
371
+ sells: Number(p.sells),
372
+ volumeBnb: p.volumeBnb,
373
+ feesBnb: p.feesBnb,
374
+ burned: p.burned,
375
+ burnedFromChest: p.burnedFromChest,
376
+ pot: p.pot,
377
+ lastBuyer: p.lastBuyer,
378
+ lastBuyTime: Number(p.lastBuyTime),
379
+ bonusPot: p.bonusPot,
380
+ bonusGiven: p.bonusGiven,
381
+ recaptured: p.recaptured ?? 0n,
382
+ recaptures: Number(p.recaptures ?? 0),
383
+ routerDue: p.routerDue ?? 0n,
384
+ chest: m.chest,
385
+ tax,
386
+ positions: (positions[i].status === "success" ? positions[i].result : []).map((x, index) => ({
387
+ index,
388
+ tickLower: Number(x.tickLower),
389
+ tickUpper: Number(x.tickUpper),
390
+ liquidity: x.liquidity,
391
+ unlockAt: Number(x.unlockAt),
392
+ })),
393
+ sqrtPriceX96: sp,
394
+ tick: s0 ? Number(s0[1]) : 0,
395
+ lpFee: s0 ? Number(s0[3]) : 0,
396
+ price: sp > 0n ? (Number(sp) / Q96) ** 2 * 10 ** (18 - m.decimals) : 0,
397
+ chainNow: Number(head.timestamp),
398
+ });
399
+ });
400
+ return out;
401
+ }
402
+ /** What a wallet can claim and has been credited, in BNB. */
403
+ async function earnings(who) {
404
+ const r = await pub.multicall({
405
+ contracts: [
406
+ { address: a.anyHook, abi: LINES.main.hAbi, functionName: "owed", args: [who] },
407
+ { address: a.anyHook, abi: LINES.main.hAbi, functionName: "routerCredit", args: [who] },
408
+ { address: a.anyHook, abi: LINES.main.hAbi, functionName: "routerFunded" },
409
+ { address: a.anyFactory, abi: LINES.main.fAbi, functionName: "partnerBps", args: [who] },
410
+ ],
411
+ allowFailure: false,
412
+ });
413
+ const taxOwed = TAX_ON ? await call(await hookOf("tax"), LINES.tax.hAbi, "owed", [who]) : 0n;
414
+ const credit = r[1];
415
+ const funded = r[2];
416
+ return { owed: r[0], routerCredit: credit, routerClaimable: credit < funded ? credit : funded, partnerBps: Number(r[3]), taxOwed };
417
+ }
418
+ /** A router contract's listing: the wallet its share goes to, and the share (0: not listed). */
419
+ async function router(routerContract) {
420
+ const r = await call(a.anyHook, LINES.main.hAbi, "routerOf", [routerContract]);
421
+ return { payout: r.payout, bps: Number(r.bps) };
422
+ }
423
+ /** A partner's share of the platform's part in main-line pools opened with it (0: not listed). */
424
+ const partnerBps = async (partner) => Number(await call(a.anyFactory, LINES.main.fAbi, "partnerBps", [partner]));
425
+ /** A wallet's lock-to-earn state in a pool. */
426
+ async function bonusOf(ref, who) {
427
+ const p = await locate(ref);
428
+ const b = await call(await hookOf(p.line), LINES[p.line].hAbi, "bonusOf", [p.id, who]);
429
+ return { bought: b.bought, locked: b.locked, owed: b.owed, total: b.total, unlockAt: Number(b.unlockAt) };
430
+ }
431
+ /** What collect() would book now (BNB fees, coins burned), without sending. */
432
+ async function pendingFees(ref) {
433
+ const p = await locate(ref);
434
+ const r = await pub.simulateContract({ address: LINES[p.line].factory, abi: LINES[p.line].fAbi, functionName: "collect", args: [p.id] });
435
+ const [bnb, coins] = r.result;
436
+ return { bnb, coins };
437
+ }
438
+ // ------------------------------------------------ quotes and trades
439
+ /**
440
+ * What a trade gets, from PancakeSwap's quoter running the real swap (quote = fill). Pass `from` (the trader) when it
441
+ * holds $hooks: the holder fee depends on tx.origin. Tax line: a buy gets the pool's coins less the buy tax that applies
442
+ * now (gross is the pool's amount); a sell is quoted on what arrives after the sell tax.
443
+ */
444
+ async function quote(q) {
445
+ const p = await locate(q.pool);
446
+ const tax = p.line === "tax" ? await taxOf(p.key.currency1) : null;
447
+ const buyBps = tax?.poolBuyBps ?? 0;
448
+ const sellBps = tax?.poolSellBps ?? 0;
449
+ try {
450
+ if (q.side === "buy") {
451
+ const r = await infiQuote(ctx, { key: p.key, buy: true, amount: q.amount, from: q.from });
452
+ return { amountOut: afterTax(r.amountOut, buyBps), gross: r.amountOut, taxBps: buyBps, gasEstimate: r.gasEstimate };
453
+ }
454
+ const r = await infiQuote(ctx, { key: p.key, buy: false, amount: afterTax(q.amount, sellBps), from: q.from, hookData: p.line === "tax" ? TAX_QUOTE_MARK : "0x" });
455
+ return { amountOut: r.amountOut, gross: r.amountOut, taxBps: sellBps, gasEstimate: r.gasEstimate };
456
+ }
457
+ catch (e) {
458
+ throw toPickhooksError(e, "The quoter refused this trade.");
459
+ }
460
+ }
461
+ /** A buy through PancakeSwap's UniversalRouter, BNB in. Main line: extraGas carries the recapture room. */
462
+ function buildBuy(b) {
463
+ const main = isAddressEqual(b.key.hooks, a.anyHook);
464
+ return buildInfiSwap(a, { key: b.key, buy: true, legs: [b.bnbIn], minOut: b.minOut, deadline: b.deadline, extraGas: main && b.recapture !== false ? LIMITS.recaptureExtraGas : 0n });
465
+ }
466
+ /** A sell through the UniversalRouter, BNB out; the router pulls the coins with Permit2 (see sellApprovals()). */
467
+ function buildSell(s) {
468
+ const main = isAddressEqual(s.key.hooks, a.anyHook);
469
+ return buildInfiSwap(a, { key: s.key, buy: false, legs: [s.amount], minOut: s.minOut, deadline: s.deadline, extraGas: main && s.recapture !== false ? LIMITS.recaptureExtraGas : 0n });
470
+ }
471
+ /**
472
+ * Tax line, while sells pay first: SETTLE(coin, amount, from the seller via Permit2), CL_SWAP_EXACT_IN_SINGLE with
473
+ * amount 0 (the router swaps the whole credit, whatever arrived after the tax), TAKE_ALL(BNB, minOut).
474
+ */
475
+ function buildSellPayFirst(s) {
476
+ const coin = s.key.currency1;
477
+ const actions = concatHex(["0x0b", "0x06", "0x0f"]);
478
+ const params = [
479
+ encodeAbiParameters([{ type: "address" }, { type: "uint256" }, { type: "bool" }], [coin, s.amount, true]),
480
+ encodeAbiParameters(SWAP_EXACT_IN_SINGLE, [{ poolKey: s.key, zeroForOne: false, amountIn: 0n, amountOutMinimum: s.minOut, hookData: "0x" }]),
481
+ encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [NATIVE, s.minOut]),
482
+ ];
483
+ const input = encodeAbiParameters([{ type: "bytes" }, { type: "bytes[]" }], [actions, params]);
484
+ return { to: a.universalRouter, value: 0n, data: encodeFunctionData({ abi: universalRouterAbi, functionName: "execute", args: ["0x10", [input], s.deadline ?? deadlineIn()] }), label: "The sell" };
485
+ }
486
+ /** The approvals a sell of `amount` needs first: the coin to Permit2 (once), Permit2 to the router (30 days). */
487
+ const sellApprovals = (owner, token, amount) => permit2Approvals(ctx, owner, token, amount);
488
+ /** The trade, the recapture and a router credit of one transaction, from its receipt (either line). */
489
+ function readTrade(receipt) {
490
+ let side = null;
491
+ let tokens = 0n;
492
+ let bnb = 0n;
493
+ let recapture = null;
494
+ let routerCredit = 0n;
495
+ for (const line of ["main", "tax"]) {
496
+ const h = hooks.get(line);
497
+ if (!h)
498
+ continue;
499
+ const logs = parseEventLogs({ abi: any3HookAbi, logs: receipt.logs.filter((l) => isAddressEqual(l.address, h)) });
500
+ for (const l of logs) {
501
+ if (l.eventName === "Traded" && side === null) {
502
+ side = l.args.buy ? "buy" : "sell";
503
+ tokens = l.args.tokens;
504
+ bnb = l.args.bnb;
505
+ }
506
+ else if (l.eventName === "Recaptured") {
507
+ recapture = { profit: l.args.profit, toTrader: l.args.toTrader, soldHere: l.args.soldHere };
508
+ }
509
+ else if (l.eventName === "RouterCredited") {
510
+ routerCredit += l.args.amount;
511
+ }
512
+ }
513
+ }
514
+ return { side, tokens, bnb, recapture, routerCredit };
515
+ }
516
+ /** Quotes, takes slippage off (on the pool's own amount), sends the buy, and reads what happened. */
517
+ async function buy(b, o = {}) {
518
+ const p = await locate(b.pool);
519
+ if (p.line === "tax")
520
+ await hookOf("tax");
521
+ const from = await senderAddress(ctx, o.wallet);
522
+ const q = await quote({ pool: p, side: "buy", amount: b.bnbIn, from });
523
+ const minOut = b.minOut ?? minusBps(q.gross, b.slippageBps ?? 100);
524
+ const sent = await sendTx(ctx, buildBuy({ key: p.key, bnbIn: b.bnbIn, minOut, recapture: b.recapture, deadline: b.deadline }), o);
525
+ const t = readTrade(sent.receipt);
526
+ return { ...sent, side: "buy", quoted: q.amountOut, minOut, amountIn: b.bnbIn, amountOut: t.tokens, recapture: t.recapture, routerCredit: t.routerCredit };
527
+ }
528
+ /** Approves what is missing, quotes, sends the sell (paying first on a taxed tax-line pool), and reads what happened. */
529
+ async function sell(s, o = {}) {
530
+ const p = await locate(s.pool);
531
+ if (p.line === "tax")
532
+ await hookOf("tax");
533
+ const { address } = await signer(ctx, o.wallet);
534
+ await sendAll(ctx, await sellApprovals(address, p.key.currency1, s.amount), o);
535
+ const q = await quote({ pool: p, side: "sell", amount: s.amount, from: address });
536
+ const minOut = s.minOut ?? minusBps(q.amountOut, s.slippageBps ?? 100);
537
+ const payFirst = p.line === "tax" && (await taxOf(p.key.currency1)).payFirst;
538
+ const tx = payFirst ? buildSellPayFirst({ key: p.key, amount: s.amount, minOut, deadline: s.deadline }) : buildSell({ key: p.key, amount: s.amount, minOut, recapture: s.recapture, deadline: s.deadline });
539
+ const sent = await sendTx(ctx, tx, o);
540
+ const t = readTrade(sent.receipt);
541
+ return { ...sent, side: "sell", quoted: q.amountOut, minOut, amountIn: s.amount, amountOut: t.bnb, recapture: t.recapture, routerCredit: t.routerCredit };
542
+ }
543
+ // ------------------------------------------------ opening a pool
544
+ /** An approval of `spender` for `amount` of `token`, or null when it is already in place. */
545
+ async function approvalFor(owner, token, spender, amount) {
546
+ const have = await call(token, erc20Abi, "allowance", [owner, spender]);
547
+ if (have >= amount)
548
+ return null;
549
+ return { to: token, value: 0n, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, maxUint256] }), label: "The approval" };
550
+ }
551
+ /** The factory's open() parameters for these options, with the site's defaults, at the coin's price now. */
552
+ async function openParams(o, checked) {
553
+ const c = checked ?? (await check(o.token));
554
+ if (!c.ok)
555
+ throw new PickhooksError(c.text, { code: "Refused", args: [c.code] });
556
+ if (c.line === "tax" && o.partner)
557
+ throw new PickhooksError("The tax line has no partners: pools of flap tax coins open without one.", { code: "input" });
558
+ const slot = o.slot ?? c.freeSlot;
559
+ if (slot === null || slot === undefined)
560
+ throw new PickhooksError("All 50 slots of this coin are taken.", { code: "input" });
561
+ if (c.slotsUsed.includes(slot))
562
+ throw new PickhooksError(`Slot ${slot} of this coin is taken; the first free one is ${c.freeSlot}.`, { code: "input" });
563
+ const range = o.range ?? "ladder10";
564
+ const { lower, upper } = typeof range === "object" ? { lower: range.tickLower, upper: range.tickUpper } : rangeFor(range, c.sqrtPrice, slot);
565
+ const holderMin = o.holderMin ?? 10000n * 10n ** 18n;
566
+ const bonus = o.bonus === false ? null : { bps: 500, lockSeconds: 7 * 86_400, minBuy: 5n * 10n ** 16n, cap: 0n, ...(o.bonus ?? {}) };
567
+ const jackpot = o.jackpot ? { potShareBps: 10_000, shareBps: 5000, silenceSeconds: 600, minBuy: 2n * 10n ** 16n, ...o.jackpot } : null;
568
+ const params = {
569
+ token: o.token,
570
+ slot,
571
+ feeBps: o.feeBps ?? 30,
572
+ holderFeeBps: holderMin > 0n ? (o.holderFeeBps ?? 5) : 0,
573
+ holderMin,
574
+ chestBps: o.chestBps ?? 200,
575
+ potShareBps: jackpot ? jackpot.potShareBps : 0,
576
+ jackpotShareBps: jackpot ? jackpot.shareBps : 0,
577
+ jackpotSilence: jackpot ? jackpot.silenceSeconds : 0,
578
+ jackpotMinBuy: jackpot ? jackpot.minBuy : 0n,
579
+ bonusBps: bonus ? bonus.bps : 0,
580
+ bonusLock: bonus ? bonus.lockSeconds : 0,
581
+ bonusMinBuy: bonus ? bonus.minBuy : 0n,
582
+ bonusCap: bonus ? bonus.cap : 0n,
583
+ expectedSqrtPrice: c.sqrtPrice,
584
+ maxMoveBps: o.maxMoveBps ?? 100,
585
+ tickLower: lower,
586
+ tickUpper: upper,
587
+ tokenAmount: o.tokenAmount,
588
+ partner: o.partner ?? ZERO_ADDRESS,
589
+ noRecapture: o.recapture === false,
590
+ };
591
+ return { params, value: range === "around" || typeof range === "object" ? (o.bnb ?? 0n) : 0n, check: c };
592
+ }
593
+ /**
594
+ * The open() call of a line's factory (main by default; the tax line takes the struct without partner and
595
+ * noRecapture). The opener must have approved tokenAmount + 1 to that factory (one unit proves the burns work).
596
+ */
597
+ function buildOpen(params, value = 0n, line = "main") {
598
+ if (line === "tax") {
599
+ const { partner: _p, noRecapture: _n, ...v2 } = params;
600
+ return { to: a.anyTaxFactory, value, data: encodeFunctionData({ abi: anyTaxFactoryAbi, functionName: "open", args: [v2] }), label: "Opening the pool" };
601
+ }
602
+ return { to: a.anyFactory, value, data: encodeFunctionData({ abi: any3FactoryAbi, functionName: "open", args: [params] }), label: "Opening the pool" };
603
+ }
604
+ /** Checks the coin (and which line takes it), approves, opens the pool and returns its id. */
605
+ async function open(o, s = {}) {
606
+ const { address } = await signer(ctx, s.wallet);
607
+ const { params, value, check: c } = await openParams(o);
608
+ const approval = await approvalFor(address, o.token, c.factory, params.tokenAmount + 1n);
609
+ if (approval)
610
+ await sendTx(ctx, approval, s);
611
+ const sent = await sendTx(ctx, buildOpen(params, value, c.line), s);
612
+ const ev = parseEventLogs({ abi: any3FactoryAbi, logs: sent.receipt.logs.filter((l) => isAddressEqual(l.address, c.factory)), eventName: "Opened" })[0];
613
+ if (!ev)
614
+ throw new PickhooksError("Opened, but the pool was not found in the receipt.", { code: "receipt", hash: sent.hash });
615
+ if (c.line === "tax")
616
+ await hookOf("tax");
617
+ return { ...sent, id: ev.args.id, params, line: c.line };
618
+ }
619
+ // ------------------------------------------------ payouts and pool tools
620
+ const tx = (to, abi, functionName, args = [], value = 0n, label) => ({
621
+ to,
622
+ value,
623
+ data: encodeFunctionData({ abi, functionName, args }),
624
+ label,
625
+ });
626
+ /** a call to the hook (or factory) of the line a pool belongs to */
627
+ const atHook = async (ref, fn, args, value = 0n, label) => {
628
+ const p = await locate(ref);
629
+ return tx(await hookOf(p.line), LINES[p.line].hAbi, fn, args(p.id), value, label);
630
+ };
631
+ const atFactory = async (ref, fn, args, label) => {
632
+ const p = await locate(ref);
633
+ return tx(LINES[p.line].factory, LINES[p.line].fAbi, fn, args(p.id), 0n, label);
634
+ };
635
+ const build = {
636
+ buy: buildBuy,
637
+ sell: buildSell,
638
+ sellPayFirst: buildSellPayFirst,
639
+ open: buildOpen,
640
+ /** main line: opener side, jackpot wins, recapture shares, a partner's share; { line: "tax" } for the tax line's hook */
641
+ claim: (o = {}) => tx(o.line === "tax" ? (hooks.get("tax") ?? ZERO_ADDRESS) : a.anyHook, any3HookAbi, "claim", [], 0n, "The claim"),
642
+ /** a router's payout wallet takes what collects have funded of its credit */
643
+ claimRouter: () => tx(a.anyHook, any3HookAbi, "claimRouter", [], 0n, "The router claim"),
644
+ /** anyone: collects the pool's fees and books them (funds router credits) */
645
+ collect: (ref) => atFactory(ref, "collect", (id) => [id], "The collect"),
646
+ settleJackpot: (ref) => atHook(ref, "settleJackpot", (id) => [id], 0n, "Settling the jackpot"),
647
+ fundPot: (ref, wei) => atHook(ref, "fundPot", (id) => [id], wei, "Funding the jackpot"),
648
+ /** these move coins into the hook: approve the coin to the hook first (approvalFor) */
649
+ feedChest: async (token, amount, line = "main") => tx(await hookOf(line), LINES[line].hAbi, "feedChest", [token, amount], 0n, "Feeding the fire"),
650
+ fundBonus: (ref, amount) => atHook(ref, "fundBonus", (id) => [id, amount], 0n, "Funding the bonus"),
651
+ lockBonus: (ref, amount) => atHook(ref, "lockBonus", (id) => [id, amount], 0n, "Locking for the bonus"),
652
+ unlockBonus: (ref) => atHook(ref, "unlockBonus", (id) => [id], 0n, "Unlocking the bonus"),
653
+ /** the opener: more liquidity (approve the coin to the factory first), or a position out after its 30 days */
654
+ addLiquidity: (token, slot, tickLower, tickUpper, tokenAmount, bnb = 0n, line = "main") => tx(LINES[line].factory, LINES[line].fAbi, "addLiquidity", [token, slot, tickLower, tickUpper, tokenAmount], bnb, "Adding liquidity"),
655
+ removeLiquidity: (ref, index) => atFactory(ref, "removeLiquidity", (id) => [id, BigInt(index)], "Taking a position out"),
656
+ };
657
+ /** Approves `spender` first when a call moves coins into it. */
658
+ async function withApproval(t, token, amount, s, spender) {
659
+ const { address } = await signer(ctx, s.wallet);
660
+ const ap = await approvalFor(address, token, spender, amount);
661
+ if (ap)
662
+ await sendTx(ctx, ap, s);
663
+ return sendTx(ctx, t, s);
664
+ }
665
+ return {
666
+ /** the main factory and hook, and whether the tax line is on */
667
+ factory: a.anyFactory,
668
+ hook: a.anyHook,
669
+ taxLineOn: TAX_ON,
670
+ terms,
671
+ check,
672
+ taxOf,
673
+ depthNeeds,
674
+ fitLadder,
675
+ rangeFor,
676
+ count,
677
+ pools,
678
+ pool,
679
+ poolsOf,
680
+ key,
681
+ poolId: poolIdOf,
682
+ earnings,
683
+ router,
684
+ partnerBps,
685
+ partnerLink: (partner) => partnerLink(partner, ctx.site),
686
+ bonusOf,
687
+ pendingFees,
688
+ quote,
689
+ sellApprovals,
690
+ approvalFor,
691
+ openParams,
692
+ readTrade,
693
+ build,
694
+ buy,
695
+ sell,
696
+ open,
697
+ claim: async (s = {}) => {
698
+ if (s.line === "tax")
699
+ await hookOf("tax");
700
+ return sendTx(ctx, build.claim({ line: s.line }), s);
701
+ },
702
+ claimRouter: (s = {}) => sendTx(ctx, build.claimRouter(), s),
703
+ collect: async (ref, s = {}) => sendTx(ctx, await build.collect(ref), s),
704
+ settleJackpot: async (ref, s = {}) => sendTx(ctx, await build.settleJackpot(ref), s),
705
+ fundPot: async (ref, wei, s = {}) => sendTx(ctx, await build.fundPot(ref, wei), s),
706
+ feedChest: async (token, amount, s = {}) => withApproval(await build.feedChest(token, amount, s.line), token, amount, s, await hookOf(s.line ?? "main")),
707
+ fundBonus: async (ref, amount, s = {}) => {
708
+ const p = await locate(ref);
709
+ return withApproval(await build.fundBonus(p, amount), p.key.currency1, amount, s, await hookOf(p.line));
710
+ },
711
+ lockBonus: async (ref, amount, s = {}) => {
712
+ const p = await locate(ref);
713
+ return withApproval(await build.lockBonus(p, amount), p.key.currency1, amount, s, await hookOf(p.line));
714
+ },
715
+ unlockBonus: async (ref, s = {}) => sendTx(ctx, await build.unlockBonus(ref), s),
716
+ addLiquidity: async (ref, range, tokenAmount, bnb = 0n, s = {}) => {
717
+ const p = await pool(ref);
718
+ if (!p)
719
+ throw new PickhooksError("There is no pickhooks pool with this id.", { code: "UnknownPool" });
720
+ const t = build.addLiquidity(p.token, p.slot, range.tickLower, range.tickUpper, tokenAmount, bnb, p.line);
721
+ return tokenAmount > 0n ? withApproval(t, p.token, tokenAmount, s, p.factory) : sendTx(ctx, t, s);
722
+ },
723
+ removeLiquidity: async (ref, index, s = {}) => sendTx(ctx, await build.removeLiquidity(ref, index), s),
724
+ /** the tax line's hook: anyone sends a surplus to its place (the coin's own into the fire, other tokens to the platform) */
725
+ sweep: async (token, s = {}) => sendTx(ctx, tx(await hookOf("tax"), LINES.tax.hAbi, "sweep", [token], 0n, "The sweep"), s),
726
+ lineOf: async (ref) => (await locate(ref)).line,
727
+ lineOfFactory,
728
+ };
729
+ }