@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,168 @@
|
|
|
1
|
+
import { PublicKey, TransactionMessage, ComputeBudgetProgram, SystemProgram, } from "@solana/web3.js";
|
|
2
|
+
const PUMP = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
|
3
|
+
const FEE = new PublicKey("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
|
4
|
+
const ATA = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
|
5
|
+
const TOKEN = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
|
6
|
+
const TOKEN22 = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
|
|
7
|
+
const WSOL = new PublicKey("So11111111111111111111111111111111111111112");
|
|
8
|
+
const SYSTEM = SystemProgram.programId;
|
|
9
|
+
const pda = (program, seed, ...keys) => PublicKey.findProgramAddressSync([Buffer.from(seed), ...keys.map((k) => k.toBuffer())], program)[0];
|
|
10
|
+
const ata = (owner, mint, token) => PublicKey.findProgramAddressSync([owner.toBuffer(), token.toBuffer(), mint.toBuffer()], ATA)[0];
|
|
11
|
+
function requirePolicy(ok, reason) {
|
|
12
|
+
if (!ok)
|
|
13
|
+
throw new Error("V2 policy: " + reason);
|
|
14
|
+
}
|
|
15
|
+
/** Offline instruction policy only. Never signs, mutates or fetches account state. */
|
|
16
|
+
export function verifyV2(tx, wallet, p) {
|
|
17
|
+
requirePolicy(p.side === "buy" || p.side === "sell", "unknown side");
|
|
18
|
+
requirePolicy(p.maxPriority >= 0n && p.maxSol >= 0n, "negative budget");
|
|
19
|
+
const m = tx.message;
|
|
20
|
+
requirePolicy(m.version === 0 && m.addressTableLookups.length === 0, "requires v0 without ALTs");
|
|
21
|
+
requirePolicy(m.header.numRequiredSignatures === 1 &&
|
|
22
|
+
m.staticAccountKeys[0]?.equals(wallet) &&
|
|
23
|
+
m.isAccountWritable(0), "payer/signer mismatch");
|
|
24
|
+
requirePolicy(tx.signatures.length <= 1 &&
|
|
25
|
+
tx.signatures.every((s) => s.length === 64 && s.every((b) => b === 0)), "must be unsigned");
|
|
26
|
+
// Account for the one 64-byte signing placeholder even when builder omits it.
|
|
27
|
+
requirePolicy(1 + 64 + m.serialize().length <= 1232, "signed envelope exceeds packet size");
|
|
28
|
+
const instructions = TransactionMessage.decompile(m).instructions;
|
|
29
|
+
const buy = p.side === "buy";
|
|
30
|
+
requirePolicy(buy
|
|
31
|
+
? instructions.length === 8
|
|
32
|
+
: instructions.length === 3 || instructions.length === 4, "unexpected instruction sequence");
|
|
33
|
+
let price = 0n, limit = 0n;
|
|
34
|
+
for (let i = 0; i < 2; i++) {
|
|
35
|
+
const ix = instructions[i], d = Buffer.from(ix.data);
|
|
36
|
+
requirePolicy(ix.programId.equals(ComputeBudgetProgram.programId) &&
|
|
37
|
+
ix.keys.length === 0, "compute program/accounts");
|
|
38
|
+
if (i === 0) {
|
|
39
|
+
requirePolicy(d.length === 9 && d[0] === 3, "canonical compute price required first");
|
|
40
|
+
price = d.readBigUInt64LE(1);
|
|
41
|
+
requirePolicy(price <= p.maxPriority, "CU price exceeds explicit cap");
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
requirePolicy((d.length === 5 || d.length === 9) && d[0] === 2, "canonical compute limit required second");
|
|
45
|
+
requirePolicy(d.length === 5 || d.readUInt32LE(5) === 0, "legacy CU high bytes nonzero");
|
|
46
|
+
limit = BigInt(d.readUInt32LE(1));
|
|
47
|
+
requirePolicy(limit >= 1n && limit <= 1400000n, "CU limit outside runtime bounds");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (p.requestedPriority !== undefined)
|
|
51
|
+
requirePolicy(price === p.requestedPriority, "priority price differs from explicit request");
|
|
52
|
+
if (p.maxPriorityLamports !== undefined)
|
|
53
|
+
requirePolicy(p.maxPriorityLamports >= 0n &&
|
|
54
|
+
(price * limit + 999999n) / 1000000n <= p.maxPriorityLamports, "total priority fee exceeds lamport cap");
|
|
55
|
+
const trade = instructions[buy ? 7 : 2], d = Buffer.from(trade.data);
|
|
56
|
+
requirePolicy(trade.programId.equals(PUMP) && trade.keys.length === (buy ? 27 : 26), "one Pump trade layout required");
|
|
57
|
+
requirePolicy(d.length === 24 &&
|
|
58
|
+
d
|
|
59
|
+
.subarray(0, 8)
|
|
60
|
+
.equals(Buffer.from(buy ? "b817ee6167c5d33d" : "5df6823ce7e940b2", "hex")), "trade discriminator/payload");
|
|
61
|
+
const quantity = d.readBigUInt64LE(8), bound = d.readBigUInt64LE(16);
|
|
62
|
+
requirePolicy(quantity > 0n && bound > 0n && bound === p.reportedCost, "zero or reported trade bound mismatch");
|
|
63
|
+
if (buy)
|
|
64
|
+
requirePolicy(quantity === p.tokensOut && bound <= p.maxSol, "buy tokensOut/trade cap mismatch");
|
|
65
|
+
else
|
|
66
|
+
requirePolicy(quantity === p.amountRaw, "sell exact raw quantity mismatch");
|
|
67
|
+
const k = trade.keys;
|
|
68
|
+
const baseToken = k[3].pubkey;
|
|
69
|
+
requirePolicy(baseToken.equals(TOKEN) || baseToken.equals(TOKEN22), "unsupported base token program");
|
|
70
|
+
const curve = pda(PUMP, "bonding-curve", p.mint), userVolume = pda(PUMP, "user_volume_accumulator", wallet);
|
|
71
|
+
const baseAta = ata(wallet, p.mint, baseToken), wsolAta = ata(wallet, WSOL, TOKEN);
|
|
72
|
+
const volumeIndex = buy ? 20 : 19, feeIndex = buy ? 22 : 21;
|
|
73
|
+
const expected = new Map([
|
|
74
|
+
[0, pda(PUMP, "global")],
|
|
75
|
+
[1, p.mint],
|
|
76
|
+
[2, WSOL],
|
|
77
|
+
[3, baseToken],
|
|
78
|
+
[4, TOKEN],
|
|
79
|
+
[5, ATA],
|
|
80
|
+
[7, ata(k[6].pubkey, WSOL, TOKEN)],
|
|
81
|
+
[9, ata(k[8].pubkey, WSOL, TOKEN)],
|
|
82
|
+
[10, curve],
|
|
83
|
+
[11, ata(curve, p.mint, baseToken)],
|
|
84
|
+
[12, ata(curve, WSOL, TOKEN)],
|
|
85
|
+
[13, wallet],
|
|
86
|
+
[14, baseAta],
|
|
87
|
+
[15, wsolAta],
|
|
88
|
+
[17, ata(k[16].pubkey, WSOL, TOKEN)],
|
|
89
|
+
[18, pda(FEE, "sharing-config", p.mint)],
|
|
90
|
+
[volumeIndex, userVolume],
|
|
91
|
+
[volumeIndex + 1, ata(userVolume, WSOL, TOKEN)],
|
|
92
|
+
// Source execute_snipe.rs: ["fee_config", pump_program_id] under pfee.
|
|
93
|
+
[feeIndex, pda(FEE, "fee_config", PUMP)],
|
|
94
|
+
[feeIndex + 1, FEE],
|
|
95
|
+
[feeIndex + 2, SYSTEM],
|
|
96
|
+
[feeIndex + 3, pda(PUMP, "__event_authority")],
|
|
97
|
+
[feeIndex + 4, PUMP],
|
|
98
|
+
]);
|
|
99
|
+
if (buy)
|
|
100
|
+
expected.set(19, pda(PUMP, "global_volume_accumulator"));
|
|
101
|
+
for (const [i, address] of expected)
|
|
102
|
+
requirePolicy(k[i].pubkey.equals(address), "trade account/PDA mismatch at " + i);
|
|
103
|
+
const readonly = new Set([
|
|
104
|
+
0,
|
|
105
|
+
1,
|
|
106
|
+
2,
|
|
107
|
+
3,
|
|
108
|
+
4,
|
|
109
|
+
5,
|
|
110
|
+
18,
|
|
111
|
+
feeIndex,
|
|
112
|
+
feeIndex + 1,
|
|
113
|
+
feeIndex + 2,
|
|
114
|
+
feeIndex + 3,
|
|
115
|
+
feeIndex + 4,
|
|
116
|
+
]);
|
|
117
|
+
k.forEach((key, i) => requirePolicy(key.isSigner === (i === 13) && key.isWritable === !readonly.has(i), "trade privilege mismatch at " + i));
|
|
118
|
+
// Privileges are message-global: wallet aliases in ATA/close authority positions
|
|
119
|
+
// are also writable signers. Exact comparison rejects elevation of fixed programs.
|
|
120
|
+
function keys(ix, addresses, writable, signer) {
|
|
121
|
+
requirePolicy(ix.keys.length === addresses.length &&
|
|
122
|
+
ix.keys.every((key, i) => key.pubkey.equals(addresses[i]) &&
|
|
123
|
+
key.isWritable === writable[i] &&
|
|
124
|
+
key.isSigner === signer[i]), "auxiliary accounts/privileges");
|
|
125
|
+
}
|
|
126
|
+
function create(ix, mint, token, address) {
|
|
127
|
+
requirePolicy(ix.programId.equals(ATA) && ix.data.length === 1 && ix.data[0] === 1, "idempotent ATA required");
|
|
128
|
+
keys(ix, [wallet, address, wallet, mint, SYSTEM, token], [true, true, true, false, false, false], [true, false, true, false, false, false]);
|
|
129
|
+
}
|
|
130
|
+
function close(ix, token, address) {
|
|
131
|
+
requirePolicy(ix.programId.equals(token) && ix.data.length === 1 && ix.data[0] === 9, "exact token close required");
|
|
132
|
+
keys(ix, [address, wallet, wallet], [true, true, true], [false, true, true]);
|
|
133
|
+
}
|
|
134
|
+
if (buy) {
|
|
135
|
+
// This current server order was simulated successfully; closing after buy is
|
|
136
|
+
// NOT equivalent in that test. This order is not a funding-lock proof.
|
|
137
|
+
create(instructions[2], WSOL, TOKEN, wsolAta);
|
|
138
|
+
const transfer = instructions[3], td = Buffer.from(transfer.data);
|
|
139
|
+
requirePolicy(transfer.programId.equals(SYSTEM) &&
|
|
140
|
+
td.length === 12 &&
|
|
141
|
+
td.readUInt32LE(0) === 2, "exact System transfer required");
|
|
142
|
+
keys(transfer, [wallet, wsolAta], [true, true], [true, false]);
|
|
143
|
+
requirePolicy(td.readBigUInt64LE(4) <= bound, "funding exceeds decoded buy bound");
|
|
144
|
+
const sync = instructions[4];
|
|
145
|
+
requirePolicy(sync.programId.equals(TOKEN) &&
|
|
146
|
+
sync.data.length === 1 &&
|
|
147
|
+
sync.data[0] === 17, "exact SyncNative required");
|
|
148
|
+
keys(sync, [wsolAta], [true], [false]);
|
|
149
|
+
close(instructions[5], TOKEN, wsolAta);
|
|
150
|
+
create(instructions[6], p.mint, baseToken, baseAta);
|
|
151
|
+
}
|
|
152
|
+
else if (instructions.length === 4)
|
|
153
|
+
close(instructions[3], baseToken, baseAta);
|
|
154
|
+
return {
|
|
155
|
+
tradeCost: buy ? bound : 0n,
|
|
156
|
+
ataCount: buy ? 2 : 0,
|
|
157
|
+
instructions: instructions.map((ix) => ({
|
|
158
|
+
program: ix.programId.toBase58(),
|
|
159
|
+
accounts: ix.keys.map((k) => ({
|
|
160
|
+
address: k.pubkey.toBase58(),
|
|
161
|
+
writable: k.isWritable,
|
|
162
|
+
signer: k.isSigner,
|
|
163
|
+
})),
|
|
164
|
+
data_hex: Buffer.from(ix.data).toString("hex"),
|
|
165
|
+
})),
|
|
166
|
+
limitations: "Partial offline policy, not a complete protocol safety proof. Dynamic fee recipients and creator vault are trusted builder/state inputs; only their associated WSOL derivations are checked. Account ownership/state, dynamic fees, rent, inventory/full-sell eligibility and program/CPI behavior are not proven. Buy maxSol is a trade cap, not an aggregate CPI-fee/rent/network-spend cap; the observed pre-buy refund does not lock funds. Caller must separately bound exact message fees/rent and require explicit --ack-incomplete-policy consent before signing.",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SolanaAgentKit, type BaseWallet } from "solana-agent-kit";
|
|
2
|
+
import { PublicKey } from "@solana/web3.js";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { createNodiusPlugin } from "../src/index.js";
|
|
5
|
+
export async function readonlyExample(env: NodeJS.ProcessEnv = process.env) {
|
|
6
|
+
const forbidden = async (): Promise<never> => {
|
|
7
|
+
throw new Error("Read-only wallet cannot sign or send");
|
|
8
|
+
};
|
|
9
|
+
const wallet: BaseWallet = {
|
|
10
|
+
publicKey: new PublicKey(
|
|
11
|
+
env.NODIUS_PUBLIC_KEY ?? "11111111111111111111111111111111",
|
|
12
|
+
),
|
|
13
|
+
signTransaction: forbidden,
|
|
14
|
+
signAllTransactions: forbidden,
|
|
15
|
+
signAndSendTransaction: forbidden,
|
|
16
|
+
signMessage: forbidden,
|
|
17
|
+
};
|
|
18
|
+
const endpoint = env.NODIUS_ENDPOINT ?? "https://rpc.nodius.xyz";
|
|
19
|
+
const agent = new SolanaAgentKit(wallet, endpoint + "/rpc", {}).use(
|
|
20
|
+
createNodiusPlugin({
|
|
21
|
+
apiKey: env.NODIUS_API_KEY,
|
|
22
|
+
endpoint,
|
|
23
|
+
journalDir: env.NODIUS_JOURNAL ?? ".nodius-journal",
|
|
24
|
+
allowExecution: false,
|
|
25
|
+
ackIncompletePolicy: false,
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
return agent.methods.nodiusRecent({ limit: 5 });
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
process.argv[1] &&
|
|
32
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
33
|
+
) {
|
|
34
|
+
readonlyExample()
|
|
35
|
+
.then((result) => console.log(JSON.stringify(result, null, 2)))
|
|
36
|
+
.catch(() => {
|
|
37
|
+
console.error(
|
|
38
|
+
"Read-only request failed; verify host endpoint/API key. No signing or sending occurred.",
|
|
39
|
+
);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
});
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodiusxyz/solana-agent-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Opt-in bounded nodius.xyz plugin for Solana Agent Kit",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.20.2"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"examples",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"test": "tsx --test test/*.test.ts",
|
|
29
|
+
"example:readonly": "tsx examples/readonly.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nodiusxyz/sdk": "0.2.15",
|
|
33
|
+
"@solana/web3.js": "1.99.0",
|
|
34
|
+
"@solana/spl-token": "0.4.15",
|
|
35
|
+
"bs58": "6.0.0",
|
|
36
|
+
"tweetnacl": "1.0.3",
|
|
37
|
+
"ws": "8.22.0",
|
|
38
|
+
"zod": "3.25.76"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"solana-agent-kit": "2.0.10"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "22.19.11",
|
|
45
|
+
"@types/ws": "8.18.1",
|
|
46
|
+
"solana-agent-kit": "2.0.10",
|
|
47
|
+
"tsx": "4.21.0",
|
|
48
|
+
"typescript": "5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"overrides": {
|
|
51
|
+
"jayson": "5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://nodius.xyz/docs/solana-agent-kit.html",
|
|
54
|
+
"author": "nodius.xyz",
|
|
55
|
+
"keywords": [
|
|
56
|
+
"solana-agent-kit",
|
|
57
|
+
"solana",
|
|
58
|
+
"pumpfun",
|
|
59
|
+
"trading",
|
|
60
|
+
"agent",
|
|
61
|
+
"plugin",
|
|
62
|
+
"nodius"
|
|
63
|
+
]
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { SolanaAgentKit, Plugin } from "solana-agent-kit";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { run } from "./internal/app.js";
|
|
4
|
+
import { endpoint as validateEndpoint, type Args } from "./internal/config.js";
|
|
5
|
+
export { decodeBuilderUnsigned } from "./internal/safety.js";
|
|
6
|
+
export interface NodiusOptions {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
journalDir: string;
|
|
10
|
+
allowExecution?: boolean;
|
|
11
|
+
ackIncompletePolicy?: boolean;
|
|
12
|
+
}
|
|
13
|
+
const integer = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
14
|
+
const decimal = z.string().regex(/^(0|[1-9]\d*)(\.\d{1,9})?$/);
|
|
15
|
+
const base = {
|
|
16
|
+
mint: z.string().min(32).max(44),
|
|
17
|
+
maxSol: decimal,
|
|
18
|
+
maxFeeCredits: integer,
|
|
19
|
+
slippageBps: integer.max(9999),
|
|
20
|
+
priorityFee: integer,
|
|
21
|
+
maxPriorityFee: integer,
|
|
22
|
+
maxPriorityLamports: integer.optional(),
|
|
23
|
+
execute: z.boolean().default(false),
|
|
24
|
+
buildOnly: z.boolean().default(false),
|
|
25
|
+
operationId: z
|
|
26
|
+
.string()
|
|
27
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
28
|
+
.optional(),
|
|
29
|
+
acceptUnquotedBuildCharge: z.boolean().default(false),
|
|
30
|
+
};
|
|
31
|
+
export const tradeSchema = z.discriminatedUnion("side", [
|
|
32
|
+
z.object({ ...base, side: z.literal("buy"), amountSol: decimal }).strict(),
|
|
33
|
+
z
|
|
34
|
+
.object({
|
|
35
|
+
...base,
|
|
36
|
+
side: z.literal("sell"),
|
|
37
|
+
amountRaw: z.string().regex(/^[1-9]\d*$/),
|
|
38
|
+
})
|
|
39
|
+
.strict(),
|
|
40
|
+
]);
|
|
41
|
+
const tradeActionSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
...base,
|
|
44
|
+
side: z.enum(["buy", "sell"]),
|
|
45
|
+
amountSol: decimal.optional(),
|
|
46
|
+
amountRaw: z
|
|
47
|
+
.string()
|
|
48
|
+
.regex(/^[1-9]\d*$/)
|
|
49
|
+
.optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict();
|
|
52
|
+
export const monitorSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
maxFeeCredits: integer,
|
|
55
|
+
seconds: integer.min(1).max(300).default(30),
|
|
56
|
+
maxEvents: integer.min(1).max(1000).default(100),
|
|
57
|
+
mint: z.string().min(32).max(44).optional(),
|
|
58
|
+
kinds: z
|
|
59
|
+
.array(z.enum(["newPair", "graduation", "walletActivity", "largeSwap"]))
|
|
60
|
+
.min(1)
|
|
61
|
+
.max(4)
|
|
62
|
+
.optional(),
|
|
63
|
+
})
|
|
64
|
+
.strict();
|
|
65
|
+
export const statusSchema = z
|
|
66
|
+
.object({ operationId: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/) })
|
|
67
|
+
.strict();
|
|
68
|
+
export const sendSchema = z
|
|
69
|
+
.object({
|
|
70
|
+
signedTxBase64: z.string().min(1).max(1644),
|
|
71
|
+
maxFeeCredits: integer,
|
|
72
|
+
execute: z.boolean().default(false),
|
|
73
|
+
operationId: z
|
|
74
|
+
.string()
|
|
75
|
+
.regex(/^[A-Za-z0-9_-]{1,80}$/)
|
|
76
|
+
.optional(),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
export type SendInput = z.input<typeof sendSchema>;
|
|
80
|
+
export type StatusInput = z.input<typeof statusSchema>;
|
|
81
|
+
export type MonitorInput = z.input<typeof monitorSchema>;
|
|
82
|
+
export type TradeInput = z.input<typeof tradeSchema>;
|
|
83
|
+
export type NodiusResult = Record<string, unknown>;
|
|
84
|
+
export function createNodiusPlugin(options: NodiusOptions) {
|
|
85
|
+
const parsed = z
|
|
86
|
+
.object({
|
|
87
|
+
apiKey: z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.regex(/^[^\r\n]+$/)
|
|
91
|
+
.optional(),
|
|
92
|
+
endpoint: z.string().optional(),
|
|
93
|
+
journalDir: z.string().min(1),
|
|
94
|
+
allowExecution: z.boolean().default(false),
|
|
95
|
+
ackIncompletePolicy: z.boolean().default(false),
|
|
96
|
+
})
|
|
97
|
+
.strict()
|
|
98
|
+
.parse(options);
|
|
99
|
+
const o = Object.freeze({
|
|
100
|
+
...parsed,
|
|
101
|
+
endpoint: validateEndpoint(parsed.endpoint ?? "https://rpc.nodius.xyz"),
|
|
102
|
+
});
|
|
103
|
+
let owner: SolanaAgentKit | undefined;
|
|
104
|
+
const recent = z
|
|
105
|
+
.object({ limit: integer.min(1).max(50).default(20) })
|
|
106
|
+
.strict();
|
|
107
|
+
function clean(value: unknown): any {
|
|
108
|
+
const text = (s: string) =>
|
|
109
|
+
o.apiKey ? s.split(o.apiKey).join("[REDACTED]") : s;
|
|
110
|
+
if (typeof value === "string") return text(value);
|
|
111
|
+
if (typeof value === "bigint") return value.toString();
|
|
112
|
+
if (Array.isArray(value)) return value.map(clean);
|
|
113
|
+
if (value && typeof value === "object")
|
|
114
|
+
return Object.fromEntries(
|
|
115
|
+
Object.entries(value)
|
|
116
|
+
.filter(([, v]) => v !== undefined)
|
|
117
|
+
.map(([k, v]) => [text(k), clean(v)]),
|
|
118
|
+
);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
async function invoke(
|
|
122
|
+
agent: SolanaAgentKit,
|
|
123
|
+
input: unknown,
|
|
124
|
+
kind: "recent" | "trade" | "monitor" | "status" | "send",
|
|
125
|
+
): Promise<NodiusResult> {
|
|
126
|
+
try {
|
|
127
|
+
return clean(await dispatch(agent, input, kind));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
throw new Error(clean(e instanceof Error ? e.message : String(e)));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function dispatch(
|
|
133
|
+
agent: SolanaAgentKit,
|
|
134
|
+
input: unknown,
|
|
135
|
+
kind: "recent" | "trade" | "monitor" | "status" | "send",
|
|
136
|
+
): Promise<NodiusResult> {
|
|
137
|
+
if (!owner || agent !== owner) throw Error("Plugin agent mismatch");
|
|
138
|
+
let args: Args = {
|
|
139
|
+
command: kind,
|
|
140
|
+
endpoint: o.endpoint ?? "https://rpc.nodius.xyz",
|
|
141
|
+
journal: o.journalDir,
|
|
142
|
+
};
|
|
143
|
+
if (kind === "recent") {
|
|
144
|
+
const v = recent.parse(input);
|
|
145
|
+
args.limit = String(v.limit);
|
|
146
|
+
} else if (kind === "send") {
|
|
147
|
+
const v = sendSchema.parse(input);
|
|
148
|
+
if (v.execute && o.allowExecution !== true)
|
|
149
|
+
throw Error("Host execution is disabled");
|
|
150
|
+
if (v.execute && o.ackIncompletePolicy !== true)
|
|
151
|
+
throw Error("Host incomplete policy acknowledgement required");
|
|
152
|
+
args["signed-tx-base64"] = v.signedTxBase64;
|
|
153
|
+
args.wallet = owner.wallet.publicKey.toBase58();
|
|
154
|
+
args["max-fee-credits"] = String(v.maxFeeCredits);
|
|
155
|
+
args.execute = v.execute;
|
|
156
|
+
if (v.operationId) args["operation-id"] = v.operationId;
|
|
157
|
+
} else if (kind === "status") {
|
|
158
|
+
const v = statusSchema.parse(input);
|
|
159
|
+
args["operation-id"] = v.operationId;
|
|
160
|
+
} else if (kind === "monitor") {
|
|
161
|
+
const v = monitorSchema.parse(input);
|
|
162
|
+
args["max-fee-credits"] = String(v.maxFeeCredits);
|
|
163
|
+
args.seconds = String(v.seconds);
|
|
164
|
+
args["max-events"] = String(v.maxEvents);
|
|
165
|
+
if (v.mint) args.mint = v.mint;
|
|
166
|
+
if (v.kinds) args.kinds = v.kinds.join(",");
|
|
167
|
+
} else {
|
|
168
|
+
const v = tradeSchema.parse(input);
|
|
169
|
+
if (v.execute && o.allowExecution !== true)
|
|
170
|
+
throw Error("Host execution is disabled");
|
|
171
|
+
if (v.execute && !v.buildOnly && o.ackIncompletePolicy !== true)
|
|
172
|
+
throw Error("Host incomplete policy acknowledgement required");
|
|
173
|
+
args.command = v.side;
|
|
174
|
+
args.wallet = owner.wallet.publicKey.toBase58();
|
|
175
|
+
const map: Record<string, string> = {
|
|
176
|
+
amountSol: "amount-sol",
|
|
177
|
+
amountRaw: "amount-raw",
|
|
178
|
+
maxSol: "max-sol",
|
|
179
|
+
maxFeeCredits: "max-fee-credits",
|
|
180
|
+
slippageBps: "slippage-bps",
|
|
181
|
+
priorityFee: "priority-fee",
|
|
182
|
+
maxPriorityFee: "max-priority-fee",
|
|
183
|
+
maxPriorityLamports: "max-priority-lamports",
|
|
184
|
+
operationId: "operation-id",
|
|
185
|
+
buildOnly: "build-only",
|
|
186
|
+
acceptUnquotedBuildCharge: "accept-unquoted-build-charge",
|
|
187
|
+
};
|
|
188
|
+
for (const [k, value] of Object.entries(v)) {
|
|
189
|
+
if (k === "side") continue;
|
|
190
|
+
args[map[k] ?? k] = typeof value === "boolean" ? value : String(value);
|
|
191
|
+
}
|
|
192
|
+
args["ack-incomplete-policy"] = o.ackIncompletePolicy === true;
|
|
193
|
+
}
|
|
194
|
+
const notifications: unknown[] = [];
|
|
195
|
+
let monitorPlan: Record<string, unknown> = {};
|
|
196
|
+
const result = await run(
|
|
197
|
+
args,
|
|
198
|
+
{ NODIUS_API_KEY: o.apiKey },
|
|
199
|
+
(v) => {
|
|
200
|
+
if (kind === "monitor") {
|
|
201
|
+
if (v.state === "monitor-plan") monitorPlan = v;
|
|
202
|
+
else notifications.push(v);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
publicKey: owner.wallet.publicKey,
|
|
207
|
+
signTransaction: (tx) => owner!.wallet.signTransaction(tx),
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
return kind === "monitor"
|
|
211
|
+
? { ...monitorPlan, ...result, notifications }
|
|
212
|
+
: result;
|
|
213
|
+
}
|
|
214
|
+
const plugin = {
|
|
215
|
+
name: "nodius",
|
|
216
|
+
methods: {
|
|
217
|
+
nodiusSend: (input: SendInput) => invoke(owner!, input, "send"),
|
|
218
|
+
nodiusStatus: (input: StatusInput) => invoke(owner!, input, "status"),
|
|
219
|
+
nodiusMonitor: (input: MonitorInput) => invoke(owner!, input, "monitor"),
|
|
220
|
+
nodiusRecent: (input: { limit?: number } = {}) =>
|
|
221
|
+
invoke(owner!, input, "recent"),
|
|
222
|
+
nodiusTrade: (input: TradeInput) => invoke(owner!, input, "trade"),
|
|
223
|
+
},
|
|
224
|
+
actions: [
|
|
225
|
+
{
|
|
226
|
+
name: "nodiusSend",
|
|
227
|
+
similes: [],
|
|
228
|
+
description:
|
|
229
|
+
"Submit independently reviewed, already signed own-wallet transaction. Defaults plan; no trade instruction proof. Requires both host permissions.",
|
|
230
|
+
examples: [],
|
|
231
|
+
schema: sendSchema,
|
|
232
|
+
handler: (a: SolanaAgentKit, i: unknown) => invoke(a, i, "send"),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "nodiusStatus",
|
|
236
|
+
similes: [],
|
|
237
|
+
description:
|
|
238
|
+
"Reconcile local operation status; never rebuild or rebroadcast",
|
|
239
|
+
examples: [],
|
|
240
|
+
schema: statusSchema,
|
|
241
|
+
handler: (a: SolanaAgentKit, i: unknown) => invoke(a, i, "status"),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "nodiusMonitor",
|
|
245
|
+
similes: [],
|
|
246
|
+
description:
|
|
247
|
+
"Paid bounded signal monitor (<=300s, <=1000 events); no autonomous trading",
|
|
248
|
+
examples: [],
|
|
249
|
+
schema: monitorSchema,
|
|
250
|
+
handler: (a: SolanaAgentKit, i: unknown) => invoke(a, i, "monitor"),
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "nodiusRecent",
|
|
254
|
+
similes: [],
|
|
255
|
+
description: "Read recent Nodius signals; never trades",
|
|
256
|
+
examples: [],
|
|
257
|
+
schema: recent,
|
|
258
|
+
handler: (a: SolanaAgentKit, i: unknown) => invoke(a, i, "recent"),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "nodiusTrade",
|
|
262
|
+
similes: [],
|
|
263
|
+
description:
|
|
264
|
+
"Buy/sell defaults to read-only plan. Host must separately authorize charged execution and partial policy.",
|
|
265
|
+
examples: [],
|
|
266
|
+
schema: tradeActionSchema,
|
|
267
|
+
handler: (a: SolanaAgentKit, i: unknown) => invoke(a, i, "trade"),
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
initialize(agent: SolanaAgentKit) {
|
|
271
|
+
if (owner && owner !== agent)
|
|
272
|
+
throw Error("Plugin instance already bound to another agent");
|
|
273
|
+
owner = agent;
|
|
274
|
+
},
|
|
275
|
+
} satisfies Plugin;
|
|
276
|
+
// Keep ZodObject identity for the published tool converters, but sanitize
|
|
277
|
+
// validation issues before the framework's pre-handler executor surfaces them.
|
|
278
|
+
for (const action of plugin.actions) {
|
|
279
|
+
const schema: z.AnyZodObject = action.schema.strict();
|
|
280
|
+
const sync = schema.safeParse.bind(schema),
|
|
281
|
+
asyncParse = schema.safeParseAsync.bind(schema);
|
|
282
|
+
schema.safeParse = (data, params) => {
|
|
283
|
+
const r = sync(data, params);
|
|
284
|
+
return r.success
|
|
285
|
+
? r
|
|
286
|
+
: { success: false, error: new z.ZodError(clean(r.error.issues)) };
|
|
287
|
+
};
|
|
288
|
+
schema.safeParseAsync = async (data, params) => {
|
|
289
|
+
const r = await asyncParse(data, params);
|
|
290
|
+
return r.success
|
|
291
|
+
? r
|
|
292
|
+
: { success: false, error: new z.ZodError(clean(r.error.issues)) };
|
|
293
|
+
};
|
|
294
|
+
action.schema = schema as typeof action.schema;
|
|
295
|
+
}
|
|
296
|
+
return plugin;
|
|
297
|
+
}
|