@pickhooks/sdk 0.0.0-stage → 0.2.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -3
  3. package/dist/abis/any3.d.ts +2123 -0
  4. package/dist/abis/any3.js +3 -0
  5. package/dist/abis/anytax.d.ts +1786 -0
  6. package/dist/abis/anytax.js +3 -0
  7. package/dist/abis/calls.d.ts +2515 -0
  8. package/dist/abis/calls.js +4 -0
  9. package/dist/abis/common.d.ts +449 -0
  10. package/dist/abis/common.js +91 -0
  11. package/dist/abis/dips.d.ts +897 -0
  12. package/dist/abis/dips.js +2 -0
  13. package/dist/abis/index.d.ts +12 -0
  14. package/dist/abis/index.js +12 -0
  15. package/dist/abis/infra.d.ts +701 -0
  16. package/dist/abis/infra.js +4 -0
  17. package/dist/abis/lever.d.ts +1710 -0
  18. package/dist/abis/lever.js +5 -0
  19. package/dist/abis/lever2.d.ts +2710 -0
  20. package/dist/abis/lever2.js +5 -0
  21. package/dist/abis/pools.d.ts +1898 -0
  22. package/dist/abis/pools.js +4 -0
  23. package/dist/abis/sticky.d.ts +953 -0
  24. package/dist/abis/sticky.js +4 -0
  25. package/dist/abis/v1.d.ts +1627 -0
  26. package/dist/abis/v1.js +5 -0
  27. package/dist/abis/v2.d.ts +3264 -0
  28. package/dist/abis/v2.js +8 -0
  29. package/dist/addresses.d.ts +87 -0
  30. package/dist/addresses.js +62 -0
  31. package/dist/any.d.ts +447 -0
  32. package/dist/any.js +729 -0
  33. package/dist/api.d.ts +62 -0
  34. package/dist/api.js +30 -0
  35. package/dist/blocks.d.ts +107 -0
  36. package/dist/blocks.js +126 -0
  37. package/dist/coin-rules-config.d.ts +8 -0
  38. package/dist/coin-rules-config.js +13 -0
  39. package/dist/coin-rules.d.ts +383 -0
  40. package/dist/coin-rules.js +878 -0
  41. package/dist/coins.d.ts +217 -0
  42. package/dist/coins.js +520 -0
  43. package/dist/dips.d.ts +133 -0
  44. package/dist/dips.js +148 -0
  45. package/dist/errors.d.ts +35 -0
  46. package/dist/errors.js +347 -0
  47. package/dist/index.d.ts +73 -0
  48. package/dist/index.js +58 -0
  49. package/dist/infinity.d.ts +63 -0
  50. package/dist/infinity.js +96 -0
  51. package/dist/infra.d.ts +112 -0
  52. package/dist/infra.js +114 -0
  53. package/dist/lever.d.ts +391 -0
  54. package/dist/lever.js +419 -0
  55. package/dist/pools.d.ts +274 -0
  56. package/dist/pools.js +405 -0
  57. package/dist/sticky-rules.d.ts +179 -0
  58. package/dist/sticky-rules.js +580 -0
  59. package/dist/sticky.d.ts +216 -0
  60. package/dist/sticky.js +292 -0
  61. package/dist/tx.d.ts +70 -0
  62. package/dist/tx.js +69 -0
  63. package/dist/v3.d.ts +42 -0
  64. package/dist/v3.js +98 -0
  65. package/package.json +58 -4
@@ -0,0 +1,216 @@
1
+ import { type Address, type Hex } from "viem";
2
+ import { type EditRow, type Pick, compile } from "./sticky-rules.js";
3
+ import { type Ctx, type SendOptions, type Sent, type TxRequest } from "./tx.js";
4
+ /**
5
+ * 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
+ * send moves lots along, so a fresh wallet takes the rules with it. The creator picks up to eight rows from templates; the
7
+ * validator checks the table at launch and the coin checks every transfer. Sticky coins trade in a PancakeSwap V3 pool
8
+ * of 1 % (plus a V2 pair), so any router can trade them; check() names the row that would stop a trade before it is sent.
9
+ */
10
+ export type StickyRow = {
11
+ /** 0 based; the site and errors say "rule i + 1" */
12
+ i: number;
13
+ /** the template's name, "Pot", or "Custom row" */
14
+ name: string;
15
+ /** the row as a sentence */
16
+ text: string;
17
+ template: string | null;
18
+ /** true when the rule moves with the coins */
19
+ sticky: boolean;
20
+ lock: boolean;
21
+ drop: boolean;
22
+ /** the creator lifted it for good */
23
+ lifted: boolean;
24
+ /** the raw row (uint256) */
25
+ raw: bigint;
26
+ };
27
+ export type StickyCoin = {
28
+ token: Address;
29
+ name: string;
30
+ symbol: string;
31
+ /** the metadata link (JSON with image, description, links) */
32
+ metadata: string;
33
+ creator: Address;
34
+ /** the PancakeSwap V3 pool (1 %) and the V2 pair */
35
+ pool: Address;
36
+ pair: Address;
37
+ start: number;
38
+ /** seconds a LOCK row's coins take to unlock */
39
+ release: number;
40
+ rows: StickyRow[];
41
+ /** how scanners see the table: green nothing, yellow a warning, red a refused test sell at times */
42
+ scanner: "green" | "yellow" | "red";
43
+ holders: number;
44
+ buys: number;
45
+ sells: number;
46
+ lastBuy: number;
47
+ /** BNB volume (wei) */
48
+ volume: bigint;
49
+ /** coins in the pot (POT rows fill it, DROP rows pay it out) */
50
+ pot: bigint;
51
+ totalSupply: bigint;
52
+ /** BNB per coin at the V3 pool's price, and the market cap in BNB */
53
+ priceBnb: number;
54
+ mcapBnb: number;
55
+ };
56
+ export type StickyLot = {
57
+ amount: bigint;
58
+ locked: bigint;
59
+ since: number;
60
+ flags: number;
61
+ open: boolean;
62
+ };
63
+ export type StickyCheck = {
64
+ ok: true;
65
+ } | {
66
+ ok: false;
67
+ /** the row that stops it (0 based), or "locked": no row stops it but the coins are not unlocked yet */
68
+ row: number | "locked";
69
+ /** "Refused by rule 2: Sniper jail." and the row's sentence */
70
+ text: string;
71
+ rule: StickyRow | null;
72
+ };
73
+ export type StickyLaunch = {
74
+ name: string;
75
+ symbol: string;
76
+ /** a link to the coin's metadata JSON (name, symbol, description, image, website, twitter, telegram) */
77
+ metadata: string;
78
+ /** templates and their numbers; or rows + release for a table built elsewhere */
79
+ picks?: Pick[];
80
+ /**
81
+ * hook script: the creator's own rows, after the templates (the factory's custom switch must be on). Built like the
82
+ * site's editor: emptyEditRow(), VARS for the variables and their units, editToRow() for the row on chain.
83
+ */
84
+ script?: EditRow[];
85
+ /** hook script rows that lock shares: hours until they unlock (10 minutes to 30 days); the longer release wins */
86
+ scriptHours?: number;
87
+ rows?: bigint[];
88
+ release?: number;
89
+ /** the creator's first buy (BNB wei); the DEV mark and the dev lock apply to it */
90
+ devBuy?: bigint;
91
+ /** share of the creator side the creator keeps (default 10000); the rest buys back and burns $hooks */
92
+ keepBps?: number;
93
+ /** a fixed salt (bytes32); default random */
94
+ salt?: Hex;
95
+ };
96
+ export type StickyTerms = {
97
+ seed: bigint;
98
+ creatorBps: number;
99
+ custom: boolean;
100
+ validator: Address;
101
+ startFdv: bigint;
102
+ platformWallet: Address;
103
+ locker: Address;
104
+ };
105
+ /** The rows of a coin as sentences, with the template each belongs to. */
106
+ export declare function describeRows(raw: readonly bigint[], release: number, off: number): StickyRow[];
107
+ export declare function stickyModule(ctx: Ctx): {
108
+ factory: `0x${string}`;
109
+ terms: () => Promise<StickyTerms>;
110
+ validate: (rows: bigint[], release: number, custom?: boolean) => Promise<string | null>;
111
+ compile: typeof compile;
112
+ describeRows: typeof describeRows;
113
+ prepareMetadata: (m: {
114
+ name: string;
115
+ symbol: string;
116
+ description?: string;
117
+ website?: string;
118
+ twitter?: string;
119
+ telegram?: string;
120
+ image: {
121
+ mime: "image/png" | "image/jpeg" | "image/webp" | "image/gif";
122
+ bytes: Uint8Array;
123
+ };
124
+ }) => Promise<{
125
+ metadata: string;
126
+ image: string;
127
+ }>;
128
+ count: () => Promise<number>;
129
+ coins: (o?: {
130
+ limit?: number;
131
+ offset?: number;
132
+ }) => Promise<StickyCoin[]>;
133
+ coin: (token: Address) => Promise<StickyCoin | null>;
134
+ isCoin: (token: Address) => Promise<boolean>;
135
+ wallet: (token: Address, who: Address) => Promise<{
136
+ balance: bigint;
137
+ sellable: bigint;
138
+ lots: StickyLot[];
139
+ }>;
140
+ check: (c: {
141
+ token: Address;
142
+ side: "buy" | "sell" | "send";
143
+ wallet: Address;
144
+ amount: bigint;
145
+ to?: Address;
146
+ }) => Promise<StickyCheck>;
147
+ quote: (q: {
148
+ token: Address;
149
+ side: "buy" | "sell";
150
+ amount: bigint;
151
+ wallet?: Address;
152
+ }) => Promise<{
153
+ amountOut: bigint;
154
+ }>;
155
+ estimateBuy: (token: Address, bnbIn: bigint) => Promise<bigint>;
156
+ sellApproval: (owner: Address, token: Address, amount: bigint) => Promise<TxRequest | null>;
157
+ build: {
158
+ launch: (l: {
159
+ name: string;
160
+ symbol: string;
161
+ metadata: string;
162
+ rows: bigint[];
163
+ release: number;
164
+ salt?: Hex;
165
+ keepBps?: number;
166
+ minCreatorBps: number;
167
+ }, value: bigint) => TxRequest;
168
+ buy: (b: {
169
+ token: Address;
170
+ bnbIn: bigint;
171
+ minOut: bigint;
172
+ recipient: Address;
173
+ deadline?: bigint;
174
+ }) => TxRequest;
175
+ sell: (x: {
176
+ token: Address;
177
+ amount: bigint;
178
+ minOut: bigint;
179
+ recipient: Address;
180
+ deadline?: bigint;
181
+ }) => TxRequest;
182
+ lift: (token: Address, row: number) => TxRequest;
183
+ };
184
+ launch: (l: StickyLaunch, s?: SendOptions) => Promise<Sent & {
185
+ token: Address;
186
+ pool: Address;
187
+ pair: Address;
188
+ rows: bigint[];
189
+ release: number;
190
+ }>;
191
+ buy: (b: {
192
+ token: Address;
193
+ bnbIn: bigint;
194
+ slippageBps?: number;
195
+ minOut?: bigint;
196
+ deadline?: bigint;
197
+ }, s?: SendOptions) => Promise<Sent & {
198
+ quoted: bigint;
199
+ minOut: bigint;
200
+ amountOut: bigint;
201
+ }>;
202
+ sell: (x: {
203
+ token: Address;
204
+ amount: bigint;
205
+ slippageBps?: number;
206
+ minOut?: bigint;
207
+ deadline?: bigint;
208
+ }, s?: SendOptions) => Promise<Sent & {
209
+ quoted: bigint;
210
+ minOut: bigint;
211
+ }>;
212
+ lift: (token: Address, row: number, s?: SendOptions) => Promise<Sent>;
213
+ /** the sender's address, for callers that check before they sign */
214
+ from: (s?: SendOptions) => Promise<`0x${string}` | undefined>;
215
+ };
216
+ export type StickyModule = ReturnType<typeof stickyModule>;
package/dist/sticky.js ADDED
@@ -0,0 +1,292 @@
1
+ import { encodeFunctionData, parseEventLogs } from "viem";
2
+ import { stickyFactoryAbi, stickyTokenAbi, stickyValidatorAbi } from "./abis/sticky.js";
3
+ import { v3PoolAbi } from "./abis/common.js";
4
+ import { ZERO_ADDRESS } from "./addresses.js";
5
+ 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 { approvalFor, buildV3Buy, buildV3Sell, receivedOf, v3EstimateBuy, v3PriceBnb, v3Quote } from "./v3.js";
9
+ const randomSalt = () => {
10
+ const b = new Uint8Array(32);
11
+ globalThis.crypto.getRandomValues(b);
12
+ return `0x${Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("")}`;
13
+ };
14
+ const toBase64 = (bytes) => {
15
+ const B = globalThis.Buffer;
16
+ if (B)
17
+ return B.from(bytes).toString("base64");
18
+ let s = "";
19
+ for (const x of bytes)
20
+ s += String.fromCharCode(x);
21
+ return btoa(s);
22
+ };
23
+ /** The rows of a coin as sentences, with the template each belongs to. */
24
+ export function describeRows(raw, release, off) {
25
+ return raw.map((x, i) => {
26
+ const r = decodeRow(x);
27
+ const id = templateOf(r);
28
+ const t = id ? TEMPLATE_BY_ID.get(id) : null;
29
+ const name = r.act === ACT.POT ? "Pot" : (t?.name ?? "Custom row");
30
+ 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
+ });
32
+ }
33
+ export function stickyModule(ctx) {
34
+ const { pub, a } = ctx;
35
+ const FA = stickyFactoryAbi;
36
+ const TA = stickyTokenAbi;
37
+ const call = (address, abi, functionName, args = []) => pub.readContract({ address, abi, functionName, args });
38
+ /** The factory's terms now: the seed every launch pays into the V2 pair, the creator side, whether own rows are open. */
39
+ async function terms() {
40
+ 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 });
42
+ return { seed: r[0], creatorBps: Number(r[1]), custom: r[2], validator: r[3], startFdv: r[4], platformWallet: r[5], locker: r[6] };
43
+ }
44
+ /** Asks the validator on chain whether a table can launch: null when it can, else the reason. */
45
+ async function validate(rows, release, custom) {
46
+ const t = custom === undefined ? await terms() : null;
47
+ try {
48
+ await call(t?.validator ?? a.stickyValidator, stickyValidatorAbi, "validate", [rows, release, custom ?? t.custom]);
49
+ return null;
50
+ }
51
+ catch (e) {
52
+ return explain(e, "The validator refused this table.").text;
53
+ }
54
+ }
55
+ /**
56
+ * Stores the image and metadata on pickhooks.com and returns the metadata link (the site's /api/sticky/prepare, rate
57
+ * limited, only while sticky rules are live on the site). A launchpad can host its own JSON and pass that link instead.
58
+ */
59
+ async function prepareMetadata(m) {
60
+ const res = await fetch(`${ctx.site}/api/sticky/prepare`, {
61
+ method: "POST",
62
+ headers: { "content-type": "application/json" },
63
+ body: JSON.stringify({ ...m, image: { mime: m.image.mime, base64: toBase64(m.image.bytes) } }),
64
+ });
65
+ const j = (await res.json().catch(() => ({})));
66
+ if (!res.ok || !j.metadata)
67
+ throw new PickhooksError(j.error ?? `The site answered ${res.status}.`, { code: "site" });
68
+ return { metadata: j.metadata, image: j.image ?? "" };
69
+ }
70
+ /** The launch call. value = the factory's seed + the creator's first buy. */
71
+ function buildLaunch(l, value) {
72
+ 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" };
74
+ }
75
+ /** Builds the table from templates and hook script rows (or takes rows), checks it locally and on chain, launches, returns the coin. */
76
+ async function launch(l, s = {}) {
77
+ let rows = l.rows ?? [];
78
+ let release = l.release ?? 0;
79
+ const t = await terms();
80
+ if (l.script?.length && !t.custom)
81
+ 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
+ }
96
+ const refused = await validate(rows, release, t.custom);
97
+ if (refused)
98
+ 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));
100
+ const sent = await sendTx(ctx, tx, s);
101
+ const ev = parseEventLogs({ abi: FA, logs: sent.receipt.logs, eventName: "Launched" })[0];
102
+ if (!ev)
103
+ throw new PickhooksError("Launched, but the coin was not found in the receipt.", { code: "receipt", hash: sent.hash });
104
+ return { ...sent, token: ev.args.token, pool: ev.args.pool, pair: ev.args.pair, rows, release };
105
+ }
106
+ const FIELDS = ["name", "symbol", "metadata", "creator", "pool", "pool2", "start", "stats", "script", "pot", "totalSupply"];
107
+ async function summaries(tokens) {
108
+ if (!tokens.length)
109
+ return [];
110
+ const per = FIELDS.length;
111
+ const res = await pub.multicall({ contracts: tokens.flatMap((t) => FIELDS.map((functionName) => ({ address: t, abi: TA, functionName }))), allowFailure: true });
112
+ const pools = tokens.map((_, i) => res[i * per + 4].result);
113
+ const slots = await pub.multicall({
114
+ contracts: pools.map((p) => ({ address: (p ?? a.wbnb), abi: v3PoolAbi, functionName: "slot0" })),
115
+ allowFailure: true,
116
+ });
117
+ const out = [];
118
+ tokens.forEach((token, i) => {
119
+ const g = (k) => res[i * per + k];
120
+ if (g(0).status !== "success" || !pools[i] || g(8).status !== "success")
121
+ return;
122
+ const stats = g(7).result;
123
+ const [raw, release, off] = g(8).result;
124
+ const supply = g(10).result ?? 10n ** 27n;
125
+ const sl = slots[i];
126
+ const priceBnb = sl.status === "success" ? v3PriceBnb(sl.result[0], token, a.wbnb) : 0;
127
+ out.push({
128
+ token,
129
+ name: String(g(0).result ?? ""),
130
+ symbol: String(g(1).result ?? ""),
131
+ metadata: String(g(2).result ?? ""),
132
+ creator: g(3).result,
133
+ pool: pools[i],
134
+ pair: g(5).result ?? ZERO_ADDRESS,
135
+ start: Number(g(6).result ?? 0),
136
+ release: Number(release),
137
+ rows: describeRows(raw, Number(release), Number(off)),
138
+ scanner: scannerOf(raw.map(decodeRow)),
139
+ holders: Number(stats[0]),
140
+ buys: Number(stats[1]),
141
+ sells: Number(stats[2]),
142
+ lastBuy: Number(stats[4]),
143
+ volume: stats[5],
144
+ pot: g(9).result ?? 0n,
145
+ totalSupply: supply,
146
+ priceBnb,
147
+ mcapBnb: priceBnb * (Number(supply) / 1e18),
148
+ });
149
+ });
150
+ return out;
151
+ }
152
+ /** How many coins the factory launched. */
153
+ const count = async () => Number(await call(a.stickyFactory, FA, "coinCount"));
154
+ /** Coins newest first: `limit` of them, skipping the newest `offset`. */
155
+ async function coins(o = {}) {
156
+ const n = await count();
157
+ const end = Math.max(0, n - (o.offset ?? 0));
158
+ const from = Math.max(0, end - (o.limit ?? 50));
159
+ if (end <= from)
160
+ return [];
161
+ const idx = Array.from({ length: end - from }, (_, i) => BigInt(end - 1 - i));
162
+ const tokens = (await pub.multicall({
163
+ contracts: idx.map((i) => ({ address: a.stickyFactory, abi: FA, functionName: "coins", args: [i] })),
164
+ allowFailure: false,
165
+ }));
166
+ return summaries(tokens);
167
+ }
168
+ const isCoin = async (token) => (await call(a.stickyFactory, FA, "isCoin", [token]).catch(() => false)) === true;
169
+ /** One sticky coin, or null when the address is not one of the factory's coins. */
170
+ async function coin(token) {
171
+ if (!(await isCoin(token)))
172
+ return null;
173
+ return (await summaries([token]))[0] ?? null;
174
+ }
175
+ /** A wallet's lots (amount, locked part, since, marks), its balance and what it can sell now. */
176
+ async function wallet(token, who) {
177
+ const r = await pub.multicall({
178
+ contracts: [
179
+ { address: token, abi: TA, functionName: "balanceOf", args: [who] },
180
+ { address: token, abi: TA, functionName: "sellable", args: [who] },
181
+ { address: token, abi: TA, functionName: "lotsOf", args: [who] },
182
+ ],
183
+ allowFailure: false,
184
+ });
185
+ const [amount, locked, since, flags, open] = r[2];
186
+ return {
187
+ balance: r[0],
188
+ sellable: r[1],
189
+ lots: amount.map((x, i) => ({ amount: x, locked: locked[i], since: Number(since[i]), flags: Number(flags[i]), open: open[i] })),
190
+ };
191
+ }
192
+ /**
193
+ * Would this trade go through: the coin's own check(), the same code a transfer runs. For a buy pass the coins it would
194
+ * get (a quote is fine: the size rows look at a share of the supply). A send is checked from `wallet` to `to`.
195
+ */
196
+ async function check(c) {
197
+ const args = c.side === "buy" ? [ON.BUY, ZERO_ADDRESS, c.wallet, c.amount] : c.side === "sell" ? [ON.SELL, c.wallet, ZERO_ADDRESS, c.amount] : [ON.SEND, c.wallet, c.to ?? ZERO_ADDRESS, c.amount];
198
+ const [r, script] = await Promise.all([call(c.token, TA, "check", args), call(c.token, TA, "script")]);
199
+ const answer = Number(r);
200
+ if (answer === NONE)
201
+ return { ok: true };
202
+ if (answer === LOCKED) {
203
+ // the coin answers "locked" too when the wallet simply holds fewer coins than the trade
204
+ const held = c.side === "buy" ? null : await call(c.token, TA, "balanceOf", [c.wallet]).catch(() => null);
205
+ const text = held !== null && held < c.amount ? "This wallet holds fewer of these coins than that." : errorText("Blocked", [LOCKED]);
206
+ return { ok: false, row: "locked", text, rule: null };
207
+ }
208
+ const rows = describeRows(script[0], Number(script[1]), Number(script[2]));
209
+ const rule = rows[answer] ?? null;
210
+ return { ok: false, row: answer, text: `Refused by rule ${answer + 1}${rule ? `: ${rule.name}. ${rule.text}` : "."}`, rule };
211
+ }
212
+ /** Coins a buy of `bnbIn` would get, estimated from the pool's liquidity at its price (see v3EstimateBuy); not a minOut. */
213
+ async function estimateBuy(token, bnbIn) {
214
+ return v3EstimateBuy(ctx, await call(token, TA, "pool"), token, bnbIn);
215
+ }
216
+ /**
217
+ * What a trade gets in the V3 pool, from PancakeSwap's QuoterV2. It runs the swap, so a refusing rule fails it, and it
218
+ * does not pass the reason on: a refused buy is checked again with an estimate of its coins and the refusal names the
219
+ * rule (for `wallet`, by default the quoter the quote ran as).
220
+ */
221
+ async function quote(q) {
222
+ try {
223
+ return { amountOut: await v3Quote(ctx, q) };
224
+ }
225
+ catch (e) {
226
+ if (q.side === "buy") {
227
+ const est = await estimateBuy(q.token, q.amount).catch(() => 0n);
228
+ const c = est > 0n ? await check({ token: q.token, side: "buy", wallet: q.wallet ?? a.v3Quoter, amount: est }).catch(() => null) : null;
229
+ if (c && !c.ok)
230
+ throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row], cause: e });
231
+ }
232
+ throw toPickhooksError(e, "The quoter refused this trade.");
233
+ }
234
+ }
235
+ /** A buy through PancakeSwap's V3 SwapRouter, BNB in (wrapped by the router). */
236
+ const buildBuy = (b) => buildV3Buy(a, b);
237
+ /** A sell through the V3 SwapRouter for native BNB: swap to the router, unwrap to the seller. Needs approval to the router. */
238
+ const buildSell = (x) => buildV3Sell(a, x);
239
+ /** The approval a sell needs (the coin to the V3 router), or null. */
240
+ const sellApproval = (owner, token, amount) => approvalFor(ctx, owner, token, amount, a.v3Router, "Allowing PancakeSwap");
241
+ const refuse = (c) => {
242
+ if (!c.ok)
243
+ throw new PickhooksError(c.text, { code: "Blocked", args: [c.row === "locked" ? LOCKED : c.row] });
244
+ };
245
+ /** Quotes, checks the coin's rules for this wallet, and buys. A refusing rule stops it before anything is signed. */
246
+ async function buy(b, s = {}) {
247
+ const { address } = await signer(ctx, s.wallet);
248
+ 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) };
253
+ }
254
+ /** Checks the rules, approves the router if needed, quotes and sells for BNB. */
255
+ async function sell(x, s = {}) {
256
+ const { address } = await signer(ctx, s.wallet);
257
+ refuse(await check({ token: x.token, side: "sell", wallet: address, amount: x.amount }));
258
+ const ap = await sellApproval(address, x.token, x.amount);
259
+ if (ap)
260
+ 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 };
265
+ }
266
+ /** The creator lifts a row for good (a drop row cannot be lifted). */
267
+ const buildLift = (token, row) => ({ to: token, value: 0n, data: encodeFunctionData({ abi: TA, functionName: "lift", args: [row] }), label: `Lifting rule ${row + 1}` });
268
+ return {
269
+ factory: a.stickyFactory,
270
+ terms,
271
+ validate,
272
+ compile,
273
+ describeRows,
274
+ prepareMetadata,
275
+ count,
276
+ coins,
277
+ coin,
278
+ isCoin,
279
+ wallet,
280
+ check,
281
+ quote,
282
+ estimateBuy,
283
+ sellApproval,
284
+ build: { launch: buildLaunch, buy: buildBuy, sell: buildSell, lift: buildLift },
285
+ launch,
286
+ buy,
287
+ sell,
288
+ lift: (token, row, s = {}) => sendTx(ctx, buildLift(token, row), s),
289
+ /** the sender's address, for callers that check before they sign */
290
+ from: (s = {}) => senderAddress(ctx, s.wallet),
291
+ };
292
+ }
package/dist/tx.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { type Account, type Address, type Hex, type PublicClient, type TransactionReceipt, type WalletClient } from "viem";
2
+ import { type Addresses } from "./addresses.js";
3
+ /**
4
+ * A transaction as plain data: send it with any wallet, wrap it in a multicall, or route it through your own contract.
5
+ * `extraGas` is what to add on top of your own gas estimate (the recapture room of a hook any coin trade); it never
6
+ * changes what the call does, only whether the hook has gas left to close a gap after the swap.
7
+ */
8
+ export type TxRequest = {
9
+ to: Address;
10
+ data: Hex;
11
+ value: bigint;
12
+ extraGas?: bigint;
13
+ /** the estimate is taken times this percentage (default 130); trades that liquidate or count rules can grow while they wait */
14
+ gasPct?: number;
15
+ /** what it does, in words (for logs and confirmations) */
16
+ label?: string;
17
+ };
18
+ export type SendOptions = {
19
+ /** sends from this wallet instead of the client's */
20
+ wallet?: WalletClient;
21
+ /** a fixed gas limit instead of estimate x 1.3 + extraGas */
22
+ gas?: bigint;
23
+ /** called with the hash as soon as the wallet returns it */
24
+ onHash?: (hash: Hex) => void;
25
+ /** how long to wait for the receipt, ms (default 120 s) */
26
+ timeout?: number;
27
+ };
28
+ export type Sent = {
29
+ hash: Hex;
30
+ receipt: TransactionReceipt;
31
+ };
32
+ export type PickhooksOptions = {
33
+ /** a viem public client on BNB Chain (or a fork of it); without it the SDK makes one from rpcUrl */
34
+ publicClient?: PublicClient;
35
+ /** used when no publicClient is given; default BNB Chain's public RPC */
36
+ rpcUrl?: string;
37
+ /** signs and sends; only the write methods need it */
38
+ walletClient?: WalletClient;
39
+ /** other contract addresses (a fork rehearsal, a new factory); merged over BSC_MAINNET */
40
+ addresses?: Partial<Addresses>;
41
+ /** the pickhooks site for its public API and partner links (default https://pickhooks.com) */
42
+ site?: string;
43
+ };
44
+ /** What every module of the SDK shares. */
45
+ export type Ctx = {
46
+ pub: PublicClient;
47
+ wallet?: WalletClient;
48
+ a: Addresses;
49
+ site: string;
50
+ };
51
+ export declare const DEFAULT_RPC = "https://bsc-dataseed.bnbchain.org";
52
+ export declare function makeCtx(o?: PickhooksOptions): Ctx;
53
+ /** The wallet a write goes out from, and its account (a local account signs locally, an address goes to the wallet). */
54
+ export declare function signer(ctx: Ctx, w?: WalletClient): Promise<{
55
+ wallet: WalletClient;
56
+ account: Account | Address;
57
+ address: Address;
58
+ }>;
59
+ /** The address a write would go out from (for quotes and checks before it), or undefined without a wallet. */
60
+ export declare function senderAddress(ctx: Ctx, w?: WalletClient): Promise<Address | undefined>;
61
+ /** The gas limit a request goes out with: estimate x gasPct (default 130 %) plus its extraGas, never above what BNB Chain accepts. */
62
+ export declare function gasFor(ctx: Ctx, tx: TxRequest, from: Address): Promise<bigint>;
63
+ /** Estimates, sends and waits for the receipt; a failure comes back as a PickhooksError with a sentence. */
64
+ export declare function sendTx(ctx: Ctx, tx: TxRequest, o?: SendOptions): Promise<Sent>;
65
+ /** Sends requests one after the other (approvals before a trade). */
66
+ export declare function sendAll(ctx: Ctx, txs: TxRequest[], o?: SendOptions): Promise<Sent[]>;
67
+ /** A deadline 10 minutes from now (seconds), for router calls. */
68
+ export declare const deadlineIn: (seconds?: number) => bigint;
69
+ /** amount less a slippage in basis points */
70
+ export declare const minusBps: (amount: bigint, bps: number) => bigint;
package/dist/tx.js ADDED
@@ -0,0 +1,69 @@
1
+ import { createPublicClient, http, } from "viem";
2
+ import { bsc } from "viem/chains";
3
+ import { BSC_MAINNET, LIMITS } from "./addresses.js";
4
+ import { PickhooksError, toPickhooksError } from "./errors.js";
5
+ export const DEFAULT_RPC = "https://bsc-dataseed.bnbchain.org";
6
+ export function makeCtx(o = {}) {
7
+ const pub = o.publicClient ?? createPublicClient({ chain: bsc, transport: http(o.rpcUrl ?? DEFAULT_RPC, { batch: true }) });
8
+ return { pub, wallet: o.walletClient, a: { ...BSC_MAINNET, ...o.addresses }, site: (o.site ?? "https://pickhooks.com").replace(/\/$/, "") };
9
+ }
10
+ /** The wallet a write goes out from, and its account (a local account signs locally, an address goes to the wallet). */
11
+ export async function signer(ctx, w) {
12
+ const wallet = w ?? ctx.wallet;
13
+ if (!wallet)
14
+ throw new PickhooksError("This needs a wallet: pass walletClient to createPickhooks or { wallet } to the call.", { code: "input" });
15
+ const account = wallet.account ?? (await wallet.getAddresses())[0];
16
+ if (!account)
17
+ throw new PickhooksError("The wallet has no account.", { code: "input" });
18
+ return { wallet, account, address: typeof account === "string" ? account : account.address };
19
+ }
20
+ /** The address a write would go out from (for quotes and checks before it), or undefined without a wallet. */
21
+ export async function senderAddress(ctx, w) {
22
+ const wallet = w ?? ctx.wallet;
23
+ if (!wallet)
24
+ return undefined;
25
+ if (wallet.account)
26
+ return wallet.account.address;
27
+ return (await wallet.getAddresses())[0];
28
+ }
29
+ /** The gas limit a request goes out with: estimate x gasPct (default 130 %) plus its extraGas, never above what BNB Chain accepts. */
30
+ export async function gasFor(ctx, tx, from) {
31
+ let est;
32
+ try {
33
+ est = await ctx.pub.estimateGas({ account: from, to: tx.to, data: tx.data, value: tx.value });
34
+ }
35
+ catch (e) {
36
+ throw toPickhooksError(e, `${tx.label ?? "The transaction"} would fail.`);
37
+ }
38
+ const gas = (est * BigInt(tx.gasPct ?? 130)) / 100n + (tx.extraGas ?? 0n);
39
+ return gas > LIMITS.gasCap ? LIMITS.gasCap : gas;
40
+ }
41
+ /** Estimates, sends and waits for the receipt; a failure comes back as a PickhooksError with a sentence. */
42
+ export async function sendTx(ctx, tx, o = {}) {
43
+ const { wallet, account, address } = await signer(ctx, o.wallet);
44
+ const gas = o.gas ?? (await gasFor(ctx, tx, address));
45
+ let hash;
46
+ try {
47
+ hash = await wallet.sendTransaction({ account, chain: (wallet.chain ?? bsc), to: tx.to, data: tx.data, value: tx.value, gas });
48
+ }
49
+ catch (e) {
50
+ throw toPickhooksError(e, `${tx.label ?? "The transaction"} was not sent.`);
51
+ }
52
+ o.onHash?.(hash);
53
+ const receipt = await ctx.pub.waitForTransactionReceipt({ hash, timeout: o.timeout ?? 120_000 });
54
+ if (receipt.status !== "success") {
55
+ throw new PickhooksError(`${tx.label ?? "The transaction"} failed on chain (${hash}).`, { code: "reverted", hash });
56
+ }
57
+ return { hash, receipt };
58
+ }
59
+ /** Sends requests one after the other (approvals before a trade). */
60
+ export async function sendAll(ctx, txs, o = {}) {
61
+ const out = [];
62
+ for (const tx of txs)
63
+ out.push(await sendTx(ctx, tx, o));
64
+ return out;
65
+ }
66
+ /** A deadline 10 minutes from now (seconds), for router calls. */
67
+ export const deadlineIn = (seconds = 600) => BigInt(Math.floor(Date.now() / 1000) + seconds);
68
+ /** amount less a slippage in basis points */
69
+ export const minusBps = (amount, bps) => (amount * BigInt(10_000 - Math.max(0, Math.min(10_000, Math.round(bps))))) / 10000n;