@nodiusxyz/solana-agent-kit 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ export type Args = { command: string; [key: string]: string | boolean };
2
+ const common = ["endpoint"];
3
+ const trade = [
4
+ "mint",
5
+ "wallet",
6
+ "keypair",
7
+ "max-sol",
8
+ "max-fee-credits",
9
+ "slippage-bps",
10
+ "priority-fee",
11
+ "max-priority-fee",
12
+ "max-priority-lamports",
13
+ "journal",
14
+ "operation-id",
15
+ "execute",
16
+ "build-only",
17
+ "accept-unquoted-build-charge",
18
+ "ack-incomplete-policy",
19
+ ];
20
+ const flags: Record<string, string[]> = {
21
+ buy: [...trade, "amount-sol"],
22
+ sell: [...trade, "amount-raw"],
23
+ send: [
24
+ "tx-file",
25
+ "wallet",
26
+ "max-fee-credits",
27
+ "journal",
28
+ "operation-id",
29
+ "execute",
30
+ ],
31
+ inspect: ["wallet"],
32
+ migrate: ["wallet"],
33
+ recent: ["limit"],
34
+ monitor: ["seconds", "max-events", "mint", "kinds"],
35
+ status: ["journal", "operation-id"],
36
+ help: [],
37
+ };
38
+ const booleans = new Set([
39
+ "execute",
40
+ "build-only",
41
+ "accept-unquoted-build-charge",
42
+ "ack-incomplete-policy",
43
+ ]);
44
+ export function parseArgs(argv: string[]): Args {
45
+ const command = argv[0] ?? "help";
46
+ if (!(command in flags)) throw new Error("Unknown command");
47
+ const out: Args = { command };
48
+ for (let i = 1; i < argv.length; i++) {
49
+ const flag = argv[i];
50
+ if (!flag.startsWith("--"))
51
+ throw new Error("Unexpected positional argument");
52
+ const k = flag.slice(2);
53
+ if (![...common, ...flags[command]].includes(k) || k in out)
54
+ throw new Error("Unknown or duplicate flag: " + flag);
55
+ if (booleans.has(k)) out[k] = true;
56
+ else {
57
+ const v = argv[++i];
58
+ if (!v || v.startsWith("--")) throw new Error("Missing value: " + flag);
59
+ out[k] = v;
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+ export function required(a: Args, k: string): string {
65
+ const v = a[k];
66
+ if (typeof v !== "string" || !v) throw new Error("Required --" + k);
67
+ return v;
68
+ }
69
+ export function integer(s: string, max = Number.MAX_SAFE_INTEGER): number {
70
+ if (!/^(0|[1-9]\d*)$/.test(s)) throw new Error("Invalid integer");
71
+ const n = Number(s);
72
+ if (!Number.isSafeInteger(n) || n > max)
73
+ throw new Error("Integer exceeds cap");
74
+ return n;
75
+ }
76
+ export function rawAmount(s: string): bigint {
77
+ if (!/^[1-9]\d*$/.test(s))
78
+ throw new Error("Amount must be a positive raw integer");
79
+ const n = BigInt(s);
80
+ if (n > 18446744073709551615n) throw new Error("Amount exceeds u64");
81
+ return n;
82
+ }
83
+ export function solLamports(s: string): bigint {
84
+ if (!/^(0|[1-9]\d*)(\.\d{1,9})?$/.test(s))
85
+ throw new Error("SOL requires plain decimal with <=9 places");
86
+ const [w, f = ""] = s.split(".");
87
+ return BigInt(w) * 1000000000n + BigInt(f.padEnd(9, "0"));
88
+ }
89
+ export function endpoint(s: string): string {
90
+ const u = new URL(s);
91
+ if (
92
+ u.username ||
93
+ u.password ||
94
+ u.search ||
95
+ u.hash ||
96
+ !["", "/"].includes(u.pathname) ||
97
+ !(
98
+ u.protocol === "https:" ||
99
+ (u.protocol === "http:" &&
100
+ ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname))
101
+ )
102
+ )
103
+ throw new Error(
104
+ "Use HTTPS origin (HTTP only for loopback), no URL credentials or path",
105
+ );
106
+ return u.origin;
107
+ }
@@ -0,0 +1,145 @@
1
+ import { mkdir, open, readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ export function fingerprint(value: unknown): string {
5
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
6
+ }
7
+ export function outcome(status: any): "confirmed" | "failed" | "unknown" {
8
+ if (status?.err != null) return "failed";
9
+ if (
10
+ status?.confirmationStatus === "confirmed" ||
11
+ status?.confirmationStatus === "finalized"
12
+ )
13
+ return "confirmed";
14
+ return "unknown";
15
+ }
16
+ export class Journal {
17
+ constructor(
18
+ readonly directory: string,
19
+ private readonly credential?: string,
20
+ ) {}
21
+ private encode(value: unknown): string {
22
+ const scrub = (v: any): any => {
23
+ const text = (s: string) =>
24
+ this.credential ? s.split(this.credential).join("[REDACTED]") : s;
25
+ if (typeof v === "string") return text(v);
26
+ if (Array.isArray(v)) return v.map(scrub);
27
+ if (v && typeof v === "object")
28
+ return Object.fromEntries(
29
+ Object.entries(v).map(([k, item]) => [text(k), scrub(item)]),
30
+ );
31
+ return v;
32
+ };
33
+ return JSON.stringify(scrub(value));
34
+ }
35
+ private async reserve(kind: string, key: string, value: unknown) {
36
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
37
+ const path = join(this.directory, kind + "-" + fingerprint(key) + ".json");
38
+ const file = await open(path, "wx", 0o600);
39
+ try {
40
+ await file.writeFile(this.encode(value) + "\n");
41
+ await file.sync();
42
+ } finally {
43
+ await file.close();
44
+ }
45
+ const dir = await open(this.directory, "r");
46
+ try {
47
+ await dir.sync();
48
+ } finally {
49
+ await dir.close();
50
+ }
51
+ }
52
+ async begin(id: string, hash: string, metadata: unknown) {
53
+ if (!/^[A-Za-z0-9_-]{1,80}$/.test(id))
54
+ throw new Error("Invalid operation id");
55
+ await this.reserve("operation", id, {
56
+ id,
57
+ metadata,
58
+ state: "unknown",
59
+ phase: "reserved",
60
+ });
61
+ const lockPath = join(this.directory, "intent-lock-" + fingerprint(hash));
62
+ const lock = await open(lockPath, "wx", 0o600);
63
+ try {
64
+ const path = join(
65
+ this.directory,
66
+ "fingerprint-" + fingerprint(hash) + ".json",
67
+ );
68
+ let existing: any;
69
+ try {
70
+ existing = JSON.parse(await readFile(path, "utf8"));
71
+ } catch (e: any) {
72
+ if (e.code !== "ENOENT") throw e;
73
+ }
74
+ if (existing) {
75
+ const previous = await this.read(existing.id);
76
+ if (
77
+ !["confirmed", "failed"].includes(previous.state) ||
78
+ !previous.signature
79
+ )
80
+ throw new Error(
81
+ "Previous identical intent is unresolved; use status, never force/rebuild",
82
+ );
83
+ }
84
+ const f = await open(path, "w", 0o600);
85
+ try {
86
+ await f.writeFile(this.encode({ id }) + "\n");
87
+ await f.sync();
88
+ } finally {
89
+ await f.close();
90
+ }
91
+ } finally {
92
+ await lock.close();
93
+ await unlink(lockPath);
94
+ }
95
+ }
96
+ async reserveSignature(signature: string) {
97
+ await this.reserve("signature", signature, { signature });
98
+ }
99
+ async event(id: string, event: Record<string, unknown>) {
100
+ const f = await open(
101
+ join(this.directory, "events-" + fingerprint(id) + ".jsonl"),
102
+ "a",
103
+ 0o600,
104
+ );
105
+ try {
106
+ await f.writeFile(
107
+ this.encode({ ...event, at: new Date().toISOString() }) + "\n",
108
+ );
109
+ await f.sync();
110
+ } finally {
111
+ await f.close();
112
+ }
113
+ }
114
+ async read(id: string): Promise<any> {
115
+ let start: any;
116
+ try {
117
+ start = JSON.parse(
118
+ await readFile(
119
+ join(this.directory, "operation-" + fingerprint(id) + ".json"),
120
+ "utf8",
121
+ ),
122
+ );
123
+ } catch {
124
+ throw new Error("Operation not found");
125
+ }
126
+ let text = "";
127
+ try {
128
+ text = await readFile(
129
+ join(this.directory, "events-" + fingerprint(id) + ".jsonl"),
130
+ "utf8",
131
+ );
132
+ } catch (e: any) {
133
+ if (e.code !== "ENOENT") throw e;
134
+ }
135
+ for (const line of text.split("\n").filter(Boolean)) {
136
+ try {
137
+ Object.assign(start, JSON.parse(line));
138
+ } catch {
139
+ start.state = "unknown";
140
+ start.phase = "corrupt-journal";
141
+ }
142
+ }
143
+ return start;
144
+ }
145
+ }
@@ -0,0 +1,141 @@
1
+ import WebSocket from "ws";
2
+ import { wsUrlFor } from "@nodiusxyz/sdk";
3
+ import { Transport, finite } from "./transport.js";
4
+ export interface MonitorOptions {
5
+ seconds: number;
6
+ maxFeeCredits?: number;
7
+ maxEvents: number;
8
+ filters: Record<string, unknown>;
9
+ signal?: AbortSignal;
10
+ }
11
+ export async function monitor(
12
+ origin: string,
13
+ apiKey: string,
14
+ o: MonitorOptions,
15
+ emit: (v: any) => void,
16
+ ) {
17
+ if (
18
+ !apiKey ||
19
+ !Number.isFinite(o.seconds) ||
20
+ o.seconds <= 0 ||
21
+ o.seconds > 300 ||
22
+ !Number.isSafeInteger(o.maxEvents) ||
23
+ o.maxEvents <= 0 ||
24
+ o.maxEvents > 1000
25
+ )
26
+ throw new Error("Monitor requires auth, <=300 seconds and <=1000 events");
27
+ const price = await new Transport(origin, apiKey).pricing();
28
+ const m = price.methods;
29
+ const upper =
30
+ finite(m.subscribeSignal) +
31
+ finite(m.signalNotification) * o.maxEvents +
32
+ finite(m.signalUnsubscribe);
33
+ if (
34
+ !Number.isSafeInteger(upper) ||
35
+ (o.maxFeeCredits !== undefined &&
36
+ (!Number.isSafeInteger(o.maxFeeCredits) ||
37
+ o.maxFeeCredits < 0 ||
38
+ upper > o.maxFeeCredits))
39
+ )
40
+ throw new Error(
41
+ "Monitor live estimate exceeds fee budget or unsafe pricing",
42
+ );
43
+ emit({
44
+ state: "monitor-plan",
45
+ seconds: o.seconds,
46
+ max_events: o.maxEvents,
47
+ estimated_stream_credits: upper,
48
+ note: "Server may bill queued events beyond locally received count; not a strict billing cap.",
49
+ });
50
+ const ws = new WebSocket(wsUrlFor(origin), {
51
+ headers: { "X-Api-Key": apiKey },
52
+ handshakeTimeout: Math.min(o.seconds * 1000, 10000),
53
+ });
54
+ let next = 0,
55
+ count = 0,
56
+ sub: string | undefined;
57
+ const pending = new Map<
58
+ number,
59
+ { resolve: (v: any) => void; reject: (e: Error) => void }
60
+ >();
61
+ let end!: () => void;
62
+ const done = new Promise<void>((r) => (end = r));
63
+ let deadlineReject!: (e: Error) => void;
64
+ const deadline = new Promise<never>((_, r) => (deadlineReject = r));
65
+ deadline.catch(() => {});
66
+ const expire = () => {
67
+ end();
68
+ deadlineReject(new Error("Monitor deadline/cancellation"));
69
+ };
70
+ const timer = setTimeout(expire, o.seconds * 1000);
71
+ o.signal?.addEventListener("abort", expire, { once: true });
72
+ ws.on("message", (b) => {
73
+ let q: any;
74
+ try {
75
+ q = JSON.parse(b.toString());
76
+ } catch {
77
+ return;
78
+ }
79
+ if (pending.has(q.id)) {
80
+ const p = pending.get(q.id)!;
81
+ pending.delete(q.id);
82
+ if (q.error) p.reject(new Error("Signals RPC rejected"));
83
+ else p.resolve(q.result);
84
+ } else if (
85
+ sub &&
86
+ q.method === "signalNotification" &&
87
+ q.params?.subscription === sub &&
88
+ count < o.maxEvents
89
+ ) {
90
+ count++;
91
+ emit(q.params.result);
92
+ if (count === o.maxEvents) end();
93
+ }
94
+ });
95
+ ws.on("error", () => expire());
96
+ ws.on("close", () => expire());
97
+ const call = (method: string, params: unknown[]) =>
98
+ new Promise<any>((resolve, reject) => {
99
+ const id = ++next;
100
+ pending.set(id, { resolve, reject });
101
+ ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }), (e) => {
102
+ if (e) {
103
+ pending.delete(id);
104
+ reject(new Error("Signals send failed"));
105
+ }
106
+ });
107
+ });
108
+ try {
109
+ await Promise.race([
110
+ new Promise<void>((resolve, reject) => {
111
+ ws.once("open", resolve);
112
+ ws.once("error", () => reject(new Error("Signals connection failed")));
113
+ }),
114
+ deadline,
115
+ ]);
116
+ sub = String(
117
+ await Promise.race([call("subscribeSignal", [o.filters]), deadline]),
118
+ );
119
+ await done;
120
+ return { state: "monitor-complete", events: count, subscription: sub };
121
+ } finally {
122
+ clearTimeout(timer);
123
+ o.signal?.removeEventListener("abort", expire);
124
+ if (sub && ws.readyState === WebSocket.OPEN) {
125
+ let cleanup: NodeJS.Timeout | undefined;
126
+ try {
127
+ await Promise.race([
128
+ call("signalUnsubscribe", [sub]),
129
+ new Promise<void>((r) => (cleanup = setTimeout(r, 500))),
130
+ ]);
131
+ } catch {
132
+ /* best effort unsubscribe, close still guaranteed */
133
+ } finally {
134
+ if (cleanup) clearTimeout(cleanup);
135
+ }
136
+ }
137
+ ws.terminate();
138
+ for (const p of pending.values()) p.reject(new Error("Monitor closed"));
139
+ pending.clear();
140
+ }
141
+ }
@@ -0,0 +1,259 @@
1
+ import {
2
+ PublicKey,
3
+ VersionedTransaction,
4
+ VersionedMessage,
5
+ TransactionMessage,
6
+ ComputeBudgetProgram,
7
+ } from "@solana/web3.js";
8
+ import bs58 from "bs58";
9
+ import { verifyV2 } from "./v2-policy.js";
10
+ import nacl from "tweetnacl";
11
+ export const PUMP = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
12
+ const ATA = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
13
+ const TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
14
+ const TOKEN2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
15
+ export function decodeBase64(s: string): Buffer {
16
+ const b = Buffer.from(s, "base64");
17
+ if (!s || b.toString("base64") !== s) throw new Error("Noncanonical base64");
18
+ return b;
19
+ }
20
+ export function decodeBuilderUnsigned(encoded: string): VersionedTransaction {
21
+ const bytes = decodeBase64(encoded);
22
+ if (bytes.length > 1232)
23
+ throw new Error("Builder transaction exceeds packet limit");
24
+ if (bytes[0] === 0) {
25
+ // A canonical zero-count signature shortvec is one byte. Never relax the
26
+ // already-signed send parser or normalize any nonzero signature count.
27
+ const message = VersionedMessage.deserialize(bytes.subarray(1));
28
+ if (
29
+ message.header.numRequiredSignatures !== 1 ||
30
+ !Buffer.from(message.serialize()).equals(bytes.subarray(1))
31
+ )
32
+ throw new Error("Noncanonical unsigned message or signer count");
33
+ return new VersionedTransaction(message);
34
+ }
35
+ const tx = VersionedTransaction.deserialize(bytes);
36
+ if (!Buffer.from(tx.serialize()).equals(bytes))
37
+ throw new Error("Noncanonical builder transaction");
38
+ return tx;
39
+ }
40
+
41
+ export function inspectEnvelope(tx: VersionedTransaction, wallet: PublicKey) {
42
+ if (tx.message.version !== 0 || tx.message.addressTableLookups.length)
43
+ throw new Error("Unsigned inspection requires v0 without lookup tables");
44
+ if (
45
+ tx.message.header.numRequiredSignatures !== 1 ||
46
+ !tx.message.staticAccountKeys[0]?.equals(wallet)
47
+ )
48
+ throw new Error("Unsigned builder payer/signer mismatch");
49
+ if (
50
+ tx.signatures.length > 1 ||
51
+ tx.signatures.some((s) => s.some((b) => b !== 0))
52
+ )
53
+ throw new Error("Builder must have zero or one empty signature");
54
+ // Server builders may omit all signatures. A zero placeholder only normalizes
55
+ // the unsigned envelope; this never signs or authorizes instruction semantics.
56
+ if (tx.signatures.length === 0) tx.signatures = [new Uint8Array(64)];
57
+ if (tx.serialize().length > 1232)
58
+ throw new Error("Transaction exceeds packet limit");
59
+ const message = TransactionMessage.decompile(tx.message);
60
+ return {
61
+ signing_authorized: false,
62
+ instruction_count: message.instructions.length,
63
+ instructions: message.instructions.map((ix) => ({
64
+ program: ix.programId.toBase58(),
65
+ accounts: ix.keys.map((k) => ({
66
+ address: k.pubkey.toBase58(),
67
+ signer: k.isSigner,
68
+ writable: k.isWritable,
69
+ })),
70
+ data_hex: Buffer.from(ix.data).toString("hex"),
71
+ })),
72
+ note: "Unsigned envelope inspection only; instruction semantics are not authorized for signing.",
73
+ };
74
+ }
75
+
76
+ export interface Policy {
77
+ side: "buy" | "sell";
78
+ mint: PublicKey;
79
+ maxSol: bigint;
80
+ amountRaw?: bigint;
81
+ maxPriority: bigint;
82
+ maxPriorityLamports?: bigint;
83
+ requestedPriority?: bigint;
84
+ tokensOut?: bigint;
85
+ minOutput?: bigint;
86
+ reportedCost: bigint;
87
+ }
88
+ export function verifyUnsigned(
89
+ tx: VersionedTransaction,
90
+ wallet: PublicKey,
91
+ p: Policy,
92
+ ) {
93
+ inspectEnvelope(tx, wallet);
94
+ if (
95
+ tx.message.header.numRequiredSignatures !== 1 ||
96
+ !tx.message.staticAccountKeys[0].equals(wallet)
97
+ )
98
+ throw new Error("Fee payer or signer mismatch");
99
+ if (tx.signatures.some((s) => s.some((b) => b !== 0)))
100
+ throw new Error("Builder must be unsigned");
101
+ if (tx.message.version === 0 && tx.message.addressTableLookups.length)
102
+ throw new Error("Address lookup tables unsupported by policy");
103
+ const msg = TransactionMessage.decompile(tx.message);
104
+ if (
105
+ msg.instructions.some(
106
+ (ix) =>
107
+ ix.programId.toBase58() === PUMP &&
108
+ ["b817ee6167c5d33d", "5df6823ce7e940b2"].includes(
109
+ Buffer.from(ix.data).subarray(0, 8).toString("hex"),
110
+ ),
111
+ )
112
+ ) {
113
+ return verifyV2(tx, wallet, p);
114
+ }
115
+ let sellToken: PublicKey | undefined, sellAta: PublicKey | undefined;
116
+ let closes = 0;
117
+ let trades = 0,
118
+ atas = 0,
119
+ tradeCost = 0n;
120
+ const instructions: any[] = [];
121
+ const seenCompute = new Set<number>();
122
+ let price = 0n,
123
+ limit = 1400000n;
124
+ for (const ix of msg.instructions) {
125
+ const program = ix.programId.toBase58(),
126
+ data = Buffer.from(ix.data);
127
+ instructions.push({
128
+ program,
129
+ accounts: ix.keys.map((k) => ({
130
+ address: k.pubkey.toBase58(),
131
+ writable: k.isWritable,
132
+ signer: k.isSigner,
133
+ })),
134
+ data_hex: data.toString("hex"),
135
+ });
136
+ if (program === ComputeBudgetProgram.programId.toBase58()) {
137
+ if (ix.keys.length || seenCompute.has(data[0]))
138
+ throw new Error("Malformed/duplicate compute instruction");
139
+ seenCompute.add(data[0]);
140
+ if (data[0] === 3 && data.length === 9) {
141
+ price = data.readBigUInt64LE(1);
142
+ if (price > p.maxPriority) throw new Error("Priority fee exceeds cap");
143
+ } else if (data[0] === 2 && (data.length === 5 || data.length === 9)) {
144
+ if (data.length === 9 && data.readUInt32LE(5) !== 0)
145
+ throw new Error("Legacy CU upper bytes must be zero");
146
+ limit = BigInt(data.readUInt32LE(1));
147
+ if (limit <= 0n || limit > 1400000n)
148
+ throw new Error("Compute unit limit exceeds runtime cap");
149
+ } else throw new Error("Unsupported compute instruction");
150
+ } else if (program === ATA && p.side === "buy") {
151
+ if (
152
+ ++atas > 1 ||
153
+ data.length !== 1 ||
154
+ data[0] !== 1 ||
155
+ ix.keys.length !== 6 ||
156
+ !ix.keys[0].pubkey.equals(wallet) ||
157
+ !ix.keys[2].pubkey.equals(wallet) ||
158
+ !ix.keys[3].pubkey.equals(p.mint) ||
159
+ ix.keys[4].pubkey.toBase58() !== "11111111111111111111111111111111" ||
160
+ ![TOKEN, TOKEN2022].includes(ix.keys[5].pubkey.toBase58())
161
+ )
162
+ throw new Error("ATA policy mismatch");
163
+ const expected = PublicKey.findProgramAddressSync(
164
+ [wallet.toBuffer(), ix.keys[5].pubkey.toBuffer(), p.mint.toBuffer()],
165
+ new PublicKey(ATA),
166
+ )[0];
167
+ if (!ix.keys[1].pubkey.equals(expected))
168
+ throw new Error("ATA derivation mismatch");
169
+ } else if (program === PUMP) {
170
+ if (
171
+ ++trades !== 1 ||
172
+ !ix.keys[2]?.pubkey.equals(p.mint) ||
173
+ !ix.keys[6]?.pubkey.equals(wallet)
174
+ )
175
+ throw new Error("Pump mint/user policy mismatch");
176
+ const disc = Buffer.from(
177
+ p.side === "buy"
178
+ ? [102, 6, 61, 18, 1, 218, 235, 234]
179
+ : [51, 230, 133, 164, 1, 127, 131, 173],
180
+ );
181
+ if (
182
+ !data.subarray(0, 8).equals(disc) ||
183
+ data.length !== (p.side === "buy" ? 25 : 24) ||
184
+ ix.keys.length !== (p.side === "buy" ? 18 : 16)
185
+ )
186
+ throw new Error(
187
+ "Only tested classic pump layout supported (v2/cashback rejected)",
188
+ );
189
+ const amount = data.readBigUInt64LE(8),
190
+ bound = data.readBigUInt64LE(16);
191
+ if (amount === 0n || bound === 0n)
192
+ throw new Error("Zero trade/slippage guard");
193
+ if (p.side === "buy") {
194
+ tradeCost = bound;
195
+ if (bound > p.maxSol || bound !== p.reportedCost)
196
+ throw new Error(
197
+ "Decoded SOL amount exceeds cap or differs from response",
198
+ );
199
+ } else if (amount !== p.amountRaw || bound !== p.reportedCost)
200
+ throw new Error("Sell amount/min-output mismatch");
201
+ if (p.side === "sell") {
202
+ sellToken = ix.keys[9].pubkey;
203
+ sellAta = ix.keys[5].pubkey;
204
+ }
205
+ } else if (p.side === "sell" && [TOKEN, TOKEN2022].includes(program)) {
206
+ const expected = PublicKey.findProgramAddressSync(
207
+ [wallet.toBuffer(), ix.programId.toBuffer(), p.mint.toBuffer()],
208
+ new PublicKey(ATA),
209
+ )[0];
210
+ if (
211
+ ++closes !== 1 ||
212
+ trades !== 1 ||
213
+ ix !== msg.instructions[msg.instructions.length - 1] ||
214
+ data.length !== 1 ||
215
+ data[0] !== 9 ||
216
+ ix.keys.length !== 3 ||
217
+ !sellToken?.equals(ix.programId) ||
218
+ !sellAta?.equals(expected) ||
219
+ !ix.keys[0].pubkey.equals(expected) ||
220
+ !ix.keys[0].isWritable ||
221
+ ix.keys[0].isSigner ||
222
+ ix.keys
223
+ .slice(1)
224
+ .some((k) => !k.pubkey.equals(wallet) || !k.isSigner || !k.isWritable)
225
+ )
226
+ throw Error("Sell close account policy mismatch");
227
+ } else throw new Error("Unexpected instruction program: " + program);
228
+ }
229
+ if (trades !== 1) throw new Error("Exactly one trade required");
230
+ if (p.requestedPriority !== undefined && price !== p.requestedPriority)
231
+ throw new Error("Priority differs from requested price");
232
+ if (
233
+ p.maxPriorityLamports !== undefined &&
234
+ (price * limit + 999999n) / 1000000n > p.maxPriorityLamports
235
+ )
236
+ throw new Error("Total priority exceeds lamport cap");
237
+ return {
238
+ tradeCost,
239
+ ataCount: atas,
240
+ instructions,
241
+ limitations:
242
+ "Partial policy: does not validate all pump PDAs, fee recipients, program state or CPI behavior. Not a complete safety proof.",
243
+ };
244
+ }
245
+ export function checkSignature(tx: VersionedTransaction): string {
246
+ const count = tx.message.header.numRequiredSignatures;
247
+ if (count < 1 || tx.signatures.length !== count)
248
+ throw new Error("Missing signatures");
249
+ for (let i = 0; i < count; i++)
250
+ if (
251
+ !nacl.sign.detached.verify(
252
+ tx.message.serialize(),
253
+ tx.signatures[i],
254
+ tx.message.staticAccountKeys[i].toBytes(),
255
+ )
256
+ )
257
+ throw new Error("Invalid transaction signature");
258
+ return bs58.encode(tx.signatures[0]);
259
+ }