@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/tlock.js ADDED
@@ -0,0 +1,185 @@
1
+ // Time-lock encryption to a drand quicknet round, the same as the site's web/src/lib/hide-tlock.mjs (find the dev) and
2
+ // what the dark launch's sealed bids use. Its own entry (@pickhooks/sdk/tlock), so the main SDK does not load BLS12-381.
3
+ //
4
+ // Ciphertext (U, V, W) for a 32 byte message M and the round's identity Q = hashToG1(sha256(round)):
5
+ // sigma random 32 bytes, r = H3(sigma, M) mod q
6
+ // U = r * G2 (G2 point, 256 bytes in EIP-2537 encoding on chain)
7
+ // V = sigma xor H2(e(Q, pk)^r)
8
+ // W = M xor H4(sigma)
9
+ // Opening needs drand's signature on the round (sig = s * Q): sigma = V xor H2(e(sig, U)), M = W xor H4(sigma).
10
+ // The contracts check U == H3(sigma, M) * G2 and W == M xor H4(sigma) with the BLS12-381 precompile (HideMath.sol).
11
+ import { bls12_381 as bls } from "@noble/curves/bls12-381";
12
+ import { sha256 } from "@noble/hashes/sha2";
13
+ import { keccak_256 } from "@noble/hashes/sha3";
14
+ export const QUICKNET = {
15
+ chainHash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
16
+ publicKey: "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
17
+ genesis: 1692803367,
18
+ period: 3,
19
+ dst: "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_",
20
+ urls: ["https://api.drand.sh", "https://api2.drand.sh", "https://api3.drand.sh", "https://drand.cloudflare.com"],
21
+ };
22
+ const Q = bls.params.r;
23
+ const TWO256 = 1n << 256n;
24
+ const TAG3 = new TextEncoder().encode("pickhooks.hide.H3");
25
+ const TAG4 = new TextEncoder().encode("pickhooks.hide.H4");
26
+ const TAG2 = new TextEncoder().encode("pickhooks.hide.H2");
27
+ const G2 = bls.G2.ProjectivePoint;
28
+ const G1 = bls.G1.ProjectivePoint;
29
+ const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
30
+ const unhex = (h) => {
31
+ const s = h.startsWith("0x") ? h.slice(2) : h;
32
+ if (s.length % 2)
33
+ throw new Error("odd hex");
34
+ const out = new Uint8Array(s.length / 2);
35
+ for (let i = 0; i < out.length; i++)
36
+ out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
37
+ return out;
38
+ };
39
+ const concat = (...parts) => {
40
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
41
+ let o = 0;
42
+ for (const p of parts) {
43
+ out.set(p, o);
44
+ o += p.length;
45
+ }
46
+ return out;
47
+ };
48
+ const xor32 = (a, b) => {
49
+ const out = new Uint8Array(32);
50
+ for (let i = 0; i < 32; i++)
51
+ out[i] = a[i] ^ b[i];
52
+ return out;
53
+ };
54
+ const toBig = (b) => BigInt("0x" + (hex(b) || "0"));
55
+ const be = (n, len) => unhex(n.toString(16).padStart(len * 2, "0"));
56
+ const bytes32 = (x) => {
57
+ const b = typeof x === "string" ? unhex(x) : x;
58
+ if (b.length !== 32)
59
+ throw new Error("need 32 bytes");
60
+ return b;
61
+ };
62
+ /** r = (keccak(TAG3 || 0x01 || sigma || M) * 2^256 + keccak(TAG3 || 0x02 || sigma || M)) mod q, never 0. */
63
+ export function h3(sigma, m) {
64
+ const a = toBig(keccak_256(concat(TAG3, Uint8Array.of(1), sigma, m)));
65
+ const b = toBig(keccak_256(concat(TAG3, Uint8Array.of(2), sigma, m)));
66
+ const r = (a * TWO256 + b) % Q;
67
+ return r === 0n ? 1n : r;
68
+ }
69
+ /** keccak(TAG4 || sigma). */
70
+ export function h4(sigma) {
71
+ return keccak_256(concat(TAG4, sigma));
72
+ }
73
+ function h2(gt) {
74
+ return keccak_256(concat(TAG2, bls.fields.Fp12.toBytes(gt)));
75
+ }
76
+ /** First second at which drand publishes the round. */
77
+ export function roundTime(round, net = QUICKNET) {
78
+ return net.genesis + (Number(round) - 1) * net.period;
79
+ }
80
+ /** First round published at or after `unixSeconds`. */
81
+ export function roundAt(unixSeconds, net = QUICKNET) {
82
+ const t = Math.max(0, Math.ceil(unixSeconds) - net.genesis);
83
+ return Math.ceil(t / net.period) + 1;
84
+ }
85
+ /** The round's identity point: hash_to_G1(sha256(round as 8 bytes big endian)), as drand quicknet signs it. */
86
+ export function roundPoint(round, net = QUICKNET) {
87
+ return G1.fromAffine(bls.G1.hashToCurve(sha256(be(BigInt(round), 8)), { DST: net.dst }).toAffine());
88
+ }
89
+ /** True when `sigHex` is drand's signature on `round` under the network's key. */
90
+ export function verifyRoundSig(round, sigHex, net = QUICKNET) {
91
+ const sig = G1.fromHex(sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex);
92
+ const pk = G2.fromHex(net.publicKey);
93
+ const lhs = bls.pairing(sig, G2.BASE);
94
+ const rhs = bls.pairing(roundPoint(round, net), pk);
95
+ return bls.fields.Fp12.eql(lhs, rhs);
96
+ }
97
+ /** G2 point to the 256 byte EIP-2537 encoding: x.c0, x.c1, y.c0, y.c1, each 64 bytes (16 zero bytes + 48). */
98
+ export function g2ToEip(point) {
99
+ const { x, y } = point.toAffine();
100
+ return `0x${[x.c0, x.c1, y.c0, y.c1].map((v) => v.toString(16).padStart(128, "0")).join("")}`;
101
+ }
102
+ export function g2FromEip(h) {
103
+ const b = unhex(h);
104
+ if (b.length !== 256)
105
+ throw new Error("U must be 256 bytes");
106
+ const f = (i) => toBig(b.slice(i * 64, i * 64 + 64));
107
+ const Fp2 = bls.fields.Fp2;
108
+ const p = G2.fromAffine({ x: Fp2.create({ c0: f(0), c1: f(1) }), y: Fp2.create({ c0: f(2), c1: f(3) }) });
109
+ p.assertValidity();
110
+ return p;
111
+ }
112
+ /** The 32 byte message of a dark launch bid: the bidder (20 bytes), the step (1 byte), 11 zero bytes. */
113
+ export function darkMessage(bidder, step) {
114
+ const a = unhex(bidder);
115
+ if (a.length !== 20)
116
+ throw new Error("bidder must be 20 bytes");
117
+ if (!(Number.isInteger(step) && step >= 0 && step < 24))
118
+ throw new Error("step must be 0 to 23");
119
+ return `0x${hex(concat(a, Uint8Array.of(step), new Uint8Array(11)))}`;
120
+ }
121
+ export function parseDarkMessage(m) {
122
+ const b = bytes32(m);
123
+ return { bidder: `0x${hex(b.slice(0, 20))}`, step: b[20], clean: b.slice(21).every((x) => x === 0) };
124
+ }
125
+ /**
126
+ * Encrypts 32 bytes to a drand round. Returns hex ready for the contracts: u (256 bytes), v and w (32 each).
127
+ * `opts.sigma` and `opts.publicKey` exist for tests only.
128
+ */
129
+ export function encrypt(round, message, opts = {}) {
130
+ const net = opts.net ?? QUICKNET;
131
+ const m = bytes32(message);
132
+ const sigma = opts.sigma ? bytes32(opts.sigma) : crypto.getRandomValues(new Uint8Array(32));
133
+ const r = h3(sigma, m);
134
+ const pk = G2.fromHex(opts.publicKey ?? net.publicKey);
135
+ const U = G2.BASE.multiply(r);
136
+ const gid = bls.pairing(roundPoint(round, net), pk.multiply(r));
137
+ const V = xor32(sigma, h2(gid));
138
+ const W = xor32(m, h4(sigma));
139
+ return { u: g2ToEip(U), v: `0x${hex(V)}`, w: `0x${hex(W)}`, round: Number(round), sigma: `0x${hex(sigma)}` };
140
+ }
141
+ /** Opens a ciphertext with drand's signature on its round: { sigma, message }, or null when it does not open. */
142
+ export function decrypt(c, sigHex) {
143
+ let U;
144
+ try {
145
+ U = g2FromEip(c.u);
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ const sig = G1.fromHex(sigHex.startsWith("0x") ? sigHex.slice(2) : sigHex);
151
+ const gid = bls.pairing(sig, U);
152
+ const sigma = xor32(bytes32(c.v), h2(gid));
153
+ const m = xor32(bytes32(c.w), h4(sigma));
154
+ if (!opens(c, sigma, m))
155
+ return null;
156
+ return { sigma: `0x${hex(sigma)}`, message: `0x${hex(m)}` };
157
+ }
158
+ /** The contract's own check, in JS: U == H3(sigma, M) * G2 and W == M xor H4(sigma). */
159
+ export function opens(c, sigma, message) {
160
+ const s = bytes32(sigma);
161
+ const m = bytes32(message);
162
+ if (hex(xor32(m, h4(s))) !== hex(bytes32(c.w)))
163
+ return false;
164
+ return g2ToEip(G2.BASE.multiply(h3(s, m))).toLowerCase() === c.u.toLowerCase();
165
+ }
166
+ /** drand's signature on `round` (hex, 48 bytes), checked against the network's key. Null while the round is not out. */
167
+ export async function fetchRoundSig(round, net = QUICKNET, fetcher = fetch) {
168
+ for (const base of net.urls) {
169
+ try {
170
+ const res = await fetcher(`${base}/${net.chainHash}/public/${round}`, { cache: "no-store" });
171
+ if (!res.ok)
172
+ continue;
173
+ const j = (await res.json());
174
+ if (Number(j.round) !== Number(round) || typeof j.signature !== "string")
175
+ continue;
176
+ if (!verifyRoundSig(round, j.signature, net))
177
+ continue;
178
+ return j.signature;
179
+ }
180
+ catch {
181
+ // next relay
182
+ }
183
+ }
184
+ return null;
185
+ }
package/dist/tx.d.ts CHANGED
@@ -66,5 +66,7 @@ export declare function sendTx(ctx: Ctx, tx: TxRequest, o?: SendOptions): Promis
66
66
  export declare function sendAll(ctx: Ctx, txs: TxRequest[], o?: SendOptions): Promise<Sent[]>;
67
67
  /** A deadline 10 minutes from now (seconds), for router calls. */
68
68
  export declare const deadlineIn: (seconds?: number) => bigint;
69
+ /** A deadline from the later of this machine's clock and the chain's (a skewed clock or a fork ahead of real time). */
70
+ export declare function chainDeadline(ctx: Ctx, seconds?: number): Promise<bigint>;
69
71
  /** amount less a slippage in basis points */
70
72
  export declare const minusBps: (amount: bigint, bps: number) => bigint;
package/dist/tx.js CHANGED
@@ -65,5 +65,14 @@ export async function sendAll(ctx, txs, o = {}) {
65
65
  }
66
66
  /** A deadline 10 minutes from now (seconds), for router calls. */
67
67
  export const deadlineIn = (seconds = 600) => BigInt(Math.floor(Date.now() / 1000) + seconds);
68
+ /** A deadline from the later of this machine's clock and the chain's (a skewed clock or a fork ahead of real time). */
69
+ export async function chainDeadline(ctx, seconds = 600) {
70
+ const local = Math.floor(Date.now() / 1000);
71
+ const chain = await ctx.pub
72
+ .getBlock()
73
+ .then((b) => Number(b.timestamp))
74
+ .catch(() => local);
75
+ return BigInt(Math.max(local, chain) + seconds);
76
+ }
68
77
  /** amount less a slippage in basis points */
69
78
  export const minusBps = (amount, bps) => (amount * BigInt(10_000 - Math.max(0, Math.min(10_000, Math.round(bps))))) / 10000n;
package/dist/v2.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { type Address } from "viem";
2
+ import type { Addresses } from "./addresses.js";
3
+ import { type Ctx, type TxRequest } from "./tx.js";
4
+ /**
5
+ * PancakeSwap V2: every sticky coin (and every rules coin since 03.10.) has a V2 pair next to its V3 pool, at the same
6
+ * start price. A dark launch puts the whole round's BNB into that pair, so right after the birth the pair is the venue
7
+ * with the BNB and the V3 curve holds only coins above the price: a sell into V3 would get almost nothing ("SPL").
8
+ * Terminals route to the better pool by themselves; the SDK quotes both and picks the better one (sticky.quote).
9
+ */
10
+ export declare const v2RouterAbi: readonly [{
11
+ readonly name: "getAmountsOut";
12
+ readonly type: "function";
13
+ readonly stateMutability: "view";
14
+ readonly inputs: readonly [{
15
+ readonly type: "uint256";
16
+ readonly name: "amountIn";
17
+ }, {
18
+ readonly type: "address[]";
19
+ readonly name: "path";
20
+ }];
21
+ readonly outputs: readonly [{
22
+ readonly type: "uint256[]";
23
+ readonly name: "amounts";
24
+ }];
25
+ }, {
26
+ readonly name: "swapExactETHForTokensSupportingFeeOnTransferTokens";
27
+ readonly type: "function";
28
+ readonly stateMutability: "payable";
29
+ readonly inputs: readonly [{
30
+ readonly type: "uint256";
31
+ readonly name: "amountOutMin";
32
+ }, {
33
+ readonly type: "address[]";
34
+ readonly name: "path";
35
+ }, {
36
+ readonly type: "address";
37
+ readonly name: "to";
38
+ }, {
39
+ readonly type: "uint256";
40
+ readonly name: "deadline";
41
+ }];
42
+ readonly outputs: readonly [];
43
+ }, {
44
+ readonly name: "swapExactTokensForETHSupportingFeeOnTransferTokens";
45
+ readonly type: "function";
46
+ readonly stateMutability: "nonpayable";
47
+ readonly inputs: readonly [{
48
+ readonly type: "uint256";
49
+ readonly name: "amountIn";
50
+ }, {
51
+ readonly type: "uint256";
52
+ readonly name: "amountOutMin";
53
+ }, {
54
+ readonly type: "address[]";
55
+ readonly name: "path";
56
+ }, {
57
+ readonly type: "address";
58
+ readonly name: "to";
59
+ }, {
60
+ readonly type: "uint256";
61
+ readonly name: "deadline";
62
+ }];
63
+ readonly outputs: readonly [];
64
+ }];
65
+ export type Venue = "v3" | "v2";
66
+ /** What a trade gets in the V2 pair, from the router (reverts when the coin has no pair or the pair is empty). */
67
+ export declare function v2Quote(ctx: Ctx, q: {
68
+ token: Address;
69
+ side: "buy" | "sell";
70
+ amount: bigint;
71
+ }): Promise<bigint>;
72
+ /** A buy through the V2 router, BNB in. */
73
+ export declare function buildV2Buy(a: Addresses, b: {
74
+ token: Address;
75
+ bnbIn: bigint;
76
+ minOut: bigint;
77
+ recipient: Address;
78
+ deadline?: bigint;
79
+ gasPct?: number;
80
+ }): TxRequest;
81
+ /** A sell through the V2 router for native BNB. Needs the coin approved to the V2 router. */
82
+ export declare function buildV2Sell(a: Addresses, s: {
83
+ token: Address;
84
+ amount: bigint;
85
+ minOut: bigint;
86
+ recipient: Address;
87
+ deadline?: bigint;
88
+ gasPct?: number;
89
+ }): TxRequest;
90
+ /**
91
+ * The better of the two venues for this trade: the V3 pool (quoter) and the V2 pair (router). A venue that cannot
92
+ * quote (no pair, no BNB below the price, a refusing rule) counts as nothing; `errors` keeps what each one said.
93
+ */
94
+ export declare function bestVenue(ctx: Ctx, quotes: {
95
+ v3: () => Promise<bigint>;
96
+ v2: () => Promise<bigint>;
97
+ }): Promise<{
98
+ venue: Venue;
99
+ amountOut: bigint;
100
+ v3: bigint | null;
101
+ v2: bigint | null;
102
+ errors: {
103
+ v3?: unknown;
104
+ v2?: unknown;
105
+ };
106
+ }>;
package/dist/v2.js ADDED
@@ -0,0 +1,61 @@
1
+ import { encodeFunctionData, parseAbi } from "viem";
2
+ import { deadlineIn } from "./tx.js";
3
+ /**
4
+ * PancakeSwap V2: every sticky coin (and every rules coin since 03.10.) has a V2 pair next to its V3 pool, at the same
5
+ * start price. A dark launch puts the whole round's BNB into that pair, so right after the birth the pair is the venue
6
+ * with the BNB and the V3 curve holds only coins above the price: a sell into V3 would get almost nothing ("SPL").
7
+ * Terminals route to the better pool by themselves; the SDK quotes both and picks the better one (sticky.quote).
8
+ */
9
+ export const v2RouterAbi = parseAbi([
10
+ "function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)",
11
+ "function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable",
12
+ "function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)",
13
+ ]);
14
+ /** What a trade gets in the V2 pair, from the router (reverts when the coin has no pair or the pair is empty). */
15
+ export async function v2Quote(ctx, q) {
16
+ const path = q.side === "buy" ? [ctx.a.wbnb, q.token] : [q.token, ctx.a.wbnb];
17
+ const amounts = (await ctx.pub.readContract({ address: ctx.a.v2Router, abi: v2RouterAbi, functionName: "getAmountsOut", args: [q.amount, path] }));
18
+ return amounts[amounts.length - 1];
19
+ }
20
+ /** A buy through the V2 router, BNB in. */
21
+ export function buildV2Buy(a, b) {
22
+ return {
23
+ to: a.v2Router,
24
+ value: b.bnbIn,
25
+ data: encodeFunctionData({ abi: v2RouterAbi, functionName: "swapExactETHForTokensSupportingFeeOnTransferTokens", args: [b.minOut, [a.wbnb, b.token], b.recipient, b.deadline ?? deadlineIn()] }),
26
+ gasPct: b.gasPct,
27
+ label: "The buy",
28
+ };
29
+ }
30
+ /** A sell through the V2 router for native BNB. Needs the coin approved to the V2 router. */
31
+ export function buildV2Sell(a, s) {
32
+ return {
33
+ to: a.v2Router,
34
+ value: 0n,
35
+ data: encodeFunctionData({ abi: v2RouterAbi, functionName: "swapExactTokensForETHSupportingFeeOnTransferTokens", args: [s.amount, s.minOut, [s.token, a.wbnb], s.recipient, s.deadline ?? deadlineIn()] }),
36
+ gasPct: s.gasPct,
37
+ label: "The sell",
38
+ };
39
+ }
40
+ /**
41
+ * The better of the two venues for this trade: the V3 pool (quoter) and the V2 pair (router). A venue that cannot
42
+ * quote (no pair, no BNB below the price, a refusing rule) counts as nothing; `errors` keeps what each one said.
43
+ */
44
+ export async function bestVenue(ctx, quotes) {
45
+ const errors = {};
46
+ const [v3, v2] = await Promise.all([
47
+ quotes.v3().catch((e) => {
48
+ errors.v3 = e;
49
+ return null;
50
+ }),
51
+ quotes.v2().catch((e) => {
52
+ errors.v2 = e;
53
+ return null;
54
+ }),
55
+ ]);
56
+ void ctx;
57
+ if (v3 === null && v2 === null)
58
+ return { venue: "v3", amountOut: 0n, v3, v2, errors };
59
+ const venue = (v2 ?? 0n) > (v3 ?? 0n) ? "v2" : "v3";
60
+ return { venue, amountOut: venue === "v2" ? v2 : v3, v3, v2, errors };
61
+ }
@@ -0,0 +1,80 @@
1
+ import { type Address } from "viem";
2
+ import type { AgentLock, AgentsModule } from "./agents.js";
3
+ import type { AnyModule, Earnings } from "./any.js";
4
+ import type { CoinsModule } from "./coins.js";
5
+ import type { DipFloor, DipsModule } from "./dips.js";
6
+ import type { Lever2Module } from "./lever.js";
7
+ import type { CallsModule, PoolsModule } from "./pools.js";
8
+ import type { StickyModule } from "./sticky.js";
9
+ import type { Ctx } from "./tx.js";
10
+ import type { WatchMode } from "./watch.js";
11
+ /**
12
+ * One address in: everything it holds on pickhooks, what it can sell right now, and what it can claim.
13
+ *
14
+ * const w = await ph.wallet(address);
15
+ * w.coins // every pickhooks coin it holds, with mode, price, value and what is sellable now
16
+ * w.claims // BNB owed by the hooks (hook any coin, pools, curve calls, agents)
17
+ * w.locks // coins locked at the agents desk (vetoes, firing)
18
+ * w.floors // dip bid floors it holds
19
+ */
20
+ export type WalletCoin = {
21
+ token: Address;
22
+ mode: WatchMode;
23
+ name: string;
24
+ symbol: string;
25
+ balance: bigint;
26
+ /** what a sell of the whole balance would be allowed right now: the balance, less (sticky: unlocked lots), or 0 with the reason */
27
+ sellable: bigint;
28
+ /** why not everything is sellable (a rule's sentence), or null */
29
+ refused: string | null;
30
+ priceBnb: number;
31
+ /** balance x price, in BNB */
32
+ valueBnb: number;
33
+ page: string;
34
+ };
35
+ export type WalletView = {
36
+ address: Address;
37
+ bnb: bigint;
38
+ coins: WalletCoin[];
39
+ /** the coins' value together, BNB */
40
+ valueBnb: number;
41
+ claims: {
42
+ /** hook any coin: recapture shares, opener and partner shares, router credits */
43
+ any: Earnings | null;
44
+ /** pools and curve calls: jackpot wins, rewards, a creator's share */
45
+ pools: bigint;
46
+ calls: bigint;
47
+ /** agents: jackpot wins, rewards, a creator's share */
48
+ agents: bigint;
49
+ /** all of the above in BNB (router credits counted as claimable) */
50
+ totalBnb: bigint;
51
+ };
52
+ /** coins locked at the agents desk, per agent coin */
53
+ locks: ({
54
+ token: Address;
55
+ symbol: string;
56
+ } & AgentLock)[];
57
+ floors: DipFloor[];
58
+ /** factories the scan covered and how many coins each has (the newest `limit` per factory are scanned) */
59
+ scanned: {
60
+ mode: WatchMode;
61
+ count: number;
62
+ scanned: number;
63
+ }[];
64
+ };
65
+ type Mods = {
66
+ sticky: StickyModule;
67
+ sticky2: StickyModule;
68
+ coins: CoinsModule;
69
+ pools: PoolsModule;
70
+ calls: CallsModule;
71
+ lever: Lever2Module;
72
+ agents: AgentsModule;
73
+ any: AnyModule;
74
+ dips: DipsModule;
75
+ };
76
+ export declare function walletModule(ctx: Ctx, mods: Mods): (who: Address, o?: {
77
+ limit?: number;
78
+ }) => Promise<WalletView>;
79
+ export type WalletModule = ReturnType<typeof walletModule>;
80
+ export {};
package/dist/wallet.js ADDED
@@ -0,0 +1,115 @@
1
+ import { formatEther, isAddressEqual } from "viem";
2
+ import { agentsFactoryAbi } from "./abis/agents.js";
3
+ import { callsFactoryAbi } from "./abis/calls.js";
4
+ import { erc20Abi } from "./abis/common.js";
5
+ import { lever2FactoryAbi } from "./abis/lever2.js";
6
+ import { poolsFactoryAbi } from "./abis/pools.js";
7
+ import { stickyFactoryAbi } from "./abis/sticky.js";
8
+ import { sticky2FactoryAbi } from "./abis/sticky2.js";
9
+ import { v1FactoryAbi } from "./abis/v1.js";
10
+ import { v2FactoryAbi } from "./abis/v2.js";
11
+ import { ZERO_ADDRESS } from "./addresses.js";
12
+ export function walletModule(ctx, mods) {
13
+ const { pub, a } = ctx;
14
+ const page = (mode, token) => {
15
+ const path = { sticky: "s", sticky2: "s", rules: "c", "rules-v1": "c", pools: "h", calls: "z", lever: "l", agents: "a", any: "any" }[mode];
16
+ return `${ctx.site}/${path}/${token}`;
17
+ };
18
+ const factories = [
19
+ { mode: "sticky", address: a.stickyFactory, abi: stickyFactoryAbi },
20
+ { mode: "sticky2", address: a.sticky2Factory, abi: sticky2FactoryAbi },
21
+ { mode: "rules", address: a.coinsV2Factory, abi: v2FactoryAbi },
22
+ { mode: "rules-v1", address: a.coinsV1Factory, abi: v1FactoryAbi },
23
+ { mode: "pools", address: a.poolsFactory, abi: poolsFactoryAbi },
24
+ { mode: "calls", address: a.callsFactory, abi: callsFactoryAbi },
25
+ { mode: "lever", address: a.lever2Factory, abi: lever2FactoryAbi },
26
+ { mode: "agents", address: a.agentsFactory, abi: agentsFactoryAbi },
27
+ ];
28
+ /** The newest `limit` coins of every factory. */
29
+ async function tokens(limit) {
30
+ const live = factories.filter((f) => !isAddressEqual(f.address, ZERO_ADDRESS));
31
+ const counts = await pub.multicall({ contracts: live.map((f) => ({ address: f.address, abi: f.abi, functionName: "coinCount" })), allowFailure: true });
32
+ const calls = [];
33
+ const owners = [];
34
+ const scanned = [];
35
+ live.forEach((f, i) => {
36
+ const n = counts[i].status === "success" ? Number(counts[i].result) : 0;
37
+ const from = Math.max(0, n - limit);
38
+ scanned.push({ mode: f.mode, count: n, scanned: n - from });
39
+ for (let j = n - 1; j >= from; j--) {
40
+ calls.push({ address: f.address, abi: f.abi, functionName: "coins", args: [BigInt(j)] });
41
+ owners.push(f.mode);
42
+ }
43
+ });
44
+ if (!calls.length)
45
+ return { list: [], scanned };
46
+ const res = await pub.multicall({ contracts: calls, allowFailure: true });
47
+ const list = [];
48
+ res.forEach((r, i) => {
49
+ if (r.status === "success")
50
+ list.push({ mode: owners[i], token: r.result });
51
+ });
52
+ return { list, scanned };
53
+ }
54
+ /** A held coin's name, symbol, price and what is sellable now, by mode. */
55
+ async function detail(mode, token, who, balance) {
56
+ const refusedOf = (r) => (r.ok ? null : (r.text ?? "The coin refused a sell right now."));
57
+ if (mode === "sticky" || mode === "sticky2") {
58
+ const m = mode === "sticky" ? mods.sticky : mods.sticky2;
59
+ const [c, w] = await Promise.all([m.coin(token), m.wallet(token, who)]);
60
+ return { name: c?.name ?? "", symbol: c?.symbol ?? "", sellable: w.sellable, refused: w.sellable < balance ? "Part of these coins is still locked by a rule; it unlocks over time." : null, priceBnb: c?.priceBnb ?? 0, valueBnb: (c?.priceBnb ?? 0) * Number(formatEther(balance)) };
61
+ }
62
+ if (mode === "rules" || mode === "rules-v1") {
63
+ const [c, r] = await Promise.all([mods.coins.coin(token), mods.coins.check({ token, side: "sell", wallet: who, amount: balance }).catch(() => ({ ok: false, kind: 0, name: "", text: "The coin could not be asked." }))]);
64
+ return { name: c?.name ?? "", symbol: c?.symbol ?? "", sellable: r.ok ? balance : 0n, refused: refusedOf(r), priceBnb: c?.priceBnb ?? 0, valueBnb: (c?.priceBnb ?? 0) * Number(formatEther(balance)) };
65
+ }
66
+ if (mode === "pools" || mode === "calls" || mode === "agents") {
67
+ const m = mode === "pools" ? mods.pools : mode === "calls" ? mods.calls : mods.agents;
68
+ const [c, r] = await Promise.all([m.coin(token), m.check({ token, side: "sell", wallet: who, amount: balance }).catch(() => ({ ok: false, kind: 0, name: "", text: "The pool could not be asked." }))]);
69
+ return { name: c?.name ?? "", symbol: c?.symbol ?? "", sellable: r.ok ? balance : 0n, refused: refusedOf(r), priceBnb: c?.priceBnb ?? 0, valueBnb: (c?.priceBnb ?? 0) * Number(formatEther(balance)) };
70
+ }
71
+ if (mode === "lever") {
72
+ const c = await mods.lever.coin(token);
73
+ return { name: c?.name ?? "", symbol: c?.symbol ?? "", sellable: balance, refused: null, priceBnb: c?.priceBnb ?? 0, valueBnb: (c?.priceBnb ?? 0) * Number(formatEther(balance)) };
74
+ }
75
+ const r = await pub.multicall({ contracts: [{ address: token, abi: erc20Abi, functionName: "name" }, { address: token, abi: erc20Abi, functionName: "symbol" }], allowFailure: true });
76
+ return { name: r[0].status === "success" ? String(r[0].result) : "", symbol: r[1].status === "success" ? String(r[1].result) : "", sellable: balance, refused: null, priceBnb: 0, valueBnb: 0 };
77
+ }
78
+ /**
79
+ * Everything a wallet holds on pickhooks: the newest `limit` coins of every factory (default 300) are checked for a
80
+ * balance, the held ones read in full. Claims and locks come from the hooks and the agents desk, floors from dip bids.
81
+ */
82
+ async function wallet(who, o = {}) {
83
+ const { list, scanned } = await tokens(o.limit ?? 300);
84
+ const [bnb, balances] = await Promise.all([
85
+ pub.getBalance({ address: who }),
86
+ list.length ? pub.multicall({ contracts: list.map((t) => ({ address: t.token, abi: erc20Abi, functionName: "balanceOf", args: [who] })), allowFailure: true }) : [],
87
+ ]);
88
+ const held = list.filter((_, i) => balances[i]?.status === "success" && balances[i].result > 0n).map((t, j) => ({ ...t, balance: balances[list.indexOf(t)].result, j }));
89
+ const coins = await Promise.all(held.map(async (h) => ({ token: h.token, mode: h.mode, balance: h.balance, page: page(h.mode, h.token), ...(await detail(h.mode, h.token, who, h.balance)) })));
90
+ coins.sort((x, y) => y.valueBnb - x.valueBnb);
91
+ const agentTokens = list.filter((t) => t.mode === "agents").map((t) => t.token);
92
+ const [any, pools, calls, agents, lockRows, floors] = await Promise.all([
93
+ mods.any.earnings(who).catch(() => null),
94
+ mods.pools.owed(who).catch(() => 0n),
95
+ mods.calls.owed(who).catch(() => 0n),
96
+ mods.agents.on ? mods.agents.owed(who).catch(() => 0n) : Promise.resolve(0n),
97
+ Promise.all(agentTokens.map((t) => mods.agents.locks(t, who).then((l) => ({ token: t, ...l })).catch(() => null))),
98
+ mods.dips.floorsOf(who).catch(() => []),
99
+ ]);
100
+ const locks = lockRows.filter((l) => l !== null && l.amount > 0n);
101
+ const lockSymbols = locks.length ? await pub.multicall({ contracts: locks.map((l) => ({ address: l.token, abi: erc20Abi, functionName: "symbol" })), allowFailure: true }) : [];
102
+ const anyBnb = any ? any.owed + any.routerClaimable + any.taxOwed : 0n;
103
+ return {
104
+ address: who,
105
+ bnb,
106
+ coins,
107
+ valueBnb: coins.reduce((s, c) => s + c.valueBnb, 0),
108
+ claims: { any, pools, calls, agents, totalBnb: anyBnb + pools + calls + agents },
109
+ locks: locks.map((l, i) => ({ ...l, symbol: lockSymbols[i]?.status === "success" ? String(lockSymbols[i].result) : "" })),
110
+ floors,
111
+ scanned,
112
+ };
113
+ }
114
+ return wallet;
115
+ }
@@ -0,0 +1,82 @@
1
+ import { type Address, type Hex } from "viem";
2
+ import type { AgentsModule } from "./agents.js";
3
+ import type { AnyModule } from "./any.js";
4
+ import type { CoinsModule } from "./coins.js";
5
+ import type { CallsModule, PoolsModule } from "./pools.js";
6
+ import type { StickyModule } from "./sticky.js";
7
+ import type { Ctx } from "./tx.js";
8
+ /**
9
+ * Live events, for terminals and bots: every launch on the pad, every trade of a coin, everything that happens to an
10
+ * agent coin. Each watcher polls the RPC (HTTP is enough) and returns a function that stops it.
11
+ *
12
+ * const stop = ph.watch.launches((l) => console.log(l.mode, l.symbol, l.token));
13
+ * const stop2 = await ph.watch.trades(token, (t) => console.log(t.buy ? "buy" : "sell", t.bnb, t.coins));
14
+ * const stop3 = await ph.watch.agent(token, (e) => console.log(e.event, e.args));
15
+ */
16
+ export type WatchMode = "sticky" | "sticky2" | "rules" | "rules-v1" | "pools" | "calls" | "lever" | "agents" | "any";
17
+ export type WatchedLaunch = {
18
+ mode: WatchMode;
19
+ token: Address;
20
+ creator: Address;
21
+ /** read from the token when `details` is on (default) */
22
+ name: string;
23
+ symbol: string;
24
+ /** the V3 pool and V2 pair (sticky, rules), the Infinity pool id (pools, calls, lever, agents, any) */
25
+ pool?: Address;
26
+ pair?: Address;
27
+ poolId?: Hex;
28
+ factory: Address;
29
+ block: bigint;
30
+ tx: Hex;
31
+ /** the raw event as viem decoded it */
32
+ args: Record<string, unknown>;
33
+ };
34
+ export type WatchedTrade = {
35
+ token: Address;
36
+ buy: boolean;
37
+ /** the trader where the event names one (the hooks do; V3 and V2 name the router's sender and the recipient) */
38
+ trader: Address | null;
39
+ /** coins moved (18 decimals), BNB moved (wei); the hooks' events carry the fee or the BNB, the pools' Swap both sides */
40
+ coins: bigint;
41
+ bnb: bigint | null;
42
+ /** the hook's fee in BNB (pools, calls, agents) */
43
+ fee: bigint | null;
44
+ /** v3, v2 (PancakeSwap pools of sticky and rules coins), or the hook's event (pools, calls, agents, any) */
45
+ source: "v3" | "v2" | "hook";
46
+ block: bigint;
47
+ tx: Hex;
48
+ };
49
+ export type AgentEvent = {
50
+ token: Address;
51
+ /** Proposed, Cancelled, Activated, Vetoed, VetoBacked, FireBacked, PilotFired, PilotHandedOver, Locked, Withdrawn, TreasuryMoved (desk); Burned, LiquidityAdded, FloorFed, Moved, Given, ProgramSet, JackpotWon, RewardEarned, Traded (hook) */
52
+ event: string;
53
+ args: Record<string, unknown>;
54
+ block: bigint;
55
+ tx: Hex;
56
+ };
57
+ export type WatchOptions = {
58
+ /** ms between polls (default 3000, about one BNB Chain block) */
59
+ pollingInterval?: number;
60
+ /** errors of the poll (an RPC hiccup); default: ignored, the next poll tries again */
61
+ onError?: (e: Error) => void;
62
+ };
63
+ type Mods = {
64
+ sticky: StickyModule;
65
+ sticky2: StickyModule;
66
+ coins: CoinsModule;
67
+ pools: PoolsModule;
68
+ calls: CallsModule;
69
+ agents: AgentsModule;
70
+ any: AnyModule;
71
+ };
72
+ export declare function watchModule(ctx: Ctx, mods: Mods): {
73
+ launches: (cb: (l: WatchedLaunch) => void, o?: WatchOptions & {
74
+ modes?: WatchMode[];
75
+ details?: boolean;
76
+ }) => () => void;
77
+ trades: (token: Address, cb: (t: WatchedTrade) => void, o?: WatchOptions) => Promise<() => void>;
78
+ agent: (token: Address, cb: (e: AgentEvent) => void, o?: WatchOptions) => Promise<() => void>;
79
+ modes: WatchMode[];
80
+ };
81
+ export type WatchModule = ReturnType<typeof watchModule>;
82
+ export {};