@pickhooks/sdk 0.2.0 → 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.
package/dist/sticky.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { type Address, type Hex } from "viem";
2
- import { type EditRow, type Pick, compile } from "./sticky-rules.js";
2
+ import { type EditRow, type Pick as RulePick, compile } from "./sticky-rules.js";
3
+ import { type SimReport } from "./sticky-sim.js";
3
4
  import { type Ctx, type SendOptions, type Sent, type TxRequest } from "./tx.js";
5
+ import { type Venue } from "./v2.js";
4
6
  /**
5
7
  * sticky rules: a coin whose rules stick to the coins, not to the wallet. Every buy is a lot with its time and marks, a
6
8
  * send moves lots along, so a fresh wallet takes the rules with it. The creator picks up to eight rows from templates; the
@@ -75,8 +77,10 @@ export type StickyLaunch = {
75
77
  symbol: string;
76
78
  /** a link to the coin's metadata JSON (name, symbol, description, image, website, twitter, telegram) */
77
79
  metadata: string;
78
- /** templates and their numbers; or rows + release for a table built elsewhere */
79
- picks?: Pick[];
80
+ /** templates and their numbers (numbers left out take the template's defaults); or rows + release for a table built elsewhere */
81
+ picks?: (Omit<RulePick, "params"> & {
82
+ params?: RulePick["params"];
83
+ })[];
80
84
  /**
81
85
  * hook script: the creator's own rows, after the templates (the factory's custom switch must be on). Built like the
82
86
  * site's editor: emptyEditRow(), VARS for the variables and their units, editToRow() for the row on chain.
@@ -92,6 +96,12 @@ export type StickyLaunch = {
92
96
  keepBps?: number;
93
97
  /** a fixed salt (bytes32); default random */
94
98
  salt?: Hex;
99
+ /**
100
+ * sticky 2 only: rules (0 based) that switch, 2 to 4 drop rows; one of them is on at a time (the first at launch) and
101
+ * the switcher turns another on. Default switcher: the rule auction, where holders burn coins to pick the game.
102
+ */
103
+ switchRows?: number[];
104
+ switcher?: Address;
95
105
  };
96
106
  export type StickyTerms = {
97
107
  seed: bigint;
@@ -104,11 +114,47 @@ export type StickyTerms = {
104
114
  };
105
115
  /** The rows of a coin as sentences, with the template each belongs to. */
106
116
  export declare function describeRows(raw: readonly bigint[], release: number, off: number): StickyRow[];
107
- export declare function stickyModule(ctx: Ctx): {
117
+ /**
118
+ * The table a launch would send: the templates' rows, then the hook script rows (the site's order), one release time per
119
+ * coin (the longer one wins); or rows + release as given. Offline; throws the problems as sentences.
120
+ */
121
+ export declare function stickyTable(l: Pick<StickyLaunch, "picks" | "script" | "scriptHours" | "rows" | "release">): {
122
+ rows: bigint[];
123
+ release: number;
124
+ };
125
+ export type StickyTestRun = {
126
+ rows: bigint[];
127
+ release: number;
128
+ /** the run's numbers, as the launcher's test run shows them */
129
+ report: SimReport;
130
+ /** the run as sentences, one per line of the launcher's card */
131
+ text: string[];
132
+ };
133
+ /** A test run as sentences: the launcher's card line by line. `names` are the rows' names (rule i + 1). */
134
+ export declare function testRunText(r: SimReport, names: string[]): string[];
135
+ /**
136
+ * The launcher's test run, offline: the table on a fresh coin with random trades (the creator's first buy, three snipers
137
+ * in the first blocks, wallets that buy, hold and sell, bags split to fresh wallets). It runs the coin's own rule engine,
138
+ * checked against the contract trade by trade. The same seed gives the same run; the chain stays the truth.
139
+ */
140
+ export declare function stickyTestRun(l: Pick<StickyLaunch, "picks" | "script" | "scriptHours" | "rows" | "release"> & {
141
+ devBuyBnb?: number;
142
+ seed?: number;
143
+ trades?: number;
144
+ start?: number;
145
+ }): StickyTestRun;
146
+ /**
147
+ * The sticky module. `v2` points it at the sticky 2 factory (same coins, same rules, same calls; its coins also count
148
+ * the creator's sold coins and can carry switch rows, see ph.sticky2).
149
+ */
150
+ export declare function stickyModule(ctx: Ctx, v2?: boolean): {
108
151
  factory: `0x${string}`;
109
152
  terms: () => Promise<StickyTerms>;
110
153
  validate: (rows: bigint[], release: number, custom?: boolean) => Promise<string | null>;
154
+ validateSwitch: (rows: bigint[], mask: number, validator?: Address) => Promise<string | null>;
111
155
  compile: typeof compile;
156
+ table: typeof stickyTable;
157
+ testRun: typeof stickyTestRun;
112
158
  describeRows: typeof describeRows;
113
159
  prepareMetadata: (m: {
114
160
  name: string;
@@ -151,6 +197,9 @@ export declare function stickyModule(ctx: Ctx): {
151
197
  wallet?: Address;
152
198
  }) => Promise<{
153
199
  amountOut: bigint;
200
+ venue: Venue;
201
+ v3: bigint | null;
202
+ v2: bigint | null;
154
203
  }>;
155
204
  estimateBuy: (token: Address, bnbIn: bigint) => Promise<bigint>;
156
205
  sellApproval: (owner: Address, token: Address, amount: bigint) => Promise<TxRequest | null>;
@@ -164,6 +213,8 @@ export declare function stickyModule(ctx: Ctx): {
164
213
  salt?: Hex;
165
214
  keepBps?: number;
166
215
  minCreatorBps: number;
216
+ switcher?: Address;
217
+ switchMask?: number;
167
218
  }, value: bigint) => TxRequest;
168
219
  buy: (b: {
169
220
  token: Address;
@@ -180,6 +231,7 @@ export declare function stickyModule(ctx: Ctx): {
180
231
  deadline?: bigint;
181
232
  }) => TxRequest;
182
233
  lift: (token: Address, row: number) => TxRequest;
234
+ collect: (token: Address) => Promise<TxRequest>;
183
235
  };
184
236
  launch: (l: StickyLaunch, s?: SendOptions) => Promise<Sent & {
185
237
  token: Address;
@@ -194,10 +246,12 @@ export declare function stickyModule(ctx: Ctx): {
194
246
  slippageBps?: number;
195
247
  minOut?: bigint;
196
248
  deadline?: bigint;
249
+ venue?: Venue;
197
250
  }, s?: SendOptions) => Promise<Sent & {
198
251
  quoted: bigint;
199
252
  minOut: bigint;
200
253
  amountOut: bigint;
254
+ venue: Venue;
201
255
  }>;
202
256
  sell: (x: {
203
257
  token: Address;
@@ -205,11 +259,27 @@ export declare function stickyModule(ctx: Ctx): {
205
259
  slippageBps?: number;
206
260
  minOut?: bigint;
207
261
  deadline?: bigint;
262
+ venue?: Venue;
208
263
  }, s?: SendOptions) => Promise<Sent & {
209
264
  quoted: bigint;
210
265
  minOut: bigint;
266
+ venue: Venue;
211
267
  }>;
212
268
  lift: (token: Address, row: number, s?: SendOptions) => Promise<Sent>;
269
+ fees: (token: Address) => Promise<{
270
+ bnb: bigint;
271
+ burned: bigint;
272
+ toCreator: bigint;
273
+ creator: Address;
274
+ creatorBps: number;
275
+ keepBps: number;
276
+ pending: bigint;
277
+ }>;
278
+ collect: (token: Address, s?: SendOptions) => Promise<Sent & {
279
+ bnb: bigint;
280
+ toCreator: bigint;
281
+ burned: bigint;
282
+ }>;
213
283
  /** the sender's address, for callers that check before they sign */
214
284
  from: (s?: SendOptions) => Promise<`0x${string}` | undefined>;
215
285
  };
package/dist/sticky.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import { encodeFunctionData, parseEventLogs } from "viem";
2
2
  import { stickyFactoryAbi, stickyTokenAbi, stickyValidatorAbi } from "./abis/sticky.js";
3
+ import { sticky2FactoryAbi, sticky2TokenAbi, sticky2ValidatorAbi } from "./abis/sticky2.js";
3
4
  import { v3PoolAbi } from "./abis/common.js";
5
+ import { v2LockerAbi } from "./abis/v2.js";
4
6
  import { ZERO_ADDRESS } from "./addresses.js";
5
7
  import { PickhooksError, errorText, explain, toPickhooksError } from "./errors.js";
6
- import { ACT, LOCKED, NONE, ON, compile, decodeRow, editToRow, rowProblems, rowText, scannerOf, TEMPLATE_BY_ID, templateOf } from "./sticky-rules.js";
7
- import { minusBps, sendTx, senderAddress, signer } from "./tx.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";
8
12
  import { approvalFor, buildV3Buy, buildV3Sell, receivedOf, v3EstimateBuy, v3PriceBnb, v3Quote } from "./v3.js";
9
13
  const randomSalt = () => {
10
14
  const b = new Uint8Array(32);
@@ -30,15 +34,80 @@ export function describeRows(raw, release, off) {
30
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 };
31
35
  });
32
36
  }
33
- export function stickyModule(ctx) {
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) {
34
102
  const { pub, a } = ctx;
35
- const FA = stickyFactoryAbi;
36
- const TA = stickyTokenAbi;
103
+ const F = v2 ? a.sticky2Factory : a.stickyFactory;
104
+ const FA = (v2 ? sticky2FactoryAbi : stickyFactoryAbi);
105
+ const TA = (v2 ? sticky2TokenAbi : stickyTokenAbi);
37
106
  const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
38
107
  /** The factory's terms now: the seed every launch pays into the V2 pair, the creator side, whether own rows are open. */
39
108
  async function terms() {
40
109
  const fns = ["seedBnb", "creatorBps", "custom", "validator", "startFdv", "platformWallet", "locker"];
41
- const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: a.stickyFactory, abi: FA, functionName })), allowFailure: false });
110
+ const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: F, abi: FA, functionName })), allowFailure: false });
42
111
  return { seed: r[0], creatorBps: Number(r[1]), custom: r[2], validator: r[3], startFdv: r[4], platformWallet: r[5], locker: r[6] };
43
112
  }
44
113
  /** Asks the validator on chain whether a table can launch: null when it can, else the reason. */
@@ -52,6 +121,19 @@ export function stickyModule(ctx) {
52
121
  return explain(e, "The validator refused this table.").text;
53
122
  }
54
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
+ }
55
137
  /**
56
138
  * Stores the image and metadata on pickhooks.com and returns the metadata link (the site's /api/sticky/prepare, rate
57
139
  * limited, only while sticky rules are live on the site). A launchpad can host its own JSON and pass that link instead.
@@ -70,35 +152,31 @@ export function stickyModule(ctx) {
70
152
  /** The launch call. value = the factory's seed + the creator's first buy. */
71
153
  function buildLaunch(l, value) {
72
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 };
73
- return { to: a.stickyFactory, value, data: encodeFunctionData({ abi: FA, functionName: "launch", args: [args] }), label: "The launch" };
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" };
74
160
  }
75
161
  /** Builds the table from templates and hook script rows (or takes rows), checks it locally and on chain, launches, returns the coin. */
76
162
  async function launch(l, s = {}) {
77
- let rows = l.rows ?? [];
78
- let release = l.release ?? 0;
79
163
  const t = await terms();
80
164
  if (l.script?.length && !t.custom)
81
165
  throw new PickhooksError("The hook script is off on the factory: templates only for now.", { code: "input" });
82
- if (l.picks || l.script?.length) {
83
- // the site's order: the templates' rows, then the own rows; one release time per coin, the longer one wins
84
- const own = (l.script ?? []).map(editToRow);
85
- const c = compile(l.picks ?? [], own);
86
- const problems = [...c.problems, ...own.flatMap(rowProblems)];
87
- const hours = l.scriptHours ?? 0;
88
- const locks = own.some((r) => r.act === ACT.LOCK);
89
- if (locks && !(hours * 3600 >= 600 && hours <= 720))
90
- problems.push("Locked shares: unlock over 10 minutes to 30 days.");
91
- if (problems.length)
92
- throw new PickhooksError(problems.join(" "), { code: "input" });
93
- rows = c.rows;
94
- release = locks ? Math.max(c.release, Math.round(hours * 3600)) : c.release;
95
- }
166
+ const { rows, release } = stickyTable(l);
96
167
  const refused = await validate(rows, release, t.custom);
97
168
  if (refused)
98
169
  throw new PickhooksError(refused, { code: "Bad" });
99
- const tx = buildLaunch({ name: l.name, symbol: l.symbol, metadata: l.metadata, rows, release, salt: l.salt, keepBps: l.keepBps, minCreatorBps: t.creatorBps }, t.seed + (l.devBuy ?? 0n));
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));
100
177
  const sent = await sendTx(ctx, tx, s);
101
- const ev = parseEventLogs({ abi: FA, logs: sent.receipt.logs, eventName: "Launched" })[0];
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];
102
180
  if (!ev)
103
181
  throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
104
182
  return { ...sent, token: ev.args.token, pool: ev.args.pool, pair: ev.args.pair, rows, release };
@@ -150,7 +228,7 @@ export function stickyModule(ctx) {
150
228
  return out;
151
229
  }
152
230
  /** How many coins the factory launched. */
153
- const count = async () => Number(await call(a.stickyFactory, FA, "coinCount"));
231
+ const count = async () => Number(await call(F, FA, "coinCount"));
154
232
  /** Coins newest first: `limit` of them, skipping the newest `offset`. */
155
233
  async function coins(o = {}) {
156
234
  const n = await count();
@@ -160,12 +238,12 @@ export function stickyModule(ctx) {
160
238
  return [];
161
239
  const idx = Array.from({ length: end - from }, (_, i) => BigInt(end - 1 - i));
162
240
  const tokens = (await pub.multicall({
163
- contracts: idx.map((i) => ({ address: a.stickyFactory, abi: FA, functionName: "coins", args: [i] })),
241
+ contracts: idx.map((i) => ({ address: F, abi: FA, functionName: "coins", args: [i] })),
164
242
  allowFailure: false,
165
243
  }));
166
244
  return summaries(tokens);
167
245
  }
168
- const isCoin = async (token) => (await call(a.stickyFactory, FA, "isCoin", [token]).catch(() => false)) === true;
246
+ const isCoin = async (token) => (await call(F, FA, "isCoin", [token]).catch(() => false)) === true;
169
247
  /** One sticky coin, or null when the address is not one of the factory's coins. */
170
248
  async function coin(token) {
171
249
  if (!(await isCoin(token)))
@@ -218,11 +296,17 @@ export function stickyModule(ctx) {
218
296
  * does not pass the reason on: a refused buy is checked again with an estimate of its coins and the refusal names the
219
297
  * rule (for `wallet`, by default the quoter the quote ran as).
220
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
+ */
221
304
  async function quote(q) {
222
- try {
223
- return { amountOut: await v3Quote(ctx, q) };
224
- }
225
- catch (e) {
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
+ {
226
310
  if (q.side === "buy") {
227
311
  const est = await estimateBuy(q.token, q.amount).catch(() => 0n);
228
312
  const c = est > 0n ? await check({ token: q.token, side: "buy", wallet: q.wallet ?? a.v3Quoter, amount: est }).catch(() => null) : null;
@@ -246,30 +330,72 @@ export function stickyModule(ctx) {
246
330
  async function buy(b, s = {}) {
247
331
  const { address } = await signer(ctx, s.wallet);
248
332
  const q = await quote({ token: b.token, side: "buy", amount: b.bnbIn, wallet: address });
249
- refuse(await check({ token: b.token, side: "buy", wallet: address, amount: q.amountOut }));
250
- const minOut = b.minOut ?? minusBps(q.amountOut, b.slippageBps ?? 100);
251
- const sent = await sendTx(ctx, buildBuy({ token: b.token, bnbIn: b.bnbIn, minOut, recipient: address, deadline: b.deadline }), s);
252
- return { ...sent, quoted: q.amountOut, minOut, amountOut: receivedOf(sent.receipt, b.token, 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 };
253
341
  }
254
342
  /** Checks the rules, approves the router if needed, quotes and sells for BNB. */
255
343
  async function sell(x, s = {}) {
256
344
  const { address } = await signer(ctx, s.wallet);
257
345
  refuse(await check({ token: x.token, side: "sell", wallet: address, amount: x.amount }));
258
- const ap = await sellApproval(address, x.token, 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");
259
350
  if (ap)
260
351
  await sendTx(ctx, ap, s);
261
- const q = await quote({ token: x.token, side: "sell", amount: x.amount });
262
- const minOut = x.minOut ?? minusBps(q.amountOut, x.slippageBps ?? 100);
263
- const sent = await sendTx(ctx, buildSell({ token: x.token, amount: x.amount, minOut, recipient: address, deadline: x.deadline }), s);
264
- return { ...sent, quoted: q.amountOut, minOut };
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 };
265
357
  }
266
358
  /** The creator lifts a row for good (a drop row cannot be lifted). */
267
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
+ }
268
391
  return {
269
- factory: a.stickyFactory,
392
+ factory: F,
270
393
  terms,
271
394
  validate,
395
+ validateSwitch,
272
396
  compile,
397
+ table: stickyTable,
398
+ testRun: stickyTestRun,
273
399
  describeRows,
274
400
  prepareMetadata,
275
401
  count,
@@ -281,11 +407,13 @@ export function stickyModule(ctx) {
281
407
  quote,
282
408
  estimateBuy,
283
409
  sellApproval,
284
- build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift },
410
+ build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift, collect: buildCollect },
285
411
  launch,
286
412
  buy,
287
413
  sell,
288
414
  lift: (token, row, s = {}) => sendTx(ctx, buildLift(token, row), s),
415
+ fees,
416
+ collect,
289
417
  /** the sender's address, for callers that check before they sign */
290
418
  from: (s = {}) => senderAddress(ctx, s.wallet),
291
419
  };
@@ -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 {};