@vault77/summon 2.0.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 +192 -0
- package/lib/config.js +305 -0
- package/lib/doctor.js +143 -0
- package/lib/errors.js +18 -0
- package/lib/swapClient.js +84 -0
- package/lib/tradeFormat.js +4 -0
- package/lib/trades.js +222 -0
- package/package.json +68 -0
- package/summon-cli.js +797 -0
- package/utils/keychain.js +86 -0
- package/utils/logger.js +45 -0
- package/utils/notify.js +58 -0
package/summon-cli.js
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import {
|
|
4
|
+
getConfigPath,
|
|
5
|
+
loadConfig,
|
|
6
|
+
saveConfig,
|
|
7
|
+
editConfig,
|
|
8
|
+
CONFIG_KEYS,
|
|
9
|
+
DEFAULT_CONFIG,
|
|
10
|
+
parseConfigValue,
|
|
11
|
+
normalizeConfigValue,
|
|
12
|
+
PRIORITY_FEE_LEVELS,
|
|
13
|
+
TX_VERSIONS,
|
|
14
|
+
} from "./lib/config.js";
|
|
15
|
+
import { storePrivateKey, getPrivateKey, deletePrivateKey, hasPrivateKey } from "./utils/keychain.js";
|
|
16
|
+
import readline from "readline";
|
|
17
|
+
import { notify } from "./utils/notify.js";
|
|
18
|
+
import { runDoctor } from "./lib/doctor.js";
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name("summon")
|
|
23
|
+
.description("Summon Solana CLI")
|
|
24
|
+
.showHelpAfterError(); // show help after invalid flags/args
|
|
25
|
+
|
|
26
|
+
const CONFIG_KEY_SET = new Set([
|
|
27
|
+
...CONFIG_KEYS.filter((key) => key !== "jito"),
|
|
28
|
+
"jito.enabled",
|
|
29
|
+
"jito.tip",
|
|
30
|
+
]);
|
|
31
|
+
const CONFIG_HELP = [
|
|
32
|
+
{ key: "rpcUrl", type: "string", note: "RPC URL (advancedTx=true is enforced)" },
|
|
33
|
+
{ key: "slippage", type: "number | auto", note: "Max slippage percentage" },
|
|
34
|
+
{ key: "priorityFee", type: "number | auto", note: "Priority fee in SOL" },
|
|
35
|
+
{
|
|
36
|
+
key: "priorityFeeLevel",
|
|
37
|
+
type: PRIORITY_FEE_LEVELS.join(" | "),
|
|
38
|
+
note: "Required when priorityFee=auto",
|
|
39
|
+
},
|
|
40
|
+
{ key: "txVersion", type: TX_VERSIONS.join(" | "), note: "Transaction version" },
|
|
41
|
+
{ key: "showQuoteDetails", type: "true | false", note: "Print quote details after swaps" },
|
|
42
|
+
{ key: "DEBUG_MODE", type: "true | false", note: "Enable verbose SDK logs" },
|
|
43
|
+
{ key: "notificationsEnabled", type: "true | false", note: "Enable macOS notifications" },
|
|
44
|
+
{ key: "jito.enabled", type: "true | false", note: "Enable Jito bundles" },
|
|
45
|
+
{ key: "jito.tip", type: "number", note: "Tip in SOL when Jito enabled" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const askQuestion = (rl, prompt) =>
|
|
49
|
+
new Promise((resolve) => rl.question(prompt, (answer) => resolve(answer.trim())));
|
|
50
|
+
|
|
51
|
+
const COLOR_ENABLED = process.stdout.isTTY;
|
|
52
|
+
const ANSI = {
|
|
53
|
+
reset: "\x1b[0m",
|
|
54
|
+
blue: "\x1b[34m",
|
|
55
|
+
purple: "\x1b[35m",
|
|
56
|
+
green: "\x1b[32m",
|
|
57
|
+
yellow: "\x1b[33m",
|
|
58
|
+
red: "\x1b[31m",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const paint = (text, color) => (COLOR_ENABLED ? `${color}${text}${ANSI.reset}` : text);
|
|
62
|
+
|
|
63
|
+
function clearScreen() {
|
|
64
|
+
if (process.stdout.isTTY) {
|
|
65
|
+
process.stdout.write("\x1Bc");
|
|
66
|
+
} else {
|
|
67
|
+
console.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderWizardHeader() {
|
|
72
|
+
console.log("⚙️ Config Wizard");
|
|
73
|
+
console.log("Press Enter to keep the current value.\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toDisplayValue(value) {
|
|
77
|
+
if (value === undefined || value === null) return "";
|
|
78
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
79
|
+
return String(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatBox({ title, rows }) {
|
|
83
|
+
const normalizedRows = rows.map(([label, value]) => [String(label), String(value)]);
|
|
84
|
+
const labelWidth = Math.max(...normalizedRows.map(([label]) => label.length), 0);
|
|
85
|
+
const valueWidth = Math.max(...normalizedRows.map(([, value]) => value.length), 0);
|
|
86
|
+
const titleText = title ? ` ${title} ` : "";
|
|
87
|
+
const innerWidth = Math.max(labelWidth + 3 + valueWidth, titleText.length);
|
|
88
|
+
const totalWidth = innerWidth + 2;
|
|
89
|
+
|
|
90
|
+
let topBorder = `┌${"─".repeat(totalWidth)}┐`;
|
|
91
|
+
if (titleText) {
|
|
92
|
+
const left = Math.floor((totalWidth - titleText.length) / 2);
|
|
93
|
+
const right = totalWidth - titleText.length - left;
|
|
94
|
+
topBorder = `┌${"─".repeat(left)}${paint(titleText, ANSI.purple)}${"─".repeat(right)}┐`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lines = normalizedRows.map(([label, value]) => {
|
|
98
|
+
const labelText = label.padEnd(labelWidth);
|
|
99
|
+
const content = `${paint(labelText, ANSI.blue)} : ${paint(value, ANSI.green)}`;
|
|
100
|
+
const contentLength = labelText.length + 3 + value.length;
|
|
101
|
+
const padding = " ".repeat(Math.max(0, innerWidth - contentLength));
|
|
102
|
+
return `│ ${content}${padding} │`;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const bottomBorder = `└${"─".repeat(totalWidth)}┘`;
|
|
106
|
+
return [topBorder, ...lines, bottomBorder].join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatPlainBox({ title, rows }) {
|
|
110
|
+
const normalizedRows = rows.map(([label, value]) => [String(label), String(value)]);
|
|
111
|
+
const labelWidth = Math.max(...normalizedRows.map(([label]) => label.length), 0);
|
|
112
|
+
const valueWidth = Math.max(...normalizedRows.map(([, value]) => value.length), 0);
|
|
113
|
+
const titleText = title ? ` ${title} ` : "";
|
|
114
|
+
const innerWidth = Math.max(labelWidth + 3 + valueWidth, titleText.length);
|
|
115
|
+
const totalWidth = innerWidth + 2;
|
|
116
|
+
|
|
117
|
+
let topBorder = `┌${"─".repeat(totalWidth)}┐`;
|
|
118
|
+
if (titleText) {
|
|
119
|
+
const left = Math.floor((totalWidth - titleText.length) / 2);
|
|
120
|
+
const right = totalWidth - titleText.length - left;
|
|
121
|
+
topBorder = `┌${"─".repeat(left)}${titleText}${"─".repeat(right)}┐`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines = normalizedRows.map(([label, value]) => {
|
|
125
|
+
const labelText = label.padEnd(labelWidth);
|
|
126
|
+
const content = `${labelText} : ${value}`;
|
|
127
|
+
const contentLength = labelText.length + 3 + value.length;
|
|
128
|
+
const padding = " ".repeat(Math.max(0, innerWidth - contentLength));
|
|
129
|
+
return `│ ${content}${padding} │`;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const bottomBorder = `└${"─".repeat(totalWidth)}┘`;
|
|
133
|
+
return [topBorder, ...lines, bottomBorder].join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderStatusBox({ title, rows, tone }) {
|
|
137
|
+
const box = formatPlainBox({ title, rows });
|
|
138
|
+
const colored = box
|
|
139
|
+
.split("\n")
|
|
140
|
+
.map((line) => paint(line, tone))
|
|
141
|
+
.join("\n");
|
|
142
|
+
console.log(colored);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderConfigSummary(cfg, configPath, title = "CONFIG") {
|
|
146
|
+
const jitoEnabled = cfg.jito?.enabled ? "true" : "false";
|
|
147
|
+
const jitoTip = cfg.jito?.enabled ? cfg.jito.tip : "-";
|
|
148
|
+
const rows = [
|
|
149
|
+
["Config path", configPath],
|
|
150
|
+
["RPC URL", cfg.rpcUrl],
|
|
151
|
+
["Slippage", cfg.slippage],
|
|
152
|
+
["Priority fee", cfg.priorityFee],
|
|
153
|
+
["Priority level", cfg.priorityFeeLevel],
|
|
154
|
+
["Tx version", cfg.txVersion],
|
|
155
|
+
["Show quote", cfg.showQuoteDetails],
|
|
156
|
+
["Debug mode", cfg.DEBUG_MODE],
|
|
157
|
+
["Notifications", cfg.notificationsEnabled],
|
|
158
|
+
["Jito enabled", jitoEnabled],
|
|
159
|
+
["Jito tip (SOL)", jitoTip],
|
|
160
|
+
];
|
|
161
|
+
console.log(formatBox({ title, rows }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function promptSelect(rl, label, options, { current, required = false } = {}) {
|
|
165
|
+
const menu = options.map((opt, index) => ` ${index + 1}) ${opt}`).join("\n");
|
|
166
|
+
while (true) {
|
|
167
|
+
console.log(`\n${label}`);
|
|
168
|
+
console.log(menu);
|
|
169
|
+
const suffix = current ? ` [${current}]` : "";
|
|
170
|
+
const answer = await askQuestion(rl, `Select${suffix}: `);
|
|
171
|
+
if (!answer) {
|
|
172
|
+
if (required) {
|
|
173
|
+
console.log("⚠️ Selection required.");
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
return current;
|
|
177
|
+
}
|
|
178
|
+
const normalized = answer.trim();
|
|
179
|
+
const index = Number(normalized);
|
|
180
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) {
|
|
181
|
+
return options[index - 1];
|
|
182
|
+
}
|
|
183
|
+
const match = options.find((opt) => opt.toLowerCase() === normalized.toLowerCase());
|
|
184
|
+
if (match) return match;
|
|
185
|
+
console.log("⚠️ Invalid selection. Choose a number or value from the list.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function promptNormalized(rl, label, key, { current, required = false } = {}) {
|
|
190
|
+
while (true) {
|
|
191
|
+
const suffix = current !== undefined ? ` [${toDisplayValue(current)}]` : "";
|
|
192
|
+
const answer = await askQuestion(rl, `${label}${suffix}: `);
|
|
193
|
+
if (!answer) {
|
|
194
|
+
if (required) {
|
|
195
|
+
console.log("⚠️ Value required.");
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
return current;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
return normalizeConfigValue(key, parseConfigValue(answer), { strict: true });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.log(`⚠️ ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function promptNumber(rl, label, { current, required = false } = {}) {
|
|
209
|
+
while (true) {
|
|
210
|
+
const suffix = current !== undefined ? ` [${toDisplayValue(current)}]` : "";
|
|
211
|
+
const answer = await askQuestion(rl, `${label}${suffix}: `);
|
|
212
|
+
if (!answer) {
|
|
213
|
+
if (required) {
|
|
214
|
+
console.log("⚠️ Value required.");
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
return current;
|
|
218
|
+
}
|
|
219
|
+
const num = Number(answer);
|
|
220
|
+
if (Number.isFinite(num) && num >= 0) {
|
|
221
|
+
return num;
|
|
222
|
+
}
|
|
223
|
+
console.log("⚠️ Invalid number. Use a non-negative value.");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runConfigWizard({ cfg, rl }) {
|
|
228
|
+
const nextCfg = { ...cfg, jito: { ...DEFAULT_CONFIG.jito, ...(cfg.jito || {}) } };
|
|
229
|
+
|
|
230
|
+
clearScreen();
|
|
231
|
+
renderWizardHeader();
|
|
232
|
+
console.log("RPC URL should be the SolanaTracker endpoint assigned to you.");
|
|
233
|
+
console.log("advancedTx=true is enforced automatically.\n");
|
|
234
|
+
nextCfg.rpcUrl = await promptNormalized(rl, "RPC URL", "rpcUrl", { current: nextCfg.rpcUrl });
|
|
235
|
+
|
|
236
|
+
clearScreen();
|
|
237
|
+
renderWizardHeader();
|
|
238
|
+
nextCfg.slippage = await promptNormalized(rl, "Max slippage (number or \"auto\")", "slippage", {
|
|
239
|
+
current: nextCfg.slippage,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
clearScreen();
|
|
243
|
+
renderWizardHeader();
|
|
244
|
+
nextCfg.priorityFee = await promptNormalized(rl, "Priority fee (number or \"auto\")", "priorityFee", {
|
|
245
|
+
current: nextCfg.priorityFee,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
clearScreen();
|
|
249
|
+
renderWizardHeader();
|
|
250
|
+
nextCfg.priorityFeeLevel = await promptSelect(
|
|
251
|
+
rl,
|
|
252
|
+
"Priority fee level (used when priorityFee is auto)",
|
|
253
|
+
PRIORITY_FEE_LEVELS,
|
|
254
|
+
{
|
|
255
|
+
current: nextCfg.priorityFeeLevel,
|
|
256
|
+
required: true,
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
clearScreen();
|
|
261
|
+
renderWizardHeader();
|
|
262
|
+
nextCfg.txVersion = await promptSelect(rl, "Transaction version", TX_VERSIONS, {
|
|
263
|
+
current: nextCfg.txVersion,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
clearScreen();
|
|
267
|
+
renderWizardHeader();
|
|
268
|
+
const showQuoteDetails = await promptSelect(rl, "Show quote details", ["true", "false"], {
|
|
269
|
+
current: nextCfg.showQuoteDetails ? "true" : "false",
|
|
270
|
+
});
|
|
271
|
+
nextCfg.showQuoteDetails = showQuoteDetails === "true";
|
|
272
|
+
|
|
273
|
+
clearScreen();
|
|
274
|
+
renderWizardHeader();
|
|
275
|
+
const debugMode = await promptSelect(rl, "Enable debug mode", ["true", "false"], {
|
|
276
|
+
current: nextCfg.DEBUG_MODE ? "true" : "false",
|
|
277
|
+
});
|
|
278
|
+
nextCfg.DEBUG_MODE = debugMode === "true";
|
|
279
|
+
|
|
280
|
+
clearScreen();
|
|
281
|
+
renderWizardHeader();
|
|
282
|
+
const notificationsEnabled = await promptSelect(rl, "Enable notifications", ["true", "false"], {
|
|
283
|
+
current: nextCfg.notificationsEnabled ? "true" : "false",
|
|
284
|
+
});
|
|
285
|
+
nextCfg.notificationsEnabled = notificationsEnabled === "true";
|
|
286
|
+
|
|
287
|
+
clearScreen();
|
|
288
|
+
renderWizardHeader();
|
|
289
|
+
const jitoEnabled = await promptSelect(rl, "Enable Jito bundles", ["true", "false"], {
|
|
290
|
+
current: nextCfg.jito.enabled ? "true" : "false",
|
|
291
|
+
});
|
|
292
|
+
nextCfg.jito.enabled = jitoEnabled === "true";
|
|
293
|
+
if (nextCfg.jito.enabled) {
|
|
294
|
+
clearScreen();
|
|
295
|
+
renderWizardHeader();
|
|
296
|
+
const requireTip = nextCfg.jito.tip === undefined || nextCfg.jito.tip === null;
|
|
297
|
+
nextCfg.jito.tip = await promptNumber(rl, "Jito tip (SOL)", {
|
|
298
|
+
current: nextCfg.jito.tip,
|
|
299
|
+
required: requireTip,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return nextCfg;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let tradeModulePromise;
|
|
307
|
+
const getTradeModule = async () => {
|
|
308
|
+
if (!tradeModulePromise) {
|
|
309
|
+
tradeModulePromise = import("./lib/trades.js");
|
|
310
|
+
}
|
|
311
|
+
return tradeModulePromise;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
async function executeTrade(type, mint, amountArg) {
|
|
315
|
+
const cfg = await loadConfig();
|
|
316
|
+
|
|
317
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) {
|
|
318
|
+
console.error("⚠️ Invalid mint format. Expected base58 address (32–44 chars).");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let amountParam = amountArg.toString().trim().toLowerCase().replace(/\s+/g, "");
|
|
323
|
+
|
|
324
|
+
if (amountParam !== "auto" && !amountParam.endsWith("%")) {
|
|
325
|
+
const num = parseFloat(amountParam);
|
|
326
|
+
if (isNaN(num) || num <= 0) {
|
|
327
|
+
console.error("⚠️ Invalid amount. Use a positive number, 'auto' during a sell, or '<percent>%'.");
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
amountParam = num;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const mintDisplay = `${mint.slice(0, 4)}…${mint.slice(-4)}`;
|
|
335
|
+
const amountDisplay = String(amountParam);
|
|
336
|
+
const baseRows = [
|
|
337
|
+
["Action", type === "buy" ? "Buy" : "Sell"],
|
|
338
|
+
["Mint", mintDisplay],
|
|
339
|
+
["Amount", amountDisplay],
|
|
340
|
+
];
|
|
341
|
+
clearScreen();
|
|
342
|
+
renderStatusBox({
|
|
343
|
+
title: "PENDING",
|
|
344
|
+
tone: ANSI.yellow,
|
|
345
|
+
rows: [
|
|
346
|
+
...baseRows,
|
|
347
|
+
["TXID", "-"],
|
|
348
|
+
["Explorer", "-"],
|
|
349
|
+
["Info", "Submitting swap..."],
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (type === "buy") {
|
|
354
|
+
if (amountParam === "auto") {
|
|
355
|
+
console.error("⚠️ Buying with 'auto' isn’t supported. Use a number or '<percent>%'.");
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { buyToken } = await getTradeModule();
|
|
360
|
+
const result = await buyToken(mint, amountParam);
|
|
361
|
+
clearScreen();
|
|
362
|
+
const info = `Received ${result.tokensReceivedDecimal} tokens | Fees ${result.totalFees} | Impact ${result.priceImpact}`;
|
|
363
|
+
const buyRows = [
|
|
364
|
+
...baseRows,
|
|
365
|
+
["TXID", result.txid],
|
|
366
|
+
["Explorer", `https://orbmarkets.io/tx/${result.txid}`],
|
|
367
|
+
["Info", info],
|
|
368
|
+
["Verification", result.verificationStatus],
|
|
369
|
+
];
|
|
370
|
+
renderStatusBox({ title: "SUCCESS", rows: buyRows, tone: ANSI.green });
|
|
371
|
+
if (cfg.showQuoteDetails) {
|
|
372
|
+
console.log(` • Quote Details : ${JSON.stringify(result.quote, null, 2)}`);
|
|
373
|
+
}
|
|
374
|
+
} else if (type === "sell") {
|
|
375
|
+
const { sellToken } = await getTradeModule();
|
|
376
|
+
const result = await sellToken(mint, amountParam);
|
|
377
|
+
clearScreen();
|
|
378
|
+
const info = `Received ${result.solReceivedDecimal} SOL | Fees ${result.totalFees} | Impact ${result.priceImpact}`;
|
|
379
|
+
const sellRows = [
|
|
380
|
+
...baseRows,
|
|
381
|
+
["TXID", result.txid],
|
|
382
|
+
["Explorer", `https://orbmarkets.io/tx/${result.txid}`],
|
|
383
|
+
["Info", info],
|
|
384
|
+
["Verification", result.verificationStatus],
|
|
385
|
+
];
|
|
386
|
+
renderStatusBox({ title: "SUCCESS", rows: sellRows, tone: ANSI.green });
|
|
387
|
+
if (cfg.showQuoteDetails) {
|
|
388
|
+
console.log(` • Quote Details : ${JSON.stringify(result.quote, null, 2)}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
process.exit(0);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
clearScreen();
|
|
394
|
+
const errorMessage = err?.message || "Unknown error";
|
|
395
|
+
const txidMatch = errorMessage.match(/[1-9A-HJ-NP-Za-km-z]{32,}/);
|
|
396
|
+
const txid = txidMatch ? txidMatch[0] : "-";
|
|
397
|
+
const explorer = txidMatch ? `https://orbmarkets.io/tx/${txid}` : "-";
|
|
398
|
+
const mintDisplay = `${mint.slice(0, 4)}…${mint.slice(-4)}`;
|
|
399
|
+
const amountDisplay = String(amountParam);
|
|
400
|
+
renderStatusBox({
|
|
401
|
+
title: "FAILED",
|
|
402
|
+
tone: ANSI.red,
|
|
403
|
+
rows: [
|
|
404
|
+
["Action", type === "buy" ? "Buy" : "Sell"],
|
|
405
|
+
["Mint", mintDisplay],
|
|
406
|
+
["Amount", amountDisplay],
|
|
407
|
+
["TXID", txid],
|
|
408
|
+
["Explorer", explorer],
|
|
409
|
+
["Error", errorMessage],
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// CONFIG subcommands
|
|
417
|
+
const configCmd = program.command("config").description("Manage CLI configuration");
|
|
418
|
+
|
|
419
|
+
configCmd
|
|
420
|
+
.command("view")
|
|
421
|
+
.description("Show current config")
|
|
422
|
+
.action(async () => {
|
|
423
|
+
const configPath = getConfigPath();
|
|
424
|
+
const cfg = await loadConfig();
|
|
425
|
+
console.log(`Config file: ${configPath}\n`);
|
|
426
|
+
renderConfigSummary(cfg, configPath);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
configCmd
|
|
430
|
+
.command("edit")
|
|
431
|
+
.description("Edit config in your $EDITOR")
|
|
432
|
+
.action(async () => {
|
|
433
|
+
await editConfig();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
configCmd
|
|
437
|
+
.command("set <key> <value>")
|
|
438
|
+
.description("Set a single config key")
|
|
439
|
+
.action(async (key, value) => {
|
|
440
|
+
const configPath = getConfigPath();
|
|
441
|
+
const cfg = await loadConfig();
|
|
442
|
+
const parsedValue = parseConfigValue(value);
|
|
443
|
+
if (!CONFIG_KEY_SET.has(key)) {
|
|
444
|
+
console.error(`⚠️ Unknown config key: ${key}`);
|
|
445
|
+
console.error("Run `summon config list` to see valid keys.");
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
if (key.startsWith("jito.")) {
|
|
450
|
+
const field = key.split(".")[1];
|
|
451
|
+
const nextJito = { ...(cfg.jito || DEFAULT_CONFIG.jito), [field]: parsedValue };
|
|
452
|
+
cfg.jito = normalizeConfigValue("jito", nextJito, { strict: true });
|
|
453
|
+
} else {
|
|
454
|
+
const normalizedValue = normalizeConfigValue(key, parsedValue, { strict: true });
|
|
455
|
+
cfg[key] = normalizedValue;
|
|
456
|
+
if (key === "priorityFee" && normalizedValue === "auto") {
|
|
457
|
+
console.log(
|
|
458
|
+
`ℹ️ priorityFeeLevel is required when priorityFee is auto. Current level: ${cfg.priorityFeeLevel}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.error(`⚠️ ${err.message}`);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
await saveConfig(cfg);
|
|
467
|
+
console.log(`✅ Updated ${key} → ${value} in ${configPath}`);
|
|
468
|
+
renderConfigSummary(cfg, configPath);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
configCmd
|
|
472
|
+
.command("list")
|
|
473
|
+
.description("List available config keys and types")
|
|
474
|
+
.action(() => {
|
|
475
|
+
console.log("Available config keys:");
|
|
476
|
+
for (const entry of CONFIG_HELP) {
|
|
477
|
+
console.log(` • ${entry.key} (${entry.type}) — ${entry.note}`);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
configCmd
|
|
482
|
+
.command("wizard")
|
|
483
|
+
.description("Interactive config editor with type validation")
|
|
484
|
+
.action(async () => {
|
|
485
|
+
const rl = readline.createInterface({
|
|
486
|
+
input: process.stdin,
|
|
487
|
+
output: process.stdout,
|
|
488
|
+
});
|
|
489
|
+
const cfg = await loadConfig();
|
|
490
|
+
const updated = await runConfigWizard({ cfg, rl });
|
|
491
|
+
await saveConfig(updated);
|
|
492
|
+
rl.close();
|
|
493
|
+
const configPath = getConfigPath();
|
|
494
|
+
console.log("✅ Config updated.");
|
|
495
|
+
renderConfigSummary(updated, configPath);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// SETUP command – interactive setup wizard
|
|
499
|
+
program
|
|
500
|
+
.command("setup")
|
|
501
|
+
.description("Run interactive setup for config and keychain")
|
|
502
|
+
.action(async () => {
|
|
503
|
+
const rl = readline.createInterface({
|
|
504
|
+
input: process.stdin,
|
|
505
|
+
output: process.stdout,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const configPath = getConfigPath();
|
|
509
|
+
const cfg = await loadConfig();
|
|
510
|
+
|
|
511
|
+
console.log("⚙️ Summon CLI Setup\n");
|
|
512
|
+
const updated = await runConfigWizard({ cfg, rl });
|
|
513
|
+
await saveConfig(updated);
|
|
514
|
+
console.log(`✅ Config saved to ${configPath}`);
|
|
515
|
+
|
|
516
|
+
// Private key
|
|
517
|
+
try {
|
|
518
|
+
if (await hasPrivateKey()) {
|
|
519
|
+
const updateKey = await askQuestion(
|
|
520
|
+
rl,
|
|
521
|
+
"🔓 Private key already stored in Keychain. Would you like to replace it? (y/N): "
|
|
522
|
+
);
|
|
523
|
+
if (updateKey.toLowerCase() === "y") {
|
|
524
|
+
const privKey = await askQuestion(rl, "Paste your new private key: ");
|
|
525
|
+
await storePrivateKey(privKey);
|
|
526
|
+
console.log("🔐 Private key updated.");
|
|
527
|
+
} else {
|
|
528
|
+
console.log("✅ Keeping existing private key.");
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
const storeKey = await askQuestion(
|
|
532
|
+
rl,
|
|
533
|
+
"Would you like to store your private key in the macOS Keychain now? (y/N): "
|
|
534
|
+
);
|
|
535
|
+
if (storeKey.toLowerCase() === "y") {
|
|
536
|
+
const privKey = await askQuestion(rl, "Paste your private key: ");
|
|
537
|
+
await storePrivateKey(privKey);
|
|
538
|
+
console.log("🔐 Private key stored securely.");
|
|
539
|
+
} else {
|
|
540
|
+
console.log("⚠️ No private key stored. You can add one later with `summon keychain store`.");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch (e) {
|
|
544
|
+
console.error("❌ Keychain error:", e.message);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
rl.close();
|
|
548
|
+
console.log("🧠 Setup complete.");
|
|
549
|
+
|
|
550
|
+
// Test macOS notifications so users can allow permissions now
|
|
551
|
+
if (updated.notificationsEnabled !== false) {
|
|
552
|
+
try {
|
|
553
|
+
notify({
|
|
554
|
+
title: "summonTheWarlord",
|
|
555
|
+
subtitle: "Setup complete",
|
|
556
|
+
message: "If you see this, notifications are enabled.",
|
|
557
|
+
sound: "Ping",
|
|
558
|
+
});
|
|
559
|
+
console.log("🔔 Test notification sent. If you see it, notifications are enabled.");
|
|
560
|
+
} catch {
|
|
561
|
+
console.warn("⚠️ Unable to send test notification. You may need to enable notifications for your terminal.");
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
console.log("🔕 Notifications are disabled in config.");
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// KEYCHAIN subcommands
|
|
569
|
+
const keychainCmd = program.command("keychain").description("Manage private key storage in macOS Keychain");
|
|
570
|
+
|
|
571
|
+
keychainCmd
|
|
572
|
+
.command("store")
|
|
573
|
+
.description("Store private key securely in macOS Keychain")
|
|
574
|
+
.action(() => {
|
|
575
|
+
const rl = readline.createInterface({
|
|
576
|
+
input: process.stdin,
|
|
577
|
+
output: process.stdout,
|
|
578
|
+
});
|
|
579
|
+
rl.question("Paste your wallet private key: ", async (input) => {
|
|
580
|
+
rl.close();
|
|
581
|
+
try {
|
|
582
|
+
await storePrivateKey(input.trim());
|
|
583
|
+
console.log("🔐 Private key securely stored in macOS Keychain.");
|
|
584
|
+
} catch (err) {
|
|
585
|
+
console.error("❌ Failed to store key:", err.message);
|
|
586
|
+
process.exitCode = 1;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
keychainCmd
|
|
592
|
+
.command("unlock")
|
|
593
|
+
.description("Test retrieval of private key from macOS Keychain")
|
|
594
|
+
.action(async () => {
|
|
595
|
+
try {
|
|
596
|
+
const key = await getPrivateKey();
|
|
597
|
+
if (key) console.log("🔓 Private key retrieved successfully.");
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.error("❌ Failed to retrieve key:", err.message);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
keychainCmd
|
|
604
|
+
.command("delete")
|
|
605
|
+
.description("Delete the private key from macOS Keychain")
|
|
606
|
+
.action(async () => {
|
|
607
|
+
await deletePrivateKey();
|
|
608
|
+
console.log("💥 Private key deleted from macOS Keychain.");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
program
|
|
612
|
+
.command("buy <mint> <amount>")
|
|
613
|
+
.description("Buy a token with SOL")
|
|
614
|
+
.action(async (mint, amount) => {
|
|
615
|
+
await executeTrade("buy", mint, amount);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
program
|
|
619
|
+
.command("sell <mint> <amount>")
|
|
620
|
+
.description("Sell a token for SOL")
|
|
621
|
+
.action(async (mint, amount) => {
|
|
622
|
+
await executeTrade("sell", mint, amount);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Trade command with options for buy and sell (deprecated)
|
|
626
|
+
program
|
|
627
|
+
.command("trade <mint>", { hidden: true })
|
|
628
|
+
.description("DEPRECATED: Trade a specific token")
|
|
629
|
+
.option("-b, --buy <amount>", "Spend <amount> SOL (number or '<percent>%') to buy token")
|
|
630
|
+
.option("-s, --sell <amount>", "Sell <amount> tokens (number, 'auto', or '<percent>%')")
|
|
631
|
+
.action(async (mint, options) => {
|
|
632
|
+
console.log("⚠️ 'summon trade' is deprecated. Use 'summon buy' or 'summon sell' instead.");
|
|
633
|
+
if (options.buy) {
|
|
634
|
+
await executeTrade("buy", mint, options.buy);
|
|
635
|
+
} else if (options.sell) {
|
|
636
|
+
await executeTrade("sell", mint, options.sell);
|
|
637
|
+
} else {
|
|
638
|
+
console.log("⚠️ Please specify --buy <amount> or --sell <amount>");
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
program
|
|
644
|
+
.command("wallet")
|
|
645
|
+
.alias("w")
|
|
646
|
+
.description("Open your wallet in the browser via SolanaTracker.io")
|
|
647
|
+
.action(async () => {
|
|
648
|
+
// Lazy-load heavier deps only when wallet command runs
|
|
649
|
+
const [{ Keypair }, { default: bs58 }, { default: open }] = await Promise.all([
|
|
650
|
+
import("@solana/web3.js"),
|
|
651
|
+
import("bs58"),
|
|
652
|
+
import("open"),
|
|
653
|
+
]);
|
|
654
|
+
try {
|
|
655
|
+
const rawKey = await getPrivateKey();
|
|
656
|
+
let keypair;
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
// Try base58 format
|
|
660
|
+
const bytes = bs58.decode(rawKey);
|
|
661
|
+
keypair = Keypair.fromSecretKey(bytes);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
try {
|
|
664
|
+
// Try JSON array format
|
|
665
|
+
const arr = JSON.parse(rawKey);
|
|
666
|
+
if (!Array.isArray(arr)) throw new Error("Not an array");
|
|
667
|
+
keypair = Keypair.fromSecretKey(Uint8Array.from(arr));
|
|
668
|
+
} catch (jsonErr) {
|
|
669
|
+
throw new Error("Private key is neither base58 nor valid JSON array.");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const pubkey = keypair.publicKey.toBase58();
|
|
674
|
+
const url = `https://www.solanatracker.io/wallet/${pubkey}`;
|
|
675
|
+
console.log(`🌐 Opening wallet in browser: ${url}`);
|
|
676
|
+
await open(url);
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.error("❌ Failed to load key from Keychain:", err.message);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// DOCTOR command
|
|
683
|
+
program
|
|
684
|
+
.command("doctor")
|
|
685
|
+
.description("Run environment and connectivity checks")
|
|
686
|
+
.option("-v, --verbose", "Show verbose output")
|
|
687
|
+
.action(async (options) => {
|
|
688
|
+
const results = await runDoctor({ verbose: Boolean(options.verbose) });
|
|
689
|
+
for (const result of results) {
|
|
690
|
+
const icon = result.status === "ok" ? "✅" : result.status === "skip" ? "⚠️" : "❌";
|
|
691
|
+
console.log(`${icon} ${result.name}: ${result.message}`);
|
|
692
|
+
if (options.verbose && result.details) {
|
|
693
|
+
console.log(` • ${result.details}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const hasFailure = results.some((item) => item.status === "fail");
|
|
697
|
+
process.exit(hasFailure ? 1 : 0);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// MANUAL command
|
|
701
|
+
program
|
|
702
|
+
.command("man")
|
|
703
|
+
.alias("m")
|
|
704
|
+
.description("Display usage and help information")
|
|
705
|
+
.action(() => {
|
|
706
|
+
console.log(`
|
|
707
|
+
📖 Summon CLI Manual
|
|
708
|
+
|
|
709
|
+
FIRST TIME QUICKSTART:
|
|
710
|
+
1) summon setup
|
|
711
|
+
Saves config + stores your private key in Keychain.
|
|
712
|
+
2) summon config wizard
|
|
713
|
+
Review RPC, fees, slippage, notifications, and Jito.
|
|
714
|
+
3) summon doctor
|
|
715
|
+
Confirms RPC + swap API are healthy.
|
|
716
|
+
4) summon buy <mint> 0.01
|
|
717
|
+
Start small while you learn.
|
|
718
|
+
|
|
719
|
+
TERMS:
|
|
720
|
+
• Mint = token address (base58). Copy it from a Solana explorer or DEX listing.
|
|
721
|
+
• Amounts:
|
|
722
|
+
- Buy uses SOL amount (e.g. 0.1)
|
|
723
|
+
- Sell uses token amount, percent (50%), or auto for full balance
|
|
724
|
+
|
|
725
|
+
USAGE:
|
|
726
|
+
summon setup
|
|
727
|
+
Run initial setup wizard (RPC, slippage, priority fees, Jito, etc.)
|
|
728
|
+
|
|
729
|
+
summon config view
|
|
730
|
+
View current configuration
|
|
731
|
+
|
|
732
|
+
summon config edit
|
|
733
|
+
Edit config in your $EDITOR
|
|
734
|
+
|
|
735
|
+
summon config set <key> <value>
|
|
736
|
+
Set a single config key
|
|
737
|
+
|
|
738
|
+
summon config wizard
|
|
739
|
+
Interactive config editor with type validation
|
|
740
|
+
|
|
741
|
+
summon config list
|
|
742
|
+
List available config keys and types
|
|
743
|
+
|
|
744
|
+
summon keychain store
|
|
745
|
+
Store your private key in the macOS Keychain (recommended)
|
|
746
|
+
• Paste either a base58-encoded string OR a JSON array like [12, 34, ...]
|
|
747
|
+
|
|
748
|
+
summon keychain unlock
|
|
749
|
+
Retrieve and verify your stored key
|
|
750
|
+
|
|
751
|
+
summon keychain delete
|
|
752
|
+
Delete the private key from macOS Keychain
|
|
753
|
+
|
|
754
|
+
summon buy <mint> <amount>
|
|
755
|
+
summon sell <mint> <amount>
|
|
756
|
+
Buy or sell a token. Amount formats:
|
|
757
|
+
• Fixed amount (e.g. 0.5 or 100)
|
|
758
|
+
• Percent of holdings (e.g. 50%)
|
|
759
|
+
• "auto" (sell only — sells your full balance)
|
|
760
|
+
|
|
761
|
+
summon wallet
|
|
762
|
+
Open your wallet on SolanaTracker.io
|
|
763
|
+
|
|
764
|
+
summon doctor
|
|
765
|
+
Run diagnostics for config, Keychain, RPC, swap API, and notifications
|
|
766
|
+
|
|
767
|
+
summon man
|
|
768
|
+
Display this manual
|
|
769
|
+
|
|
770
|
+
NOTES:
|
|
771
|
+
• This tool relies on SolanaTracker.io as its backend and won't work without them.
|
|
772
|
+
You can use the default RPC URL, but may see errors and issues because it’s free & public.
|
|
773
|
+
Signup for a free account here: https://www.solanatracker.io/solana-rpc
|
|
774
|
+
Use the new URL you are assigned in the config file.
|
|
775
|
+
• You may see errors about rate limits. This is largely due to using the free endpoint,
|
|
776
|
+
but they do happen occasionally. Your trade may still go through because those errors happen
|
|
777
|
+
while we're waiting for trade confirmation.
|
|
778
|
+
• Use summon buy or summon sell for trades
|
|
779
|
+
• Buying with "auto" is NOT supported — use a number or percent
|
|
780
|
+
• Your private key is never stored in plain text — use the Keychain for secure access
|
|
781
|
+
• Notifications are optional. Toggle notificationsEnabled in config if you want silence.
|
|
782
|
+
• Swaps show Pending → Success/Failed panes. If Verification is pending, open:
|
|
783
|
+
https://orbmarkets.io/tx/<txid>
|
|
784
|
+
• Quote details can be toggled in config or during setup
|
|
785
|
+
• Always confirm transactions via returned TXID and fees
|
|
786
|
+
|
|
787
|
+
Enjoy the chaos. 🪖
|
|
788
|
+
`);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// If no subcommand provided, show help
|
|
792
|
+
if (!process.argv.slice(2).length) {
|
|
793
|
+
program.outputHelp();
|
|
794
|
+
process.exit(0);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
program.parse(process.argv);
|