kinetic-mcp 1.2.0 → 1.2.3
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/README.md +1 -1
- package/build/config.js +1 -1
- package/build/config.js.map +1 -1
- package/build/index.js +17 -1
- package/build/index.js.map +1 -1
- package/build/tools/token-price.js +44 -0
- package/build/tools/token-price.js.map +1 -0
- package/build/tools/transaction-history.js +71 -0
- package/build/tools/transaction-history.js.map +1 -0
- package/build/utils/api-client.js +3 -3
- package/build/utils/api-client.js.map +1 -1
- package/package.json +6 -1
- package/scripts/setup.mjs +586 -263
package/scripts/setup.mjs
CHANGED
|
@@ -1,42 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Interactive setup wizard for
|
|
4
|
+
* Interactive setup wizard for Kinetic MCP Server.
|
|
5
5
|
*
|
|
6
|
-
* Run:
|
|
6
|
+
* Run: npx kinetic-mcp setup
|
|
7
7
|
*
|
|
8
8
|
* This is a standalone .mjs file — no compilation required.
|
|
9
|
-
* Uses
|
|
9
|
+
* Uses @clack/prompts for beautiful terminal UI.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
} from "node:fs";
|
|
14
20
|
import { homedir } from "node:os";
|
|
15
21
|
import { join, resolve, dirname } from "node:path";
|
|
16
22
|
import { execSync } from "node:child_process";
|
|
17
|
-
import {
|
|
23
|
+
import { platform, execPath } from "node:process";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
|
|
18
26
|
import { Keypair } from "@solana/web3.js";
|
|
19
27
|
import bs58 from "bs58";
|
|
28
|
+
import * as p from "@clack/prompts";
|
|
29
|
+
import pc from "picocolors";
|
|
20
30
|
|
|
21
31
|
// ---------------------------------------------------------------------------
|
|
22
|
-
//
|
|
32
|
+
// Version
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const { version } = require("../package.json");
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Non-TTY check
|
|
23
40
|
// ---------------------------------------------------------------------------
|
|
24
41
|
|
|
25
|
-
|
|
42
|
+
if (!process.stdin.isTTY) {
|
|
43
|
+
console.error("Error: Setup wizard requires an interactive terminal.");
|
|
44
|
+
console.error("Run: npx kinetic-mcp setup");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
26
47
|
|
|
27
|
-
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// ASCII Banner
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
const BANNER = `
|
|
53
|
+
██╗ ██╗██╗███╗ ██╗███████╗████████╗██╗ ██████╗
|
|
54
|
+
██║ ██╔╝██║████╗ ██║██╔════╝╚══██╔══╝██║██╔════╝
|
|
55
|
+
█████╔╝ ██║██╔██╗ ██║█████╗ ██║ ██║██║
|
|
56
|
+
██╔═██╗ ██║██║╚██╗██║██╔══╝ ██║ ██║██║
|
|
57
|
+
██║ ██╗██║██║ ╚████║███████╗ ██║ ██║╚██████╗
|
|
58
|
+
╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝`;
|
|
59
|
+
|
|
60
|
+
async function renderBanner() {
|
|
61
|
+
let rendered;
|
|
62
|
+
try {
|
|
63
|
+
const gradient = (await import("gradient-string")).default;
|
|
64
|
+
const kineticGradient = gradient(["#FEDA75", "#FA7E1E", "#D62976", "#962FBF", "#4F5BD5"]);
|
|
65
|
+
rendered = kineticGradient.multiline(BANNER);
|
|
66
|
+
} catch {
|
|
67
|
+
rendered = pc.bold(pc.magenta(BANNER));
|
|
32
68
|
}
|
|
33
|
-
|
|
69
|
+
|
|
70
|
+
console.log(rendered);
|
|
71
|
+
console.log(pc.dim(` v${version} — Solana Trading via Claude Desktop`));
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Helpers
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
function handleCancel(value) {
|
|
80
|
+
if (p.isCancel(value)) {
|
|
81
|
+
p.cancel("Setup cancelled.");
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function expandHome(filePath) {
|
|
88
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
89
|
+
return resolve(homedir(), filePath.slice(2));
|
|
90
|
+
}
|
|
91
|
+
return filePath;
|
|
34
92
|
}
|
|
35
93
|
|
|
36
94
|
function isWSL() {
|
|
37
95
|
try {
|
|
38
|
-
const
|
|
39
|
-
return /microsoft|wsl/i.test(
|
|
96
|
+
const ver = readFileSync("/proc/version", "utf-8");
|
|
97
|
+
return /microsoft|wsl/i.test(ver);
|
|
40
98
|
} catch {
|
|
41
99
|
return false;
|
|
42
100
|
}
|
|
@@ -48,11 +106,13 @@ function getClaudeConfigPath() {
|
|
|
48
106
|
case "darwin":
|
|
49
107
|
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
50
108
|
case "win32":
|
|
51
|
-
return join(
|
|
109
|
+
return join(
|
|
110
|
+
process.env.APPDATA || join(home, "AppData", "Roaming"),
|
|
111
|
+
"Claude",
|
|
112
|
+
"claude_desktop_config.json",
|
|
113
|
+
);
|
|
52
114
|
case "linux":
|
|
53
|
-
if (isWSL())
|
|
54
|
-
return null; // WSL handled separately
|
|
55
|
-
}
|
|
115
|
+
if (isWSL()) return null;
|
|
56
116
|
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
57
117
|
default:
|
|
58
118
|
return null;
|
|
@@ -63,81 +123,6 @@ function getDefaultKeypairPath() {
|
|
|
63
123
|
return join(homedir(), ".config", "solana", "trading-keypair.json");
|
|
64
124
|
}
|
|
65
125
|
|
|
66
|
-
/** Read a line with masked input (asterisks). Falls back to plain input on non-TTY. */
|
|
67
|
-
function readMasked(prompt) {
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
stdout.write(prompt);
|
|
70
|
-
|
|
71
|
-
if (!stdin.isTTY) {
|
|
72
|
-
// Non-interactive: read without masking
|
|
73
|
-
const lineRl = createInterface({ input: stdin });
|
|
74
|
-
lineRl.on("line", (line) => {
|
|
75
|
-
lineRl.close();
|
|
76
|
-
resolve(line.trim());
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
stdin.setRawMode(true);
|
|
82
|
-
stdin.resume();
|
|
83
|
-
stdin.setEncoding("utf8");
|
|
84
|
-
let input = "";
|
|
85
|
-
|
|
86
|
-
const onData = (ch) => {
|
|
87
|
-
if (ch === "\r" || ch === "\n") {
|
|
88
|
-
stdin.setRawMode(false);
|
|
89
|
-
stdin.pause();
|
|
90
|
-
stdin.removeListener("data", onData);
|
|
91
|
-
stdout.write("\n");
|
|
92
|
-
resolve(input);
|
|
93
|
-
} else if (ch === "\u0003") {
|
|
94
|
-
// Ctrl+C
|
|
95
|
-
stdin.setRawMode(false);
|
|
96
|
-
stdin.pause();
|
|
97
|
-
stdin.removeListener("data", onData);
|
|
98
|
-
stdout.write("\n");
|
|
99
|
-
reject(new Error("Cancelled by user"));
|
|
100
|
-
} else if (ch === "\u007f" || ch === "\b") {
|
|
101
|
-
// Backspace
|
|
102
|
-
if (input.length > 0) {
|
|
103
|
-
input = input.slice(0, -1);
|
|
104
|
-
stdout.write("\b \b");
|
|
105
|
-
}
|
|
106
|
-
} else if (ch.charCodeAt(0) >= 32) {
|
|
107
|
-
// Printable character
|
|
108
|
-
input += ch;
|
|
109
|
-
stdout.write("*");
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
stdin.on("data", onData);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function askYesNo(question, defaultYes = true) {
|
|
118
|
-
const suffix = defaultYes ? "(Y/n)" : "(y/N)";
|
|
119
|
-
const answer = await rl.question(`${question} ${suffix} `);
|
|
120
|
-
const trimmed = answer.trim().toLowerCase();
|
|
121
|
-
if (trimmed === "") return defaultYes;
|
|
122
|
-
return trimmed === "y" || trimmed === "yes";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function askChoice(question, choices) {
|
|
126
|
-
console.log(`\n${question}`);
|
|
127
|
-
choices.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
|
|
128
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
129
|
-
const answer = await rl.question(`Choice (1-${choices.length}): `);
|
|
130
|
-
const num = parseInt(answer.trim(), 10);
|
|
131
|
-
if (num >= 1 && num <= choices.length) return num;
|
|
132
|
-
console.log(" Invalid choice. Try again.");
|
|
133
|
-
}
|
|
134
|
-
throw new Error("Too many invalid attempts.");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// Keypair handling
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
126
|
function validateKeypairFile(filePath) {
|
|
142
127
|
const expanded = expandHome(filePath);
|
|
143
128
|
if (!existsSync(expanded)) {
|
|
@@ -148,112 +133,304 @@ function validateKeypairFile(filePath) {
|
|
|
148
133
|
try {
|
|
149
134
|
parsed = JSON.parse(raw);
|
|
150
135
|
} catch {
|
|
151
|
-
throw new Error(
|
|
136
|
+
throw new Error(
|
|
137
|
+
"File is not valid JSON. Expected a JSON array of 64 integers (Solana CLI format).",
|
|
138
|
+
);
|
|
152
139
|
}
|
|
153
140
|
if (
|
|
154
141
|
!Array.isArray(parsed) ||
|
|
155
142
|
parsed.length !== 64 ||
|
|
156
143
|
!parsed.every((n) => typeof n === "number" && Number.isInteger(n))
|
|
157
144
|
) {
|
|
158
|
-
throw new Error("
|
|
145
|
+
throw new Error("Invalid Solana keypair. Expected a JSON array of exactly 64 integers.");
|
|
159
146
|
}
|
|
160
147
|
return Keypair.fromSecretKey(Uint8Array.from(parsed));
|
|
161
148
|
}
|
|
162
149
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(" How to export from Phantom:");
|
|
167
|
-
console.log(" 1. Open Phantom > Settings > Security & Privacy > Export Private Key");
|
|
168
|
-
console.log(" 2. Enter your password and copy the base58 string");
|
|
169
|
-
console.log("");
|
|
170
|
-
console.log(" SECURITY: Your key will be masked. It is NOT saved to shell history.");
|
|
171
|
-
console.log("");
|
|
172
|
-
|
|
173
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
174
|
-
try {
|
|
175
|
-
const key = await readMasked(" Paste your private key: ");
|
|
176
|
-
if (!key || key.trim().length === 0) {
|
|
177
|
-
console.log(" No key entered. Try again.");
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Step 2: Build Check
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
180
153
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} catch {
|
|
185
|
-
console.log(" Invalid private key format. Expected a base58-encoded string from Phantom.");
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
154
|
+
async function checkBuild() {
|
|
155
|
+
const buildExists = existsSync(resolve("build", "index.js"));
|
|
156
|
+
if (buildExists) return;
|
|
188
157
|
|
|
189
|
-
|
|
190
|
-
console.log(` Expected 64-byte secret key, got ${decoded.length} bytes.`);
|
|
191
|
-
if (decoded.length === 32) {
|
|
192
|
-
console.log(" This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.");
|
|
193
|
-
}
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
158
|
+
p.log.warn("Project hasn't been built yet — build/index.js not found.");
|
|
196
159
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
160
|
+
const runBuild = handleCancel(
|
|
161
|
+
await p.confirm({ message: "Run npm run build now?", initialValue: true }),
|
|
162
|
+
);
|
|
200
163
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
164
|
+
if (runBuild) {
|
|
165
|
+
const s = p.spinner();
|
|
166
|
+
s.start("Building project...");
|
|
167
|
+
try {
|
|
168
|
+
execSync("npm run build", { stdio: "pipe" });
|
|
169
|
+
s.stop("Build complete");
|
|
170
|
+
} catch {
|
|
171
|
+
s.stop("Build failed");
|
|
172
|
+
p.log.warn("You can try again later with: npm run build");
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
p.log.info("Skipped. Remember to run npm run build before using the server.");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
205
178
|
|
|
206
|
-
|
|
207
|
-
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Step 3: Keypair Setup
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
208
182
|
|
|
209
|
-
|
|
210
|
-
|
|
183
|
+
async function importFromPhantom() {
|
|
184
|
+
p.note(
|
|
185
|
+
[
|
|
186
|
+
`${pc.bold("How to export from Phantom:")}`,
|
|
187
|
+
"",
|
|
188
|
+
"1. Open Phantom → Settings → Manage Accounts → [Your Account] → Show Private Key",
|
|
189
|
+
"2. Enter your password and copy the base58 string",
|
|
190
|
+
"",
|
|
191
|
+
`${pc.dim("Your key will be masked. It is NOT saved to shell history.")}`,
|
|
192
|
+
].join("\n"),
|
|
193
|
+
"Import Private Key",
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const key = handleCancel(
|
|
197
|
+
await p.password({
|
|
198
|
+
message: "Paste your private key:",
|
|
199
|
+
mask: "*",
|
|
200
|
+
validate(value) {
|
|
201
|
+
if (!value || value.trim().length === 0) return "Private key is required.";
|
|
202
|
+
|
|
203
|
+
let decoded;
|
|
211
204
|
try {
|
|
212
|
-
|
|
205
|
+
decoded = bs58.decode(value.trim());
|
|
213
206
|
} catch {
|
|
214
|
-
|
|
207
|
+
return "Invalid format. Expected a base58-encoded string from Phantom.";
|
|
215
208
|
}
|
|
216
|
-
}
|
|
217
209
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
210
|
+
if (decoded.length !== 64) {
|
|
211
|
+
if (decoded.length === 32) {
|
|
212
|
+
return "This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.";
|
|
213
|
+
}
|
|
214
|
+
return `Expected 64-byte secret key, got ${decoded.length} bytes.`;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const decoded = bs58.decode(key.trim());
|
|
221
|
+
const keypair = Keypair.fromSecretKey(decoded);
|
|
222
|
+
|
|
223
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
224
|
+
p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
|
|
225
|
+
p.log.message(pc.dim("Verify this matches your Phantom wallet address."));
|
|
226
|
+
|
|
227
|
+
// Save keypair to default path
|
|
228
|
+
const defaultPath = getDefaultKeypairPath();
|
|
229
|
+
|
|
230
|
+
// Check if file exists
|
|
231
|
+
if (existsSync(defaultPath)) {
|
|
232
|
+
const overwrite = handleCancel(
|
|
233
|
+
await p.confirm({
|
|
234
|
+
message: `Keypair file already exists at ${pc.dim(defaultPath)}. Overwrite?`,
|
|
235
|
+
initialValue: false,
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!overwrite) {
|
|
240
|
+
p.log.info("Keeping existing keypair file.");
|
|
241
|
+
return { publicKey, path: defaultPath };
|
|
223
242
|
}
|
|
224
243
|
}
|
|
225
|
-
throw new Error("Too many invalid attempts. Exiting.");
|
|
226
|
-
}
|
|
227
244
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
234
|
-
const filePath = await rl.question(" Path to keypair file: ");
|
|
235
|
-
if (!filePath.trim()) {
|
|
236
|
-
console.log(" No path entered. Try again.");
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
245
|
+
mkdirSync(dirname(defaultPath), { recursive: true, mode: 0o700 });
|
|
246
|
+
writeFileSync(defaultPath, JSON.stringify(Array.from(keypair.secretKey)) + "\n", { mode: 0o600 });
|
|
247
|
+
|
|
248
|
+
if (platform !== "win32") {
|
|
239
249
|
try {
|
|
240
|
-
|
|
241
|
-
const keypair = validateKeypairFile(expanded);
|
|
242
|
-
console.log(`\n Wallet address: ${keypair.publicKey.toBase58()}`);
|
|
243
|
-
return { keypair, path: expanded };
|
|
250
|
+
chmodSync(defaultPath, 0o600);
|
|
244
251
|
} catch (err) {
|
|
245
|
-
|
|
246
|
-
if (attempt < MAX_RETRIES - 1) console.log(" Try again.");
|
|
252
|
+
p.log.warn(`Could not set file permissions: ${err.message}. Run: chmod 600 ${defaultPath}`);
|
|
247
253
|
}
|
|
248
254
|
}
|
|
249
|
-
|
|
255
|
+
|
|
256
|
+
p.log.success(`Keypair saved to ${pc.dim(defaultPath)}`);
|
|
257
|
+
return { publicKey, path: defaultPath };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function useExistingKeypair() {
|
|
261
|
+
const filePath = handleCancel(
|
|
262
|
+
await p.text({
|
|
263
|
+
message: "Path to keypair file:",
|
|
264
|
+
placeholder: "~/.config/solana/id.json",
|
|
265
|
+
validate(value) {
|
|
266
|
+
if (!value || value.trim().length === 0) return "Path is required.";
|
|
267
|
+
try {
|
|
268
|
+
const expanded = expandHome(value.trim());
|
|
269
|
+
validateKeypairFile(expanded);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return err.message;
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const expanded = expandHome(filePath.trim());
|
|
278
|
+
const keypair = validateKeypairFile(expanded);
|
|
279
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
280
|
+
|
|
281
|
+
p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
|
|
282
|
+
|
|
283
|
+
return { publicKey, path: expanded };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function setupKeypair() {
|
|
287
|
+
const method = handleCancel(
|
|
288
|
+
await p.select({
|
|
289
|
+
message: "How would you like to set up your Solana keypair?",
|
|
290
|
+
options: [
|
|
291
|
+
{
|
|
292
|
+
value: "phantom",
|
|
293
|
+
label: "Import from Phantom",
|
|
294
|
+
hint: "paste your base58 private key",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
value: "existing",
|
|
298
|
+
label: "Use existing keypair file",
|
|
299
|
+
hint: "Solana CLI JSON format",
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (method === "phantom") {
|
|
306
|
+
return importFromPhantom();
|
|
307
|
+
} else {
|
|
308
|
+
return useExistingKeypair();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Step 4: API Key
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
async function setupApiKey() {
|
|
317
|
+
const apiKey = handleCancel(
|
|
318
|
+
await p.password({
|
|
319
|
+
message: "Kinetic API key (optional) (press Enter to skip):",
|
|
320
|
+
mask: "*",
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return apiKey?.trim() || null;
|
|
250
325
|
}
|
|
251
326
|
|
|
252
327
|
// ---------------------------------------------------------------------------
|
|
253
|
-
//
|
|
328
|
+
// Step 5: Advanced Settings
|
|
254
329
|
// ---------------------------------------------------------------------------
|
|
255
330
|
|
|
256
|
-
|
|
331
|
+
const DEFAULTS = {
|
|
332
|
+
rpcUrl: "https://api.mainnet-beta.solana.com",
|
|
333
|
+
maxTradeSol: "10",
|
|
334
|
+
priorityFeeLamports: "400000",
|
|
335
|
+
jitoEnabled: false,
|
|
336
|
+
dynamicSlippageMaxBps: "300",
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
async function advancedSettings() {
|
|
340
|
+
const configure = handleCancel(
|
|
341
|
+
await p.confirm({
|
|
342
|
+
message: "Configure advanced settings?",
|
|
343
|
+
initialValue: false,
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (!configure) return {};
|
|
348
|
+
|
|
349
|
+
p.log.info(pc.dim("Press Enter to keep defaults shown."));
|
|
350
|
+
|
|
351
|
+
const rpcUrl = handleCancel(
|
|
352
|
+
await p.text({
|
|
353
|
+
message: "Solana RPC URL:",
|
|
354
|
+
placeholder: DEFAULTS.rpcUrl,
|
|
355
|
+
defaultValue: DEFAULTS.rpcUrl,
|
|
356
|
+
validate(value) {
|
|
357
|
+
if (!value) return;
|
|
358
|
+
try {
|
|
359
|
+
const url = new URL(value);
|
|
360
|
+
if (url.protocol !== "https:") return "URL must use https://";
|
|
361
|
+
} catch {
|
|
362
|
+
return "Invalid URL format.";
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const maxTradeSol = handleCancel(
|
|
369
|
+
await p.text({
|
|
370
|
+
message: "Max trade size (SOL):",
|
|
371
|
+
placeholder: DEFAULTS.maxTradeSol,
|
|
372
|
+
defaultValue: DEFAULTS.maxTradeSol,
|
|
373
|
+
validate(value) {
|
|
374
|
+
if (!value) return;
|
|
375
|
+
const num = Number(value);
|
|
376
|
+
if (isNaN(num) || num <= 0) return "Must be a positive number.";
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const priorityFeeLamports = handleCancel(
|
|
382
|
+
await p.text({
|
|
383
|
+
message: `Priority fee (lamports): ${pc.dim("400000 = ~0.0004 SOL")}`,
|
|
384
|
+
placeholder: DEFAULTS.priorityFeeLamports,
|
|
385
|
+
defaultValue: DEFAULTS.priorityFeeLamports,
|
|
386
|
+
validate(value) {
|
|
387
|
+
if (!value) return;
|
|
388
|
+
const num = Number(value);
|
|
389
|
+
if (isNaN(num) || !Number.isInteger(num) || num < 0)
|
|
390
|
+
return "Must be a non-negative integer.";
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const jitoEnabled = handleCancel(
|
|
396
|
+
await p.confirm({
|
|
397
|
+
message: "Enable Jito MEV protection?",
|
|
398
|
+
initialValue: DEFAULTS.jitoEnabled,
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const dynamicSlippageMaxBps = handleCancel(
|
|
403
|
+
await p.text({
|
|
404
|
+
message: "Max dynamic slippage (BPS):",
|
|
405
|
+
placeholder: DEFAULTS.dynamicSlippageMaxBps,
|
|
406
|
+
defaultValue: DEFAULTS.dynamicSlippageMaxBps,
|
|
407
|
+
validate(value) {
|
|
408
|
+
if (!value) return;
|
|
409
|
+
const num = Number(value);
|
|
410
|
+
if (isNaN(num) || !Number.isInteger(num) || num <= 0) return "Must be a positive integer.";
|
|
411
|
+
if (num > 1000) return "Cannot exceed 1000 BPS (10%).";
|
|
412
|
+
},
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Return only non-default values
|
|
417
|
+
const settings = {};
|
|
418
|
+
if (rpcUrl !== DEFAULTS.rpcUrl) settings.SOLANA_RPC_URL = rpcUrl;
|
|
419
|
+
if (maxTradeSol !== DEFAULTS.maxTradeSol) settings.MAX_TRADE_SOL = maxTradeSol;
|
|
420
|
+
if (priorityFeeLamports !== DEFAULTS.priorityFeeLamports)
|
|
421
|
+
settings.PRIORITY_FEE_LAMPORTS = priorityFeeLamports;
|
|
422
|
+
if (jitoEnabled !== DEFAULTS.jitoEnabled) settings.JITO_ENABLED = jitoEnabled ? "true" : "false";
|
|
423
|
+
if (dynamicSlippageMaxBps !== DEFAULTS.dynamicSlippageMaxBps)
|
|
424
|
+
settings.DYNAMIC_SLIPPAGE_MAX_BPS = dynamicSlippageMaxBps;
|
|
425
|
+
|
|
426
|
+
return settings;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// Step 6: Claude Desktop Config
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
function generateConfigEntry(keypairPath, apiKey, advancedEnv) {
|
|
257
434
|
const projectRoot = resolve(".");
|
|
258
435
|
const entry = {
|
|
259
436
|
command: execPath,
|
|
@@ -265,55 +442,84 @@ function generateConfigEntry(keypairPath, apiKey) {
|
|
|
265
442
|
if (apiKey) {
|
|
266
443
|
entry.env.API_KEY = apiKey;
|
|
267
444
|
}
|
|
445
|
+
// Add non-default advanced settings
|
|
446
|
+
if (advancedEnv) {
|
|
447
|
+
Object.assign(entry.env, advancedEnv);
|
|
448
|
+
}
|
|
268
449
|
return entry;
|
|
269
450
|
}
|
|
270
451
|
|
|
271
|
-
function
|
|
452
|
+
function formatConfigSnippet(entry) {
|
|
272
453
|
const snippet = {
|
|
273
454
|
mcpServers: {
|
|
274
455
|
"solana-trading": entry,
|
|
275
456
|
},
|
|
276
457
|
};
|
|
277
|
-
|
|
278
|
-
console.log("");
|
|
279
|
-
console.log(" " + JSON.stringify(snippet, null, 2).split("\n").join("\n "));
|
|
280
|
-
console.log("");
|
|
281
|
-
console.log(" If you already have other MCP servers configured,");
|
|
282
|
-
console.log(' merge the "solana-trading" entry into your existing "mcpServers" object.');
|
|
458
|
+
return JSON.stringify(snippet, null, 2);
|
|
283
459
|
}
|
|
284
460
|
|
|
285
|
-
async function configureClaudeDesktop(keypairPath, apiKey) {
|
|
286
|
-
|
|
461
|
+
async function configureClaudeDesktop(keypairPath, apiKey, advancedEnv) {
|
|
462
|
+
const entry = generateConfigEntry(keypairPath, apiKey, advancedEnv);
|
|
287
463
|
|
|
288
464
|
if (isWSL()) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
465
|
+
p.note(
|
|
466
|
+
[
|
|
467
|
+
"WSL detected. Claude Desktop runs on the Windows host.",
|
|
468
|
+
"",
|
|
469
|
+
`Config file: ${pc.dim("%APPDATA%\\Claude\\claude_desktop_config.json")}`,
|
|
470
|
+
"",
|
|
471
|
+
"Add this to your config:",
|
|
472
|
+
"",
|
|
473
|
+
formatConfigSnippet(entry),
|
|
474
|
+
"",
|
|
475
|
+
pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
|
|
476
|
+
].join("\n"),
|
|
477
|
+
"Claude Desktop Config",
|
|
478
|
+
);
|
|
296
479
|
return;
|
|
297
480
|
}
|
|
298
481
|
|
|
299
482
|
const configPath = getClaudeConfigPath();
|
|
300
483
|
if (!configPath) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
484
|
+
p.note(
|
|
485
|
+
[
|
|
486
|
+
"Could not detect Claude Desktop config path.",
|
|
487
|
+
"",
|
|
488
|
+
"Add this to your claude_desktop_config.json:",
|
|
489
|
+
"",
|
|
490
|
+
formatConfigSnippet(entry),
|
|
491
|
+
].join("\n"),
|
|
492
|
+
"Claude Desktop Config",
|
|
493
|
+
);
|
|
304
494
|
return;
|
|
305
495
|
}
|
|
306
496
|
|
|
307
|
-
|
|
308
|
-
const entry = generateConfigEntry(keypairPath, apiKey);
|
|
497
|
+
p.log.info(`Config file: ${pc.dim(configPath)}`);
|
|
309
498
|
|
|
310
|
-
const autoModify =
|
|
499
|
+
const autoModify = handleCancel(
|
|
500
|
+
await p.confirm({
|
|
501
|
+
message: "Automatically add this MCP server to Claude Desktop config?",
|
|
502
|
+
initialValue: true,
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
311
505
|
|
|
312
506
|
if (!autoModify) {
|
|
313
|
-
|
|
507
|
+
p.note(
|
|
508
|
+
[
|
|
509
|
+
"Add this to your claude_desktop_config.json:",
|
|
510
|
+
"",
|
|
511
|
+
formatConfigSnippet(entry),
|
|
512
|
+
"",
|
|
513
|
+
pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
|
|
514
|
+
].join("\n"),
|
|
515
|
+
"Manual Config",
|
|
516
|
+
);
|
|
314
517
|
return;
|
|
315
518
|
}
|
|
316
519
|
|
|
520
|
+
const s = p.spinner();
|
|
521
|
+
s.start("Writing Claude Desktop config...");
|
|
522
|
+
|
|
317
523
|
// Read existing config
|
|
318
524
|
let config = {};
|
|
319
525
|
if (existsSync(configPath)) {
|
|
@@ -321,117 +527,234 @@ async function configureClaudeDesktop(keypairPath, apiKey) {
|
|
|
321
527
|
const raw = readFileSync(configPath, "utf-8");
|
|
322
528
|
config = JSON.parse(raw);
|
|
323
529
|
} catch {
|
|
324
|
-
|
|
325
|
-
|
|
530
|
+
s.stop("Existing config has invalid JSON");
|
|
531
|
+
p.note(
|
|
532
|
+
[
|
|
533
|
+
"Could not parse existing config file.",
|
|
534
|
+
"",
|
|
535
|
+
"Add this manually to your claude_desktop_config.json:",
|
|
536
|
+
"",
|
|
537
|
+
formatConfigSnippet(entry),
|
|
538
|
+
].join("\n"),
|
|
539
|
+
"Manual Config",
|
|
540
|
+
);
|
|
326
541
|
return;
|
|
327
542
|
}
|
|
328
543
|
|
|
329
544
|
// Backup
|
|
330
545
|
const backupPath = configPath + ".bak";
|
|
331
546
|
copyFileSync(configPath, backupPath);
|
|
332
|
-
|
|
547
|
+
p.log.info(`Backup created: ${pc.dim(backupPath)}`);
|
|
333
548
|
}
|
|
334
549
|
|
|
335
|
-
//
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
550
|
+
// Check for existing entry
|
|
551
|
+
if (config.mcpServers?.["solana-trading"]) {
|
|
552
|
+
s.stop("Existing entry found");
|
|
553
|
+
const overwrite = handleCancel(
|
|
554
|
+
await p.confirm({
|
|
555
|
+
message: "A 'solana-trading' entry already exists. Overwrite?",
|
|
556
|
+
initialValue: false,
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
339
559
|
|
|
340
|
-
if (config.mcpServers["solana-trading"]) {
|
|
341
|
-
const overwrite = await askYesNo(" A 'solana-trading' entry already exists. Overwrite?");
|
|
342
560
|
if (!overwrite) {
|
|
343
|
-
|
|
561
|
+
p.log.info("Existing config preserved.");
|
|
344
562
|
return;
|
|
345
563
|
}
|
|
564
|
+
|
|
565
|
+
s.start("Writing Claude Desktop config...");
|
|
346
566
|
}
|
|
347
567
|
|
|
568
|
+
// Merge
|
|
569
|
+
if (!config.mcpServers) {
|
|
570
|
+
config.mcpServers = {};
|
|
571
|
+
}
|
|
348
572
|
config.mcpServers["solana-trading"] = entry;
|
|
349
573
|
|
|
350
574
|
// Write
|
|
351
575
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
352
576
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
353
|
-
|
|
577
|
+
|
|
578
|
+
s.stop("Claude Desktop config updated");
|
|
354
579
|
}
|
|
355
580
|
|
|
356
581
|
// ---------------------------------------------------------------------------
|
|
357
|
-
//
|
|
582
|
+
// Step 7: Verification
|
|
358
583
|
// ---------------------------------------------------------------------------
|
|
359
584
|
|
|
360
|
-
|
|
361
|
-
console.log("");
|
|
362
|
-
console.log(" Solana Trading MCP Server — Setup Wizard");
|
|
363
|
-
console.log(" =========================================");
|
|
364
|
-
console.log("");
|
|
585
|
+
const MAX_VERIFICATION_RETRIES = 3;
|
|
365
586
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
587
|
+
async function runVerification(keypairPath, rpcUrl, apiKey) {
|
|
588
|
+
for (let attempt = 0; attempt <= MAX_VERIFICATION_RETRIES; attempt++) {
|
|
589
|
+
const s = p.spinner();
|
|
590
|
+
let hasFailure = false;
|
|
591
|
+
|
|
592
|
+
// 1. Load keypair
|
|
593
|
+
s.start("Loading keypair...");
|
|
594
|
+
let keypair;
|
|
595
|
+
try {
|
|
596
|
+
keypair = validateKeypairFile(keypairPath);
|
|
597
|
+
s.stop(`Keypair loaded — ${pc.cyan(keypair.publicKey.toBase58())}`);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
s.stop(`Keypair load failed: ${err.message}`);
|
|
600
|
+
hasFailure = true;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 2. Check SOL balance (only if keypair loaded)
|
|
604
|
+
if (keypair) {
|
|
605
|
+
const effectiveRpc = rpcUrl || DEFAULTS.rpcUrl;
|
|
606
|
+
s.start("Checking SOL balance...");
|
|
373
607
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
608
|
+
const response = await fetch(effectiveRpc, {
|
|
609
|
+
method: "POST",
|
|
610
|
+
headers: { "Content-Type": "application/json" },
|
|
611
|
+
body: JSON.stringify({
|
|
612
|
+
jsonrpc: "2.0",
|
|
613
|
+
id: 1,
|
|
614
|
+
method: "getBalance",
|
|
615
|
+
params: [keypair.publicKey.toBase58()],
|
|
616
|
+
}),
|
|
617
|
+
signal: AbortSignal.timeout(10_000),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const data = await response.json();
|
|
621
|
+
const lamports = data.result?.value ?? 0;
|
|
622
|
+
const sol = (lamports / 1e9).toFixed(4);
|
|
623
|
+
|
|
624
|
+
if (lamports === 0) {
|
|
625
|
+
s.stop(`Balance: ${pc.yellow("0 SOL")}`);
|
|
626
|
+
p.log.warn("Fund this wallet with SOL before trading.");
|
|
627
|
+
} else {
|
|
628
|
+
s.stop(`Balance: ${pc.green(sol + " SOL")}`);
|
|
629
|
+
}
|
|
630
|
+
} catch (err) {
|
|
631
|
+
const msg =
|
|
632
|
+
err.name === "TimeoutError" || err.name === "AbortError"
|
|
633
|
+
? "Request timed out"
|
|
634
|
+
: err.message;
|
|
635
|
+
s.stop(`Balance check failed: ${pc.yellow(msg)}`);
|
|
636
|
+
hasFailure = true;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 3. Test Kinetic API (only if API key provided)
|
|
641
|
+
if (apiKey) {
|
|
642
|
+
s.start("Testing Kinetic API...");
|
|
643
|
+
try {
|
|
644
|
+
const response = await fetch("https://auth.kinetic.xyz/v1/token", {
|
|
645
|
+
method: "POST",
|
|
646
|
+
headers: { "Content-Type": "application/json" },
|
|
647
|
+
body: JSON.stringify({ apiKey }),
|
|
648
|
+
signal: AbortSignal.timeout(10_000),
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (response.ok) {
|
|
652
|
+
s.stop(`Kinetic API: ${pc.green("connected")}`);
|
|
653
|
+
} else {
|
|
654
|
+
s.stop(`Kinetic API: ${pc.yellow(`${response.status} ${response.statusText}`)}`);
|
|
655
|
+
hasFailure = true;
|
|
656
|
+
}
|
|
657
|
+
} catch (err) {
|
|
658
|
+
const msg =
|
|
659
|
+
err.name === "TimeoutError" || err.name === "AbortError"
|
|
660
|
+
? "Request timed out"
|
|
661
|
+
: err.message;
|
|
662
|
+
s.stop(`Kinetic API: ${pc.yellow(msg)}`);
|
|
663
|
+
hasFailure = true;
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
p.log.info(pc.dim("Skipping Kinetic API test (no API key)."));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!hasFailure) return;
|
|
670
|
+
|
|
671
|
+
// Retry on failure (up to MAX_VERIFICATION_RETRIES)
|
|
672
|
+
if (attempt < MAX_VERIFICATION_RETRIES) {
|
|
673
|
+
const retry = handleCancel(
|
|
674
|
+
await p.confirm({
|
|
675
|
+
message: "Some checks failed. Retry verification?",
|
|
676
|
+
initialValue: true,
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
if (!retry) {
|
|
681
|
+
p.log.warn("Continuing with warnings. You can verify connectivity later.");
|
|
682
|
+
return;
|
|
379
683
|
}
|
|
380
684
|
} else {
|
|
381
|
-
|
|
685
|
+
p.log.warn("Maximum retries reached. Continuing with warnings.");
|
|
686
|
+
return;
|
|
382
687
|
}
|
|
383
688
|
}
|
|
689
|
+
}
|
|
384
690
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"Use an existing Solana CLI keypair file",
|
|
389
|
-
]);
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// Main
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
390
694
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
695
|
+
async function main() {
|
|
696
|
+
await renderBanner();
|
|
697
|
+
p.intro(pc.bold("Setup Wizard"));
|
|
698
|
+
|
|
699
|
+
// Step 2: Build check
|
|
700
|
+
await checkBuild();
|
|
701
|
+
|
|
702
|
+
// Step 3: Keypair setup
|
|
703
|
+
const keypairResult = await setupKeypair();
|
|
704
|
+
|
|
705
|
+
// Step 4: API key
|
|
706
|
+
const apiKey = await setupApiKey();
|
|
707
|
+
|
|
708
|
+
// Step 5: Advanced settings
|
|
709
|
+
const advancedEnv = await advancedSettings();
|
|
710
|
+
|
|
711
|
+
// Step 6: Claude Desktop config
|
|
712
|
+
await configureClaudeDesktop(keypairResult.path, apiKey, advancedEnv);
|
|
713
|
+
|
|
714
|
+
// Step 7: Verification
|
|
715
|
+
const rpcUrl = advancedEnv.SOLANA_RPC_URL || null;
|
|
716
|
+
await runVerification(keypairResult.path, rpcUrl, apiKey);
|
|
717
|
+
|
|
718
|
+
// Step 8: Outro
|
|
719
|
+
const summaryLines = [
|
|
720
|
+
`Wallet: ${pc.cyan(keypairResult.publicKey)}`,
|
|
721
|
+
`Keypair: ${pc.dim(keypairResult.path)}`,
|
|
722
|
+
];
|
|
723
|
+
if (apiKey) summaryLines.push(`API Key: ${pc.green("configured")}`);
|
|
724
|
+
|
|
725
|
+
const advancedKeys = Object.keys(advancedEnv);
|
|
726
|
+
if (advancedKeys.length > 0) {
|
|
727
|
+
summaryLines.push("");
|
|
728
|
+
summaryLines.push(pc.bold("Custom settings:"));
|
|
729
|
+
for (const [key, val] of Object.entries(advancedEnv)) {
|
|
730
|
+
summaryLines.push(` ${pc.dim(key)}: ${val}`);
|
|
731
|
+
}
|
|
396
732
|
}
|
|
397
733
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
console.log(" Next steps:");
|
|
416
|
-
console.log(" 1. Restart Claude Desktop (fully quit and reopen)");
|
|
417
|
-
console.log(' 2. Look for the hammer icon in the chat input');
|
|
418
|
-
console.log(' 3. Try: "What\'s my SOL balance?"');
|
|
419
|
-
console.log("");
|
|
420
|
-
console.log(" Safety guardrails:");
|
|
421
|
-
console.log(" - Swaps always show a preview first (confirm=false)");
|
|
422
|
-
console.log(" - Max trade size: 10 SOL (configurable via MAX_TRADE_SOL)");
|
|
423
|
-
console.log(" - Slippage capped at 10% (1000 bps)");
|
|
424
|
-
console.log("");
|
|
425
|
-
|
|
426
|
-
rl.close();
|
|
734
|
+
p.note(summaryLines.join("\n"), "Setup Complete");
|
|
735
|
+
|
|
736
|
+
p.note(
|
|
737
|
+
[
|
|
738
|
+
`1. ${pc.bold("Restart Claude Desktop")} (fully quit and reopen)`,
|
|
739
|
+
`2. Look for the ${pc.bold("hammer icon")} in the chat input`,
|
|
740
|
+
`3. Try: ${pc.cyan('"What\'s my SOL balance?"')}`,
|
|
741
|
+
"",
|
|
742
|
+
pc.dim("Safety guardrails:"),
|
|
743
|
+
pc.dim(` • Swaps always show a preview first`),
|
|
744
|
+
pc.dim(` • Max trade size: ${advancedEnv.MAX_TRADE_SOL || "10"} SOL`),
|
|
745
|
+
pc.dim(` • Slippage capped at ${advancedEnv.DYNAMIC_SLIPPAGE_MAX_BPS || "300"} BPS`),
|
|
746
|
+
].join("\n"),
|
|
747
|
+
"Next Steps",
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
p.outro(pc.green("You're all set! Happy trading."));
|
|
427
751
|
}
|
|
428
752
|
|
|
429
753
|
main().catch((err) => {
|
|
430
754
|
if (err.message === "Cancelled by user") {
|
|
431
|
-
|
|
755
|
+
p.cancel("Setup cancelled.");
|
|
432
756
|
} else {
|
|
433
|
-
|
|
757
|
+
p.log.error(`Setup failed: ${err.message}`);
|
|
434
758
|
}
|
|
435
|
-
rl.close();
|
|
436
759
|
process.exit(1);
|
|
437
760
|
});
|