@vault77/summon 2.0.1 β 2.1.2
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 +54 -7
- package/lib/config.js +1 -1
- package/lib/doctor.js +89 -13
- package/lib/swapClient.js +42 -5
- package/lib/tradeInput.js +104 -0
- package/lib/trades.js +179 -17
- package/package.json +6 -8
- package/summon-cli.js +184 -25
- package/utils/notify.js +30 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# summonTheWarlord β a VAULT77 π77 relic
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
**Version:** 2.0.
|
|
7
|
+
**Version:** 2.0.2
|
|
8
8
|
|
|
9
9
|
> _Relic software unearthed from VAULT77.
|
|
10
10
|
> For trench operators only. macOSβnative. Handle with care._
|
|
@@ -22,6 +22,31 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
## Before `summon setup`
|
|
26
|
+
|
|
27
|
+
First-time operator? Run `summon man` first for the built-in walkthrough.
|
|
28
|
+
|
|
29
|
+
Have these inputs ready:
|
|
30
|
+
|
|
31
|
+
- SolanaTracker RPC URL assigned to your account (full `https://...` endpoint; `advancedTx=true` can be present or omitted because summon enforces it automatically)
|
|
32
|
+
- Wallet private key in one accepted format: base58 string or JSON byte array string (example: `[12,34,...]`)
|
|
33
|
+
|
|
34
|
+
During `summon setup`, you'll be asked for:
|
|
35
|
+
|
|
36
|
+
- `rpcUrl`
|
|
37
|
+
- `slippage` (`number` or `"auto"`)
|
|
38
|
+
- `priorityFee` (`number` or `"auto"`)
|
|
39
|
+
- `priorityFeeLevel` (`min|low|medium|high|veryHigh`)
|
|
40
|
+
- `txVersion` (`v0` or `legacy`)
|
|
41
|
+
- `showQuoteDetails` (`true`/`false`)
|
|
42
|
+
- `DEBUG_MODE` (`true`/`false`)
|
|
43
|
+
- `notificationsEnabled` (`true`/`false`)
|
|
44
|
+
- `jito.enabled` (`true`/`false`)
|
|
45
|
+
- `jito.tip` (SOL, only when Jito is enabled)
|
|
46
|
+
- Whether to store/replace your private key now (`y/N`) and, if yes, paste the key
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
25
50
|
## π‘ Connect with VAULT77
|
|
26
51
|
|
|
27
52
|
- **VAULT77 Community:** https://x.com/i/communities/1962257350309650488
|
|
@@ -43,6 +68,8 @@ npm install -g @vault77/summon
|
|
|
43
68
|
summon setup
|
|
44
69
|
```
|
|
45
70
|
|
|
71
|
+
If this is your first time, run `summon man` before setup for the full command walkthrough.
|
|
72
|
+
|
|
46
73
|
This:
|
|
47
74
|
|
|
48
75
|
- Creates/updates your config (RPC URL, slippage, priority fees, etc.)
|
|
@@ -67,6 +94,31 @@ summon sell <TOKEN_MINT> 50%
|
|
|
67
94
|
|
|
68
95
|
---
|
|
69
96
|
|
|
97
|
+
# π Command Reference
|
|
98
|
+
|
|
99
|
+
For the full first-time walkthrough:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
summon man
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `summon setup` β interactive setup for config plus Keychain/private key prompts
|
|
106
|
+
- `summon config view` β show current config
|
|
107
|
+
- `summon config edit` β edit config in your `$EDITOR`
|
|
108
|
+
- `summon config set <key> <value>` β set one config value
|
|
109
|
+
- `summon config wizard` β interactive, validated config editor
|
|
110
|
+
- `summon config list` β list config keys and expected types
|
|
111
|
+
- `summon keychain store` β store private key in macOS Keychain
|
|
112
|
+
- `summon keychain unlock` β verify key retrieval from Keychain
|
|
113
|
+
- `summon keychain delete` β delete stored private key
|
|
114
|
+
- `summon buy <TOKEN_MINT> <amount>` β buy with SOL amount (`auto` is not supported for buys)
|
|
115
|
+
- `summon sell <TOKEN_MINT> <amount>` β sell fixed amount, percent (like `50%`), or `auto`
|
|
116
|
+
- `summon wallet` β open your wallet page in browser
|
|
117
|
+
- `summon doctor` β run config/Keychain/RPC/swap/notification diagnostics
|
|
118
|
+
- `summon man` β display the built-in manual
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
70
122
|
# π§° Local Development (optional)
|
|
71
123
|
|
|
72
124
|
```bash
|
|
@@ -178,15 +230,10 @@ Dependencies:
|
|
|
178
230
|
- `commander` β [MIT](https://github.com/tj/commander.js/blob/HEAD/LICENSE)
|
|
179
231
|
- `fs-extra` β [MIT](https://github.com/jprichardson/node-fs-extra/blob/HEAD/LICENSE)
|
|
180
232
|
- `keytar` β [MIT](https://github.com/atom/node-keytar/blob/HEAD/LICENSE)
|
|
181
|
-
- `npm` β [Artistic-2.0](https://github.com/npm/cli/blob/HEAD/LICENSE)
|
|
182
233
|
- `open` β [MIT](https://github.com/sindresorhus/open/blob/HEAD/LICENSE)
|
|
183
234
|
- `solana-swap` β [ISC](https://github.com/YZYLAB/solana-swap/blob/HEAD/LICENSE)
|
|
184
235
|
|
|
185
236
|
Tooling:
|
|
186
237
|
|
|
187
238
|
- `eslint` β [MIT](https://github.com/eslint/eslint/blob/HEAD/LICENSE)
|
|
188
|
-
- `eslint-config-standard` β [MIT](https://github.com/standard/eslint-config-standard/blob/HEAD/LICENSE)
|
|
189
|
-
- `eslint-plugin-import` β [MIT](https://github.com/import-js/eslint-plugin-import/blob/HEAD/LICENSE)
|
|
190
|
-
- `eslint-plugin-n` β [MIT](https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/LICENSE)
|
|
191
|
-
- `eslint-plugin-promise` β [ISC](https://github.com/eslint-community/eslint-plugin-promise/blob/HEAD/LICENSE)
|
|
192
239
|
- `jest` β [MIT](https://github.com/jestjs/jest/blob/HEAD/LICENSE)
|
package/lib/config.js
CHANGED
package/lib/doctor.js
CHANGED
|
@@ -7,8 +7,8 @@ const WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
|
7
7
|
const TRUMP_MINT = "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN";
|
|
8
8
|
const MIN_SWAP_SOL = 0.0001;
|
|
9
9
|
|
|
10
|
-
function makeResult(name, status, message, details) {
|
|
11
|
-
return { name, status, message, details };
|
|
10
|
+
function makeResult(name, status, message, details, hint) {
|
|
11
|
+
return { name, status, message, details, hint };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
async function checkConfig() {
|
|
@@ -16,7 +16,16 @@ async function checkConfig() {
|
|
|
16
16
|
const cfg = await loadConfig();
|
|
17
17
|
return { cfg, result: makeResult("config", "ok", "Loaded and normalized config.") };
|
|
18
18
|
} catch (err) {
|
|
19
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
cfg: null,
|
|
21
|
+
result: makeResult(
|
|
22
|
+
"config",
|
|
23
|
+
"fail",
|
|
24
|
+
"Failed to load config.",
|
|
25
|
+
err?.message,
|
|
26
|
+
"Run `summon setup` or `summon config wizard`."
|
|
27
|
+
),
|
|
28
|
+
};
|
|
20
29
|
}
|
|
21
30
|
}
|
|
22
31
|
|
|
@@ -24,18 +33,42 @@ async function checkKeychain() {
|
|
|
24
33
|
try {
|
|
25
34
|
const exists = await hasPrivateKey();
|
|
26
35
|
if (!exists) {
|
|
27
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
result: makeResult(
|
|
39
|
+
"keychain",
|
|
40
|
+
"fail",
|
|
41
|
+
"No private key stored.",
|
|
42
|
+
undefined,
|
|
43
|
+
"Run `summon keychain store`."
|
|
44
|
+
),
|
|
45
|
+
};
|
|
28
46
|
}
|
|
29
47
|
await getPrivateKey();
|
|
30
48
|
return { ok: true, result: makeResult("keychain", "ok", "Private key accessible.") };
|
|
31
49
|
} catch (err) {
|
|
32
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
result: makeResult(
|
|
53
|
+
"keychain",
|
|
54
|
+
"fail",
|
|
55
|
+
"Unable to read private key.",
|
|
56
|
+
err?.message,
|
|
57
|
+
"Run `summon keychain store`."
|
|
58
|
+
),
|
|
59
|
+
};
|
|
33
60
|
}
|
|
34
61
|
}
|
|
35
62
|
|
|
36
63
|
async function checkRpc(rpcUrl) {
|
|
37
64
|
if (!rpcUrl) {
|
|
38
|
-
return makeResult(
|
|
65
|
+
return makeResult(
|
|
66
|
+
"rpc",
|
|
67
|
+
"fail",
|
|
68
|
+
"RPC URL not configured.",
|
|
69
|
+
undefined,
|
|
70
|
+
"Update `rpcUrl` via `summon config wizard`."
|
|
71
|
+
);
|
|
39
72
|
}
|
|
40
73
|
const healthUrl = ensureAdvancedTx(rpcUrl);
|
|
41
74
|
try {
|
|
@@ -45,15 +78,33 @@ async function checkRpc(rpcUrl) {
|
|
|
45
78
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getHealth" }),
|
|
46
79
|
});
|
|
47
80
|
if (!res.ok) {
|
|
48
|
-
return makeResult(
|
|
81
|
+
return makeResult(
|
|
82
|
+
"rpc",
|
|
83
|
+
"fail",
|
|
84
|
+
"RPC health check failed.",
|
|
85
|
+
`HTTP ${res.status}`,
|
|
86
|
+
"Update `rpcUrl` via `summon config wizard`."
|
|
87
|
+
);
|
|
49
88
|
}
|
|
50
89
|
const body = await res.json();
|
|
51
90
|
if (body.result !== "ok") {
|
|
52
|
-
return makeResult(
|
|
91
|
+
return makeResult(
|
|
92
|
+
"rpc",
|
|
93
|
+
"fail",
|
|
94
|
+
"RPC returned unhealthy status.",
|
|
95
|
+
JSON.stringify(body),
|
|
96
|
+
"Update `rpcUrl` via `summon config wizard`."
|
|
97
|
+
);
|
|
53
98
|
}
|
|
54
99
|
return makeResult("rpc", "ok", "RPC reachable.");
|
|
55
100
|
} catch (err) {
|
|
56
|
-
return makeResult(
|
|
101
|
+
return makeResult(
|
|
102
|
+
"rpc",
|
|
103
|
+
"fail",
|
|
104
|
+
"RPC health check error.",
|
|
105
|
+
err?.message,
|
|
106
|
+
"Update `rpcUrl` via `summon config wizard`."
|
|
107
|
+
);
|
|
57
108
|
}
|
|
58
109
|
}
|
|
59
110
|
|
|
@@ -84,11 +135,23 @@ async function checkSwapApi(cfg, keychainOk) {
|
|
|
84
135
|
);
|
|
85
136
|
const quote = swapResp?.quote ?? swapResp?.rate;
|
|
86
137
|
if (!quote) {
|
|
87
|
-
return makeResult(
|
|
138
|
+
return makeResult(
|
|
139
|
+
"swap",
|
|
140
|
+
"fail",
|
|
141
|
+
"Swap API response missing quote.",
|
|
142
|
+
undefined,
|
|
143
|
+
"Rerun `summon doctor -v` and verify SolanaTracker account/RPC."
|
|
144
|
+
);
|
|
88
145
|
}
|
|
89
146
|
return makeResult("swap", "ok", "Swap API reachable.");
|
|
90
147
|
} catch (err) {
|
|
91
|
-
return makeResult(
|
|
148
|
+
return makeResult(
|
|
149
|
+
"swap",
|
|
150
|
+
"fail",
|
|
151
|
+
"Swap API check failed.",
|
|
152
|
+
err?.message,
|
|
153
|
+
"Rerun `summon doctor -v` and verify SolanaTracker account/RPC."
|
|
154
|
+
);
|
|
92
155
|
}
|
|
93
156
|
}
|
|
94
157
|
|
|
@@ -108,11 +171,23 @@ async function checkNotifications(cfg) {
|
|
|
108
171
|
throwOnError: true,
|
|
109
172
|
});
|
|
110
173
|
if (!ok) {
|
|
111
|
-
return makeResult(
|
|
174
|
+
return makeResult(
|
|
175
|
+
"notifications",
|
|
176
|
+
"fail",
|
|
177
|
+
"Notification failed.",
|
|
178
|
+
undefined,
|
|
179
|
+
"Enable terminal notifications or disable `notificationsEnabled`."
|
|
180
|
+
);
|
|
112
181
|
}
|
|
113
182
|
return makeResult("notifications", "ok", "Notification sent.");
|
|
114
183
|
} catch (err) {
|
|
115
|
-
return makeResult(
|
|
184
|
+
return makeResult(
|
|
185
|
+
"notifications",
|
|
186
|
+
"fail",
|
|
187
|
+
"Notification failed.",
|
|
188
|
+
err?.message,
|
|
189
|
+
"Enable terminal notifications or disable `notificationsEnabled`."
|
|
190
|
+
);
|
|
116
191
|
}
|
|
117
192
|
}
|
|
118
193
|
|
|
@@ -137,6 +212,7 @@ export async function runDoctor({ verbose = false } = {}) {
|
|
|
137
212
|
return results.map((item) => ({
|
|
138
213
|
...item,
|
|
139
214
|
details: item.details || undefined,
|
|
215
|
+
hint: item.hint || undefined,
|
|
140
216
|
}));
|
|
141
217
|
}
|
|
142
218
|
return results;
|
package/lib/swapClient.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SolanaTracker } from "solana-swap";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
|
-
import { getPrivateKey } from "../utils/keychain.js";
|
|
4
3
|
import { logger } from "../utils/logger.js";
|
|
5
4
|
import { ConfigError, SwapError } from "./errors.js";
|
|
6
5
|
import { notify } from "../utils/notify.js";
|
|
@@ -9,6 +8,8 @@ const SWAP_DISCOUNT_CODE = "jduck-d815-4c28-b85d-17e9fc3a21a8";
|
|
|
9
8
|
|
|
10
9
|
let clientPromise = null;
|
|
11
10
|
let clientFactory = defaultFactory;
|
|
11
|
+
let memoizedRpcUrl = null;
|
|
12
|
+
let warnedOnConflictingCfg = false;
|
|
12
13
|
|
|
13
14
|
export function ensureAdvancedTx(rpcUrl) {
|
|
14
15
|
if (!rpcUrl || typeof rpcUrl !== "string") {
|
|
@@ -24,20 +25,56 @@ export function ensureAdvancedTx(rpcUrl) {
|
|
|
24
25
|
export function setSwapClientFactory(factory) {
|
|
25
26
|
clientFactory = factory;
|
|
26
27
|
clientPromise = null;
|
|
28
|
+
memoizedRpcUrl = null;
|
|
29
|
+
warnedOnConflictingCfg = false;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
function getConfigRpcUrl(cfg) {
|
|
33
|
+
if (!cfg || typeof cfg !== "object" || typeof cfg.rpcUrl !== "string") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return ensureAdvancedTx(cfg.rpcUrl);
|
|
38
|
+
} catch {
|
|
39
|
+
// Keep getSwapClient backward-compatible: ignore unusable cfg hints once client is memoized.
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the shared swap client instance.
|
|
46
|
+
* `options.cfg` is only consumed during initial client creation; subsequent calls always reuse
|
|
47
|
+
* the memoized client to preserve existing behavior.
|
|
48
|
+
*/
|
|
49
|
+
export async function getSwapClient(options = {}) {
|
|
50
|
+
const requestedRpcUrl = getConfigRpcUrl(options?.cfg);
|
|
51
|
+
|
|
30
52
|
if (!clientPromise) {
|
|
31
|
-
|
|
53
|
+
memoizedRpcUrl = requestedRpcUrl;
|
|
54
|
+
clientPromise = clientFactory(options).catch((err) => {
|
|
32
55
|
clientPromise = null;
|
|
56
|
+
memoizedRpcUrl = null;
|
|
57
|
+
warnedOnConflictingCfg = false;
|
|
33
58
|
throw err;
|
|
34
59
|
});
|
|
60
|
+
} else if (
|
|
61
|
+
requestedRpcUrl
|
|
62
|
+
&& memoizedRpcUrl
|
|
63
|
+
&& requestedRpcUrl !== memoizedRpcUrl
|
|
64
|
+
&& !warnedOnConflictingCfg
|
|
65
|
+
) {
|
|
66
|
+
warnedOnConflictingCfg = true;
|
|
67
|
+
logger.warn(
|
|
68
|
+
"getSwapClient received cfg.rpcUrl that differs from the memoized client; reusing existing client."
|
|
69
|
+
);
|
|
35
70
|
}
|
|
71
|
+
|
|
36
72
|
return clientPromise;
|
|
37
73
|
}
|
|
38
74
|
|
|
39
|
-
async function defaultFactory() {
|
|
40
|
-
const cfg = await loadConfig();
|
|
75
|
+
async function defaultFactory(options = {}) {
|
|
76
|
+
const cfg = options.cfg ?? await loadConfig();
|
|
77
|
+
const { getPrivateKey } = await import("../utils/keychain.js");
|
|
41
78
|
let raw;
|
|
42
79
|
try {
|
|
43
80
|
raw = await getPrivateKey();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const MINT_EXAMPLE = "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN";
|
|
2
|
+
|
|
3
|
+
const MINT_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
4
|
+
const FIELD_ORDER = ["mint", "amount"];
|
|
5
|
+
|
|
6
|
+
const AMOUNT_EXAMPLES = Object.freeze({
|
|
7
|
+
buy: ["0.01", "25%"],
|
|
8
|
+
sell: ["100", "25%", "auto"],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function getAmountExamples(type) {
|
|
12
|
+
return AMOUNT_EXAMPLES[type] || ["0.01"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeMint(rawMint) {
|
|
16
|
+
return String(rawMint ?? "").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateMint(rawMint) {
|
|
20
|
+
const mint = normalizeMint(rawMint);
|
|
21
|
+
if (!mint) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
field: "mint",
|
|
25
|
+
code: "missing",
|
|
26
|
+
message: "Missing mint address. Provide a base58 token mint (32-44 chars).",
|
|
27
|
+
value: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!MINT_REGEX.test(mint)) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
field: "mint",
|
|
34
|
+
code: "invalid",
|
|
35
|
+
message: "Invalid mint format. Expected base58 address (32-44 chars).",
|
|
36
|
+
value: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, field: "mint", code: null, message: null, value: mint };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeAmount(rawAmount) {
|
|
43
|
+
return String(rawAmount ?? "")
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/\s+/g, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseAmount(rawAmount) {
|
|
50
|
+
const normalized = normalizeAmount(rawAmount);
|
|
51
|
+
if (!normalized) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
field: "amount",
|
|
55
|
+
code: "missing",
|
|
56
|
+
message: "Missing amount.",
|
|
57
|
+
value: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (normalized !== "auto" && !normalized.endsWith("%")) {
|
|
61
|
+
const parsed = parseFloat(normalized);
|
|
62
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
field: "amount",
|
|
66
|
+
code: "invalid",
|
|
67
|
+
message: "Invalid amount. Use a positive number, 'auto' during a sell, or '<percent>%'.",
|
|
68
|
+
value: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, field: "amount", code: null, message: null, value: parsed };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, field: "amount", code: null, message: null, value: normalized };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateTradeInput({ type, mint, amount }) {
|
|
77
|
+
const mintResult = validateMint(mint);
|
|
78
|
+
const amountResult = parseAmount(amount);
|
|
79
|
+
const issues = [];
|
|
80
|
+
|
|
81
|
+
if (!mintResult.ok) {
|
|
82
|
+
issues.push(mintResult);
|
|
83
|
+
}
|
|
84
|
+
if (!amountResult.ok) {
|
|
85
|
+
issues.push(amountResult);
|
|
86
|
+
} else if (type === "buy" && amountResult.value === "auto") {
|
|
87
|
+
issues.push({
|
|
88
|
+
ok: false,
|
|
89
|
+
field: "amount",
|
|
90
|
+
code: "buy_auto_not_supported",
|
|
91
|
+
message: "Buying with 'auto' isn't supported. Use a number or '<percent>%'.",
|
|
92
|
+
value: null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
issues.sort((left, right) => FIELD_ORDER.indexOf(left.field) - FIELD_ORDER.indexOf(right.field));
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
ok: issues.length === 0,
|
|
100
|
+
mint: mintResult.ok ? mintResult.value : null,
|
|
101
|
+
amount: amountResult.ok ? amountResult.value : null,
|
|
102
|
+
issues,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/lib/trades.js
CHANGED
|
@@ -5,25 +5,185 @@ import { getSwapClient } from "./swapClient.js";
|
|
|
5
5
|
|
|
6
6
|
// Wrapped SOL mint address on Solana
|
|
7
7
|
const WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const VERIFY_DELAY_SCHEDULE_MS = [500, 1000, 2000, 3000, 4000, 5000];
|
|
9
|
+
const CONFIRMED_TRANSACTION_STATUSES = new Set([
|
|
10
|
+
"processed",
|
|
11
|
+
"confirmed",
|
|
12
|
+
"finalized",
|
|
13
|
+
"success",
|
|
14
|
+
"succeeded",
|
|
15
|
+
"ok",
|
|
16
|
+
]);
|
|
17
|
+
const FAILED_TRANSACTION_STATUSES = new Set([
|
|
18
|
+
"failed",
|
|
19
|
+
"failure",
|
|
20
|
+
"error",
|
|
21
|
+
"errored",
|
|
22
|
+
"reverted",
|
|
23
|
+
"dropped",
|
|
24
|
+
]);
|
|
10
25
|
|
|
11
26
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
const hasOwn = (value, key) => Boolean(value) && Object.prototype.hasOwnProperty.call(value, key);
|
|
28
|
+
|
|
29
|
+
function formatTransactionError(err) {
|
|
30
|
+
if (typeof err === "string") {
|
|
31
|
+
return err;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(err);
|
|
35
|
+
} catch {
|
|
36
|
+
return String(err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function classifyTransactionStatus(status) {
|
|
41
|
+
if (status == null) {
|
|
42
|
+
return "unknown";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof status === "string") {
|
|
46
|
+
const normalized = status.trim().toLowerCase();
|
|
47
|
+
if (CONFIRMED_TRANSACTION_STATUSES.has(normalized)) {
|
|
48
|
+
return "confirmed";
|
|
49
|
+
}
|
|
50
|
+
if (FAILED_TRANSACTION_STATUSES.has(normalized)) {
|
|
51
|
+
return "failed";
|
|
52
|
+
}
|
|
53
|
+
return "unknown";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof status === "object") {
|
|
57
|
+
if ((hasOwn(status, "Err") && status.Err != null)
|
|
58
|
+
|| (hasOwn(status, "err") && status.err != null)
|
|
59
|
+
|| (hasOwn(status, "error") && status.error != null)) {
|
|
60
|
+
return "failed";
|
|
61
|
+
}
|
|
62
|
+
if (hasOwn(status, "Ok")) {
|
|
63
|
+
return "confirmed";
|
|
64
|
+
}
|
|
65
|
+
if (hasOwn(status, "confirmationStatus")) {
|
|
66
|
+
return classifyTransactionStatus(status.confirmationStatus);
|
|
67
|
+
}
|
|
68
|
+
if (hasOwn(status, "status")) {
|
|
69
|
+
return classifyTransactionStatus(status.status);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractStatusError(status) {
|
|
77
|
+
if (status == null) {
|
|
78
|
+
return "unknown transaction status";
|
|
79
|
+
}
|
|
80
|
+
if (typeof status === "string") {
|
|
81
|
+
return status;
|
|
82
|
+
}
|
|
83
|
+
if (typeof status === "object") {
|
|
84
|
+
return status.Err ?? status.err ?? status.error ?? status;
|
|
85
|
+
}
|
|
86
|
+
return status;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getVerificationState(details) {
|
|
90
|
+
if (!details) {
|
|
91
|
+
return "pending";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hasMetaErr = hasOwn(details.meta, "err");
|
|
95
|
+
if (hasMetaErr && details.meta.err != null) {
|
|
96
|
+
throw new Error(`Transaction failed: ${formatTransactionError(details.meta.err)}`);
|
|
97
|
+
}
|
|
98
|
+
if (hasMetaErr && details.meta.err === null) {
|
|
99
|
+
return "confirmed";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const directErrors = [details.err, details.error, details.value?.err, details.value?.error];
|
|
103
|
+
for (const err of directErrors) {
|
|
104
|
+
if (err != null) {
|
|
105
|
+
throw new Error(`Transaction failed: ${formatTransactionError(err)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const statusCandidates = [
|
|
110
|
+
details.confirmationStatus,
|
|
111
|
+
details.status,
|
|
112
|
+
details.meta?.status,
|
|
113
|
+
details.value?.confirmationStatus,
|
|
114
|
+
details.value?.status,
|
|
115
|
+
];
|
|
116
|
+
const hasStatusHint = statusCandidates.some((status) => status != null);
|
|
117
|
+
|
|
118
|
+
for (const status of statusCandidates) {
|
|
119
|
+
const state = classifyTransactionStatus(status);
|
|
120
|
+
if (state === "confirmed") {
|
|
121
|
+
return "confirmed";
|
|
122
|
+
}
|
|
123
|
+
if (state === "failed") {
|
|
124
|
+
throw new Error(`Transaction failed: ${formatTransactionError(extractStatusError(status))}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!hasStatusHint && details.meta && typeof details.meta === "object") {
|
|
129
|
+
return "confirmed";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return "pending";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isTransientTransactionDetailsError(err) {
|
|
136
|
+
const status = err?.status ?? err?.statusCode ?? err?.response?.status;
|
|
137
|
+
if (typeof status === "number" && [408, 425, 429, 500, 502, 503, 504].includes(status)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const code = String(err?.code ?? "").toUpperCase();
|
|
142
|
+
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN"].includes(code)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const message = String(err?.message ?? err ?? "").toLowerCase();
|
|
147
|
+
return [
|
|
148
|
+
"timeout",
|
|
149
|
+
"timed out",
|
|
150
|
+
"network",
|
|
151
|
+
"fetch failed",
|
|
152
|
+
"socket hang up",
|
|
153
|
+
"temporarily unavailable",
|
|
154
|
+
"too many requests",
|
|
155
|
+
"rate limit",
|
|
156
|
+
].some((needle) => message.includes(needle));
|
|
157
|
+
}
|
|
12
158
|
|
|
13
159
|
async function verifySwap(tracker, txid) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
throw new Error(`Transaction failed: ${errText}`);
|
|
160
|
+
const checkDetails = async () => {
|
|
161
|
+
let details;
|
|
162
|
+
try {
|
|
163
|
+
details = await tracker.getTransactionDetails(txid);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (isTransientTransactionDetailsError(err)) {
|
|
166
|
+
return "pending";
|
|
22
167
|
}
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return getVerificationState(details);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const immediateState = await checkDetails();
|
|
175
|
+
if (immediateState === "confirmed") {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const waitMs of VERIFY_DELAY_SCHEDULE_MS) {
|
|
180
|
+
await sleep(waitMs);
|
|
181
|
+
const state = await checkDetails();
|
|
182
|
+
if (state === "confirmed") {
|
|
23
183
|
return true;
|
|
24
184
|
}
|
|
25
|
-
await sleep(VERIFY_DELAY_MS);
|
|
26
185
|
}
|
|
186
|
+
|
|
27
187
|
return false;
|
|
28
188
|
}
|
|
29
189
|
|
|
@@ -31,11 +191,12 @@ async function verifySwap(tracker, txid) {
|
|
|
31
191
|
* Buy tokens: spend a specific amount of SOL to acquire <mint>.
|
|
32
192
|
* @param {string} mint SPL token mint address
|
|
33
193
|
* @param {number|string} amountSol Amount in SOL to spend, or "auto"/"<percent>%"
|
|
194
|
+
* @param {{ cfg?: object }} [context] Optional context carrying preloaded config
|
|
34
195
|
* @returns Promise resolving with txid, tokens received, and quote/rate details
|
|
35
196
|
*/
|
|
36
|
-
export async function buyToken(mint, amountSol) {
|
|
37
|
-
const cfg = await loadConfig();
|
|
38
|
-
const tracker = await getSwapClient();
|
|
197
|
+
export async function buyToken(mint, amountSol, context = {}) {
|
|
198
|
+
const cfg = context?.cfg ?? await loadConfig();
|
|
199
|
+
const tracker = await getSwapClient({ cfg });
|
|
39
200
|
const debugEnabled = Boolean(cfg.DEBUG_MODE || process.env.NODE_ENV === "development");
|
|
40
201
|
const notificationsEnabled = cfg.notificationsEnabled !== false;
|
|
41
202
|
|
|
@@ -131,11 +292,12 @@ export async function buyToken(mint, amountSol) {
|
|
|
131
292
|
* Sell tokens: swap a specified amount (decimal, 'auto', or '<percent>%') back to SOL.
|
|
132
293
|
* @param {string} mint SPL token mint address
|
|
133
294
|
* @param {number|string} amount Decimal amount, "auto", or "<percent>%"
|
|
295
|
+
* @param {{ cfg?: object }} [context] Optional context carrying preloaded config
|
|
134
296
|
* @returns Promise resolving with txid, SOL received, and quote/rate details
|
|
135
297
|
*/
|
|
136
|
-
export async function sellToken(mint, amount) {
|
|
137
|
-
const cfg = await loadConfig();
|
|
138
|
-
const tracker = await getSwapClient();
|
|
298
|
+
export async function sellToken(mint, amount, context = {}) {
|
|
299
|
+
const cfg = context?.cfg ?? await loadConfig();
|
|
300
|
+
const tracker = await getSwapClient({ cfg });
|
|
139
301
|
const debugEnabled = Boolean(cfg.DEBUG_MODE || process.env.NODE_ENV === "development");
|
|
140
302
|
const notificationsEnabled = cfg.notificationsEnabled !== false;
|
|
141
303
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vault77/summon",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "A recovered VAULT77 relic for macOS operators. summonTheWarlord is a high-performance CLI tool for ultra-fast, low-fee Solana swaps on macOS. Private keys are secured using the native macOS Keychain, never written to disk or exposed to JavaScript memory longer than required. Built for serious CLI workflows with system notifications, local-first execution, and zero browser dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solana",
|
|
@@ -47,21 +47,19 @@
|
|
|
47
47
|
"lint:fix": "eslint . --fix"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"eslint": "^
|
|
51
|
-
"eslint
|
|
50
|
+
"@eslint/js": "^9.39.2",
|
|
51
|
+
"eslint": "^9.39.2",
|
|
52
52
|
"eslint-plugin-import": "^2.32.0",
|
|
53
|
-
"
|
|
54
|
-
"eslint-plugin-promise": "^6.6.0",
|
|
53
|
+
"globals": "^17.3.0",
|
|
55
54
|
"jest": "^30.2.0"
|
|
56
55
|
},
|
|
57
56
|
"dependencies": {
|
|
58
57
|
"@solana/web3.js": "^1.98.4",
|
|
59
|
-
"axios": "^1.13.
|
|
58
|
+
"axios": "^1.13.5",
|
|
60
59
|
"bs58": "^6.0.0",
|
|
61
|
-
"commander": "^14.0.
|
|
60
|
+
"commander": "^14.0.3",
|
|
62
61
|
"fs-extra": "^11.3.3",
|
|
63
62
|
"keytar": "^7.9.0",
|
|
64
|
-
"npm": "^11.8.0",
|
|
65
63
|
"open": "^11.0.0",
|
|
66
64
|
"solana-swap": "1.3.0"
|
|
67
65
|
}
|
package/summon-cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { storePrivateKey, getPrivateKey, deletePrivateKey, hasPrivateKey } from
|
|
|
16
16
|
import readline from "readline";
|
|
17
17
|
import { notify } from "./utils/notify.js";
|
|
18
18
|
import { runDoctor } from "./lib/doctor.js";
|
|
19
|
+
import { MINT_EXAMPLE, getAmountExamples, validateTradeInput } from "./lib/tradeInput.js";
|
|
19
20
|
|
|
20
21
|
const program = new Command();
|
|
21
22
|
program
|
|
@@ -44,6 +45,59 @@ const CONFIG_HELP = [
|
|
|
44
45
|
{ key: "jito.enabled", type: "true | false", note: "Enable Jito bundles" },
|
|
45
46
|
{ key: "jito.tip", type: "number", note: "Tip in SOL when Jito enabled" },
|
|
46
47
|
];
|
|
48
|
+
const WIZARD_FIELD_GUIDANCE = {
|
|
49
|
+
rpcUrl: {
|
|
50
|
+
helper: "Use your SolanaTracker RPC URL. advancedTx=true is appended automatically.",
|
|
51
|
+
recommended: "Your dedicated SolanaTracker endpoint",
|
|
52
|
+
defaultValue: DEFAULT_CONFIG.rpcUrl,
|
|
53
|
+
},
|
|
54
|
+
slippage: {
|
|
55
|
+
helper: "Max swap slippage percent. Use auto to let the backend choose dynamically.",
|
|
56
|
+
recommended: DEFAULT_CONFIG.slippage,
|
|
57
|
+
defaultValue: DEFAULT_CONFIG.slippage,
|
|
58
|
+
},
|
|
59
|
+
priorityFee: {
|
|
60
|
+
helper:
|
|
61
|
+
"Use auto for adaptive fees, or set a fixed SOL value (example: 0.0005). When set to auto, priorityFeeLevel controls aggressiveness.",
|
|
62
|
+
recommended: `${DEFAULT_CONFIG.priorityFee} + ${DEFAULT_CONFIG.priorityFeeLevel}`,
|
|
63
|
+
defaultValue: DEFAULT_CONFIG.priorityFee,
|
|
64
|
+
},
|
|
65
|
+
priorityFeeLevel: {
|
|
66
|
+
helper: "Used only when priorityFee is auto. Fixed priorityFee values ignore this setting.",
|
|
67
|
+
recommended: DEFAULT_CONFIG.priorityFeeLevel,
|
|
68
|
+
defaultValue: DEFAULT_CONFIG.priorityFeeLevel,
|
|
69
|
+
},
|
|
70
|
+
txVersion: {
|
|
71
|
+
helper: "v0 supports address lookup tables. legacy is only for compatibility edge cases.",
|
|
72
|
+
recommended: DEFAULT_CONFIG.txVersion,
|
|
73
|
+
defaultValue: DEFAULT_CONFIG.txVersion,
|
|
74
|
+
},
|
|
75
|
+
showQuoteDetails: {
|
|
76
|
+
helper: "Print full quote payload after swaps. Enable only when debugging swap math.",
|
|
77
|
+
recommended: DEFAULT_CONFIG.showQuoteDetails,
|
|
78
|
+
defaultValue: DEFAULT_CONFIG.showQuoteDetails,
|
|
79
|
+
},
|
|
80
|
+
DEBUG_MODE: {
|
|
81
|
+
helper: "Verbose SDK/network logging. Useful for diagnostics, noisy for regular trading.",
|
|
82
|
+
recommended: DEFAULT_CONFIG.DEBUG_MODE,
|
|
83
|
+
defaultValue: DEFAULT_CONFIG.DEBUG_MODE,
|
|
84
|
+
},
|
|
85
|
+
notificationsEnabled: {
|
|
86
|
+
helper: "macOS desktop notifications for trade and setup events.",
|
|
87
|
+
recommended: DEFAULT_CONFIG.notificationsEnabled,
|
|
88
|
+
defaultValue: DEFAULT_CONFIG.notificationsEnabled,
|
|
89
|
+
},
|
|
90
|
+
jitoEnabled: {
|
|
91
|
+
helper: "Bundle via Jito relays. Keep disabled unless you intentionally use Jito flow.",
|
|
92
|
+
recommended: DEFAULT_CONFIG.jito.enabled,
|
|
93
|
+
defaultValue: DEFAULT_CONFIG.jito.enabled,
|
|
94
|
+
},
|
|
95
|
+
jitoTip: {
|
|
96
|
+
helper: "Tip in SOL attached to Jito bundles when enabled.",
|
|
97
|
+
recommended: DEFAULT_CONFIG.jito.tip,
|
|
98
|
+
defaultValue: DEFAULT_CONFIG.jito.tip,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
47
101
|
|
|
48
102
|
const askQuestion = (rl, prompt) =>
|
|
49
103
|
new Promise((resolve) => rl.question(prompt, (answer) => resolve(answer.trim())));
|
|
@@ -73,6 +127,16 @@ function renderWizardHeader() {
|
|
|
73
127
|
console.log("Press Enter to keep the current value.\n");
|
|
74
128
|
}
|
|
75
129
|
|
|
130
|
+
function renderWizardFieldGuidance(field) {
|
|
131
|
+
const guidance = WIZARD_FIELD_GUIDANCE[field];
|
|
132
|
+
if (!guidance) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`Help: ${guidance.helper}`);
|
|
136
|
+
console.log(`Recommended: ${toDisplayValue(guidance.recommended)}`);
|
|
137
|
+
console.log(`Default: ${toDisplayValue(guidance.defaultValue)}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
76
140
|
function toDisplayValue(value) {
|
|
77
141
|
if (value === undefined || value === null) return "";
|
|
78
142
|
if (typeof value === "object") return JSON.stringify(value);
|
|
@@ -229,24 +293,26 @@ async function runConfigWizard({ cfg, rl }) {
|
|
|
229
293
|
|
|
230
294
|
clearScreen();
|
|
231
295
|
renderWizardHeader();
|
|
232
|
-
|
|
233
|
-
console.log("advancedTx=true is enforced automatically.\n");
|
|
296
|
+
renderWizardFieldGuidance("rpcUrl");
|
|
234
297
|
nextCfg.rpcUrl = await promptNormalized(rl, "RPC URL", "rpcUrl", { current: nextCfg.rpcUrl });
|
|
235
298
|
|
|
236
299
|
clearScreen();
|
|
237
300
|
renderWizardHeader();
|
|
301
|
+
renderWizardFieldGuidance("slippage");
|
|
238
302
|
nextCfg.slippage = await promptNormalized(rl, "Max slippage (number or \"auto\")", "slippage", {
|
|
239
303
|
current: nextCfg.slippage,
|
|
240
304
|
});
|
|
241
305
|
|
|
242
306
|
clearScreen();
|
|
243
307
|
renderWizardHeader();
|
|
308
|
+
renderWizardFieldGuidance("priorityFee");
|
|
244
309
|
nextCfg.priorityFee = await promptNormalized(rl, "Priority fee (number or \"auto\")", "priorityFee", {
|
|
245
310
|
current: nextCfg.priorityFee,
|
|
246
311
|
});
|
|
247
312
|
|
|
248
313
|
clearScreen();
|
|
249
314
|
renderWizardHeader();
|
|
315
|
+
renderWizardFieldGuidance("priorityFeeLevel");
|
|
250
316
|
nextCfg.priorityFeeLevel = await promptSelect(
|
|
251
317
|
rl,
|
|
252
318
|
"Priority fee level (used when priorityFee is auto)",
|
|
@@ -259,12 +325,14 @@ async function runConfigWizard({ cfg, rl }) {
|
|
|
259
325
|
|
|
260
326
|
clearScreen();
|
|
261
327
|
renderWizardHeader();
|
|
328
|
+
renderWizardFieldGuidance("txVersion");
|
|
262
329
|
nextCfg.txVersion = await promptSelect(rl, "Transaction version", TX_VERSIONS, {
|
|
263
330
|
current: nextCfg.txVersion,
|
|
264
331
|
});
|
|
265
332
|
|
|
266
333
|
clearScreen();
|
|
267
334
|
renderWizardHeader();
|
|
335
|
+
renderWizardFieldGuidance("showQuoteDetails");
|
|
268
336
|
const showQuoteDetails = await promptSelect(rl, "Show quote details", ["true", "false"], {
|
|
269
337
|
current: nextCfg.showQuoteDetails ? "true" : "false",
|
|
270
338
|
});
|
|
@@ -272,6 +340,7 @@ async function runConfigWizard({ cfg, rl }) {
|
|
|
272
340
|
|
|
273
341
|
clearScreen();
|
|
274
342
|
renderWizardHeader();
|
|
343
|
+
renderWizardFieldGuidance("DEBUG_MODE");
|
|
275
344
|
const debugMode = await promptSelect(rl, "Enable debug mode", ["true", "false"], {
|
|
276
345
|
current: nextCfg.DEBUG_MODE ? "true" : "false",
|
|
277
346
|
});
|
|
@@ -279,6 +348,7 @@ async function runConfigWizard({ cfg, rl }) {
|
|
|
279
348
|
|
|
280
349
|
clearScreen();
|
|
281
350
|
renderWizardHeader();
|
|
351
|
+
renderWizardFieldGuidance("notificationsEnabled");
|
|
282
352
|
const notificationsEnabled = await promptSelect(rl, "Enable notifications", ["true", "false"], {
|
|
283
353
|
current: nextCfg.notificationsEnabled ? "true" : "false",
|
|
284
354
|
});
|
|
@@ -286,6 +356,7 @@ async function runConfigWizard({ cfg, rl }) {
|
|
|
286
356
|
|
|
287
357
|
clearScreen();
|
|
288
358
|
renderWizardHeader();
|
|
359
|
+
renderWizardFieldGuidance("jitoEnabled");
|
|
289
360
|
const jitoEnabled = await promptSelect(rl, "Enable Jito bundles", ["true", "false"], {
|
|
290
361
|
current: nextCfg.jito.enabled ? "true" : "false",
|
|
291
362
|
});
|
|
@@ -293,6 +364,7 @@ async function runConfigWizard({ cfg, rl }) {
|
|
|
293
364
|
if (nextCfg.jito.enabled) {
|
|
294
365
|
clearScreen();
|
|
295
366
|
renderWizardHeader();
|
|
367
|
+
renderWizardFieldGuidance("jitoTip");
|
|
296
368
|
const requireTip = nextCfg.jito.tip === undefined || nextCfg.jito.tip === null;
|
|
297
369
|
nextCfg.jito.tip = await promptNumber(rl, "Jito tip (SOL)", {
|
|
298
370
|
current: nextCfg.jito.tip,
|
|
@@ -311,25 +383,78 @@ const getTradeModule = async () => {
|
|
|
311
383
|
return tradeModulePromise;
|
|
312
384
|
};
|
|
313
385
|
|
|
314
|
-
|
|
315
|
-
const
|
|
386
|
+
function getTradeCommandExample(type, mint = MINT_EXAMPLE) {
|
|
387
|
+
const amountExample = type === "buy" ? "0.01" : "auto";
|
|
388
|
+
return `summon ${type} ${mint} ${amountExample}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderTradeValidationErrors(type, validation) {
|
|
392
|
+
console.error(`Usage: summon ${type} [mint] [amount]`);
|
|
393
|
+
for (const issue of validation.issues) {
|
|
394
|
+
console.error(`β οΈ ${issue.message}`);
|
|
395
|
+
if (issue.field === "mint") {
|
|
396
|
+
console.error(` Example: ${getTradeCommandExample(type)}`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (issue.field === "amount") {
|
|
400
|
+
const amountExamples = getAmountExamples(type).join(", ");
|
|
401
|
+
const mintForExample = validation.mint || MINT_EXAMPLE;
|
|
402
|
+
console.error(` Amount examples: ${amountExamples}`);
|
|
403
|
+
console.error(` Example: ${getTradeCommandExample(type, mintForExample)}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
316
407
|
|
|
317
|
-
|
|
318
|
-
|
|
408
|
+
async function resolveTradeInput(type, mintArg, amountArg) {
|
|
409
|
+
let mint = mintArg;
|
|
410
|
+
let amount = amountArg;
|
|
411
|
+
let validation = validateTradeInput({ type, mint, amount });
|
|
412
|
+
if (validation.ok) {
|
|
413
|
+
return validation;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
417
|
+
if (!canPrompt) {
|
|
418
|
+
renderTradeValidationErrors(type, validation);
|
|
319
419
|
process.exit(1);
|
|
320
420
|
}
|
|
321
421
|
|
|
322
|
-
|
|
422
|
+
const rl = readline.createInterface({
|
|
423
|
+
input: process.stdin,
|
|
424
|
+
output: process.stdout,
|
|
425
|
+
});
|
|
323
426
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
427
|
+
try {
|
|
428
|
+
while (!validation.ok) {
|
|
429
|
+
const mintIssue = validation.issues.find((issue) => issue.field === "mint");
|
|
430
|
+
if (mintIssue) {
|
|
431
|
+
console.log(`β οΈ ${mintIssue.message}`);
|
|
432
|
+
console.log(` Example mint: ${MINT_EXAMPLE}`);
|
|
433
|
+
mint = await askQuestion(rl, "Mint address: ");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const amountIssue = validation.issues.find((issue) => issue.field === "amount");
|
|
437
|
+
if (amountIssue) {
|
|
438
|
+
console.log(`β οΈ ${amountIssue.message}`);
|
|
439
|
+
console.log(` Amount examples: ${getAmountExamples(type).join(", ")}`);
|
|
440
|
+
amount = await askQuestion(rl, "Amount: ");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
validation = validateTradeInput({ type, mint, amount });
|
|
329
444
|
}
|
|
330
|
-
|
|
445
|
+
} finally {
|
|
446
|
+
rl.close();
|
|
331
447
|
}
|
|
332
448
|
|
|
449
|
+
return validation;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function executeTrade(type, mintArg, amountArg) {
|
|
453
|
+
const cfg = await loadConfig();
|
|
454
|
+
const validated = await resolveTradeInput(type, mintArg, amountArg);
|
|
455
|
+
const mint = validated.mint;
|
|
456
|
+
const amountParam = validated.amount;
|
|
457
|
+
|
|
333
458
|
try {
|
|
334
459
|
const mintDisplay = `${mint.slice(0, 4)}β¦${mint.slice(-4)}`;
|
|
335
460
|
const amountDisplay = String(amountParam);
|
|
@@ -357,7 +482,7 @@ async function executeTrade(type, mint, amountArg) {
|
|
|
357
482
|
}
|
|
358
483
|
|
|
359
484
|
const { buyToken } = await getTradeModule();
|
|
360
|
-
const result = await buyToken(mint, amountParam);
|
|
485
|
+
const result = await buyToken(mint, amountParam, { cfg });
|
|
361
486
|
clearScreen();
|
|
362
487
|
const info = `Received ${result.tokensReceivedDecimal} tokens | Fees ${result.totalFees} | Impact ${result.priceImpact}`;
|
|
363
488
|
const buyRows = [
|
|
@@ -373,7 +498,7 @@ async function executeTrade(type, mint, amountArg) {
|
|
|
373
498
|
}
|
|
374
499
|
} else if (type === "sell") {
|
|
375
500
|
const { sellToken } = await getTradeModule();
|
|
376
|
-
const result = await sellToken(mint, amountParam);
|
|
501
|
+
const result = await sellToken(mint, amountParam, { cfg });
|
|
377
502
|
clearScreen();
|
|
378
503
|
const info = `Received ${result.solReceivedDecimal} SOL | Fees ${result.totalFees} | Impact ${result.priceImpact}`;
|
|
379
504
|
const sellRows = [
|
|
@@ -609,15 +734,15 @@ keychainCmd
|
|
|
609
734
|
});
|
|
610
735
|
|
|
611
736
|
program
|
|
612
|
-
.command("buy
|
|
613
|
-
.description("Buy a token with SOL")
|
|
737
|
+
.command("buy [mint] [amount]")
|
|
738
|
+
.description("Buy a token with SOL (prompts for missing values in interactive TTY)")
|
|
614
739
|
.action(async (mint, amount) => {
|
|
615
740
|
await executeTrade("buy", mint, amount);
|
|
616
741
|
});
|
|
617
742
|
|
|
618
743
|
program
|
|
619
|
-
.command("sell
|
|
620
|
-
.description("Sell a token for SOL")
|
|
744
|
+
.command("sell [mint] [amount]")
|
|
745
|
+
.description("Sell a token for SOL (prompts for missing values in interactive TTY)")
|
|
621
746
|
.action(async (mint, amount) => {
|
|
622
747
|
await executeTrade("sell", mint, amount);
|
|
623
748
|
});
|
|
@@ -659,13 +784,13 @@ program
|
|
|
659
784
|
// Try base58 format
|
|
660
785
|
const bytes = bs58.decode(rawKey);
|
|
661
786
|
keypair = Keypair.fromSecretKey(bytes);
|
|
662
|
-
} catch
|
|
787
|
+
} catch {
|
|
663
788
|
try {
|
|
664
789
|
// Try JSON array format
|
|
665
790
|
const arr = JSON.parse(rawKey);
|
|
666
791
|
if (!Array.isArray(arr)) throw new Error("Not an array");
|
|
667
792
|
keypair = Keypair.fromSecretKey(Uint8Array.from(arr));
|
|
668
|
-
} catch
|
|
793
|
+
} catch {
|
|
669
794
|
throw new Error("Private key is neither base58 nor valid JSON array.");
|
|
670
795
|
}
|
|
671
796
|
}
|
|
@@ -679,6 +804,23 @@ program
|
|
|
679
804
|
}
|
|
680
805
|
});
|
|
681
806
|
|
|
807
|
+
function extractSuggestedCommands(results) {
|
|
808
|
+
const commands = new Set();
|
|
809
|
+
for (const result of results) {
|
|
810
|
+
if (result.status !== "fail" || !result.hint) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const matches = [...result.hint.matchAll(/`([^`]+)`/g)];
|
|
814
|
+
for (const match of matches) {
|
|
815
|
+
const candidate = match[1].trim();
|
|
816
|
+
if (candidate.startsWith("summon ")) {
|
|
817
|
+
commands.add(candidate);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return [...commands];
|
|
822
|
+
}
|
|
823
|
+
|
|
682
824
|
// DOCTOR command
|
|
683
825
|
program
|
|
684
826
|
.command("doctor")
|
|
@@ -689,12 +831,28 @@ program
|
|
|
689
831
|
for (const result of results) {
|
|
690
832
|
const icon = result.status === "ok" ? "β
" : result.status === "skip" ? "β οΈ" : "β";
|
|
691
833
|
console.log(`${icon} ${result.name}: ${result.message}`);
|
|
834
|
+
if (result.status === "fail" && result.hint) {
|
|
835
|
+
console.log(` β’ Hint: ${result.hint}`);
|
|
836
|
+
}
|
|
692
837
|
if (options.verbose && result.details) {
|
|
693
838
|
console.log(` β’ ${result.details}`);
|
|
694
839
|
}
|
|
695
840
|
}
|
|
696
|
-
const
|
|
697
|
-
|
|
841
|
+
const failures = results.filter((item) => item.status === "fail");
|
|
842
|
+
const suggestedCommands = extractSuggestedCommands(results);
|
|
843
|
+
|
|
844
|
+
console.log("");
|
|
845
|
+
console.log(
|
|
846
|
+
`Doctor summary: ${failures.length} failure${failures.length === 1 ? "" : "s"} out of ${results.length} checks.`
|
|
847
|
+
);
|
|
848
|
+
if (suggestedCommands.length) {
|
|
849
|
+
console.log("Suggested commands:");
|
|
850
|
+
for (const command of suggestedCommands) {
|
|
851
|
+
console.log(` β’ ${command}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
process.exit(failures.length ? 1 : 0);
|
|
698
856
|
});
|
|
699
857
|
|
|
700
858
|
// MANUAL command
|
|
@@ -751,12 +909,13 @@ USAGE:
|
|
|
751
909
|
summon keychain delete
|
|
752
910
|
Delete the private key from macOS Keychain
|
|
753
911
|
|
|
754
|
-
summon buy
|
|
755
|
-
summon sell
|
|
912
|
+
summon buy [mint] [amount]
|
|
913
|
+
summon sell [mint] [amount]
|
|
756
914
|
Buy or sell a token. Amount formats:
|
|
757
915
|
β’ Fixed amount (e.g. 0.5 or 100)
|
|
758
916
|
β’ Percent of holdings (e.g. 50%)
|
|
759
917
|
β’ "auto" (sell only β sells your full balance)
|
|
918
|
+
In an interactive terminal, missing/invalid mint or amount will be prompted.
|
|
760
919
|
|
|
761
920
|
summon wallet
|
|
762
921
|
Open your wallet on SolanaTracker.io
|
package/utils/notify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { NotificationError } from "../lib/errors.js";
|
|
3
3
|
import { logger } from "./logger.js";
|
|
4
4
|
|
|
@@ -16,6 +16,8 @@ function escapeAppleScriptString(str) {
|
|
|
16
16
|
* @param {string} [options.message=""]
|
|
17
17
|
* @param {string} [options.subtitle=""]
|
|
18
18
|
* @param {string} [options.sound]
|
|
19
|
+
* @param {boolean} [options.blocking] When true, waits for osascript completion.
|
|
20
|
+
* Defaults to throwOnError so strict callers stay blocking.
|
|
19
21
|
*/
|
|
20
22
|
export function notify({
|
|
21
23
|
title = "summonTheWarlord",
|
|
@@ -23,6 +25,7 @@ export function notify({
|
|
|
23
25
|
subtitle = "",
|
|
24
26
|
sound,
|
|
25
27
|
throwOnError = false,
|
|
28
|
+
blocking = throwOnError,
|
|
26
29
|
} = {}) {
|
|
27
30
|
if (process.platform !== "darwin") {
|
|
28
31
|
const bell = "\u0007";
|
|
@@ -42,10 +45,33 @@ export function notify({
|
|
|
42
45
|
script += ` sound name "${escapeAppleScriptString(sound)}"`;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
if (blocking) {
|
|
49
|
+
const result = spawnSync("osascript", ["-e", script], { stdio: "ignore" });
|
|
50
|
+
if (result.error || result.status !== 0) {
|
|
51
|
+
throw new NotificationError(`osascript exited with ${result.status}`, { cause: result.error });
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
48
54
|
}
|
|
55
|
+
let handledFailure = false;
|
|
56
|
+
const fallback = () => {
|
|
57
|
+
if (handledFailure) return;
|
|
58
|
+
handledFailure = true;
|
|
59
|
+
console.log(`π ${title}: ${message}${subtitle ? ` - ${subtitle}` : ""}`);
|
|
60
|
+
};
|
|
61
|
+
const child = spawn("osascript", ["-e", script], { stdio: "ignore" });
|
|
62
|
+
child.once("error", (err) => {
|
|
63
|
+
logger.warn("Notification failed.", { error: err?.message });
|
|
64
|
+
fallback();
|
|
65
|
+
});
|
|
66
|
+
child.once("exit", (code) => {
|
|
67
|
+
if (code !== 0) {
|
|
68
|
+
logger.warn("Notification failed.", { error: `osascript exited with ${code}` });
|
|
69
|
+
fallback();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Intentionally keep the child process referenced so CLI flows that call
|
|
73
|
+
// process.exit(...) immediately after notify still allow exit/error handlers
|
|
74
|
+
// to run and emit the console fallback when delivery fails.
|
|
49
75
|
return true;
|
|
50
76
|
} catch (err) {
|
|
51
77
|
logger.warn("Notification failed.", { error: err?.message });
|