@graveyardprotocol/gp-cli 1.0.0
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.md +35 -0
- package/README.md +144 -0
- package/THIRD_PARTY_LICENSES +615 -0
- package/bin/gp.mjs +47 -0
- package/package.json +56 -0
- package/src/api.mjs +99 -0
- package/src/commands/addWallet.mjs +10 -0
- package/src/commands/closeEmpty.mjs +284 -0
- package/src/commands/listWallets.mjs +10 -0
- package/src/commands/removeWallet.mjs +10 -0
- package/src/config.mjs +16 -0
- package/src/display.mjs +188 -0
- package/src/solana.mjs +70 -0
- package/src/walletManager.mjs +265 -0
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graveyardprotocol/gp-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Graveyard Protocol CLI — close empty Solana SPL token accounts and reclaim locked SOL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"gp": "bin/gp.mjs"
|
|
11
|
+
},
|
|
12
|
+
"preferGlobal": true,
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"src/",
|
|
16
|
+
"LICENSE.md",
|
|
17
|
+
"THIRD_PARTY_LICENSES"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node bin/gp.mjs"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@solana/spl-token": "^0.4.9",
|
|
24
|
+
"@solana/web3.js": "^1.98.0",
|
|
25
|
+
"bs58": "^6.0.0",
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
|
+
"inquirer": "^13.3.2"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"solana",
|
|
31
|
+
"spl",
|
|
32
|
+
"token",
|
|
33
|
+
"graveyard-protocol",
|
|
34
|
+
"cli",
|
|
35
|
+
"reclaim",
|
|
36
|
+
"rent-exempt",
|
|
37
|
+
"refund your sol",
|
|
38
|
+
"unclaimed sol",
|
|
39
|
+
"claim your sol",
|
|
40
|
+
"sol incinerator"
|
|
41
|
+
],
|
|
42
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
43
|
+
"author": "Graveyard Protocol <dev@graveyardprotocol.io>",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/GraveyardProtocol/gp-cli.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/graveyardprotocol/gp-cli/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/graveyardprotocol/gp-cli#readme",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"javascript-obfuscator": "^5.3.1",
|
|
54
|
+
"license-checker": "^25.0.1"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/api.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { API_BASE_URL } from './config.mjs';
|
|
10
|
+
|
|
11
|
+
// ── Internal fetch helper ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
async function apiFetch(method, path, body) {
|
|
14
|
+
const url = `${API_BASE_URL}${path}`;
|
|
15
|
+
|
|
16
|
+
const options = {
|
|
17
|
+
method,
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
};
|
|
20
|
+
if (body !== undefined) {
|
|
21
|
+
options.body = JSON.stringify(body);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(url, options);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(`Network error calling ${url}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let json;
|
|
32
|
+
try {
|
|
33
|
+
json = await res.json();
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error(`Non-JSON response from ${url} (HTTP ${res.status})`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!res.ok || json.success === false) {
|
|
39
|
+
const msg = json.error || json.message || `HTTP ${res.status}`;
|
|
40
|
+
throw new Error(`API error [${path}]: ${msg}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return json;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── 1. Scan Wallet ────────────────────────────────────────────────────────────
|
|
47
|
+
// GET /v2/wallet/scan/{walletAddress}
|
|
48
|
+
// Returns: { wallet, total_empty_accounts, total_rent_sol,
|
|
49
|
+
// estimated_ghost_rewards, estimated_fees, scanTimestamp }
|
|
50
|
+
|
|
51
|
+
export async function scanWallet(walletAddress) {
|
|
52
|
+
const res = await apiFetch('GET', `/v2/wallet/scan/${walletAddress}`);
|
|
53
|
+
return res.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── 2. Get Batch Count ────────────────────────────────────────────────────────
|
|
57
|
+
// GET /v2/wallet/getBatchCount/{walletAddress}
|
|
58
|
+
// Returns: { wallet, totalBatches }
|
|
59
|
+
|
|
60
|
+
export async function getBatchCount(walletAddress) {
|
|
61
|
+
const res = await apiFetch('GET', `/v2/wallet/getBatchCount/${walletAddress}`);
|
|
62
|
+
return res.totalBatches;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 3. Process / Build Batch ──────────────────────────────────────────────────
|
|
66
|
+
// POST /v2/wallet/processBatch
|
|
67
|
+
// Body: { walletAddress, batchId }
|
|
68
|
+
// Returns: { batches: [{ intentID, instructionsBatch }] }
|
|
69
|
+
|
|
70
|
+
export async function processBatch(walletAddress, batchId) {
|
|
71
|
+
const res = await apiFetch('POST', '/v2/wallet/processBatch', {
|
|
72
|
+
walletAddress,
|
|
73
|
+
batchId,
|
|
74
|
+
});
|
|
75
|
+
return res.data.batches;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 4. Execute Batch ──────────────────────────────────────────────────────────
|
|
79
|
+
// POST /v2/wallet/executeBatch
|
|
80
|
+
// Body: { walletAddress, transactions: [{ intentID, signedTransaction }] }
|
|
81
|
+
// Returns: { success, results: [{ intentID, txSignature, batchAccountsClosed,
|
|
82
|
+
// batchRentSol, success, error? }] }
|
|
83
|
+
|
|
84
|
+
export async function executeBatch(walletAddress, transactions) {
|
|
85
|
+
const res = await apiFetch('POST', '/v2/wallet/executeBatch', {
|
|
86
|
+
walletAddress,
|
|
87
|
+
transactions,
|
|
88
|
+
});
|
|
89
|
+
return res.results;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── 5. Get Latest Blockhash ───────────────────────────────────────────────────
|
|
93
|
+
// GET /api/solana/blockhash
|
|
94
|
+
// Returns: { blockhash, lastValidBlockHeight }
|
|
95
|
+
|
|
96
|
+
export async function getLatestBlockhash() {
|
|
97
|
+
const res = await apiFetch('GET', '/api/solana/blockhash');
|
|
98
|
+
return res.data; // { blockhash, lastValidBlockHeight }
|
|
99
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { addWallet } from '../walletManager.mjs';
|
|
10
|
+
export default addWallet;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
import { loadWallet, loadWalletFile } from '../walletManager.mjs';
|
|
11
|
+
import { scanWallet, getBatchCount, processBatch, executeBatch, getLatestBlockhash } from '../api.mjs';
|
|
12
|
+
import { buildAndSignAll } from '../solana.mjs';
|
|
13
|
+
import {
|
|
14
|
+
printBanner,
|
|
15
|
+
printHeader,
|
|
16
|
+
printScanSummary,
|
|
17
|
+
printProgress,
|
|
18
|
+
createSpinner,
|
|
19
|
+
printBatchResult,
|
|
20
|
+
printFinalSummary,
|
|
21
|
+
printError,
|
|
22
|
+
printInfo,
|
|
23
|
+
printWarning,
|
|
24
|
+
printSuccess,
|
|
25
|
+
} from '../display.mjs';
|
|
26
|
+
|
|
27
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
// Detect inquirer's ExitPromptError (thrown on Ctrl+C in v9+)
|
|
30
|
+
function isExitPrompt(err) {
|
|
31
|
+
return err?.name === 'ExitPromptError';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Returns a spinner when verbose, or a silent no-op when not.
|
|
35
|
+
// The silent fail is a no-op — the caller's catch block handles the throw.
|
|
36
|
+
function maybeSpinner(verbose, text) {
|
|
37
|
+
if (verbose) return createSpinner(text);
|
|
38
|
+
return { start: () => {}, succeed: () => {}, fail: () => {} };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Wallet selection prompt ───────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
async function selectWallet(walletFile) {
|
|
44
|
+
if (!walletFile.wallets.length) {
|
|
45
|
+
throw new Error('No wallets saved. Run `gp add-wallet` first.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'rawlist',
|
|
53
|
+
name: 'selected',
|
|
54
|
+
message: 'Select a wallet to use:',
|
|
55
|
+
choices: walletFile.wallets.map((w) => ({
|
|
56
|
+
name: `${w.description || 'No description'} (${w.publicKey})`,
|
|
57
|
+
value: w.publicKey,
|
|
58
|
+
})),
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (isExitPrompt(err)) { console.log('\nAborted.'); process.exit(0); }
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result.selected;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Main close-empty handler ──────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export default async function closeEmpty(options) {
|
|
72
|
+
printBanner();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const walletFile = loadWalletFile();
|
|
76
|
+
let walletAddresses = [];
|
|
77
|
+
|
|
78
|
+
if (options.all) {
|
|
79
|
+
if (!walletFile.wallets.length) {
|
|
80
|
+
throw new Error('No wallets saved. Run `gp add-wallet` first.');
|
|
81
|
+
}
|
|
82
|
+
walletAddresses = walletFile.wallets.map(w => w.publicKey);
|
|
83
|
+
printInfo(`Processing all ${walletAddresses.length} wallet(s).\n`);
|
|
84
|
+
} else {
|
|
85
|
+
const selected = await selectWallet(walletFile);
|
|
86
|
+
walletAddresses = [selected];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const walletAddress of walletAddresses) {
|
|
90
|
+
await processWallet(walletAddress, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} catch (err) {
|
|
94
|
+
printError(err.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Process a single wallet ───────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
async function processWallet(walletAddress, options) {
|
|
102
|
+
const verbose = Boolean(options.verbose);
|
|
103
|
+
const dryRun = Boolean(options.dryRun);
|
|
104
|
+
|
|
105
|
+
printHeader(`Wallet: ${walletAddress}`);
|
|
106
|
+
|
|
107
|
+
// ── Step 1: Scan ─────────────────────────────────────────────────────────
|
|
108
|
+
const scanSpinner = createSpinner('Scanning for empty token accounts...');
|
|
109
|
+
scanSpinner.start();
|
|
110
|
+
|
|
111
|
+
let scanData;
|
|
112
|
+
try {
|
|
113
|
+
scanData = await scanWallet(walletAddress);
|
|
114
|
+
scanSpinner.succeed('Scan complete.');
|
|
115
|
+
} catch (err) {
|
|
116
|
+
scanSpinner.fail('Scan failed.');
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (scanData.total_empty_accounts === 0) {
|
|
121
|
+
printInfo('No empty token accounts found. Nothing to close.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Always show the scan summary — it's the key info before confirming
|
|
126
|
+
printScanSummary(scanData);
|
|
127
|
+
|
|
128
|
+
// ── Step 2: Confirm with user ─────────────────────────────────────────────
|
|
129
|
+
const confirmMsg = dryRun
|
|
130
|
+
? `Run dry-run for ${scanData.total_empty_accounts} account(s)?`
|
|
131
|
+
: `Close ${scanData.total_empty_accounts} account(s) and reclaim SOL?`;
|
|
132
|
+
|
|
133
|
+
let confirmAnswer;
|
|
134
|
+
try {
|
|
135
|
+
confirmAnswer = await inquirer.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'confirm',
|
|
138
|
+
name: 'confirm',
|
|
139
|
+
message: confirmMsg,
|
|
140
|
+
default: false,
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (isExitPrompt(err)) { console.log('\nAborted.'); process.exit(0); }
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!confirmAnswer.confirm) {
|
|
149
|
+
printWarning('Aborted by user.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Step 3: Unlock wallet ─────────────────────────────────────────────────
|
|
154
|
+
let keypair;
|
|
155
|
+
try {
|
|
156
|
+
keypair = await loadWallet(walletAddress);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw new Error(`Failed to unlock wallet: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Step 4: Get batch count ───────────────────────────────────────────────
|
|
162
|
+
const countSpinner = createSpinner('Fetching batch count...');
|
|
163
|
+
countSpinner.start();
|
|
164
|
+
|
|
165
|
+
let totalBatches;
|
|
166
|
+
try {
|
|
167
|
+
totalBatches = await getBatchCount(walletAddress);
|
|
168
|
+
countSpinner.succeed(`${totalBatches} batch(es) to process.`);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
countSpinner.fail('Failed to fetch batch count.');
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (totalBatches === 0) {
|
|
175
|
+
printWarning('Scan cache expired. Please run the command again to rescan.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Step 5: Process each DDB batch ───────────────────────────────────────
|
|
180
|
+
const allResults = [];
|
|
181
|
+
|
|
182
|
+
for (let batchId = 1; batchId <= totalBatches; batchId++) {
|
|
183
|
+
|
|
184
|
+
// Normal mode: one progress line per batch
|
|
185
|
+
if (!verbose) {
|
|
186
|
+
printProgress(`Processing batch ${batchId} of ${totalBatches}...`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log('');
|
|
189
|
+
printProgress(`Processing batch ${batchId} of ${totalBatches}...`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 5a. Fetch sub-batch instructions from Lambda
|
|
193
|
+
const buildSpinner = maybeSpinner(verbose, `Fetching instructions (batch ${batchId})...`);
|
|
194
|
+
buildSpinner.start();
|
|
195
|
+
|
|
196
|
+
let subBatches;
|
|
197
|
+
try {
|
|
198
|
+
subBatches = await processBatch(walletAddress, batchId);
|
|
199
|
+
buildSpinner.succeed(`${subBatches.length} transaction(s) built.`);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
buildSpinner.fail(`Failed to build batch ${batchId}.`);
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 5b. Fetch ONE blockhash for this entire DDB batch
|
|
206
|
+
const hashSpinner = maybeSpinner(verbose, 'Fetching latest blockhash...');
|
|
207
|
+
hashSpinner.start();
|
|
208
|
+
|
|
209
|
+
let blockhash;
|
|
210
|
+
try {
|
|
211
|
+
({ blockhash } = await getLatestBlockhash());
|
|
212
|
+
hashSpinner.succeed('Blockhash ready.');
|
|
213
|
+
} catch (err) {
|
|
214
|
+
hashSpinner.fail('Failed to fetch blockhash.');
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 5c. Build + sign ALL sub-batch transactions at once
|
|
219
|
+
const signSpinner = maybeSpinner(verbose, `Signing ${subBatches.length} transaction(s)...`);
|
|
220
|
+
signSpinner.start();
|
|
221
|
+
|
|
222
|
+
let signedTransactions;
|
|
223
|
+
try {
|
|
224
|
+
signedTransactions = buildAndSignAll(subBatches, blockhash, keypair);
|
|
225
|
+
signSpinner.succeed(`Signed ${signedTransactions.length} transaction(s).`);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
signSpinner.fail('Signing failed.');
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 5d. Execute or simulate depending on dry-run flag
|
|
232
|
+
if (dryRun) {
|
|
233
|
+
// Dry-run: skip executeBatch, fabricate success results from cache metadata
|
|
234
|
+
const dryResults = subBatches.map(sub => ({
|
|
235
|
+
intentID: sub.intentID,
|
|
236
|
+
txSignature: '(dry-run)',
|
|
237
|
+
batchAccountsClosed: sub.batchAccountsClosed ?? 0,
|
|
238
|
+
batchRentSol: sub.batchRentSol ?? 0,
|
|
239
|
+
success: true,
|
|
240
|
+
}));
|
|
241
|
+
allResults.push(...dryResults);
|
|
242
|
+
|
|
243
|
+
if (!verbose) {
|
|
244
|
+
printSuccess(`Batch ${batchId} of ${totalBatches} — dry-run complete.`);
|
|
245
|
+
} else {
|
|
246
|
+
printSuccess(`Batch ${batchId}: dry-run — ${dryResults.length} transaction(s) would be submitted.`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
} else {
|
|
250
|
+
// Live: send to execute Lambda
|
|
251
|
+
const execSpinner = maybeSpinner(verbose, 'Submitting to Solana...');
|
|
252
|
+
execSpinner.start();
|
|
253
|
+
|
|
254
|
+
let batchResults;
|
|
255
|
+
try {
|
|
256
|
+
batchResults = await executeBatch(walletAddress, signedTransactions);
|
|
257
|
+
const ok = batchResults.filter(r => r.success).length;
|
|
258
|
+
execSpinner.succeed(`Executed — ${ok}/${batchResults.length} succeeded.`);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
execSpinner.fail('Execution failed.');
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
allResults.push(...batchResults);
|
|
265
|
+
|
|
266
|
+
if (!verbose) {
|
|
267
|
+
const ok = batchResults.filter(r => r.success).length;
|
|
268
|
+
const failed = batchResults.length - ok;
|
|
269
|
+
if (failed === 0) {
|
|
270
|
+
printSuccess(`Batch ${batchId} of ${totalBatches} processed successfully.`);
|
|
271
|
+
} else {
|
|
272
|
+
printWarning(`Batch ${batchId} of ${totalBatches} — ${ok} succeeded, ${failed} failed.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Step 6: Results ───────────────────────────────────────────────────────
|
|
279
|
+
printHeader(dryRun ? 'Dry-run Results' : 'Results');
|
|
280
|
+
if (verbose) {
|
|
281
|
+
allResults.forEach((r, i) => printBatchResult(r, i));
|
|
282
|
+
}
|
|
283
|
+
printFinalSummary(allResults, dryRun);
|
|
284
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { listWallets } from '../walletManager.mjs';
|
|
10
|
+
export default listWallets;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { removeWallet } from '../walletManager.mjs';
|
|
10
|
+
export default removeWallet;
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── API ───────────────────────────────────────────────────────────────────────
|
|
10
|
+
export const API_BASE_URL = 'https://api.graveyardprotocol.io';
|
|
11
|
+
|
|
12
|
+
// ── Protocol constants - FOR INFORMATION ONLY──────────────────────────────────
|
|
13
|
+
// These parameters are handled in the backend.
|
|
14
|
+
// export const BASE_GHOST_POINTS = 100;
|
|
15
|
+
// export const PROTOCOL_FEE_PERCENT = 0.20; // 20%
|
|
16
|
+
// export const REAPER_FEE_RECIPIENT = 'GRAVEbqZNUN1K7WBgvwgWUYs69M51eprZbSkeXWbQjjE';
|
package/src/display.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Graveyard Protocol CLI
|
|
4
|
+
* Copyright (c) 2026 Graveyard Protocol. All rights reserved.
|
|
5
|
+
* This software and its source code are proprietary.
|
|
6
|
+
* Unauthorized copying, modification, or distribution is strictly prohibited.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Lightweight display helpers — no heavy dependencies.
|
|
10
|
+
// Uses ANSI escape codes directly so it works on every platform.
|
|
11
|
+
|
|
12
|
+
const C = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
grey: '\x1b[90m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function c(color, text) {
|
|
25
|
+
return `${C[color]}${text}${C.reset}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Gradient banner ───────────────────────────────────────────────────────────
|
|
29
|
+
// Applies a per-character 24-bit RGB gradient across each banner line.
|
|
30
|
+
// Purple (128, 0, 255) → Green (0, 220, 100).
|
|
31
|
+
// Degrades gracefully on terminals without true-color support.
|
|
32
|
+
|
|
33
|
+
const GRADIENT_START = { r: 128, g: 0, b: 255 }; // purple
|
|
34
|
+
const GRADIENT_END = { r: 0, g: 220, b: 100 }; // green
|
|
35
|
+
|
|
36
|
+
function trueColor(r, g, b, text) {
|
|
37
|
+
return `\x1b[1m\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function gradientLine(text) {
|
|
41
|
+
// Count printable characters (skip leading spaces used for indent)
|
|
42
|
+
const total = text.length || 1;
|
|
43
|
+
let result = '';
|
|
44
|
+
for (let i = 0; i < text.length; i++) {
|
|
45
|
+
const t = i / (total - 1);
|
|
46
|
+
const r = Math.round(GRADIENT_START.r + (GRADIENT_END.r - GRADIENT_START.r) * t);
|
|
47
|
+
const g = Math.round(GRADIENT_START.g + (GRADIENT_END.g - GRADIENT_START.g) * t);
|
|
48
|
+
const b = Math.round(GRADIENT_START.b + (GRADIENT_END.b - GRADIENT_START.b) * t);
|
|
49
|
+
result += trueColor(r, g, b, text[i]);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function printBanner() {
|
|
55
|
+
const lines = [
|
|
56
|
+
' ░██████╗░██████╗ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░',
|
|
57
|
+
' ██╔════╝ ██╔══██╗ Graveyard Protocol CLI ',
|
|
58
|
+
' ██║░░██╗ ██████╔╝ v1.0.0 ',
|
|
59
|
+
' ██║░░╚═╝ ██╔═══╝ ',
|
|
60
|
+
' ╚██████╗ ██║ Close empty SPL token accounts · Reclaim SOL ',
|
|
61
|
+
' ╚═════╝ ╚═╝ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░',
|
|
62
|
+
' '
|
|
63
|
+
];
|
|
64
|
+
console.log('');
|
|
65
|
+
lines.forEach(line => console.log(gradientLine(line)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Section header ────────────────────────────────────────────────────────────
|
|
69
|
+
export function printHeader(text) {
|
|
70
|
+
const line = '─'.repeat(50);
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(c('cyan', ` ${line}`));
|
|
73
|
+
console.log(c('cyan', c('bold', ` ${text}`)));
|
|
74
|
+
console.log(c('cyan', ` ${line}`));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Scan summary ──────────────────────────────────────────────────────────────
|
|
78
|
+
export function printScanSummary(data) {
|
|
79
|
+
const solNet = (data.total_rent_sol - data.estimated_fees).toFixed(6);
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(c('bold', ' Scan Results'));
|
|
82
|
+
console.log(c('grey', ' ──────────────────────────────────────'));
|
|
83
|
+
console.log(` Empty accounts found : ${c('yellow', data.total_empty_accounts)}`);
|
|
84
|
+
console.log(` Total rent locked : ${c('white', data.total_rent_sol.toFixed(6))} SOL`);
|
|
85
|
+
console.log(` Protocol fee (20%) : ${c('grey', data.estimated_fees.toFixed(6))} SOL`);
|
|
86
|
+
console.log(` ${c('bold', 'You will receive')} : ${c('green', solNet)} SOL`);
|
|
87
|
+
console.log(` Ghost Points earned : ${c('cyan', data.estimated_ghost_rewards.toLocaleString())}`);
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Dry-run notice ────────────────────────────────────────────────────────────
|
|
92
|
+
export function printDryRunNotice() {
|
|
93
|
+
console.log(c('yellow', ' ⚠ Dry-run mode — no transactions were submitted.\n'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Progress line ─────────────────────────────────────────────────────────────
|
|
97
|
+
export function printProgress(text) {
|
|
98
|
+
process.stdout.write(` ${c('grey', '→')} ${text}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Spinner (simple write/clear on same line) ─────────────────────────────────
|
|
102
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
103
|
+
const ERASE_LINE = '\x1b[2K'; // ANSI: erase entire current line
|
|
104
|
+
|
|
105
|
+
export function createSpinner(text) {
|
|
106
|
+
let i = 0;
|
|
107
|
+
let timer;
|
|
108
|
+
const isTTY = process.stdout.isTTY;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
start() {
|
|
112
|
+
if (!isTTY) { process.stdout.write(` ${text}...\n`); return; }
|
|
113
|
+
timer = setInterval(() => {
|
|
114
|
+
process.stdout.write(`\r${ERASE_LINE} ${c('cyan', SPINNER_FRAMES[i++ % SPINNER_FRAMES.length])} ${text}`);
|
|
115
|
+
}, 80);
|
|
116
|
+
},
|
|
117
|
+
succeed(msg) {
|
|
118
|
+
clearInterval(timer);
|
|
119
|
+
if (isTTY) process.stdout.write(`\r${ERASE_LINE} ${c('green', '✔')} ${msg || text}\n`);
|
|
120
|
+
else process.stdout.write(` ✔ ${msg || text}\n`);
|
|
121
|
+
},
|
|
122
|
+
fail(msg) {
|
|
123
|
+
clearInterval(timer);
|
|
124
|
+
if (isTTY) process.stdout.write(`\r${ERASE_LINE} ${c('red', '✖')} ${msg || text}\n`);
|
|
125
|
+
else process.stdout.write(` ✖ ${msg || text}\n`);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Per-batch result row ──────────────────────────────────────────────────────
|
|
131
|
+
export function printBatchResult(result, index) {
|
|
132
|
+
if (result.success) {
|
|
133
|
+
console.log(
|
|
134
|
+
` ${c('green', '✔')} Batch ${index + 1}` +
|
|
135
|
+
` ${c('yellow', result.batchAccountsClosed)} accounts closed` +
|
|
136
|
+
` ${c('green', result.batchRentSol?.toFixed(6))} SOL reclaimed`
|
|
137
|
+
);
|
|
138
|
+
console.log(` ${c('grey', 'tx:')} ${c('dim', result.txSignature)}`);
|
|
139
|
+
} else {
|
|
140
|
+
console.log(
|
|
141
|
+
` ${c('red', '✖')} Batch ${index + 1} ${c('red', result.error || 'Unknown error')}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Final summary ─────────────────────────────────────────────────────────────
|
|
147
|
+
export function printFinalSummary(results, dryRun = false) {
|
|
148
|
+
const succeeded = results.filter(r => r.success);
|
|
149
|
+
const failed = results.filter(r => !r.success);
|
|
150
|
+
const totalAcct = succeeded.reduce((s, r) => s + (r.batchAccountsClosed || 0), 0);
|
|
151
|
+
const totalSol = succeeded.reduce((s, r) => s + (r.batchRentSol || 0), 0);
|
|
152
|
+
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(c('bold', dryRun ? ' Dry-run Summary' : ' Summary'));
|
|
155
|
+
console.log(c('grey', ' ──────────────────────────────────────'));
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
console.log(` Batches simulated : ${c('cyan', succeeded.length)}`);
|
|
158
|
+
console.log(` Accounts that would close : ${c('yellow', totalAcct)}`);
|
|
159
|
+
console.log(` SOL that would be reclaimed (~80%) : ${c('green', totalSol.toFixed(6))} SOL`);
|
|
160
|
+
console.log(c('yellow', '\n ⚠ Dry-run — no transactions were submitted.'));
|
|
161
|
+
} else {
|
|
162
|
+
console.log(` Batches succeeded : ${c('green', succeeded.length)}`);
|
|
163
|
+
if (failed.length) {
|
|
164
|
+
console.log(` Batches failed : ${c('red', failed.length)}`);
|
|
165
|
+
}
|
|
166
|
+
console.log(` Accounts closed : ${c('yellow', totalAcct)}`);
|
|
167
|
+
console.log(` SOL reclaimed (~80%) : ${c('green', totalSol.toFixed(6))} SOL`);
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Error ─────────────────────────────────────────────────────────────────────
|
|
173
|
+
export function printError(msg) {
|
|
174
|
+
console.error(`\n ${c('red', '✖')} ${c('bold', 'Error:')} ${msg}\n`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Info ──────────────────────────────────────────────────────────────────────
|
|
178
|
+
export function printInfo(msg) {
|
|
179
|
+
console.log(` ${c('cyan', 'ℹ')} ${msg}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function printSuccess(msg) {
|
|
183
|
+
console.log(` ${c('green', '✔')} ${msg}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function printWarning(msg) {
|
|
187
|
+
console.log(` ${c('yellow', '⚠')} ${msg}`);
|
|
188
|
+
}
|