@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/README.md +32 -2
- package/dist/abis/index.d.ts +1 -0
- package/dist/abis/index.js +1 -0
- package/dist/abis/sticky3.d.ts +2292 -0
- package/dist/abis/sticky3.js +8 -0
- package/dist/addresses.d.ts +11 -0
- package/dist/addresses.js +12 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +45 -1
- package/dist/modes-catalog.d.ts +59 -0
- package/dist/modes-catalog.js +77 -0
- package/dist/modes.d.ts +71 -0
- package/dist/modes.js +86 -0
- package/dist/simulate.d.ts +75 -0
- package/dist/simulate.js +225 -0
- package/dist/sticky.d.ts +21 -2
- package/dist/sticky.js +119 -9
- package/dist/text-launch.d.ts +52 -0
- package/dist/text-launch.js +178 -0
- package/dist/tlock.d.ts +10 -0
- package/dist/tlock.js +16 -0
- package/package.json +3 -2
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,
|
|
118
|
+
export function stickyModule(ctx, version = false) {
|
|
102
119
|
const { pub, a } = ctx;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|