@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SolanaTracker } from "solana-swap";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { getPrivateKey } from "../utils/keychain.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { ConfigError, SwapError } from "./errors.js";
|
|
6
|
+
import { notify } from "../utils/notify.js";
|
|
7
|
+
|
|
8
|
+
const SWAP_DISCOUNT_CODE = "jduck-d815-4c28-b85d-17e9fc3a21a8";
|
|
9
|
+
|
|
10
|
+
let clientPromise = null;
|
|
11
|
+
let clientFactory = defaultFactory;
|
|
12
|
+
|
|
13
|
+
export function ensureAdvancedTx(rpcUrl) {
|
|
14
|
+
if (!rpcUrl || typeof rpcUrl !== "string") {
|
|
15
|
+
throw new ConfigError("RPC URL is missing or invalid.");
|
|
16
|
+
}
|
|
17
|
+
if (rpcUrl.includes("advancedTx")) {
|
|
18
|
+
return rpcUrl;
|
|
19
|
+
}
|
|
20
|
+
const separator = rpcUrl.includes("?") ? "&" : "?";
|
|
21
|
+
return `${rpcUrl}${separator}advancedTx=true`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setSwapClientFactory(factory) {
|
|
25
|
+
clientFactory = factory;
|
|
26
|
+
clientPromise = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getSwapClient() {
|
|
30
|
+
if (!clientPromise) {
|
|
31
|
+
clientPromise = clientFactory().catch((err) => {
|
|
32
|
+
clientPromise = null;
|
|
33
|
+
throw err;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return clientPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function defaultFactory() {
|
|
40
|
+
const cfg = await loadConfig();
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = await getPrivateKey();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (cfg.notificationsEnabled !== false) {
|
|
46
|
+
notify({
|
|
47
|
+
title: "🔑 Keychain Missing",
|
|
48
|
+
subtitle: "No private key found",
|
|
49
|
+
message: "Run `summon keychain store` to add your wallet.",
|
|
50
|
+
sound: "Ping",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Lazy-load heavy deps only when needed
|
|
57
|
+
const [{ default: bs58 }, { Keypair }] = await Promise.all([
|
|
58
|
+
import("bs58"),
|
|
59
|
+
import("@solana/web3.js"),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
let secretBytes;
|
|
63
|
+
if (raw.trim().startsWith("[")) {
|
|
64
|
+
secretBytes = Uint8Array.from(JSON.parse(raw));
|
|
65
|
+
} else {
|
|
66
|
+
secretBytes = bs58.decode(raw.trim());
|
|
67
|
+
}
|
|
68
|
+
const keypair = Keypair.fromSecretKey(secretBytes);
|
|
69
|
+
|
|
70
|
+
const rpcUrl = ensureAdvancedTx(cfg.rpcUrl);
|
|
71
|
+
const apiKey = SWAP_DISCOUNT_CODE;
|
|
72
|
+
const debugEnabled = Boolean(cfg.DEBUG_MODE || process.env.NODE_ENV === "development");
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const tracker = new SolanaTracker(keypair, rpcUrl, apiKey, debugEnabled);
|
|
76
|
+
if (debugEnabled && typeof tracker.setDebug === "function") {
|
|
77
|
+
tracker.setDebug(true);
|
|
78
|
+
}
|
|
79
|
+
return tracker;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.error("Failed to initialize swap client.", { error: err?.message });
|
|
82
|
+
throw new SwapError("Unable to initialize swap client.", { cause: err });
|
|
83
|
+
}
|
|
84
|
+
}
|
package/lib/trades.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { notify } from "../utils/notify.js";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { SwapError } from "./errors.js";
|
|
4
|
+
import { getSwapClient } from "./swapClient.js";
|
|
5
|
+
|
|
6
|
+
// Wrapped SOL mint address on Solana
|
|
7
|
+
const WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
8
|
+
const VERIFY_ATTEMPTS = 20;
|
|
9
|
+
const VERIFY_DELAY_MS = 1000;
|
|
10
|
+
|
|
11
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
13
|
+
async function verifySwap(tracker, txid) {
|
|
14
|
+
for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt += 1) {
|
|
15
|
+
const details = await tracker.getTransactionDetails(txid);
|
|
16
|
+
if (details) {
|
|
17
|
+
if (details.meta?.err) {
|
|
18
|
+
const errText = typeof details.meta.err === "string"
|
|
19
|
+
? details.meta.err
|
|
20
|
+
: JSON.stringify(details.meta.err);
|
|
21
|
+
throw new Error(`Transaction failed: ${errText}`);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
await sleep(VERIFY_DELAY_MS);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Buy tokens: spend a specific amount of SOL to acquire <mint>.
|
|
32
|
+
* @param {string} mint SPL token mint address
|
|
33
|
+
* @param {number|string} amountSol Amount in SOL to spend, or "auto"/"<percent>%"
|
|
34
|
+
* @returns Promise resolving with txid, tokens received, and quote/rate details
|
|
35
|
+
*/
|
|
36
|
+
export async function buyToken(mint, amountSol) {
|
|
37
|
+
const cfg = await loadConfig();
|
|
38
|
+
const tracker = await getSwapClient();
|
|
39
|
+
const debugEnabled = Boolean(cfg.DEBUG_MODE || process.env.NODE_ENV === "development");
|
|
40
|
+
const notificationsEnabled = cfg.notificationsEnabled !== false;
|
|
41
|
+
|
|
42
|
+
const slippage = cfg.slippage;
|
|
43
|
+
const priorityFeeArg = cfg.priorityFee;
|
|
44
|
+
const priorityFeeLevel = cfg.priorityFeeLevel || "medium";
|
|
45
|
+
const jito = cfg.jito?.enabled ? { enabled: true, tip: cfg.jito.tip } : null;
|
|
46
|
+
|
|
47
|
+
// Advanced opts: keep HTTP-only; include txVersion/priority level; include software fee here
|
|
48
|
+
const opts = {
|
|
49
|
+
txVersion: cfg.txVersion || "v0",
|
|
50
|
+
priorityFeeLevel,
|
|
51
|
+
fee: { wallet: "8aBKXBErcp1Bi5LmaeGnaXCj9ot7PE4T2wuqHQfeT5E6", percentage: 0.4 }, // 0.4%
|
|
52
|
+
feeType: "add"
|
|
53
|
+
// other optional flags that you may expose later:
|
|
54
|
+
// onlyDirectRoutes: false,
|
|
55
|
+
// customTip: undefined,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Build swap instructions: WSOL -> target token
|
|
59
|
+
const swapResp = await tracker.getSwapInstructions(
|
|
60
|
+
WRAPPED_SOL_MINT,
|
|
61
|
+
mint,
|
|
62
|
+
amountSol, // supports numbers, "auto", or "NN%"
|
|
63
|
+
slippage,
|
|
64
|
+
tracker.keypair.publicKey.toBase58(),
|
|
65
|
+
priorityFeeArg,
|
|
66
|
+
false, // forceLegacy; keep false when using txVersion in opts
|
|
67
|
+
opts
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Execute the swap (HTTP polling only; no websockets)
|
|
71
|
+
let txid;
|
|
72
|
+
try {
|
|
73
|
+
const performOpts = { debug: debugEnabled };
|
|
74
|
+
if (jito) {
|
|
75
|
+
performOpts.jito = jito;
|
|
76
|
+
}
|
|
77
|
+
const result = await tracker.performSwap(swapResp, performOpts);
|
|
78
|
+
txid = result.signature ?? result;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (notificationsEnabled) {
|
|
81
|
+
notify({
|
|
82
|
+
title: "❌ Swap Failed",
|
|
83
|
+
subtitle: "Buy failed",
|
|
84
|
+
message: err?.message || "Swap failed during execution.",
|
|
85
|
+
sound: "Basso",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
throw new SwapError(`Swap failed: ${err.message || err}`, { cause: err });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Normalize quote/rate and extract received tokens
|
|
92
|
+
const quote = swapResp.quote ?? swapResp.rate ?? {};
|
|
93
|
+
const tokensReceivedDecimal = Number(quote.amountOut ?? 0);
|
|
94
|
+
const fee = Number(quote.fee ?? 0);
|
|
95
|
+
const platformFee = Number(quote.platformFeeUI ?? 0);
|
|
96
|
+
const totalFees = fee + platformFee;
|
|
97
|
+
const priceImpact = quote.priceImpact;
|
|
98
|
+
|
|
99
|
+
let verificationStatus = "pending";
|
|
100
|
+
try {
|
|
101
|
+
const verified = await verifySwap(tracker, txid);
|
|
102
|
+
verificationStatus = verified ? "confirmed" : "pending";
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (notificationsEnabled) {
|
|
105
|
+
notify({
|
|
106
|
+
title: "❌ Swap Failed",
|
|
107
|
+
subtitle: "Buy failed",
|
|
108
|
+
message: err?.message || "Swap failed during verification.",
|
|
109
|
+
sound: "Basso",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
throw new SwapError(`Swap failed: ${err.message || err}`, { cause: err });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const shortMint = `${mint.slice(0, 4)}…${mint.slice(-4)}`;
|
|
116
|
+
const amountSolDisplay = typeof amountSol === "number"
|
|
117
|
+
? `${amountSol} SOL`
|
|
118
|
+
: `${amountSol} of SOL balance`;
|
|
119
|
+
if (notificationsEnabled) {
|
|
120
|
+
notify({
|
|
121
|
+
title: "🟢 Buy Completed",
|
|
122
|
+
subtitle: `Token: ${shortMint}`,
|
|
123
|
+
message: `Spent ${amountSolDisplay}\nReceived ${tokensReceivedDecimal.toFixed(4)} tokens`,
|
|
124
|
+
sound: "Ping",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { txid, tokensReceivedDecimal, totalFees, priceImpact, quote, verificationStatus };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sell tokens: swap a specified amount (decimal, 'auto', or '<percent>%') back to SOL.
|
|
132
|
+
* @param {string} mint SPL token mint address
|
|
133
|
+
* @param {number|string} amount Decimal amount, "auto", or "<percent>%"
|
|
134
|
+
* @returns Promise resolving with txid, SOL received, and quote/rate details
|
|
135
|
+
*/
|
|
136
|
+
export async function sellToken(mint, amount) {
|
|
137
|
+
const cfg = await loadConfig();
|
|
138
|
+
const tracker = await getSwapClient();
|
|
139
|
+
const debugEnabled = Boolean(cfg.DEBUG_MODE || process.env.NODE_ENV === "development");
|
|
140
|
+
const notificationsEnabled = cfg.notificationsEnabled !== false;
|
|
141
|
+
|
|
142
|
+
const slippage = cfg.slippage;
|
|
143
|
+
const priorityFeeArg = cfg.priorityFee;
|
|
144
|
+
const priorityFeeLevel = cfg.priorityFeeLevel || "medium";
|
|
145
|
+
const jito = cfg.jito?.enabled ? { enabled: true, tip: cfg.jito.tip } : null;
|
|
146
|
+
const opts = {
|
|
147
|
+
txVersion: cfg.txVersion || "v0",
|
|
148
|
+
priorityFeeLevel,
|
|
149
|
+
// add operator fee to sells as well:
|
|
150
|
+
fee: { wallet: "8aBKXBErcp1Bi5LmaeGnaXCj9ot7PE4T2wuqHQfeT5E6", percentage: 0.4 }, // 0.4%
|
|
151
|
+
feeType: "deduct"
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Build swap instructions: token -> WSOL
|
|
155
|
+
const swapResp = await tracker.getSwapInstructions(
|
|
156
|
+
mint,
|
|
157
|
+
WRAPPED_SOL_MINT,
|
|
158
|
+
amount,
|
|
159
|
+
slippage,
|
|
160
|
+
tracker.keypair.publicKey.toBase58(),
|
|
161
|
+
priorityFeeArg,
|
|
162
|
+
false,
|
|
163
|
+
opts
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Execute the swap (HTTP polling only; no websockets)
|
|
167
|
+
let txid;
|
|
168
|
+
try {
|
|
169
|
+
const performOpts = { debug: debugEnabled };
|
|
170
|
+
if (jito) {
|
|
171
|
+
performOpts.jito = jito;
|
|
172
|
+
}
|
|
173
|
+
const result = await tracker.performSwap(swapResp, performOpts);
|
|
174
|
+
txid = result.signature ?? result;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (notificationsEnabled) {
|
|
177
|
+
notify({
|
|
178
|
+
title: "❌ Swap Failed",
|
|
179
|
+
subtitle: "Sell failed",
|
|
180
|
+
message: err?.message || "Swap failed during execution.",
|
|
181
|
+
sound: "Basso",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
throw new SwapError(`Swap failed: ${err.message || err}`, { cause: err });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Normalize quote/rate and extract SOL received
|
|
188
|
+
const quote = swapResp.quote ?? swapResp.rate ?? {};
|
|
189
|
+
const solReceivedDecimal = Number(quote.outAmount ?? quote.amountOut ?? 0);
|
|
190
|
+
const fee = Number(quote.fee ?? 0);
|
|
191
|
+
const platformFee = Number(quote.platformFeeUI ?? 0);
|
|
192
|
+
const totalFees = fee + platformFee;
|
|
193
|
+
const priceImpact = quote.priceImpact;
|
|
194
|
+
|
|
195
|
+
let verificationStatus = "pending";
|
|
196
|
+
try {
|
|
197
|
+
const verified = await verifySwap(tracker, txid);
|
|
198
|
+
verificationStatus = verified ? "confirmed" : "pending";
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (notificationsEnabled) {
|
|
201
|
+
notify({
|
|
202
|
+
title: "❌ Swap Failed",
|
|
203
|
+
subtitle: "Sell failed",
|
|
204
|
+
message: err?.message || "Swap failed during verification.",
|
|
205
|
+
sound: "Basso",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
throw new SwapError(`Swap failed: ${err.message || err}`, { cause: err });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const shortMint = `${mint.slice(0, 4)}…${mint.slice(-4)}`;
|
|
212
|
+
const soldDisplay = amount === "auto" ? "full balance" : amount;
|
|
213
|
+
if (notificationsEnabled) {
|
|
214
|
+
notify({
|
|
215
|
+
title: "🔴 Sell Completed",
|
|
216
|
+
subtitle: `Token: ${shortMint}`,
|
|
217
|
+
message: `Sold ${soldDisplay} tokens\nReceived ${solReceivedDecimal.toFixed(4)} SOL`,
|
|
218
|
+
sound: "Ping",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { txid, solReceivedDecimal, totalFees, priceImpact, quote, verificationStatus };
|
|
222
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vault77/summon",
|
|
3
|
+
"version": "2.0.1",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"solana",
|
|
7
|
+
"solana-cli",
|
|
8
|
+
"solana-swap",
|
|
9
|
+
"cli",
|
|
10
|
+
"macos",
|
|
11
|
+
"keychain",
|
|
12
|
+
"trading",
|
|
13
|
+
"memecoin",
|
|
14
|
+
"developer-tools"
|
|
15
|
+
],
|
|
16
|
+
"author": "Humble Digital, llc <SummonTheWarlord@pm.me>",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/monthviewsales/summonTheWarlord.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/monthviewsales/summonTheWarlord#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/monthviewsales/summonTheWarlord/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"main": "summon-cli.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"summon": "./summon-cli.js"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib/",
|
|
36
|
+
"utils/",
|
|
37
|
+
"summon-cli.js",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"package.json"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
44
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
45
|
+
"test:ci": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"lint:fix": "eslint . --fix"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"eslint": "^8.57.1",
|
|
51
|
+
"eslint-config-standard": "^17.1.0",
|
|
52
|
+
"eslint-plugin-import": "^2.32.0",
|
|
53
|
+
"eslint-plugin-n": "^16.6.2",
|
|
54
|
+
"eslint-plugin-promise": "^6.6.0",
|
|
55
|
+
"jest": "^30.2.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@solana/web3.js": "^1.98.4",
|
|
59
|
+
"axios": "^1.13.3",
|
|
60
|
+
"bs58": "^6.0.0",
|
|
61
|
+
"commander": "^14.0.2",
|
|
62
|
+
"fs-extra": "^11.3.3",
|
|
63
|
+
"keytar": "^7.9.0",
|
|
64
|
+
"npm": "^11.8.0",
|
|
65
|
+
"open": "^11.0.0",
|
|
66
|
+
"solana-swap": "1.3.0"
|
|
67
|
+
}
|
|
68
|
+
}
|