@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.
- package/LICENSE +21 -0
- package/README.md +131 -2
- package/dist/index.d.ts +281 -0
- package/dist/index.js +277 -0
- package/dist/internal/admission.d.ts +11 -0
- package/dist/internal/admission.js +96 -0
- package/dist/internal/app.d.ts +6 -0
- package/dist/internal/app.js +422 -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 +135 -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 +199 -0
- package/dist/internal/transport.d.ts +23 -0
- package/dist/internal/transport.js +97 -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 +62 -4
- package/src/index.ts +297 -0
- package/src/internal/admission.ts +131 -0
- package/src/internal/app.ts +509 -0
- package/src/internal/config.ts +107 -0
- package/src/internal/journal.ts +145 -0
- package/src/internal/monitor.ts +141 -0
- package/src/internal/safety.ts +259 -0
- package/src/internal/transport.ts +112 -0
- package/src/internal/v2-policy.ts +315 -0
|
@@ -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,199 @@
|
|
|
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 sellToken, sellAta;
|
|
79
|
+
let closes = 0;
|
|
80
|
+
let trades = 0, atas = 0, tradeCost = 0n;
|
|
81
|
+
const instructions = [];
|
|
82
|
+
const seenCompute = new Set();
|
|
83
|
+
let price = 0n, limit = 1400000n;
|
|
84
|
+
for (const ix of msg.instructions) {
|
|
85
|
+
const program = ix.programId.toBase58(), data = Buffer.from(ix.data);
|
|
86
|
+
instructions.push({
|
|
87
|
+
program,
|
|
88
|
+
accounts: ix.keys.map((k) => ({
|
|
89
|
+
address: k.pubkey.toBase58(),
|
|
90
|
+
writable: k.isWritable,
|
|
91
|
+
signer: k.isSigner,
|
|
92
|
+
})),
|
|
93
|
+
data_hex: data.toString("hex"),
|
|
94
|
+
});
|
|
95
|
+
if (program === ComputeBudgetProgram.programId.toBase58()) {
|
|
96
|
+
if (ix.keys.length || seenCompute.has(data[0]))
|
|
97
|
+
throw new Error("Malformed/duplicate compute instruction");
|
|
98
|
+
seenCompute.add(data[0]);
|
|
99
|
+
if (data[0] === 3 && data.length === 9) {
|
|
100
|
+
price = data.readBigUInt64LE(1);
|
|
101
|
+
if (price > p.maxPriority)
|
|
102
|
+
throw new Error("Priority fee exceeds cap");
|
|
103
|
+
}
|
|
104
|
+
else if (data[0] === 2 && (data.length === 5 || data.length === 9)) {
|
|
105
|
+
if (data.length === 9 && data.readUInt32LE(5) !== 0)
|
|
106
|
+
throw new Error("Legacy CU upper bytes must be zero");
|
|
107
|
+
limit = BigInt(data.readUInt32LE(1));
|
|
108
|
+
if (limit <= 0n || limit > 1400000n)
|
|
109
|
+
throw new Error("Compute unit limit exceeds runtime cap");
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
throw new Error("Unsupported compute instruction");
|
|
113
|
+
}
|
|
114
|
+
else if (program === ATA && p.side === "buy") {
|
|
115
|
+
if (++atas > 1 ||
|
|
116
|
+
data.length !== 1 ||
|
|
117
|
+
data[0] !== 1 ||
|
|
118
|
+
ix.keys.length !== 6 ||
|
|
119
|
+
!ix.keys[0].pubkey.equals(wallet) ||
|
|
120
|
+
!ix.keys[2].pubkey.equals(wallet) ||
|
|
121
|
+
!ix.keys[3].pubkey.equals(p.mint) ||
|
|
122
|
+
ix.keys[4].pubkey.toBase58() !== "11111111111111111111111111111111" ||
|
|
123
|
+
![TOKEN, TOKEN2022].includes(ix.keys[5].pubkey.toBase58()))
|
|
124
|
+
throw new Error("ATA policy mismatch");
|
|
125
|
+
const expected = PublicKey.findProgramAddressSync([wallet.toBuffer(), ix.keys[5].pubkey.toBuffer(), p.mint.toBuffer()], new PublicKey(ATA))[0];
|
|
126
|
+
if (!ix.keys[1].pubkey.equals(expected))
|
|
127
|
+
throw new Error("ATA derivation mismatch");
|
|
128
|
+
}
|
|
129
|
+
else if (program === PUMP) {
|
|
130
|
+
if (++trades !== 1 ||
|
|
131
|
+
!ix.keys[2]?.pubkey.equals(p.mint) ||
|
|
132
|
+
!ix.keys[6]?.pubkey.equals(wallet))
|
|
133
|
+
throw new Error("Pump mint/user policy mismatch");
|
|
134
|
+
const disc = Buffer.from(p.side === "buy"
|
|
135
|
+
? [102, 6, 61, 18, 1, 218, 235, 234]
|
|
136
|
+
: [51, 230, 133, 164, 1, 127, 131, 173]);
|
|
137
|
+
if (!data.subarray(0, 8).equals(disc) ||
|
|
138
|
+
data.length !== (p.side === "buy" ? 25 : 24) ||
|
|
139
|
+
ix.keys.length !== (p.side === "buy" ? 18 : 16))
|
|
140
|
+
throw new Error("Only tested classic pump layout supported (v2/cashback rejected)");
|
|
141
|
+
const amount = data.readBigUInt64LE(8), bound = data.readBigUInt64LE(16);
|
|
142
|
+
if (amount === 0n || bound === 0n)
|
|
143
|
+
throw new Error("Zero trade/slippage guard");
|
|
144
|
+
if (p.side === "buy") {
|
|
145
|
+
tradeCost = bound;
|
|
146
|
+
if (bound > p.maxSol || bound !== p.reportedCost)
|
|
147
|
+
throw new Error("Decoded SOL amount exceeds cap or differs from response");
|
|
148
|
+
}
|
|
149
|
+
else if (amount !== p.amountRaw || bound !== p.reportedCost)
|
|
150
|
+
throw new Error("Sell amount/min-output mismatch");
|
|
151
|
+
if (p.side === "sell") {
|
|
152
|
+
sellToken = ix.keys[9].pubkey;
|
|
153
|
+
sellAta = ix.keys[5].pubkey;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (p.side === "sell" && [TOKEN, TOKEN2022].includes(program)) {
|
|
157
|
+
const expected = PublicKey.findProgramAddressSync([wallet.toBuffer(), ix.programId.toBuffer(), p.mint.toBuffer()], new PublicKey(ATA))[0];
|
|
158
|
+
if (++closes !== 1 ||
|
|
159
|
+
trades !== 1 ||
|
|
160
|
+
ix !== msg.instructions[msg.instructions.length - 1] ||
|
|
161
|
+
data.length !== 1 ||
|
|
162
|
+
data[0] !== 9 ||
|
|
163
|
+
ix.keys.length !== 3 ||
|
|
164
|
+
!sellToken?.equals(ix.programId) ||
|
|
165
|
+
!sellAta?.equals(expected) ||
|
|
166
|
+
!ix.keys[0].pubkey.equals(expected) ||
|
|
167
|
+
!ix.keys[0].isWritable ||
|
|
168
|
+
ix.keys[0].isSigner ||
|
|
169
|
+
ix.keys
|
|
170
|
+
.slice(1)
|
|
171
|
+
.some((k) => !k.pubkey.equals(wallet) || !k.isSigner || !k.isWritable))
|
|
172
|
+
throw Error("Sell close account policy mismatch");
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
throw new Error("Unexpected instruction program: " + program);
|
|
176
|
+
}
|
|
177
|
+
if (trades !== 1)
|
|
178
|
+
throw new Error("Exactly one trade required");
|
|
179
|
+
if (p.requestedPriority !== undefined && price !== p.requestedPriority)
|
|
180
|
+
throw new Error("Priority differs from requested price");
|
|
181
|
+
if (p.maxPriorityLamports !== undefined &&
|
|
182
|
+
(price * limit + 999999n) / 1000000n > p.maxPriorityLamports)
|
|
183
|
+
throw new Error("Total priority exceeds lamport cap");
|
|
184
|
+
return {
|
|
185
|
+
tradeCost,
|
|
186
|
+
ataCount: atas,
|
|
187
|
+
instructions,
|
|
188
|
+
limitations: "Partial policy: does not validate all pump PDAs, fee recipients, program state or CPI behavior. Not a complete safety proof.",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function checkSignature(tx) {
|
|
192
|
+
const count = tx.message.header.numRequiredSignatures;
|
|
193
|
+
if (count < 1 || tx.signatures.length !== count)
|
|
194
|
+
throw new Error("Missing signatures");
|
|
195
|
+
for (let i = 0; i < count; i++)
|
|
196
|
+
if (!nacl.sign.detached.verify(tx.message.serialize(), tx.signatures[i], tx.message.staticAccountKeys[i].toBytes()))
|
|
197
|
+
throw new Error("Invalid transaction signature");
|
|
198
|
+
return bs58.encode(tx.signatures[0]);
|
|
199
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { NodiusClient } from "@nodiusxyz/sdk";
|
|
2
|
+
import { Connection } from "@solana/web3.js";
|
|
3
|
+
import { endpoint, rawAmount } from "./config.js";
|
|
4
|
+
export function finite(n) {
|
|
5
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < 0)
|
|
6
|
+
throw new Error("Invalid live numeric field");
|
|
7
|
+
return n;
|
|
8
|
+
}
|
|
9
|
+
export function estimateBuy(p, lamports) {
|
|
10
|
+
const c = finite(p.credits_per_usdc), q = p.sol_usdc, m = p.methods?.executeSnipe;
|
|
11
|
+
if (!q || finite(q.age_secs) > 15 || finite(q.price) <= 0 || !m || c <= 0)
|
|
12
|
+
throw new Error("Fresh authoritative buy pricing unavailable");
|
|
13
|
+
const v = finite(m.build_fee_credits) +
|
|
14
|
+
Math.ceil(((Number(lamports) / 1e9) * q.price * c * finite(m.flow_fee_bps)) / 10000);
|
|
15
|
+
if (!Number.isSafeInteger(v) || v <= 0 || !Number.isSafeInteger(c) || c <= 0)
|
|
16
|
+
throw new Error("Unsafe pricing estimate/rate");
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
export function sendFee(p) {
|
|
20
|
+
const m = p.methods?.sendAndConfirm;
|
|
21
|
+
const base = finite(m?.base_credits);
|
|
22
|
+
// landed_credits is the terminal TOTAL, not a surcharge on base_credits.
|
|
23
|
+
const fee = Math.max(base, finite(m?.landed_credits));
|
|
24
|
+
if (!Number.isSafeInteger(fee) || fee <= 0)
|
|
25
|
+
throw new Error("Invalid sendAndConfirm live fee");
|
|
26
|
+
return fee;
|
|
27
|
+
}
|
|
28
|
+
export class Transport {
|
|
29
|
+
apiKey;
|
|
30
|
+
sdk;
|
|
31
|
+
connection;
|
|
32
|
+
origin;
|
|
33
|
+
constructor(origin, apiKey) {
|
|
34
|
+
this.apiKey = apiKey;
|
|
35
|
+
this.origin = endpoint(origin);
|
|
36
|
+
this.sdk = new NodiusClient(this.origin, {
|
|
37
|
+
apiKey,
|
|
38
|
+
autoPay: false,
|
|
39
|
+
maxRetries: 0,
|
|
40
|
+
timeout: 10000,
|
|
41
|
+
});
|
|
42
|
+
this.connection = new Connection(this.origin + "/rpc", {
|
|
43
|
+
commitment: "confirmed",
|
|
44
|
+
httpHeaders: apiKey ? { "X-Api-Key": apiKey } : {},
|
|
45
|
+
disableRetryOnRateLimit: true,
|
|
46
|
+
fetch: (url, init) => fetch(url, {
|
|
47
|
+
...init,
|
|
48
|
+
signal: AbortSignal.timeout(10000),
|
|
49
|
+
redirect: "error",
|
|
50
|
+
cache: "no-store",
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async pricing() {
|
|
55
|
+
const r = await fetch(this.origin + "/pricing", {
|
|
56
|
+
signal: AbortSignal.timeout(10000),
|
|
57
|
+
cache: "no-store",
|
|
58
|
+
redirect: "error",
|
|
59
|
+
});
|
|
60
|
+
if (!r.ok)
|
|
61
|
+
throw new Error("Pricing HTTP failure");
|
|
62
|
+
const p = (await r.json());
|
|
63
|
+
if (p.ok !== true)
|
|
64
|
+
throw new Error("Invalid pricing response");
|
|
65
|
+
finite(p.credits_per_usdc);
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
// The published SDK converts bigint to a JSON STRING, but rpc.rs requires as_u64().
|
|
69
|
+
// Emit validated digits as an unquoted numeric JSON literal, never through Number.
|
|
70
|
+
async sell(mint, amount, slippage, priority) {
|
|
71
|
+
rawAmount(amount);
|
|
72
|
+
const params = JSON.stringify({
|
|
73
|
+
mint,
|
|
74
|
+
slippage_bps: slippage,
|
|
75
|
+
priority_fee_micro_lamports: priority,
|
|
76
|
+
});
|
|
77
|
+
const body = `{"jsonrpc":"2.0","id":1,"method":"executeSell","params":[${params.slice(0, -1)},"token_amount":${amount}}]}`;
|
|
78
|
+
const r = await fetch(this.origin + "/rpc", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
...(this.apiKey ? { "X-Api-Key": this.apiKey } : {}),
|
|
83
|
+
},
|
|
84
|
+
body,
|
|
85
|
+
signal: AbortSignal.timeout(10000),
|
|
86
|
+
redirect: "error",
|
|
87
|
+
});
|
|
88
|
+
if (!r.ok)
|
|
89
|
+
throw new Error("Sell build HTTP failure (charge may be uncertain)");
|
|
90
|
+
const v = (await r.json());
|
|
91
|
+
if (v.error)
|
|
92
|
+
throw new Error("Sell builder rejected; consult account ledger");
|
|
93
|
+
if (!v.result)
|
|
94
|
+
throw new Error("Malformed sell build result");
|
|
95
|
+
return v.result;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
|
|
2
|
+
export interface V2Policy {
|
|
3
|
+
side: "buy" | "sell";
|
|
4
|
+
mint: PublicKey;
|
|
5
|
+
maxSol: bigint;
|
|
6
|
+
amountRaw?: bigint;
|
|
7
|
+
maxPriority: bigint;
|
|
8
|
+
maxPriorityLamports?: bigint;
|
|
9
|
+
requestedPriority?: bigint;
|
|
10
|
+
reportedCost: bigint;
|
|
11
|
+
tokensOut?: bigint;
|
|
12
|
+
}
|
|
13
|
+
/** Offline instruction policy only. Never signs, mutates or fetches account state. */
|
|
14
|
+
export declare function verifyV2(tx: VersionedTransaction, wallet: PublicKey, p: V2Policy): {
|
|
15
|
+
tradeCost: bigint;
|
|
16
|
+
ataCount: number;
|
|
17
|
+
instructions: any[];
|
|
18
|
+
limitations: string;
|
|
19
|
+
};
|