@pickhooks/sdk 0.2.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.
@@ -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
+ }
@@ -0,0 +1,77 @@
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
+ /** 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>;
77
+ export {};
package/dist/tlock.js ADDED
@@ -0,0 +1,201 @@
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
+ }
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/dist/tx.d.ts CHANGED
@@ -66,5 +66,7 @@ export declare function sendTx(ctx: Ctx, tx: TxRequest, o?: SendOptions): Promis
66
66
  export declare function sendAll(ctx: Ctx, txs: TxRequest[], o?: SendOptions): Promise<Sent[]>;
67
67
  /** A deadline 10 minutes from now (seconds), for router calls. */
68
68
  export declare const deadlineIn: (seconds?: number) => bigint;
69
+ /** A deadline from the later of this machine's clock and the chain's (a skewed clock or a fork ahead of real time). */
70
+ export declare function chainDeadline(ctx: Ctx, seconds?: number): Promise<bigint>;
69
71
  /** amount less a slippage in basis points */
70
72
  export declare const minusBps: (amount: bigint, bps: number) => bigint;
package/dist/tx.js CHANGED
@@ -65,5 +65,14 @@ export async function sendAll(ctx, txs, o = {}) {
65
65
  }
66
66
  /** A deadline 10 minutes from now (seconds), for router calls. */
67
67
  export const deadlineIn = (seconds = 600) => BigInt(Math.floor(Date.now() / 1000) + seconds);
68
+ /** A deadline from the later of this machine's clock and the chain's (a skewed clock or a fork ahead of real time). */
69
+ export async function chainDeadline(ctx, seconds = 600) {
70
+ const local = Math.floor(Date.now() / 1000);
71
+ const chain = await ctx.pub
72
+ .getBlock()
73
+ .then((b) => Number(b.timestamp))
74
+ .catch(() => local);
75
+ return BigInt(Math.max(local, chain) + seconds);
76
+ }
68
77
  /** amount less a slippage in basis points */
69
78
  export const minusBps = (amount, bps) => (amount * BigInt(10_000 - Math.max(0, Math.min(10_000, Math.round(bps))))) / 10000n;
package/dist/v2.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { type Address } from "viem";
2
+ import type { Addresses } from "./addresses.js";
3
+ import { type Ctx, type TxRequest } from "./tx.js";
4
+ /**
5
+ * PancakeSwap V2: every sticky coin (and every rules coin since 03.10.) has a V2 pair next to its V3 pool, at the same
6
+ * start price. A dark launch puts the whole round's BNB into that pair, so right after the birth the pair is the venue
7
+ * with the BNB and the V3 curve holds only coins above the price: a sell into V3 would get almost nothing ("SPL").
8
+ * Terminals route to the better pool by themselves; the SDK quotes both and picks the better one (sticky.quote).
9
+ */
10
+ export declare const v2RouterAbi: readonly [{
11
+ readonly name: "getAmountsOut";
12
+ readonly type: "function";
13
+ readonly stateMutability: "view";
14
+ readonly inputs: readonly [{
15
+ readonly type: "uint256";
16
+ readonly name: "amountIn";
17
+ }, {
18
+ readonly type: "address[]";
19
+ readonly name: "path";
20
+ }];
21
+ readonly outputs: readonly [{
22
+ readonly type: "uint256[]";
23
+ readonly name: "amounts";
24
+ }];
25
+ }, {
26
+ readonly name: "swapExactETHForTokensSupportingFeeOnTransferTokens";
27
+ readonly type: "function";
28
+ readonly stateMutability: "payable";
29
+ readonly inputs: readonly [{
30
+ readonly type: "uint256";
31
+ readonly name: "amountOutMin";
32
+ }, {
33
+ readonly type: "address[]";
34
+ readonly name: "path";
35
+ }, {
36
+ readonly type: "address";
37
+ readonly name: "to";
38
+ }, {
39
+ readonly type: "uint256";
40
+ readonly name: "deadline";
41
+ }];
42
+ readonly outputs: readonly [];
43
+ }, {
44
+ readonly name: "swapExactTokensForETHSupportingFeeOnTransferTokens";
45
+ readonly type: "function";
46
+ readonly stateMutability: "nonpayable";
47
+ readonly inputs: readonly [{
48
+ readonly type: "uint256";
49
+ readonly name: "amountIn";
50
+ }, {
51
+ readonly type: "uint256";
52
+ readonly name: "amountOutMin";
53
+ }, {
54
+ readonly type: "address[]";
55
+ readonly name: "path";
56
+ }, {
57
+ readonly type: "address";
58
+ readonly name: "to";
59
+ }, {
60
+ readonly type: "uint256";
61
+ readonly name: "deadline";
62
+ }];
63
+ readonly outputs: readonly [];
64
+ }];
65
+ export type Venue = "v3" | "v2";
66
+ /** What a trade gets in the V2 pair, from the router (reverts when the coin has no pair or the pair is empty). */
67
+ export declare function v2Quote(ctx: Ctx, q: {
68
+ token: Address;
69
+ side: "buy" | "sell";
70
+ amount: bigint;
71
+ }): Promise<bigint>;
72
+ /** A buy through the V2 router, BNB in. */
73
+ export declare function buildV2Buy(a: Addresses, b: {
74
+ token: Address;
75
+ bnbIn: bigint;
76
+ minOut: bigint;
77
+ recipient: Address;
78
+ deadline?: bigint;
79
+ gasPct?: number;
80
+ }): TxRequest;
81
+ /** A sell through the V2 router for native BNB. Needs the coin approved to the V2 router. */
82
+ export declare function buildV2Sell(a: Addresses, s: {
83
+ token: Address;
84
+ amount: bigint;
85
+ minOut: bigint;
86
+ recipient: Address;
87
+ deadline?: bigint;
88
+ gasPct?: number;
89
+ }): TxRequest;
90
+ /**
91
+ * The better of the two venues for this trade: the V3 pool (quoter) and the V2 pair (router). A venue that cannot
92
+ * quote (no pair, no BNB below the price, a refusing rule) counts as nothing; `errors` keeps what each one said.
93
+ */
94
+ export declare function bestVenue(ctx: Ctx, quotes: {
95
+ v3: () => Promise<bigint>;
96
+ v2: () => Promise<bigint>;
97
+ }): Promise<{
98
+ venue: Venue;
99
+ amountOut: bigint;
100
+ v3: bigint | null;
101
+ v2: bigint | null;
102
+ errors: {
103
+ v3?: unknown;
104
+ v2?: unknown;
105
+ };
106
+ }>;