@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.
package/dist/sticky.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { type Address, type Hex } from "viem";
2
- import { type EditRow, type Pick, compile } from "./sticky-rules.js";
2
+ import { type ModeName } from "./addresses.js";
3
+ import { type ModeSettings } from "./modes-catalog.js";
4
+ import { type EditRow, type Pick as RulePick, compile } from "./sticky-rules.js";
5
+ import { type SimReport } from "./sticky-sim.js";
3
6
  import { type Ctx, type SendOptions, type Sent, type TxRequest } from "./tx.js";
7
+ import { type Venue } from "./v2.js";
4
8
  /**
5
9
  * 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
10
  * send moves lots along, so a fresh wallet takes the rules with it. The creator picks up to eight rows from templates; the
@@ -52,6 +56,13 @@ export type StickyCoin = {
52
56
  /** BNB per coin at the V3 pool's price, and the market cap in BNB */
53
57
  priceBnb: number;
54
58
  mcapBnb: number;
59
+ /** sticky 3: the coin's mode (its name, or "other" for a mode the SDK does not know), its share of buys and the game pot */
60
+ mode?: {
61
+ name: ModeName | "other" | null;
62
+ address: Address;
63
+ gameBps: number;
64
+ gamePot: bigint;
65
+ };
55
66
  };
56
67
  export type StickyLot = {
57
68
  amount: bigint;
@@ -75,8 +86,10 @@ export type StickyLaunch = {
75
86
  symbol: string;
76
87
  /** a link to the coin's metadata JSON (name, symbol, description, image, website, twitter, telegram) */
77
88
  metadata: string;
78
- /** templates and their numbers; or rows + release for a table built elsewhere */
79
- picks?: Pick[];
89
+ /** templates and their numbers (numbers left out take the template's defaults); or rows + release for a table built elsewhere */
90
+ picks?: (Omit<RulePick, "params"> & {
91
+ params?: RulePick["params"];
92
+ })[];
80
93
  /**
81
94
  * hook script: the creator's own rows, after the templates (the factory's custom switch must be on). Built like the
82
95
  * site's editor: emptyEditRow(), VARS for the variables and their units, editToRow() for the row on chain.
@@ -92,6 +105,15 @@ export type StickyLaunch = {
92
105
  keepBps?: number;
93
106
  /** a fixed salt (bytes32); default random */
94
107
  salt?: Hex;
108
+ /**
109
+ * 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
110
+ * the switcher turns another on. Default switcher: the rule auction, where holders burn coins to pick the game.
111
+ */
112
+ switchRows?: number[];
113
+ switcher?: Address;
114
+ /** sticky 3 only: the coin's mode (fixed for good) and its settings within the mode's bounds (defaults when left out) */
115
+ mode?: ModeName;
116
+ modeSettings?: ModeSettings;
95
117
  };
96
118
  export type StickyTerms = {
97
119
  seed: bigint;
@@ -104,11 +126,50 @@ export type StickyTerms = {
104
126
  };
105
127
  /** The rows of a coin as sentences, with the template each belongs to. */
106
128
  export declare function describeRows(raw: readonly bigint[], release: number, off: number): StickyRow[];
107
- export declare function stickyModule(ctx: Ctx): {
129
+ /**
130
+ * The table a launch would send: the templates' rows, then the hook script rows (the site's order), one release time per
131
+ * coin (the longer one wins); or rows + release as given. Offline; throws the problems as sentences.
132
+ */
133
+ export declare function stickyTable(l: Pick<StickyLaunch, "picks" | "script" | "scriptHours" | "rows" | "release">): {
134
+ rows: bigint[];
135
+ release: number;
136
+ };
137
+ export type StickyTestRun = {
138
+ rows: bigint[];
139
+ release: number;
140
+ /** the run's numbers, as the launcher's test run shows them */
141
+ report: SimReport;
142
+ /** the run as sentences, one per line of the launcher's card */
143
+ text: string[];
144
+ };
145
+ /** A test run as sentences: the launcher's card line by line. `names` are the rows' names (rule i + 1). */
146
+ export declare function testRunText(r: SimReport, names: string[]): string[];
147
+ /**
148
+ * The launcher's test run, offline: the table on a fresh coin with random trades (the creator's first buy, three snipers
149
+ * in the first blocks, wallets that buy, hold and sell, bags split to fresh wallets). It runs the coin's own rule engine,
150
+ * checked against the contract trade by trade. The same seed gives the same run; the chain stays the truth.
151
+ */
152
+ export declare function stickyTestRun(l: Pick<StickyLaunch, "picks" | "script" | "scriptHours" | "rows" | "release"> & {
153
+ devBuyBnb?: number;
154
+ seed?: number;
155
+ trades?: number;
156
+ start?: number;
157
+ }): StickyTestRun;
158
+ /**
159
+ * The sticky module. `v2` points it at the sticky 2 factory (same coins, same rules, same calls; its coins also count
160
+ * the creator's sold coins and can carry switch rows, see ph.sticky2). `3` points it at sticky 3: the same again, plus a
161
+ * mode per coin (launch with `mode` and `modeSettings`; the game itself is ph.modes); a sticky 3 coin leaves its lot
162
+ * views to the lens.
163
+ */
164
+ export declare function stickyModule(ctx: Ctx, version?: boolean | 3): {
108
165
  factory: `0x${string}`;
109
166
  terms: () => Promise<StickyTerms>;
110
167
  validate: (rows: bigint[], release: number, custom?: boolean) => Promise<string | null>;
168
+ validateSwitch: (rows: bigint[], mask: number, validator?: Address) => Promise<string | null>;
169
+ validateMode: (mode: ModeName, settings: ModeSettings | undefined, rows: bigint[], validator?: Address, creator?: Address) => Promise<string | null>;
111
170
  compile: typeof compile;
171
+ table: typeof stickyTable;
172
+ testRun: typeof stickyTestRun;
112
173
  describeRows: typeof describeRows;
113
174
  prepareMetadata: (m: {
114
175
  name: string;
@@ -136,6 +197,7 @@ export declare function stickyModule(ctx: Ctx): {
136
197
  balance: bigint;
137
198
  sellable: bigint;
138
199
  lots: StickyLot[];
200
+ held?: bigint;
139
201
  }>;
140
202
  check: (c: {
141
203
  token: Address;
@@ -151,8 +213,12 @@ export declare function stickyModule(ctx: Ctx): {
151
213
  wallet?: Address;
152
214
  }) => Promise<{
153
215
  amountOut: bigint;
216
+ venue: Venue;
217
+ v3: bigint | null;
218
+ v2: bigint | null;
154
219
  }>;
155
220
  estimateBuy: (token: Address, bnbIn: bigint) => Promise<bigint>;
221
+ buyTakeBps: (token: Address) => Promise<number>;
156
222
  sellApproval: (owner: Address, token: Address, amount: bigint) => Promise<TxRequest | null>;
157
223
  build: {
158
224
  launch: (l: {
@@ -164,6 +230,10 @@ export declare function stickyModule(ctx: Ctx): {
164
230
  salt?: Hex;
165
231
  keepBps?: number;
166
232
  minCreatorBps: number;
233
+ switcher?: Address;
234
+ switchMask?: number;
235
+ mode?: ModeName;
236
+ modeSettings?: ModeSettings;
167
237
  }, value: bigint) => TxRequest;
168
238
  buy: (b: {
169
239
  token: Address;
@@ -180,6 +250,7 @@ export declare function stickyModule(ctx: Ctx): {
180
250
  deadline?: bigint;
181
251
  }) => TxRequest;
182
252
  lift: (token: Address, row: number) => TxRequest;
253
+ collect: (token: Address) => Promise<TxRequest>;
183
254
  };
184
255
  launch: (l: StickyLaunch, s?: SendOptions) => Promise<Sent & {
185
256
  token: Address;
@@ -194,10 +265,12 @@ export declare function stickyModule(ctx: Ctx): {
194
265
  slippageBps?: number;
195
266
  minOut?: bigint;
196
267
  deadline?: bigint;
268
+ venue?: Venue;
197
269
  }, s?: SendOptions) => Promise<Sent & {
198
270
  quoted: bigint;
199
271
  minOut: bigint;
200
272
  amountOut: bigint;
273
+ venue: Venue;
201
274
  }>;
202
275
  sell: (x: {
203
276
  token: Address;
@@ -205,11 +278,27 @@ export declare function stickyModule(ctx: Ctx): {
205
278
  slippageBps?: number;
206
279
  minOut?: bigint;
207
280
  deadline?: bigint;
281
+ venue?: Venue;
208
282
  }, s?: SendOptions) => Promise<Sent & {
209
283
  quoted: bigint;
210
284
  minOut: bigint;
285
+ venue: Venue;
211
286
  }>;
212
287
  lift: (token: Address, row: number, s?: SendOptions) => Promise<Sent>;
288
+ fees: (token: Address) => Promise<{
289
+ bnb: bigint;
290
+ burned: bigint;
291
+ toCreator: bigint;
292
+ creator: Address;
293
+ creatorBps: number;
294
+ keepBps: number;
295
+ pending: bigint;
296
+ }>;
297
+ collect: (token: Address, s?: SendOptions) => Promise<Sent & {
298
+ bnb: bigint;
299
+ toCreator: bigint;
300
+ burned: bigint;
301
+ }>;
213
302
  /** the sender's address, for callers that check before they sign */
214
303
  from: (s?: SendOptions) => Promise<`0x${string}` | undefined>;
215
304
  };
package/dist/sticky.js CHANGED
@@ -1,11 +1,30 @@
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";
4
+ import { sticky3FactoryAbi, sticky3LensAbi, sticky3TokenAbi, sticky3ValidatorAbi } from "./abis/sticky3.js";
5
+ import { MODE_NAMES } from "./addresses.js";
6
+ import { encodeModeData } from "./modes-catalog.js";
3
7
  import { v3PoolAbi } from "./abis/common.js";
8
+ import { v2LockerAbi } from "./abis/v2.js";
4
9
  import { ZERO_ADDRESS } from "./addresses.js";
5
10
  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";
11
+ import { ACT, LOCKED, NONE, ON, compile, decodeRow, defaults, editToRow, rowProblems, rowText, scannerOf, TEMPLATE_BY_ID, templateOf } from "./sticky-rules.js";
12
+ import { simulate } from "./sticky-sim.js";
13
+ import { chainDeadline, minusBps, sendTx, senderAddress, signer } from "./tx.js";
14
+ import { bestVenue, buildV2Buy, buildV2Sell, v2Quote } from "./v2.js";
8
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
+ ];
9
28
  const randomSalt = () => {
10
29
  const b = new Uint8Array(32);
11
30
  globalThis.crypto.getRandomValues(b);
@@ -30,15 +49,85 @@ export function describeRows(raw, release, off) {
30
49
  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
50
  });
32
51
  }
33
- export function stickyModule(ctx) {
52
+ /**
53
+ * The table a launch would send: the templates' rows, then the hook script rows (the site's order), one release time per
54
+ * coin (the longer one wins); or rows + release as given. Offline; throws the problems as sentences.
55
+ */
56
+ export function stickyTable(l) {
57
+ if (!l.picks && !l.script?.length)
58
+ return { rows: l.rows ?? [], release: l.release ?? 0 };
59
+ // a template's numbers left out take its defaults
60
+ const picks = (l.picks ?? []).map((p) => {
61
+ const t = TEMPLATE_BY_ID.get(p.id);
62
+ if (!t)
63
+ throw new PickhooksError(`There is no template "${p.id}". Templates: ${[...TEMPLATE_BY_ID.keys()].join(", ")}.`, { code: "input" });
64
+ return { id: p.id, params: { ...defaults(t), ...p.params } };
65
+ });
66
+ const own = (l.script ?? []).map(editToRow);
67
+ const c = compile(picks, own);
68
+ const problems = [...c.problems, ...own.flatMap(rowProblems)];
69
+ const hours = l.scriptHours ?? 0;
70
+ const locks = own.some((r) => r.act === ACT.LOCK);
71
+ if (locks && !(hours * 3600 >= 600 && hours <= 720))
72
+ problems.push("Locked shares: unlock over 10 minutes to 30 days.");
73
+ if (problems.length)
74
+ throw new PickhooksError(problems.join(" "), { code: "input" });
75
+ return { rows: c.rows, release: locks ? Math.max(c.release, Math.round(hours * 3600)) : c.release };
76
+ }
77
+ 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`;
78
+ /** A test run as sentences: the launcher's card line by line. `names` are the rows' names (rule i + 1). */
79
+ export function testRunText(r, names) {
80
+ const rule = (row) => (row === LOCKED ? "still locked" : `rule ${row + 1}, ${names[row] ?? ""}`);
81
+ const pct = (x) => +x.toFixed(2);
82
+ const out = [
83
+ `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.`,
84
+ `Refused: ${r.refusals.length ? r.refusals.map((x) => `${rule(x.row)} ${x.count}x`).join(", ") : "nothing"}.`,
85
+ `Longest wait to sell: ${r.longestWait ? `${dur(r.longestWait)}${r.stillWaiting ? `, ${r.stillWaiting} wallets still waiting at the end` : ""}` : "nobody had to wait"}.`,
86
+ `Snipers: ${r.snipers.firstSellAfter !== null ? `first sold ${dur(r.snipers.firstSellAfter)} after launch` : "could not sell during the run"}.`,
87
+ `Split bags: ${r.splits.count ? `${r.splits.count} sent to fresh wallets, refused there again ${r.splits.refusedAfter} times` : "none"}.`,
88
+ `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"}.`,
89
+ ];
90
+ if (r.burnedPct > 0)
91
+ out.push(`Burned: ${pct(r.burnedPct)} % of the supply.`);
92
+ if (r.pot.inPct > 0)
93
+ 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.`);
94
+ out.push(`A scanner's test: ${r.scanner.buy !== null
95
+ ? `its test buy is refused by ${rule(r.scanner.buy)}`
96
+ : r.scanner.sell !== null
97
+ ? `its sell right after the buy is refused by ${rule(r.scanner.sell)}: it can show a honeypot`
98
+ : "a small buy and an instant sell pass"}.`);
99
+ return out;
100
+ }
101
+ /**
102
+ * The launcher's test run, offline: the table on a fresh coin with random trades (the creator's first buy, three snipers
103
+ * in the first blocks, wallets that buy, hold and sell, bags split to fresh wallets). It runs the coin's own rule engine,
104
+ * checked against the contract trade by trade. The same seed gives the same run; the chain stays the truth.
105
+ */
106
+ export function stickyTestRun(l) {
107
+ const { rows, release } = stickyTable(l);
108
+ const report = simulate(rows, release, { seed: l.seed ?? 7, trades: l.trades ?? 200, devBuyBnb: l.devBuyBnb ?? 0, start: l.start });
109
+ const names = describeRows(rows, release, 0).map((x) => x.name);
110
+ return { rows, release, report, text: testRunText(report, names) };
111
+ }
112
+ /**
113
+ * The sticky module. `v2` points it at the sticky 2 factory (same coins, same rules, same calls; its coins also count
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.
117
+ */
118
+ export function stickyModule(ctx, version = false) {
34
119
  const { pub, a } = ctx;
35
- const FA = stickyFactoryAbi;
36
- const TA = 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");
37
126
  const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
38
127
  /** The factory's terms now: the seed every launch pays into the V2 pair, the creator side, whether own rows are open. */
39
128
  async function terms() {
40
129
  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 });
130
+ const r = await pub.multicall({ contracts: fns.map((functionName) => ({ address: F, abi: FA, functionName })), allowFailure: false });
42
131
  return { seed: r[0], creatorBps: Number(r[1]), custom: r[2], validator: r[3], startFdv: r[4], platformWallet: r[5], locker: r[6] };
43
132
  }
44
133
  /** Asks the validator on chain whether a table can launch: null when it can, else the reason. */
@@ -52,6 +141,19 @@ export function stickyModule(ctx) {
52
141
  return explain(e, "The validator refused this table.").text;
53
142
  }
54
143
  }
144
+ const maskOf = (rowsOn) => (rowsOn ?? []).reduce((m, i) => m | (1 << i), 0);
145
+ /** sticky 2: asks the validator whether these rows can switch (drop rows only, 2 to 4): null when they can, else why not */
146
+ async function validateSwitch(rows, mask, validator) {
147
+ if (!v2)
148
+ return "Switch rows are a sticky 2 feature.";
149
+ try {
150
+ await call(validator ?? (await terms()).validator, (v3 ? sticky3ValidatorAbi : sticky2ValidatorAbi), "switchRows", [rows, mask]);
151
+ return null;
152
+ }
153
+ catch (e) {
154
+ return explain(e, "The validator refused these switch rows.").text;
155
+ }
156
+ }
55
157
  /**
56
158
  * Stores the image and metadata on pickhooks.com and returns the metadata link (the site's /api/sticky/prepare, rate
57
159
  * limited, only while sticky rules are live on the site). A launchpad can host its own JSON and pass that link instead.
@@ -70,35 +172,65 @@ export function stickyModule(ctx) {
70
172
  /** The launch call. value = the factory's seed + the creator's first buy. */
71
173
  function buildLaunch(l, value) {
72
174
  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" };
175
+ if (!v2 && l.switchMask)
176
+ throw new PickhooksError("Switch rows are a sticky 2 feature.", { code: "input" });
177
+ // sticky 2 takes a switcher and its switch rows too: none from a plain launch
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;
185
+ return { to: F, value, data: encodeFunctionData({ abi: FA, functionName: "launch", args: [full] }), label: "The launch" };
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
+ }
74
209
  }
75
210
  /** Builds the table from templates and hook script rows (or takes rows), checks it locally and on chain, launches, returns the coin. */
76
211
  async function launch(l, s = {}) {
77
- let rows = l.rows ?? [];
78
- let release = l.release ?? 0;
79
212
  const t = await terms();
80
213
  if (l.script?.length && !t.custom)
81
214
  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
- }
215
+ const { rows, release } = stickyTable(l);
96
216
  const refused = await validate(rows, release, t.custom);
97
217
  if (refused)
98
218
  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));
219
+ const switchMask = maskOf(l.switchRows);
220
+ if (switchMask) {
221
+ const no = await validateSwitch(rows, switchMask, t.validator);
222
+ if (no)
223
+ throw new PickhooksError(no, { code: "Bad" });
224
+ }
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));
100
231
  const sent = await sendTx(ctx, tx, s);
101
- const ev = parseEventLogs({ abi: FA, logs: sent.receipt.logs, eventName: "Launched" })[0];
232
+ // both factories emit the same Launched event
233
+ const ev = parseEventLogs({ abi: stickyFactoryAbi, logs: sent.receipt.logs.filter((x) => x.address.toLowerCase() === F.toLowerCase()), eventName: "Launched" })[0];
102
234
  if (!ev)
103
235
  throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
104
236
  return { ...sent, token: ev.args.token, pool: ev.args.pool, pair: ev.args.pair, rows, release };
@@ -114,6 +246,16 @@ export function stickyModule(ctx) {
114
246
  contracts: pools.map((p) => ({ address: (p ?? a.wbnb), abi: v3PoolAbi, functionName: "slot0" })),
115
247
  allowFailure: true,
116
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
+ : [];
117
259
  const out = [];
118
260
  tokens.forEach((token, i) => {
119
261
  const g = (k) => res[i * per + k];
@@ -145,12 +287,22 @@ export function stickyModule(ctx) {
145
287
  totalSupply: supply,
146
288
  priceBnb,
147
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
+ : {}),
148
300
  });
149
301
  });
150
302
  return out;
151
303
  }
152
304
  /** How many coins the factory launched. */
153
- const count = async () => Number(await call(a.stickyFactory, FA, "coinCount"));
305
+ const count = async () => Number(await call(F, FA, "coinCount"));
154
306
  /** Coins newest first: `limit` of them, skipping the newest `offset`. */
155
307
  async function coins(o = {}) {
156
308
  const n = await count();
@@ -160,12 +312,12 @@ export function stickyModule(ctx) {
160
312
  return [];
161
313
  const idx = Array.from({ length: end - from }, (_, i) => BigInt(end - 1 - i));
162
314
  const tokens = (await pub.multicall({
163
- contracts: idx.map((i) => ({ address: a.stickyFactory, abi: FA, functionName: "coins", args: [i] })),
315
+ contracts: idx.map((i) => ({ address: F, abi: FA, functionName: "coins", args: [i] })),
164
316
  allowFailure: false,
165
317
  }));
166
318
  return summaries(tokens);
167
319
  }
168
- const isCoin = async (token) => (await call(a.stickyFactory, FA, "isCoin", [token]).catch(() => false)) === true;
320
+ const isCoin = async (token) => (await call(F, FA, "isCoin", [token]).catch(() => false)) === true;
169
321
  /** One sticky coin, or null when the address is not one of the factory's coins. */
170
322
  async function coin(token) {
171
323
  if (!(await isCoin(token)))
@@ -174,6 +326,20 @@ export function stickyModule(ctx) {
174
326
  }
175
327
  /** A wallet's lots (amount, locked part, since, marks), its balance and what it can sell now. */
176
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
+ }
177
343
  const r = await pub.multicall({
178
344
  contracts: [
179
345
  { address: token, abi: TA, functionName: "balanceOf", args: [who] },
@@ -218,11 +384,17 @@ export function stickyModule(ctx) {
218
384
  * does not pass the reason on: a refused buy is checked again with an estimate of its coins and the refusal names the
219
385
  * rule (for `wallet`, by default the quoter the quote ran as).
220
386
  */
387
+ /**
388
+ * 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;
389
+ * 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).
390
+ * Terminals pick the pool by themselves; the SDK does the same here. The venue field says which one buy/sell will use.
391
+ */
221
392
  async function quote(q) {
222
- try {
223
- return { amountOut: await v3Quote(ctx, q) };
224
- }
225
- catch (e) {
393
+ const best = await bestVenue(ctx, { v3: () => v3Quote(ctx, q), v2: () => v2Quote(ctx, q) });
394
+ if (best.v3 !== null || best.v2 !== null)
395
+ return { amountOut: best.amountOut, venue: best.venue, v3: best.v3, v2: best.v2 };
396
+ const e = best.errors.v3 ?? best.errors.v2;
397
+ {
226
398
  if (q.side === "buy") {
227
399
  const est = await estimateBuy(q.token, q.amount).catch(() => 0n);
228
400
  const c = est > 0n ? await check({ token: q.token, side: "buy", wallet: q.wallet ?? a.v3Quoter, amount: est }).catch(() => null) : null;
@@ -242,34 +414,97 @@ export function stickyModule(ctx) {
242
414
  if (!c.ok)
243
415
  throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row] });
244
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
+ }
245
436
  /** Quotes, checks the coin's rules for this wallet, and buys. A refusing rule stops it before anything is signed. */
246
437
  async function buy(b, s = {}) {
247
438
  const { address } = await signer(ctx, s.wallet);
248
439
  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) };
440
+ const venue = b.venue ?? q.venue;
441
+ const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
442
+ refuse(await check({ token: b.token, side: "buy", wallet: address, amount: quoted }));
443
+ const arrives = venue === "v2" ? minusBps(quoted, await buyTakeBps(b.token)) : quoted;
444
+ const minOut = b.minOut ?? minusBps(arrives, b.slippageBps ?? 100);
445
+ const deadline = b.deadline ?? (await chainDeadline(ctx));
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 });
447
+ const sent = await sendTx(ctx, tx, s);
448
+ return { ...sent, quoted, minOut, amountOut: receivedOf(sent.receipt, b.token, address), venue };
253
449
  }
254
450
  /** Checks the rules, approves the router if needed, quotes and sells for BNB. */
255
451
  async function sell(x, s = {}) {
256
452
  const { address } = await signer(ctx, s.wallet);
257
453
  refuse(await check({ token: x.token, side: "sell", wallet: address, amount: x.amount }));
258
- const ap = await sellApproval(address, x.token, x.amount);
454
+ const q = await quote({ token: x.token, side: "sell", amount: x.amount });
455
+ const venue = x.venue ?? q.venue;
456
+ const quoted = (venue === "v2" ? q.v2 : q.v3) ?? q.amountOut;
457
+ const ap = await approvalFor(ctx, address, x.token, x.amount, venue === "v2" ? a.v2Router : a.v3Router, "Allowing PancakeSwap");
259
458
  if (ap)
260
459
  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 };
460
+ const minOut = x.minOut ?? minusBps(quoted, x.slippageBps ?? 100);
461
+ const deadline = x.deadline ?? (await chainDeadline(ctx));
462
+ 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 });
463
+ const sent = await sendTx(ctx, tx, s);
464
+ return { ...sent, quoted, minOut, venue };
265
465
  }
266
466
  /** The creator lifts a row for good (a drop row cannot be lifted). */
267
467
  const buildLift = (token, row) => ({ to: token, value: 0n, data: encodeFunctionData({ abi: TA, functionName: "lift", args: [row] }), label: `Lifting rule ${row + 1}` });
468
+ const lockerOf = (token) => call(token, TA, "locker");
469
+ /** Anyone: collects the coin's trading fees from its locked position; the locker pays the creator's side to the creator. */
470
+ const buildCollect = async (token) => ({
471
+ to: await lockerOf(token),
472
+ value: 0n,
473
+ data: encodeFunctionData({ abi: v2LockerAbi, functionName: "collect", args: [token] }),
474
+ label: "The collect",
475
+ });
476
+ /**
477
+ * What collect() would book now (simulated): the BNB fees of the coin's position and the coins it burns, what of it goes
478
+ * to the creator (creatorBps of the fees is the creator side, keepBps of that the creator keeps, the rest buys back and
479
+ * burns $hooks), and BNB the locker holds for the creator because a payout bounced (withdrawPending() takes it).
480
+ */
481
+ async function fees(token) {
482
+ const locker = await lockerOf(token);
483
+ const pos = await call(locker, v2LockerAbi, "positions", [token]);
484
+ const [sim, pending] = await Promise.all([
485
+ pub.simulateContract({ address: locker, abi: v2LockerAbi, functionName: "collect", args: [token] }).then((r) => r.result),
486
+ call(locker, v2LockerAbi, "pending", [pos[2]]),
487
+ ]);
488
+ // the locker's own split
489
+ const side = (sim[0] * BigInt(pos[3])) / 10000n;
490
+ const toCreator = ((side - (side * BigInt(pos[4])) / 10000n) * BigInt(pos[5])) / 10000n;
491
+ return { bnb: sim[0], burned: sim[1], toCreator, creator: pos[2], creatorBps: Number(pos[3]), keepBps: Number(pos[5]), pending };
492
+ }
493
+ /** Collects the coin's fees; returns what the locker booked (its Collected event). */
494
+ async function collect(token, s = {}) {
495
+ const sent = await sendTx(ctx, await buildCollect(token), s);
496
+ const ev = parseEventLogs({ abi: v2LockerAbi, logs: sent.receipt.logs, eventName: "Collected" })[0];
497
+ return { ...sent, bnb: ev?.args.bnb ?? 0n, toCreator: ev?.args.toCreator ?? 0n, burned: ev?.args.burned ?? 0n };
498
+ }
268
499
  return {
269
- factory: a.stickyFactory,
500
+ factory: F,
270
501
  terms,
271
502
  validate,
503
+ validateSwitch,
504
+ validateMode,
272
505
  compile,
506
+ table: stickyTable,
507
+ testRun: stickyTestRun,
273
508
  describeRows,
274
509
  prepareMetadata,
275
510
  count,
@@ -280,12 +515,15 @@ export function stickyModule(ctx) {
280
515
  check,
281
516
  quote,
282
517
  estimateBuy,
518
+ buyTakeBps,
283
519
  sellApproval,
284
- build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift },
520
+ build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift, collect: buildCollect },
285
521
  launch,
286
522
  buy,
287
523
  sell,
288
524
  lift: (token, row, s = {}) => sendTx(ctx, buildLift(token, row), s),
525
+ fees,
526
+ collect,
289
527
  /** the sender's address, for callers that check before they sign */
290
528
  from: (s = {}) => senderAddress(ctx, s.wallet),
291
529
  };