@pickhooks/sdk 0.0.0-stage → 0.4.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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -3
  3. package/dist/abis/agents.d.ts +2418 -0
  4. package/dist/abis/agents.js +4 -0
  5. package/dist/abis/any3.d.ts +2123 -0
  6. package/dist/abis/any3.js +3 -0
  7. package/dist/abis/anytax.d.ts +1786 -0
  8. package/dist/abis/anytax.js +3 -0
  9. package/dist/abis/calls.d.ts +2515 -0
  10. package/dist/abis/calls.js +4 -0
  11. package/dist/abis/common.d.ts +449 -0
  12. package/dist/abis/common.js +91 -0
  13. package/dist/abis/dips.d.ts +897 -0
  14. package/dist/abis/dips.js +2 -0
  15. package/dist/abis/index.d.ts +14 -0
  16. package/dist/abis/index.js +14 -0
  17. package/dist/abis/infra.d.ts +701 -0
  18. package/dist/abis/infra.js +4 -0
  19. package/dist/abis/lever.d.ts +1710 -0
  20. package/dist/abis/lever.js +5 -0
  21. package/dist/abis/lever2.d.ts +2710 -0
  22. package/dist/abis/lever2.js +5 -0
  23. package/dist/abis/pools.d.ts +1898 -0
  24. package/dist/abis/pools.js +4 -0
  25. package/dist/abis/sticky.d.ts +953 -0
  26. package/dist/abis/sticky.js +4 -0
  27. package/dist/abis/sticky2.d.ts +3750 -0
  28. package/dist/abis/sticky2.js +8 -0
  29. package/dist/abis/v1.d.ts +1627 -0
  30. package/dist/abis/v1.js +5 -0
  31. package/dist/abis/v2.d.ts +3264 -0
  32. package/dist/abis/v2.js +8 -0
  33. package/dist/addresses.d.ts +100 -0
  34. package/dist/addresses.js +71 -0
  35. package/dist/agents-catalog.d.ts +318 -0
  36. package/dist/agents-catalog.js +353 -0
  37. package/dist/agents.d.ts +543 -0
  38. package/dist/agents.js +667 -0
  39. package/dist/any.d.ts +447 -0
  40. package/dist/any.js +729 -0
  41. package/dist/api.d.ts +62 -0
  42. package/dist/api.js +30 -0
  43. package/dist/blocks.d.ts +107 -0
  44. package/dist/blocks.js +126 -0
  45. package/dist/coin-rules-config.d.ts +8 -0
  46. package/dist/coin-rules-config.js +13 -0
  47. package/dist/coin-rules.d.ts +383 -0
  48. package/dist/coin-rules.js +878 -0
  49. package/dist/coins.d.ts +217 -0
  50. package/dist/coins.js +520 -0
  51. package/dist/desks.d.ts +298 -0
  52. package/dist/desks.js +343 -0
  53. package/dist/dips.d.ts +133 -0
  54. package/dist/dips.js +148 -0
  55. package/dist/errors.d.ts +35 -0
  56. package/dist/errors.js +414 -0
  57. package/dist/index.d.ts +104 -0
  58. package/dist/index.js +91 -0
  59. package/dist/infinity.d.ts +63 -0
  60. package/dist/infinity.js +96 -0
  61. package/dist/infra.d.ts +112 -0
  62. package/dist/infra.js +114 -0
  63. package/dist/lever.d.ts +391 -0
  64. package/dist/lever.js +419 -0
  65. package/dist/pools.d.ts +274 -0
  66. package/dist/pools.js +405 -0
  67. package/dist/sticky-rules.d.ts +179 -0
  68. package/dist/sticky-rules.js +580 -0
  69. package/dist/sticky-sim.d.ts +125 -0
  70. package/dist/sticky-sim.js +637 -0
  71. package/dist/sticky.d.ts +286 -0
  72. package/dist/sticky.js +420 -0
  73. package/dist/tlock.d.ts +67 -0
  74. package/dist/tlock.js +185 -0
  75. package/dist/tx.d.ts +72 -0
  76. package/dist/tx.js +78 -0
  77. package/dist/v2.d.ts +106 -0
  78. package/dist/v2.js +61 -0
  79. package/dist/v3.d.ts +42 -0
  80. package/dist/v3.js +98 -0
  81. package/dist/wallet.d.ts +80 -0
  82. package/dist/wallet.js +115 -0
  83. package/dist/watch.d.ts +82 -0
  84. package/dist/watch.js +194 -0
  85. package/package.json +67 -4
package/dist/sticky.js ADDED
@@ -0,0 +1,420 @@
1
+ import { encodeFunctionData, parseEventLogs } from "viem";
2
+ import { stickyFactoryAbi, stickyTokenAbi, stickyValidatorAbi } from "./abis/sticky.js";
3
+ import { sticky2FactoryAbi, sticky2TokenAbi, sticky2ValidatorAbi } from "./abis/sticky2.js";
4
+ import { v3PoolAbi } from "./abis/common.js";
5
+ import { v2LockerAbi } from "./abis/v2.js";
6
+ import { ZERO_ADDRESS } from "./addresses.js";
7
+ import { PickhooksError, errorText, explain, toPickhooksError } from "./errors.js";
8
+ import { ACT, LOCKED, NONE, ON, compile, decodeRow, defaults, editToRow, rowProblems, rowText, scannerOf, TEMPLATE_BY_ID, templateOf } from "./sticky-rules.js";
9
+ import { simulate } from "./sticky-sim.js";
10
+ import { chainDeadline, minusBps, sendTx, senderAddress, signer } from "./tx.js";
11
+ import { bestVenue, buildV2Buy, buildV2Sell, v2Quote } from "./v2.js";
12
+ import { approvalFor, buildV3Buy, buildV3Sell, receivedOf, v3EstimateBuy, v3PriceBnb, v3Quote } from "./v3.js";
13
+ const randomSalt = () => {
14
+ const b = new Uint8Array(32);
15
+ globalThis.crypto.getRandomValues(b);
16
+ return `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
17
+ };
18
+ const toBase64 = (bytes) => {
19
+ const B = globalThis.Buffer;
20
+ if (B)
21
+ return B.from(bytes).toString("base64");
22
+ let s = "";
23
+ for (const x of bytes)
24
+ s += String.fromCharCode(x);
25
+ return btoa(s);
26
+ };
27
+ /** The rows of a coin as sentences, with the template each belongs to. */
28
+ export function describeRows(raw, release, off) {
29
+ return raw.map((x, i) => {
30
+ const r = decodeRow(x);
31
+ const id = templateOf(r);
32
+ const t = id ? TEMPLATE_BY_ID.get(id) : null;
33
+ const name = r.act === ACT.POT ? "Pot" : (t?.name ?? "Custom row");
34
+ return { i, name, text: rowText(r, release), template: id, sticky: t?.sticky ?? false, lock: r.act === ACT.LOCK, drop: r.act === ACT.DROP, lifted: (off & (1 << i)) !== 0, raw: x };
35
+ });
36
+ }
37
+ /**
38
+ * The table a launch would send: the templates' rows, then the hook script rows (the site's order), one release time per
39
+ * coin (the longer one wins); or rows + release as given. Offline; throws the problems as sentences.
40
+ */
41
+ export function stickyTable(l) {
42
+ if (!l.picks && !l.script?.length)
43
+ return { rows: l.rows ?? [], release: l.release ?? 0 };
44
+ // a template's numbers left out take its defaults
45
+ const picks = (l.picks ?? []).map((p) => {
46
+ const t = TEMPLATE_BY_ID.get(p.id);
47
+ if (!t)
48
+ throw new PickhooksError(`There is no template "${p.id}". Templates: ${[...TEMPLATE_BY_ID.keys()].join(", ")}.`, { code: "input" });
49
+ return { id: p.id, params: { ...defaults(t), ...p.params } };
50
+ });
51
+ const own = (l.script ?? []).map(editToRow);
52
+ const c = compile(picks, own);
53
+ const problems = [...c.problems, ...own.flatMap(rowProblems)];
54
+ const hours = l.scriptHours ?? 0;
55
+ const locks = own.some((r) => r.act === ACT.LOCK);
56
+ if (locks && !(hours * 3600 >= 600 && hours <= 720))
57
+ problems.push("Locked shares: unlock over 10 minutes to 30 days.");
58
+ if (problems.length)
59
+ throw new PickhooksError(problems.join(" "), { code: "input" });
60
+ return { rows: c.rows, release: locks ? Math.max(c.release, Math.round(hours * 3600)) : c.release };
61
+ }
62
+ const dur = (s) => s < 90 ? `${Math.round(s)} s` : s < 5400 ? `${Math.round(s / 60)} min` : s < 2 * 86_400 ? `${+(s / 3600).toFixed(1)} h` : `${+(s / 86_400).toFixed(1)} days`;
63
+ /** A test run as sentences: the launcher's card line by line. `names` are the rows' names (rule i + 1). */
64
+ export function testRunText(r, names) {
65
+ const rule = (row) => (row === LOCKED ? "still locked" : `rule ${row + 1}, ${names[row] ?? ""}`);
66
+ const pct = (x) => +x.toFixed(2);
67
+ const out = [
68
+ `Trades: ${r.buys.ok + r.buys.refused} buys, ${r.sells.ok + r.sells.refused} sells, ${r.sends.ok + r.sends.refused} sends in ${+r.hours.toFixed(1)} hours.`,
69
+ `Refused: ${r.refusals.length ? r.refusals.map((x) => `${rule(x.row)} ${x.count}x`).join(", ") : "nothing"}.`,
70
+ `Longest wait to sell: ${r.longestWait ? `${dur(r.longestWait)}${r.stillWaiting ? `, ${r.stillWaiting} wallets still waiting at the end` : ""}` : "nobody had to wait"}.`,
71
+ `Snipers: ${r.snipers.firstSellAfter !== null ? `first sold ${dur(r.snipers.firstSellAfter)} after launch` : "could not sell during the run"}.`,
72
+ `Split bags: ${r.splits.count ? `${r.splits.count} sent to fresh wallets, refused there again ${r.splits.refusedAfter} times` : "none"}.`,
73
+ `Everyone free: ${r.allFreeAfter === 0 ? "every wallet can sell everything right after the run" : r.allFreeAfter !== null ? `every wallet can sell everything ${dur(r.allFreeAfter)} after the run` : "at the latest when every lot is 30 days old"}.`,
74
+ ];
75
+ if (r.burnedPct > 0)
76
+ out.push(`Burned: ${pct(r.burnedPct)} % of the supply.`);
77
+ if (r.pot.inPct > 0)
78
+ out.push(`Pot: ${pct(r.pot.inPct)} % of the supply in, ${r.pot.drops} drops paid ${pct(r.pot.paidPct)} %, ${pct(r.pot.leftPct)} % left.`);
79
+ out.push(`A scanner's test: ${r.scanner.buy !== null
80
+ ? `its test buy is refused by ${rule(r.scanner.buy)}`
81
+ : r.scanner.sell !== null
82
+ ? `its sell right after the buy is refused by ${rule(r.scanner.sell)}: it can show a honeypot`
83
+ : "a small buy and an instant sell pass"}.`);
84
+ return out;
85
+ }
86
+ /**
87
+ * The launcher's test run, offline: the table on a fresh coin with random trades (the creator's first buy, three snipers
88
+ * in the first blocks, wallets that buy, hold and sell, bags split to fresh wallets). It runs the coin's own rule engine,
89
+ * checked against the contract trade by trade. The same seed gives the same run; the chain stays the truth.
90
+ */
91
+ export function stickyTestRun(l) {
92
+ const { rows, release } = stickyTable(l);
93
+ const report = simulate(rows, release, { seed: l.seed ?? 7, trades: l.trades ?? 200, devBuyBnb: l.devBuyBnb ?? 0, start: l.start });
94
+ const names = describeRows(rows, release, 0).map((x) => x.name);
95
+ return { rows, release, report, text: testRunText(report, names) };
96
+ }
97
+ /**
98
+ * The sticky module. `v2` points it at the sticky 2 factory (same coins, same rules, same calls; its coins also count
99
+ * the creator's sold coins and can carry switch rows, see ph.sticky2).
100
+ */
101
+ export function stickyModule(ctx, v2 = false) {
102
+ const { pub, a } = ctx;
103
+ const F = v2 ? a.sticky2Factory : a.stickyFactory;
104
+ const FA = (v2 ? sticky2FactoryAbi : stickyFactoryAbi);
105
+ const TA = (v2 ? sticky2TokenAbi : stickyTokenAbi);
106
+ const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
107
+ /** The factory's terms now: the seed every launch pays into the V2 pair, the creator side, whether own rows are open. */
108
+ async function terms() {
109
+ const fns = ["seedBnb", "creatorBps", "custom", "validator", "startFdv", "platformWallet", "locker"];
110
+ const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: F, abi: FA, functionName })), allowFailure: false });
111
+ return { seed: r[0], creatorBps: Number(r[1]), custom: r[2], validator: r[3], startFdv: r[4], platformWallet: r[5], locker: r[6] };
112
+ }
113
+ /** Asks the validator on chain whether a table can launch: null when it can, else the reason. */
114
+ async function validate(rows, release, custom) {
115
+ const t = custom === undefined ? await terms() : null;
116
+ try {
117
+ await call(t?.validator ?? a.stickyValidator, stickyValidatorAbi, "validate", [rows, release, custom ?? t.custom]);
118
+ return null;
119
+ }
120
+ catch (e) {
121
+ return explain(e, "The validator refused this table.").text;
122
+ }
123
+ }
124
+ const maskOf = (rowsOn) => (rowsOn ?? []).reduce((m, i) => m | (1 << i), 0);
125
+ /** sticky 2: asks the validator whether these rows can switch (drop rows only, 2 to 4): null when they can, else why not */
126
+ async function validateSwitch(rows, mask, validator) {
127
+ if (!v2)
128
+ return "Switch rows are a sticky 2 feature.";
129
+ try {
130
+ await call(validator ?? (await terms()).validator, sticky2ValidatorAbi, "switchRows", [rows, mask]);
131
+ return null;
132
+ }
133
+ catch (e) {
134
+ return explain(e, "The validator refused these switch rows.").text;
135
+ }
136
+ }
137
+ /**
138
+ * Stores the image and metadata on pickhooks.com and returns the metadata link (the site's /api/sticky/prepare, rate
139
+ * limited, only while sticky rules are live on the site). A launchpad can host its own JSON and pass that link instead.
140
+ */
141
+ async function prepareMetadata(m) {
142
+ const res = await fetch(`${ctx.site}/api/sticky/prepare`, {
143
+ method: "POST",
144
+ headers: { "content-type": "application/json" },
145
+ body: JSON.stringify({ ...m, image: { mime: m.image.mime, base64: toBase64(m.image.bytes) } }),
146
+ });
147
+ const j = (await res.json().catch(() => ({})));
148
+ if (!res.ok || !j.metadata)
149
+ throw new PickhooksError(j.error ?? `The site answered ${res.status}.`, { code: "site" });
150
+ return { metadata: j.metadata, image: j.image ?? "" };
151
+ }
152
+ /** The launch call. value = the factory's seed + the creator's first buy. */
153
+ function buildLaunch(l, value) {
154
+ const args = { name: l.name, symbol: l.symbol, metadata: l.metadata, rows: l.rows, release: l.release, salt: l.salt ?? randomSalt(), keepBps: l.keepBps ?? 10_000, minCreatorBps: l.minCreatorBps };
155
+ if (!v2 && l.switchMask)
156
+ throw new PickhooksError("Switch rows are a sticky 2 feature.", { code: "input" });
157
+ // sticky 2 takes a switcher and its switch rows too: none from a plain launch
158
+ const full = v2 ? { ...args, switcher: l.switchMask ? (l.switcher ?? a.ruleAuction) : ZERO_ADDRESS, switchMask: l.switchMask ?? 0 } : args;
159
+ return { to: F, value, data: encodeFunctionData({ abi: FA, functionName: "launch", args: [full] }), label: "The launch" };
160
+ }
161
+ /** Builds the table from templates and hook script rows (or takes rows), checks it locally and on chain, launches, returns the coin. */
162
+ async function launch(l, s = {}) {
163
+ const t = await terms();
164
+ if (l.script?.length && !t.custom)
165
+ throw new PickhooksError("The hook script is off on the factory: templates only for now.", { code: "input" });
166
+ const { rows, release } = stickyTable(l);
167
+ const refused = await validate(rows, release, t.custom);
168
+ if (refused)
169
+ throw new PickhooksError(refused, { code: "Bad" });
170
+ const switchMask = maskOf(l.switchRows);
171
+ if (switchMask) {
172
+ const no = await validateSwitch(rows, switchMask, t.validator);
173
+ if (no)
174
+ throw new PickhooksError(no, { code: "Bad" });
175
+ }
176
+ const tx = buildLaunch({ name: l.name, symbol: l.symbol, metadata: l.metadata, rows, release, salt: l.salt, keepBps: l.keepBps, minCreatorBps: t.creatorBps, switcher: l.switcher, switchMask }, t.seed + (l.devBuy ?? 0n));
177
+ const sent = await sendTx(ctx, tx, s);
178
+ // both factories emit the same Launched event
179
+ const ev = parseEventLogs({ abi: stickyFactoryAbi, logs: sent.receipt.logs.filter((x) => x.address.toLowerCase() === F.toLowerCase()), eventName: "Launched" })[0];
180
+ if (!ev)
181
+ throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
182
+ return { ...sent, token: ev.args.token, pool: ev.args.pool, pair: ev.args.pair, rows, release };
183
+ }
184
+ const FIELDS = ["name", "symbol", "metadata", "creator", "pool", "pool2", "start", "stats", "script", "pot", "totalSupply"];
185
+ async function summaries(tokens) {
186
+ if (!tokens.length)
187
+ return [];
188
+ const per = FIELDS.length;
189
+ const res = await pub.multicall({ contracts: tokens.flatMap((t) => FIELDS.map((functionName) => ({ address: t, abi: TA, functionName }))), allowFailure: true });
190
+ const pools = tokens.map((_, i) => res[i * per + 4].result);
191
+ const slots = await pub.multicall({
192
+ contracts: pools.map((p) => ({ address: (p ?? a.wbnb), abi: v3PoolAbi, functionName: "slot0" })),
193
+ allowFailure: true,
194
+ });
195
+ const out = [];
196
+ tokens.forEach((token, i) => {
197
+ const g = (k) => res[i * per + k];
198
+ if (g(0).status !== "success" || !pools[i] || g(8).status !== "success")
199
+ return;
200
+ const stats = g(7).result;
201
+ const [raw, release, off] = g(8).result;
202
+ const supply = g(10).result ?? 10n ** 27n;
203
+ const sl = slots[i];
204
+ const priceBnb = sl.status === "success" ? v3PriceBnb(sl.result[0], token, a.wbnb) : 0;
205
+ out.push({
206
+ token,
207
+ name: String(g(0).result ?? ""),
208
+ symbol: String(g(1).result ?? ""),
209
+ metadata: String(g(2).result ?? ""),
210
+ creator: g(3).result,
211
+ pool: pools[i],
212
+ pair: g(5).result ?? ZERO_ADDRESS,
213
+ start: Number(g(6).result ?? 0),
214
+ release: Number(release),
215
+ rows: describeRows(raw, Number(release), Number(off)),
216
+ scanner: scannerOf(raw.map(decodeRow)),
217
+ holders: Number(stats[0]),
218
+ buys: Number(stats[1]),
219
+ sells: Number(stats[2]),
220
+ lastBuy: Number(stats[4]),
221
+ volume: stats[5],
222
+ pot: g(9).result ?? 0n,
223
+ totalSupply: supply,
224
+ priceBnb,
225
+ mcapBnb: priceBnb * (Number(supply) / 1e18),
226
+ });
227
+ });
228
+ return out;
229
+ }
230
+ /** How many coins the factory launched. */
231
+ const count = async () => Number(await call(F, FA, "coinCount"));
232
+ /** Coins newest first: `limit` of them, skipping the newest `offset`. */
233
+ async function coins(o = {}) {
234
+ const n = await count();
235
+ const end = Math.max(0, n - (o.offset ?? 0));
236
+ const from = Math.max(0, end - (o.limit ?? 50));
237
+ if (end <= from)
238
+ return [];
239
+ const idx = Array.from({ length: end - from }, (_, i) => BigInt(end - 1 - i));
240
+ const tokens = (await pub.multicall({
241
+ contracts: idx.map((i) => ({ address: F, abi: FA, functionName: "coins", args: [i] })),
242
+ allowFailure: false,
243
+ }));
244
+ return summaries(tokens);
245
+ }
246
+ const isCoin = async (token) => (await call(F, FA, "isCoin", [token]).catch(() => false)) === true;
247
+ /** One sticky coin, or null when the address is not one of the factory's coins. */
248
+ async function coin(token) {
249
+ if (!(await isCoin(token)))
250
+ return null;
251
+ return (await summaries([token]))[0] ?? null;
252
+ }
253
+ /** A wallet's lots (amount, locked part, since, marks), its balance and what it can sell now. */
254
+ async function wallet(token, who) {
255
+ const r = await pub.multicall({
256
+ contracts: [
257
+ { address: token, abi: TA, functionName: "balanceOf", args: [who] },
258
+ { address: token, abi: TA, functionName: "sellable", args: [who] },
259
+ { address: token, abi: TA, functionName: "lotsOf", args: [who] },
260
+ ],
261
+ allowFailure: false,
262
+ });
263
+ const [amount, locked, since, flags, open] = r[2];
264
+ return {
265
+ balance: r[0],
266
+ sellable: r[1],
267
+ lots: amount.map((x, i) => ({ amount: x, locked: locked[i], since: Number(since[i]), flags: Number(flags[i]), open: open[i] })),
268
+ };
269
+ }
270
+ /**
271
+ * Would this trade go through: the coin's own check(), the same code a transfer runs. For a buy pass the coins it would
272
+ * get (a quote is fine: the size rows look at a share of the supply). A send is checked from `wallet` to `to`.
273
+ */
274
+ async function check(c) {
275
+ const args = c.side === "buy" ? [ON.BUY, ZERO_ADDRESS, c.wallet, c.amount] : c.side === "sell" ? [ON.SELL, c.wallet, ZERO_ADDRESS, c.amount] : [ON.SEND, c.wallet, c.to ?? ZERO_ADDRESS, c.amount];
276
+ const [r, script] = await Promise.all([call(c.token, TA, "check", args), call(c.token, TA, "script")]);
277
+ const answer = Number(r);
278
+ if (answer === NONE)
279
+ return { ok: true };
280
+ if (answer === LOCKED) {
281
+ // the coin answers "locked" too when the wallet simply holds fewer coins than the trade
282
+ const held = c.side === "buy" ? null : await call(c.token, TA, "balanceOf", [c.wallet]).catch(() => null);
283
+ const text = held !== null && held < c.amount ? "This wallet holds fewer of these coins than that." : errorText("Blocked", [LOCKED]);
284
+ return { ok: false, row: "locked", text, rule: null };
285
+ }
286
+ const rows = describeRows(script[0], Number(script[1]), Number(script[2]));
287
+ const rule = rows[answer] ?? null;
288
+ return { ok: false, row: answer, text: `Refused by rule ${answer + 1}${rule ? `: ${rule.name}. ${rule.text}` : "."}`, rule };
289
+ }
290
+ /** Coins a buy of `bnbIn` would get, estimated from the pool's liquidity at its price (see v3EstimateBuy); not a minOut. */
291
+ async function estimateBuy(token, bnbIn) {
292
+ return v3EstimateBuy(ctx, await call(token, TA, "pool"), token, bnbIn);
293
+ }
294
+ /**
295
+ * What a trade gets in the V3 pool, from PancakeSwap's QuoterV2. It runs the swap, so a refusing rule fails it, and it
296
+ * does not pass the reason on: a refused buy is checked again with an estimate of its coins and the refusal names the
297
+ * rule (for `wallet`, by default the quoter the quote ran as).
298
+ */
299
+ /**
300
+ * The better of the coin's two venues: its V3 pool and its V2 pair (every sticky coin has both at the same start price;
301
+ * after a dark launch the round's BNB sits in the pair, and a sell into the one sided V3 curve would get almost nothing).
302
+ * Terminals pick the pool by themselves; the SDK does the same here. The venue field says which one buy/sell will use.
303
+ */
304
+ async function quote(q) {
305
+ const best = await bestVenue(ctx, { v3: () => v3Quote(ctx, q), v2: () => v2Quote(ctx, q) });
306
+ if (best.v3 !== null || best.v2 !== null)
307
+ return { amountOut: best.amountOut, venue: best.venue, v3: best.v3, v2: best.v2 };
308
+ const e = best.errors.v3 ?? best.errors.v2;
309
+ {
310
+ if (q.side === "buy") {
311
+ const est = await estimateBuy(q.token, q.amount).catch(() => 0n);
312
+ const c = est > 0n ? await check({ token: q.token, side: "buy", wallet: q.wallet ?? a.v3Quoter, amount: est }).catch(() => null) : null;
313
+ if (c && !c.ok)
314
+ throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row], cause: e });
315
+ }
316
+ throw toPickhooksError(e, "The quoter refused this trade.");
317
+ }
318
+ }
319
+ /** A buy through PancakeSwap's V3 SwapRouter, BNB in (wrapped by the router). */
320
+ const buildBuy = (b) => buildV3Buy(a, b);
321
+ /** A sell through the V3 SwapRouter for native BNB: swap to the router, unwrap to the seller. Needs approval to the router. */
322
+ const buildSell = (x) => buildV3Sell(a, x);
323
+ /** The approval a sell needs (the coin to the V3 router), or null. */
324
+ const sellApproval = (owner, token, amount) => approvalFor(ctx, owner, token, amount, a.v3Router, "Allowing PancakeSwap");
325
+ const refuse = (c) => {
326
+ if (!c.ok)
327
+ throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row] });
328
+ };
329
+ /** Quotes, checks the coin's rules for this wallet, and buys. A refusing rule stops it before anything is signed. */
330
+ async function buy(b, s = {}) {
331
+ const { address } = await signer(ctx, s.wallet);
332
+ const q = await quote({ token: b.token, side: "buy", amount: b.bnbIn, wallet: address });
333
+ const venue = b.venue ?? q.venue;
334
+ const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
335
+ refuse(await check({ token: b.token, side: "buy", wallet: address, amount: quoted }));
336
+ const minOut = b.minOut ?? minusBps(quoted, b.slippageBps ?? 100);
337
+ const deadline = b.deadline ?? (await chainDeadline(ctx));
338
+ const tx = venue === "v2" ? buildV2Buy(a, { token: b.token, bnbIn: b.bnbIn, minOut, recipient: address, deadline }) : buildBuy({ token: b.token, bnbIn: b.bnbIn, minOut, recipient: address, deadline });
339
+ const sent = await sendTx(ctx, tx, s);
340
+ return { ...sent, quoted, minOut, amountOut: receivedOf(sent.receipt, b.token, address), venue };
341
+ }
342
+ /** Checks the rules, approves the router if needed, quotes and sells for BNB. */
343
+ async function sell(x, s = {}) {
344
+ const { address } = await signer(ctx, s.wallet);
345
+ refuse(await check({ token: x.token, side: "sell", wallet: address, amount: x.amount }));
346
+ const q = await quote({ token: x.token, side: "sell", amount: x.amount });
347
+ const venue = x.venue ?? q.venue;
348
+ const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
349
+ const ap = await approvalFor(ctx, address, x.token, x.amount, venue === "v2" ? a.v2Router : a.v3Router, "Allowing PancakeSwap");
350
+ if (ap)
351
+ await sendTx(ctx, ap, s);
352
+ const minOut = x.minOut ?? minusBps(quoted, x.slippageBps ?? 100);
353
+ const deadline = x.deadline ?? (await chainDeadline(ctx));
354
+ const tx = venue === "v2" ? buildV2Sell(a, { token: x.token, amount: x.amount, minOut, recipient: address, deadline }) : buildSell({ token: x.token, amount: x.amount, minOut, recipient: address, deadline });
355
+ const sent = await sendTx(ctx, tx, s);
356
+ return { ...sent, quoted, minOut, venue };
357
+ }
358
+ /** The creator lifts a row for good (a drop row cannot be lifted). */
359
+ const buildLift = (token, row) => ({ to: token, value: 0n, data: encodeFunctionData({ abi: TA, functionName: "lift", args: [row] }), label: `Lifting rule ${row + 1}` });
360
+ const lockerOf = (token) => call(token, TA, "locker");
361
+ /** Anyone: collects the coin's trading fees from its locked position; the locker pays the creator's side to the creator. */
362
+ const buildCollect = async (token) => ({
363
+ to: await lockerOf(token),
364
+ value: 0n,
365
+ data: encodeFunctionData({ abi: v2LockerAbi, functionName: "collect", args: [token] }),
366
+ label: "The collect",
367
+ });
368
+ /**
369
+ * What collect() would book now (simulated): the BNB fees of the coin's position and the coins it burns, what of it goes
370
+ * to the creator (creatorBps of the fees is the creator side, keepBps of that the creator keeps, the rest buys back and
371
+ * burns $hooks), and BNB the locker holds for the creator because a payout bounced (withdrawPending() takes it).
372
+ */
373
+ async function fees(token) {
374
+ const locker = await lockerOf(token);
375
+ const pos = await call(locker, v2LockerAbi, "positions", [token]);
376
+ const [sim, pending] = await Promise.all([
377
+ pub.simulateContract({ address: locker, abi: v2LockerAbi, functionName: "collect", args: [token] }).then((r) => r.result),
378
+ call(locker, v2LockerAbi, "pending", [pos[2]]),
379
+ ]);
380
+ // the locker's own split
381
+ const side = (sim[0] * BigInt(pos[3])) / 10000n;
382
+ const toCreator = ((side - (side * BigInt(pos[4])) / 10000n) * BigInt(pos[5])) / 10000n;
383
+ return { bnb: sim[0], burned: sim[1], toCreator, creator: pos[2], creatorBps: Number(pos[3]), keepBps: Number(pos[5]), pending };
384
+ }
385
+ /** Collects the coin's fees; returns what the locker booked (its Collected event). */
386
+ async function collect(token, s = {}) {
387
+ const sent = await sendTx(ctx, await buildCollect(token), s);
388
+ const ev = parseEventLogs({ abi: v2LockerAbi, logs: sent.receipt.logs, eventName: "Collected" })[0];
389
+ return { ...sent, bnb: ev?.args.bnb ?? 0n, toCreator: ev?.args.toCreator ?? 0n, burned: ev?.args.burned ?? 0n };
390
+ }
391
+ return {
392
+ factory: F,
393
+ terms,
394
+ validate,
395
+ validateSwitch,
396
+ compile,
397
+ table: stickyTable,
398
+ testRun: stickyTestRun,
399
+ describeRows,
400
+ prepareMetadata,
401
+ count,
402
+ coins,
403
+ coin,
404
+ isCoin,
405
+ wallet,
406
+ check,
407
+ quote,
408
+ estimateBuy,
409
+ sellApproval,
410
+ build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift, collect: buildCollect },
411
+ launch,
412
+ buy,
413
+ sell,
414
+ lift: (token, row, s = {}) => sendTx(ctx, buildLift(token, row), s),
415
+ fees,
416
+ collect,
417
+ /** the sender's address, for callers that check before they sign */
418
+ from: (s = {}) => senderAddress(ctx, s.wallet),
419
+ };
420
+ }
@@ -0,0 +1,67 @@
1
+ import type { Hex } from "viem";
2
+ export type Net = {
3
+ chainHash: string;
4
+ publicKey: string;
5
+ genesis: number;
6
+ period: number;
7
+ dst: string;
8
+ urls: string[];
9
+ };
10
+ export type Sealed = {
11
+ u: Hex;
12
+ v: Hex;
13
+ w: Hex;
14
+ round: number;
15
+ sigma: Hex;
16
+ };
17
+ export declare const QUICKNET: Net;
18
+ declare const G2: import("@noble/curves/abstract/weierstrass").ProjConstructor<import("@noble/curves/abstract/tower").Fp2>;
19
+ type G2P = InstanceType<typeof G2>;
20
+ /** r = (keccak(TAG3 || 0x01 || sigma || M) * 2^256 + keccak(TAG3 || 0x02 || sigma || M)) mod q, never 0. */
21
+ export declare function h3(sigma: Uint8Array, m: Uint8Array): bigint;
22
+ /** keccak(TAG4 || sigma). */
23
+ export declare function h4(sigma: Uint8Array): Uint8Array;
24
+ /** First second at which drand publishes the round. */
25
+ export declare function roundTime(round: number | bigint, net?: Net): number;
26
+ /** First round published at or after `unixSeconds`. */
27
+ export declare function roundAt(unixSeconds: number, net?: Net): number;
28
+ /** The round's identity point: hash_to_G1(sha256(round as 8 bytes big endian)), as drand quicknet signs it. */
29
+ export declare function roundPoint(round: number | bigint, net?: Net): import("@noble/curves/abstract/weierstrass").ProjPointType<bigint>;
30
+ /** True when `sigHex` is drand's signature on `round` under the network's key. */
31
+ export declare function verifyRoundSig(round: number | bigint, sigHex: string, net?: Net): boolean;
32
+ /** G2 point to the 256 byte EIP-2537 encoding: x.c0, x.c1, y.c0, y.c1, each 64 bytes (16 zero bytes + 48). */
33
+ export declare function g2ToEip(point: G2P): Hex;
34
+ export declare function g2FromEip(h: string): G2P;
35
+ /** The 32 byte message of a dark launch bid: the bidder (20 bytes), the step (1 byte), 11 zero bytes. */
36
+ export declare function darkMessage(bidder: string, step: number): Hex;
37
+ export declare function parseDarkMessage(m: string): {
38
+ bidder: Hex;
39
+ step: number;
40
+ clean: boolean;
41
+ };
42
+ /**
43
+ * Encrypts 32 bytes to a drand round. Returns hex ready for the contracts: u (256 bytes), v and w (32 each).
44
+ * `opts.sigma` and `opts.publicKey` exist for tests only.
45
+ */
46
+ export declare function encrypt(round: number | bigint, message: string | Uint8Array, opts?: {
47
+ net?: Net;
48
+ sigma?: string | Uint8Array;
49
+ publicKey?: string;
50
+ }): Sealed;
51
+ /** Opens a ciphertext with drand's signature on its round: { sigma, message }, or null when it does not open. */
52
+ export declare function decrypt(c: {
53
+ u: string;
54
+ v: string;
55
+ w: string;
56
+ }, sigHex: string): {
57
+ sigma: Hex;
58
+ message: Hex;
59
+ } | null;
60
+ /** The contract's own check, in JS: U == H3(sigma, M) * G2 and W == M xor H4(sigma). */
61
+ export declare function opens(c: {
62
+ u: string;
63
+ w: string;
64
+ }, sigma: string | Uint8Array, message: string | Uint8Array): boolean;
65
+ /** drand's signature on `round` (hex, 48 bytes), checked against the network's key. Null while the round is not out. */
66
+ export declare function fetchRoundSig(round: number | bigint, net?: Net, fetcher?: typeof fetch): Promise<string | null>;
67
+ export {};
package/dist/tlock.js ADDED
@@ -0,0 +1,185 @@
1
+ // Time-lock encryption to a drand quicknet round, the same as the site's web/src/lib/hide-tlock.mjs (find the dev) and
2
+ // what the dark launch's sealed bids use. Its own entry (@pickhooks/sdk/tlock), so the main SDK does not load BLS12-381.
3
+ //
4
+ // Ciphertext (U, V, W) for a 32 byte message M and the round's identity Q = hashToG1(sha256(round)):
5
+ // sigma random 32 bytes, r = H3(sigma, M) mod q
6
+ // U = r * G2 (G2 point, 256 bytes in EIP-2537 encoding on chain)
7
+ // V = sigma xor H2(e(Q, pk)^r)
8
+ // W = M xor H4(sigma)
9
+ // Opening needs drand's signature on the round (sig = s * Q): sigma = V xor H2(e(sig, U)), M = W xor H4(sigma).
10
+ // The contracts check U == H3(sigma, M) * G2 and W == M xor H4(sigma) with the BLS12-381 precompile (HideMath.sol).
11
+ import { bls12_381 as bls } from "@noble/curves/bls12-381";
12
+ import { sha256 } from "@noble/hashes/sha2";
13
+ import { keccak_256 } from "@noble/hashes/sha3";
14
+ export const QUICKNET = {
15
+ chainHash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
16
+ publicKey: "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
17
+ genesis: 1692803367,
18
+ period: 3,
19
+ dst: "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_",
20
+ urls: ["https://api.drand.sh", "https://api2.drand.sh", "https://api3.drand.sh", "https://drand.cloudflare.com"],
21
+ };
22
+ const Q = bls.params.r;
23
+ const TWO256 = 1n << 256n;
24
+ const TAG3 = new TextEncoder().encode("pickhooks.hide.H3");
25
+ const TAG4 = new TextEncoder().encode("pickhooks.hide.H4");
26
+ const TAG2 = new TextEncoder().encode("pickhooks.hide.H2");
27
+ const G2 = bls.G2.ProjectivePoint;
28
+ const G1 = bls.G1.ProjectivePoint;
29
+ const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
30
+ const unhex = (h) => {
31
+ const s = h.startsWith("0x") ? h.slice(2) : h;
32
+ if (s.length % 2)
33
+ throw new Error("odd hex");
34
+ const out = new Uint8Array(s.length / 2);
35
+ for (let i = 0; i < out.length; i++)
36
+ out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
37
+ return out;
38
+ };
39
+ const concat = (...parts) => {
40
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
41
+ let o = 0;
42
+ for (const p of parts) {
43
+ out.set(p, o);
44
+ o += p.length;
45
+ }
46
+ return out;
47
+ };
48
+ const xor32 = (a, b) => {
49
+ const out = new Uint8Array(32);
50
+ for (let i = 0; i < 32; i++)
51
+ out[i] = a[i] ^ b[i];
52
+ return out;
53
+ };
54
+ const toBig = (b) => BigInt("0x" + (hex(b) || "0"));
55
+ const be = (n, len) => unhex(n.toString(16).padStart(len * 2, "0"));
56
+ const bytes32 = (x) => {
57
+ const b = typeof x === "string" ? unhex(x) : x;
58
+ if (b.length !== 32)
59
+ throw new Error("need 32 bytes");
60
+ return b;
61
+ };
62
+ /** r = (keccak(TAG3 || 0x01 || sigma || M) * 2^256 + keccak(TAG3 || 0x02 || sigma || M)) mod q, never 0. */
63
+ export function h3(sigma, m) {
64
+ const a = toBig(keccak_256(concat(TAG3, Uint8Array.of(1), sigma, m)));
65
+ const b = toBig(keccak_256(concat(TAG3, Uint8Array.of(2), sigma, m)));
66
+ const r = (a * TWO256 + b) % Q;
67
+ return r === 0n ? 1n : r;
68
+ }
69
+ /** keccak(TAG4 || sigma). */
70
+ export function h4(sigma) {
71
+ return keccak_256(concat(TAG4, sigma));
72
+ }
73
+ function h2(gt) {
74
+ return keccak_256(concat(TAG2, bls.fields.Fp12.toBytes(gt)));
75
+ }
76
+ /** First second at which drand publishes the round. */
77
+ export function roundTime(round, net = QUICKNET) {
78
+ return net.genesis + (Number(round) - 1) * net.period;
79
+ }
80
+ /** First round published at or after `unixSeconds`. */
81
+ export function roundAt(unixSeconds, net = QUICKNET) {
82
+ const t = Math.max(0, Math.ceil(unixSeconds) - net.genesis);
83
+ return Math.ceil(t / net.period) + 1;
84
+ }
85
+ /** The round's identity point: hash_to_G1(sha256(round as 8 bytes big endian)), as drand quicknet signs it. */
86
+ export function roundPoint(round, net = QUICKNET) {
87
+ return G1.fromAffine(bls.G1.hashToCurve(sha256(be(BigInt(round), 8)), { DST: net.dst }).toAffine());
88
+ }
89
+ /** True when `sigHex` is drand's signature on `round` under the network's key. */
90
+ export function verifyRoundSig(round, sigHex, net = QUICKNET) {
91
+ const sig = G1.fromHex(sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex);
92
+ const pk = G2.fromHex(net.publicKey);
93
+ const lhs = bls.pairing(sig, G2.BASE);
94
+ const rhs = bls.pairing(roundPoint(round, net), pk);
95
+ return bls.fields.Fp12.eql(lhs, rhs);
96
+ }
97
+ /** G2 point to the 256 byte EIP-2537 encoding: x.c0, x.c1, y.c0, y.c1, each 64 bytes (16 zero bytes + 48). */
98
+ export function g2ToEip(point) {
99
+ const { x, y } = point.toAffine();
100
+ return `0x${[x.c0, x.c1, y.c0, y.c1].map((v) => v.toString(16).padStart(128, "0")).join("")}`;
101
+ }
102
+ export function g2FromEip(h) {
103
+ const b = unhex(h);
104
+ if (b.length !== 256)
105
+ throw new Error("U must be 256 bytes");
106
+ const f = (i) => toBig(b.slice(i * 64, i * 64 + 64));
107
+ const Fp2 = bls.fields.Fp2;
108
+ const p = G2.fromAffine({ x: Fp2.create({ c0: f(0), c1: f(1) }), y: Fp2.create({ c0: f(2), c1: f(3) }) });
109
+ p.assertValidity();
110
+ return p;
111
+ }
112
+ /** The 32 byte message of a dark launch bid: the bidder (20 bytes), the step (1 byte), 11 zero bytes. */
113
+ export function darkMessage(bidder, step) {
114
+ const a = unhex(bidder);
115
+ if (a.length !== 20)
116
+ throw new Error("bidder must be 20 bytes");
117
+ if (!(Number.isInteger(step) && step >= 0 && step < 24))
118
+ throw new Error("step must be 0 to 23");
119
+ return `0x${hex(concat(a, Uint8Array.of(step), new Uint8Array(11)))}`;
120
+ }
121
+ export function parseDarkMessage(m) {
122
+ const b = bytes32(m);
123
+ return { bidder: `0x${hex(b.slice(0, 20))}`, step: b[20], clean: b.slice(21).every((x) => x === 0) };
124
+ }
125
+ /**
126
+ * Encrypts 32 bytes to a drand round. Returns hex ready for the contracts: u (256 bytes), v and w (32 each).
127
+ * `opts.sigma` and `opts.publicKey` exist for tests only.
128
+ */
129
+ export function encrypt(round, message, opts = {}) {
130
+ const net = opts.net ?? QUICKNET;
131
+ const m = bytes32(message);
132
+ const sigma = opts.sigma ? bytes32(opts.sigma) : crypto.getRandomValues(new Uint8Array(32));
133
+ const r = h3(sigma, m);
134
+ const pk = G2.fromHex(opts.publicKey ?? net.publicKey);
135
+ const U = G2.BASE.multiply(r);
136
+ const gid = bls.pairing(roundPoint(round, net), pk.multiply(r));
137
+ const V = xor32(sigma, h2(gid));
138
+ const W = xor32(m, h4(sigma));
139
+ return { u: g2ToEip(U), v: `0x${hex(V)}`, w: `0x${hex(W)}`, round: Number(round), sigma: `0x${hex(sigma)}` };
140
+ }
141
+ /** Opens a ciphertext with drand's signature on its round: { sigma, message }, or null when it does not open. */
142
+ export function decrypt(c, sigHex) {
143
+ let U;
144
+ try {
145
+ U = g2FromEip(c.u);
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ const sig = G1.fromHex(sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex);
151
+ const gid = bls.pairing(sig, U);
152
+ const sigma = xor32(bytes32(c.v), h2(gid));
153
+ const m = xor32(bytes32(c.w), h4(sigma));
154
+ if (!opens(c, sigma, m))
155
+ return null;
156
+ return { sigma: `0x${hex(sigma)}`, message: `0x${hex(m)}` };
157
+ }
158
+ /** The contract's own check, in JS: U == H3(sigma, M) * G2 and W == M xor H4(sigma). */
159
+ export function opens(c, sigma, message) {
160
+ const s = bytes32(sigma);
161
+ const m = bytes32(message);
162
+ if (hex(xor32(m, h4(s))) !== hex(bytes32(c.w)))
163
+ return false;
164
+ return g2ToEip(G2.BASE.multiply(h3(s, m))).toLowerCase() === c.u.toLowerCase();
165
+ }
166
+ /** drand's signature on `round` (hex, 48 bytes), checked against the network's key. Null while the round is not out. */
167
+ export async function fetchRoundSig(round, net = QUICKNET, fetcher = fetch) {
168
+ for (const base of net.urls) {
169
+ try {
170
+ const res = await fetcher(`${base}/${net.chainHash}/public/${round}`, { cache: "no-store" });
171
+ if (!res.ok)
172
+ continue;
173
+ const j = (await res.json());
174
+ if (Number(j.round) !== Number(round) || typeof j.signature !== "string")
175
+ continue;
176
+ if (!verifyRoundSig(round, j.signature, net))
177
+ continue;
178
+ return j.signature;
179
+ }
180
+ catch {
181
+ // next relay
182
+ }
183
+ }
184
+ return null;
185
+ }