@pickhooks/sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sticky.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { encodeFunctionData, parseEventLogs } from "viem";
2
2
  import { stickyFactoryAbi, stickyTokenAbi, stickyValidatorAbi } from "./abis/sticky.js";
3
3
  import { sticky2FactoryAbi, sticky2TokenAbi, sticky2ValidatorAbi } from "./abis/sticky2.js";
4
+ import { sticky3FactoryAbi, sticky3LensAbi, sticky3TokenAbi, sticky3ValidatorAbi } from "./abis/sticky3.js";
5
+ import { MODE_NAMES } from "./addresses.js";
6
+ import { encodeModeData } from "./modes-catalog.js";
4
7
  import { v3PoolAbi } from "./abis/common.js";
5
8
  import { v2LockerAbi } from "./abis/v2.js";
6
9
  import { ZERO_ADDRESS } from "./addresses.js";
@@ -10,6 +13,18 @@ import { simulate } from "./sticky-sim.js";
10
13
  import { chainDeadline, minusBps, sendTx, senderAddress, signer } from "./tx.js";
11
14
  import { bestVenue, buildV2Buy, buildV2Sell, v2Quote } from "./v2.js";
12
15
  import { approvalFor, buildV3Buy, buildV3Sell, receivedOf, v3EstimateBuy, v3PriceBnb, v3Quote } from "./v3.js";
16
+ const sticky3ModeQuoteAbi = [
17
+ {
18
+ type: "function",
19
+ name: "quote",
20
+ stateMutability: "view",
21
+ inputs: [
22
+ { name: "creator", type: "address" },
23
+ { name: "data", type: "bytes" },
24
+ ],
25
+ outputs: [{ name: "gameBps", type: "uint16" }],
26
+ },
27
+ ];
13
28
  const randomSalt = () => {
14
29
  const b = new Uint8Array(32);
15
30
  globalThis.crypto.getRandomValues(b);
@@ -96,13 +111,18 @@ export function stickyTestRun(l) {
96
111
  }
97
112
  /**
98
113
  * 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).
114
+ * the creator's sold coins and can carry switch rows, see ph.sticky2). `3` points it at sticky 3: the same again, plus a
115
+ * mode per coin (launch with `mode` and `modeSettings`; the game itself is ph.modes); a sticky 3 coin leaves its lot
116
+ * views to the lens.
100
117
  */
101
- export function stickyModule(ctx, v2 = false) {
118
+ export function stickyModule(ctx, version = false) {
102
119
  const { pub, a } = ctx;
103
- const F = v2 ? a.sticky2Factory : a.stickyFactory;
104
- const FA = (v2 ? sticky2FactoryAbi : stickyFactoryAbi);
105
- const TA = (v2 ? sticky2TokenAbi : stickyTokenAbi);
120
+ const v3 = version === 3;
121
+ const v2 = version === true || v3; // sticky 3 has everything sticky 2 has
122
+ const F = v3 ? a.sticky3Factory : v2 ? a.sticky2Factory : a.stickyFactory;
123
+ const FA = (v3 ? sticky3FactoryAbi : v2 ? sticky2FactoryAbi : stickyFactoryAbi);
124
+ const TA = (v3 ? sticky3TokenAbi : v2 ? sticky2TokenAbi : stickyTokenAbi);
125
+ const modeName = (m) => m.toLowerCase() === ZERO_ADDRESS ? null : (MODE_NAMES.find((n) => a.modes[n].toLowerCase() === m.toLowerCase()) ?? "other");
106
126
  const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
107
127
  /** The factory's terms now: the seed every launch pays into the V2 pair, the creator side, whether own rows are open. */
108
128
  async function terms() {
@@ -127,7 +147,7 @@ export function stickyModule(ctx, v2 = false) {
127
147
  if (!v2)
128
148
  return "Switch rows are a sticky 2 feature.";
129
149
  try {
130
- await call(validator ?? (await terms()).validator, sticky2ValidatorAbi, "switchRows", [rows, mask]);
150
+ await call(validator ?? (await terms()).validator, (v3 ? sticky3ValidatorAbi : sticky2ValidatorAbi), "switchRows", [rows, mask]);
131
151
  return null;
132
152
  }
133
153
  catch (e) {
@@ -155,9 +175,38 @@ export function stickyModule(ctx, v2 = false) {
155
175
  if (!v2 && l.switchMask)
156
176
  throw new PickhooksError("Switch rows are a sticky 2 feature.", { code: "input" });
157
177
  // 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;
178
+ const full2 = v2 ? { ...args, switcher: l.switchMask ? (l.switcher ?? a.ruleAuction) : ZERO_ADDRESS, switchMask: l.switchMask ?? 0 } : args;
179
+ if (!v3 && l.mode)
180
+ throw new PickhooksError("Modes are a sticky 3 feature.", { code: "input" });
181
+ const modeAddr = l.mode ? a.modes[l.mode] : ZERO_ADDRESS;
182
+ if (l.mode && modeAddr.toLowerCase() === ZERO_ADDRESS)
183
+ throw new PickhooksError(`The mode ${l.mode} is not deployed yet.`, { code: "input" });
184
+ const full = v3 ? { ...full2, mode: modeAddr, modeData: l.mode ? encodeModeData(l.mode, l.modeSettings) : "0x" } : full2;
159
185
  return { to: F, value, data: encodeFunctionData({ abi: FA, functionName: "launch", args: [full] }), label: "The launch" };
160
186
  }
187
+ /**
188
+ * sticky 3: asks the mode for its share of buys with these settings (it refuses settings out of bounds) and the validator
189
+ * whether that share fits next to the rows' burns and pots (10 % of a buy at most). null when the launch can go.
190
+ */
191
+ async function validateMode(mode, settings, rows, validator, creator) {
192
+ if (!v3)
193
+ return "Modes are a sticky 3 feature.";
194
+ const m = a.modes[mode];
195
+ if (m.toLowerCase() === ZERO_ADDRESS)
196
+ return `The mode ${mode} is not deployed yet.`;
197
+ try {
198
+ const listed = await call(F, FA, "modes", [m]);
199
+ if (!listed)
200
+ return `The mode ${mode} is not open for launches yet.`;
201
+ const from = creator ?? (await senderAddress(ctx)) ?? ZERO_ADDRESS;
202
+ const bps = await call(m, sticky3ModeQuoteAbi, "quote", [from, encodeModeData(mode, settings)]);
203
+ await call(validator ?? (await terms()).validator, sticky3ValidatorAbi, "game", [rows, bps]);
204
+ return null;
205
+ }
206
+ catch (e) {
207
+ return explain(e, "The mode refused these settings.").text;
208
+ }
209
+ }
161
210
  /** Builds the table from templates and hook script rows (or takes rows), checks it locally and on chain, launches, returns the coin. */
162
211
  async function launch(l, s = {}) {
163
212
  const t = await terms();
@@ -173,7 +222,12 @@ export function stickyModule(ctx, v2 = false) {
173
222
  if (no)
174
223
  throw new PickhooksError(no, { code: "Bad" });
175
224
  }
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));
225
+ if (l.mode) {
226
+ const no = await validateMode(l.mode, l.modeSettings, rows, t.validator);
227
+ if (no)
228
+ throw new PickhooksError(no, { code: "Bad" });
229
+ }
230
+ 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, mode: l.mode, modeSettings: l.modeSettings }, t.seed + (l.devBuy ?? 0n));
177
231
  const sent = await sendTx(ctx, tx, s);
178
232
  // both factories emit the same Launched event
179
233
  const ev = parseEventLogs({ abi: stickyFactoryAbi, logs: sent.receipt.logs.filter((x) => x.address.toLowerCase() === F.toLowerCase()), eventName: "Launched" })[0];
@@ -192,6 +246,16 @@ export function stickyModule(ctx, v2 = false) {
192
246
  contracts: pools.map((p) => ({ address: (p ?? a.wbnb), abi: v3PoolAbi, functionName: "slot0" })),
193
247
  allowFailure: true,
194
248
  });
249
+ const modeRes = v3
250
+ ? await pub.multicall({
251
+ contracts: tokens.flatMap((t) => [
252
+ { address: t, abi: TA, functionName: "mode" },
253
+ { address: t, abi: TA, functionName: "gameBps" },
254
+ { address: t, abi: TA, functionName: "gamePot" },
255
+ ]),
256
+ allowFailure: true,
257
+ })
258
+ : [];
195
259
  const out = [];
196
260
  tokens.forEach((token, i) => {
197
261
  const g = (k) => res[i * per + k];
@@ -223,6 +287,16 @@ export function stickyModule(ctx, v2 = false) {
223
287
  totalSupply: supply,
224
288
  priceBnb,
225
289
  mcapBnb: priceBnb * (Number(supply) / 1e18),
290
+ ...(v3 && modeRes[i * 3]?.status === "success"
291
+ ? {
292
+ mode: {
293
+ name: modeName(modeRes[i * 3].result),
294
+ address: modeRes[i * 3].result,
295
+ gameBps: Number(modeRes[i * 3 + 1].result ?? 0),
296
+ gamePot: modeRes[i * 3 + 2].result ?? 0n,
297
+ },
298
+ }
299
+ : {}),
226
300
  });
227
301
  });
228
302
  return out;
@@ -252,6 +326,20 @@ export function stickyModule(ctx, v2 = false) {
252
326
  }
253
327
  /** A wallet's lots (amount, locked part, since, marks), its balance and what it can sell now. */
254
328
  async function wallet(token, who) {
329
+ if (v3) {
330
+ const [bal, sellable, lots, held] = await Promise.all([
331
+ call(token, TA, "balanceOf", [who]),
332
+ call(a.sticky3Lens, sticky3LensAbi, "sellable", [token, who]),
333
+ call(a.sticky3Lens, sticky3LensAbi, "lotsOf", [token, who]),
334
+ call(token, TA, "heldOf", [who]),
335
+ ]);
336
+ return {
337
+ balance: bal,
338
+ sellable,
339
+ lots: lots.amount.map((x, i) => ({ amount: x, locked: lots.locked[i], since: Number(lots.since[i]), flags: Number(lots.flags[i]), open: lots.open[i] })),
340
+ held: held[0],
341
+ };
342
+ }
255
343
  const r = await pub.multicall({
256
344
  contracts: [
257
345
  { address: token, abi: TA, functionName: "balanceOf", args: [who] },
@@ -326,6 +414,25 @@ export function stickyModule(ctx, v2 = false) {
326
414
  if (!c.ok)
327
415
  throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row] });
328
416
  };
417
+ /**
418
+ * The share of a buy the coin keeps, basis points: its burn and pot rows on buys that are not lifted, and a sticky 3
419
+ * coin's game share (at most 10 %). The V2 router measures what arrives in the wallet, after the coin took its share,
420
+ * so a V2 buy's minimum leaves it out; the V3 router measures what the pool sends, before it.
421
+ */
422
+ async function buyTakeBps(token) {
423
+ const [raw, , off] = await call(token, TA, "script");
424
+ let take = 0;
425
+ raw.forEach((x, i) => {
426
+ if ((Number(off) >> i) & 1)
427
+ return;
428
+ const r = decodeRow(x);
429
+ if (r.on & ON.BUY && (r.act === ACT.BURN || r.act === ACT.POT))
430
+ take += r.arg;
431
+ });
432
+ if (v3)
433
+ take += Number(await call(token, TA, "gameBps").catch(() => 0));
434
+ return Math.min(take, 1000);
435
+ }
329
436
  /** Quotes, checks the coin's rules for this wallet, and buys. A refusing rule stops it before anything is signed. */
330
437
  async function buy(b, s = {}) {
331
438
  const { address } = await signer(ctx, s.wallet);
@@ -333,7 +440,8 @@ export function stickyModule(ctx, v2 = false) {
333
440
  const venue = b.venue ?? q.venue;
334
441
  const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
335
442
  refuse(await check({ token: b.token, side: "buy", wallet: address, amount: quoted }));
336
- const minOut = b.minOut ?? minusBps(quoted, b.slippageBps ?? 100);
443
+ const arrives = venue === "v2" ? minusBps(quoted, await buyTakeBps(b.token)) : quoted;
444
+ const minOut = b.minOut ?? minusBps(arrives, b.slippageBps ?? 100);
337
445
  const deadline = b.deadline ?? (await chainDeadline(ctx));
338
446
  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
447
  const sent = await sendTx(ctx, tx, s);
@@ -393,6 +501,7 @@ export function stickyModule(ctx, v2 = false) {
393
501
  terms,
394
502
  validate,
395
503
  validateSwitch,
504
+ validateMode,
396
505
  compile,
397
506
  table: stickyTable,
398
507
  testRun: stickyTestRun,
@@ -406,6 +515,7 @@ export function stickyModule(ctx, v2 = false) {
406
515
  check,
407
516
  quote,
408
517
  estimateBuy,
518
+ buyTakeBps,
409
519
  sellApproval,
410
520
  build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift, collect: buildCollect },
411
521
  launch,
@@ -0,0 +1,52 @@
1
+ import type { ModeName } from "./addresses.js";
2
+ import { MODES, type ModeSettings } from "./modes-catalog.js";
3
+ import { type TemplateId } from "./sticky-rules.js";
4
+ /**
5
+ * launch from a sentence: "CAT, combo, dev lock 7 days, sniper jail 24h, dev buy 0.05" becomes a sticky 3 launch
6
+ * (name, ticker, mode with its settings, rule templates, first buy). The fixed parser reads the usual phrasings (English,
7
+ * and the German units the pad's users write: Tage, Std, Minuten) with no model and no key. For anything else pass your
8
+ * own reader (a model with your key): it gets the sentence and the catalog and must answer with the same draft. Neither
9
+ * can produce anything but templates and a listed mode; the validator and the mode check it on chain before a launch.
10
+ */
11
+ export type TextPick = {
12
+ id: TemplateId;
13
+ params: Record<string, number>;
14
+ };
15
+ export type TextDraft = {
16
+ name: string;
17
+ symbol: string;
18
+ mode: ModeName | null;
19
+ modeSettings?: ModeSettings;
20
+ picks: TextPick[];
21
+ devBuyBnb: number;
22
+ };
23
+ export type TextRead = {
24
+ /** null when the sentence names no ticker */
25
+ draft: TextDraft | null;
26
+ /** clauses the parser did not understand (a reader of your own can take them) */
27
+ unknown: string[];
28
+ /** what sticky coins can never do, with the closest rule that can */
29
+ refusals: string[];
30
+ notes: string[];
31
+ };
32
+ /** What sticky coins can never do, and the closest rule that can. */
33
+ export declare const NOT_POSSIBLE: {
34
+ test: RegExp;
35
+ reply: string;
36
+ }[];
37
+ /**
38
+ * Reads a sentence. The ticker comes first ("$CAT" or "CAT"), then the name ("CAT Cat Coin, combo, ..."), then
39
+ * clauses split by commas, semicolons, "and" / "und". ok is up to the caller: a draft with unknown clauses is a
40
+ * question, not a launch.
41
+ */
42
+ export declare function readLaunch(text: string): TextRead;
43
+ /**
44
+ * Reads a sentence with the fixed parser, and hands what it did not understand to `reader` (your model, your key) with
45
+ * the catalog of modes and templates. The reader answers with a full draft or null.
46
+ */
47
+ export declare function understandLaunch(text: string, reader?: (text: string, catalog: {
48
+ modes: typeof MODES;
49
+ templates: string[];
50
+ }) => Promise<TextDraft | null>): Promise<TextRead & {
51
+ by: "parser" | "reader" | null;
52
+ }>;
@@ -0,0 +1,178 @@
1
+ import { MODES } from "./modes-catalog.js";
2
+ import { TEMPLATE_BY_ID } from "./sticky-rules.js";
3
+ /** What sticky coins can never do, and the closest rule that can. */
4
+ export const NOT_POSSIBLE = [
5
+ {
6
+ test: /\b(sell|selling|paper ?hands?|exit)\b[^,;\n]{0,24}\b(tax|fee|fees|pay|pays|penalty)\b|\b(tax|taxes|steuer)\b/i,
7
+ reply: "Sell fees are not possible on sticky coins: a sell or a send is never taxed. Closest: a hold time (sells wait a while) or drip (big buys unlock over hours).",
8
+ },
9
+ { test: /\b(blacklist|blocklist|ban|freeze|pause)\b/i, reply: "Nobody can freeze or ban a wallet on sticky coins. Closest: sniper jail (early buys wait before they can sell)." },
10
+ { test: /\b(mint|minting|more supply|inflation)\b/i, reply: "The supply is fixed at 1 billion, nobody can mint. Closest: auto burn (a share of every buy burns)." },
11
+ ];
12
+ /** the words that name each mode */
13
+ const MODE_WORDS = [
14
+ ["forward", /\bpay\s*it\s*forward\b|\bforward\s*mode\b/i],
15
+ ["combo", /\bcombo\b/i],
16
+ ];
17
+ const num = (s) => Number(s.replace(",", "."));
18
+ const UNIT = "(m|mins?|minutes?|minuten|h|hrs?|hours?|std|stunden?|d|days?|tage?n?)";
19
+ const minutes = (n, unit) => {
20
+ const u = unit.toLowerCase();
21
+ const x = num(n);
22
+ if (u.startsWith("d") || u.startsWith("t"))
23
+ return x * 1440;
24
+ if (u.startsWith("h") || u.startsWith("s"))
25
+ return x * 60;
26
+ return x;
27
+ };
28
+ const pick = (d, id, params) => {
29
+ const t = TEMPLATE_BY_ID.get(id);
30
+ if (!t)
31
+ return;
32
+ const defaults = Object.fromEntries(t.params.map((p) => [p.key, p.def]));
33
+ const had = d.picks.find((p) => p.id === id);
34
+ if (had)
35
+ Object.assign(had.params, params);
36
+ else
37
+ d.picks.push({ id, params: { ...defaults, ...params } });
38
+ };
39
+ const READERS = [
40
+ // a mode, with its share: "combo", "combo 3%", "pay it forward 3 %"
41
+ (c, d) => {
42
+ const hit = MODE_WORDS.find(([, re]) => re.test(c));
43
+ if (!hit)
44
+ return false;
45
+ d.mode = hit[0];
46
+ const m = c.match(/(\d+(?:[.,]\d+)?)\s*%/);
47
+ const info = MODES[hit[0]];
48
+ if (m && info.settings.includes("bps"))
49
+ d.modeSettings = { ...d.modeSettings, bps: Math.round(num(m[1]) * 100) };
50
+ return true;
51
+ },
52
+ // hold 1h / hold time 30 min / sells wait 2h
53
+ (c, d) => {
54
+ const m = c.match(new RegExp(`\\b(?:hold(?:\\s*time)?|sells?\\s+wait|hold\\s+for|haltezeit)\\s*(\\d+(?:[.,]\\d+)?)\\s*${UNIT}\\b`, "i"));
55
+ if (!m)
56
+ return false;
57
+ pick(d, "hold", { m: minutes(m[1], m[2]) });
58
+ return true;
59
+ },
60
+ // sniper jail / anti bundle / snipers wait 12 hours
61
+ (c, d) => {
62
+ if (!/\bsnipers?\b|\bbundl/i.test(c))
63
+ return false;
64
+ const m = c.match(new RegExp(`(\\d+(?:[.,]\\d+)?)\\s*${UNIT}`, "i"));
65
+ const b = c.match(/first\s+(\d+)\s+blocks?/i);
66
+ pick(d, "sniper", { ...(m && { h: minutes(m[1], m[2]) / 60 }), ...(b && { b: num(b[1]) }) });
67
+ return true;
68
+ },
69
+ // dev lock 7 days / dev lock 7 Tage
70
+ (c, d) => {
71
+ if (!/\bdev\s*(?:lock|locked|can'?t\s+sell|sperre)/i.test(c))
72
+ return false;
73
+ const m = c.match(new RegExp(`(\\d+(?:[.,]\\d+)?)\\s*${UNIT}`, "i"));
74
+ pick(d, "devlock", m ? { d: minutes(m[1], m[2]) / 1440 } : {});
75
+ return true;
76
+ },
77
+ // drip / drip over 6h / big buys drip
78
+ (c, d) => {
79
+ if (!/\bdrip/i.test(c))
80
+ return false;
81
+ const m = c.match(new RegExp(`(\\d+(?:[.,]\\d+)?)\\s*${UNIT}`, "i"));
82
+ const big = c.match(/(?:from|over|above)\s*(\d+(?:[.,]\d+)?)\s*%/i);
83
+ pick(d, "drip", { ...(m && { h: minutes(m[1], m[2]) / 60 }), ...(big && { big: num(big[1]) }), ...(/\b(every|all)\s+buys?\b/i.test(c) && { big: 0 }) });
84
+ return true;
85
+ },
86
+ // max buy 1%
87
+ (c, d) => {
88
+ const m = c.match(/\bmax(?:imum)?\s*(?:per\s*)?buy\s*(?:of\s*)?(\d+(?:[.,]\d+)?)\s*%/i);
89
+ if (!m)
90
+ return false;
91
+ pick(d, "maxbuy", { pct: num(m[1]) });
92
+ return true;
93
+ },
94
+ // max wallet 2%
95
+ (c, d) => {
96
+ const m = c.match(/\bmax(?:imum)?\s*wallet\s*(?:of\s*)?(\d+(?:[.,]\d+)?)\s*%/i);
97
+ if (!m)
98
+ return false;
99
+ pick(d, "maxwallet", { pct: num(m[1]) });
100
+ return true;
101
+ },
102
+ // burn 1% / auto burn 2%
103
+ (c, d) => {
104
+ const m = c.match(/\b(?:auto\s*)?burns?\s*(\d+(?:[.,]\d+)?)\s*%/i) ?? c.match(/(\d+(?:[.,]\d+)?)\s*%\s*(?:of\s+(?:every|each)\s+buy\s+)?(?:is\s+)?burn(?:s|ed)?\b/i);
105
+ if (!m)
106
+ return false;
107
+ pick(d, "burn", { pct: num(m[1]) });
108
+ return true;
109
+ },
110
+ // market hours 6-22 (UTC)
111
+ (c, d) => {
112
+ const m = c.match(/\bmarket\s*hours?\s*(\d{1,2})\s*(?:-|to|until|bis)\s*(\d{1,2})/i);
113
+ if (!m)
114
+ return false;
115
+ pick(d, "hours", { open: num(m[1]), close: num(m[2]) });
116
+ return true;
117
+ },
118
+ // dev buy 0.02 (bnb)
119
+ (c, d) => {
120
+ const m = c.match(/\b(?:dev\s*buy|first\s*buy|buy\s*first|erster\s*kauf)\s*(\d+(?:[.,]\d+)?)\s*(?:bnb)?\b/i);
121
+ if (!m)
122
+ return false;
123
+ d.devBuyBnb = num(m[1]);
124
+ return true;
125
+ },
126
+ ];
127
+ /**
128
+ * Reads a sentence. The ticker comes first ("$CAT" or "CAT"), then the name ("CAT Cat Coin, combo, ..."), then
129
+ * clauses split by commas, semicolons, "and" / "und". ok is up to the caller: a draft with unknown clauses is a
130
+ * question, not a launch.
131
+ */
132
+ export function readLaunch(text) {
133
+ const refusals = NOT_POSSIBLE.filter((n) => n.test.test(text)).map((n) => n.reply);
134
+ const body = text.replace(/@\w+/g, " ").replace(/^\s*\/?launch\b/i, " ").trim();
135
+ const parts = body.split(/[,;\n]|\band\b|\bund\b/i).map((x) => x.trim()).filter(Boolean);
136
+ const notes = [];
137
+ if (!parts.length)
138
+ return { draft: null, unknown: [], refusals, notes: ["Nothing to read: start with the ticker, like CAT or $CAT."] };
139
+ const head = parts[0].match(/^\$?([A-Za-z0-9]{1,10})\b\s*[-:]?\s*(.*)$/);
140
+ if (!head)
141
+ return { draft: null, unknown: [], refusals, notes: ["No ticker: start with it, like CAT or $CAT."] };
142
+ const symbol = head[1].toUpperCase();
143
+ const name = head[2].trim() || symbol;
144
+ const d = { name: name.slice(0, 32), symbol, mode: null, picks: [], devBuyBnb: 0 };
145
+ const unknown = [];
146
+ for (const c of parts.slice(1)) {
147
+ if (NOT_POSSIBLE.some((n) => n.test.test(c)))
148
+ continue;
149
+ if (!READERS.some((r) => r(c, d)))
150
+ unknown.push(c);
151
+ }
152
+ if (name === symbol)
153
+ notes.push(`No name after the ticker: the coin is called ${symbol}.`);
154
+ if (!d.mode)
155
+ notes.push("No mode named: the coin launches with rules only (like sticky 2).");
156
+ return { draft: d, unknown, refusals, notes };
157
+ }
158
+ /**
159
+ * Reads a sentence with the fixed parser, and hands what it did not understand to `reader` (your model, your key) with
160
+ * the catalog of modes and templates. The reader answers with a full draft or null.
161
+ */
162
+ export async function understandLaunch(text, reader) {
163
+ const r = readLaunch(text);
164
+ if (r.draft && !r.unknown.length)
165
+ return { ...r, by: "parser" };
166
+ if (!reader)
167
+ return { ...r, by: null };
168
+ const d = await reader(text, { modes: MODES, templates: [...TEMPLATE_BY_ID.keys()] });
169
+ if (!d)
170
+ return { ...r, by: null };
171
+ // a reader can only pick what exists
172
+ if (d.mode && !(d.mode in MODES))
173
+ return { ...r, notes: [...r.notes, `The reader named a mode that does not exist: ${d.mode}.`], by: null };
174
+ const bad = d.picks.filter((p) => !TEMPLATE_BY_ID.has(p.id));
175
+ if (bad.length)
176
+ return { ...r, notes: [...r.notes, `The reader named templates that do not exist: ${bad.map((p) => p.id).join(", ")}.`], by: null };
177
+ return { draft: d, unknown: [], refusals: r.refusals, notes: r.notes, by: "reader" };
178
+ }
package/dist/tlock.d.ts CHANGED
@@ -64,4 +64,14 @@ export declare function opens(c: {
64
64
  }, sigma: string | Uint8Array, message: string | Uint8Array): boolean;
65
65
  /** drand's signature on `round` (hex, 48 bytes), checked against the network's key. Null while the round is not out. */
66
66
  export declare function fetchRoundSig(round: number | bigint, net?: Net, fetcher?: typeof fetch): Promise<string | null>;
67
+ /** A G1 point (drand's compressed signature, 48 bytes hex) in the 128 byte EIP-2537 encoding the DrandBeacon checks. */
68
+ export declare function g1ToEip(sigHex: string): Hex;
69
+ /**
70
+ * drand's signature on `round` ready for DrandBeacon.submit (128 bytes, checked against quicknet's key), and the
71
+ * randomness the beacon will keep: keccak256 of those bytes. Null while the round is not out.
72
+ */
73
+ export declare function beaconSig(round: number | bigint, net?: Net, fetcher?: typeof fetch): Promise<{
74
+ sig: Hex;
75
+ randomness: Hex;
76
+ } | null>;
67
77
  export {};
package/dist/tlock.js CHANGED
@@ -183,3 +183,19 @@ export async function fetchRoundSig(round, net = QUICKNET, fetcher = fetch) {
183
183
  }
184
184
  return null;
185
185
  }
186
+ /** A G1 point (drand's compressed signature, 48 bytes hex) in the 128 byte EIP-2537 encoding the DrandBeacon checks. */
187
+ export function g1ToEip(sigHex) {
188
+ const { x, y } = G1.fromHex(sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex).toAffine();
189
+ return `0x${[x, y].map((v) => v.toString(16).padStart(128, "0")).join("")}`;
190
+ }
191
+ /**
192
+ * drand's signature on `round` ready for DrandBeacon.submit (128 bytes, checked against quicknet's key), and the
193
+ * randomness the beacon will keep: keccak256 of those bytes. Null while the round is not out.
194
+ */
195
+ export async function beaconSig(round, net = QUICKNET, fetcher = fetch) {
196
+ const s = await fetchRoundSig(round, net, fetcher);
197
+ if (!s)
198
+ return null;
199
+ const sig = g1ToEip(s);
200
+ return { sig, randomness: `0x${hex(keccak_256(unhex(sig)))}` };
201
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pickhooks/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Read, quote, trade and launch on pickhooks (BNB Chain) from a bot, a launchpad or a trading terminal. Built on viem.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,8 @@
42
42
  "fork-proof-2": "node test/fork-proof-2.mjs",
43
43
  "fork-proof-infra": "node test/fork-proof-infra.mjs",
44
44
  "mainnet-read-agents": "node test/mainnet-read-agents.mjs",
45
- "release": "node scripts/publish.mjs"
45
+ "release": "node scripts/publish.mjs",
46
+ "fork-proof-sticky3": "node test/fork-proof-sticky3.mjs"
46
47
  },
47
48
  "dependencies": {
48
49
  "@noble/curves": "^1.9.1",