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