@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/src/solana.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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 {
|
|
10
|
+
PublicKey,
|
|
11
|
+
TransactionInstruction,
|
|
12
|
+
TransactionMessage,
|
|
13
|
+
VersionedTransaction,
|
|
14
|
+
} from '@solana/web3.js';
|
|
15
|
+
|
|
16
|
+
// ── Instruction deserialisation ───────────────────────────────────────────────
|
|
17
|
+
function deserialiseInstruction(raw) {
|
|
18
|
+
const programId = new PublicKey(
|
|
19
|
+
typeof raw.programId === 'string' ? raw.programId : raw.programId.toString()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const keys = (raw.keys || []).map((k) => ({
|
|
23
|
+
pubkey: new PublicKey(typeof k.pubkey === 'string' ? k.pubkey : k.pubkey.toString()),
|
|
24
|
+
isSigner: Boolean(k.isSigner),
|
|
25
|
+
isWritable: Boolean(k.isWritable),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
let data;
|
|
29
|
+
if (Buffer.isBuffer(raw.data)) {
|
|
30
|
+
data = raw.data;
|
|
31
|
+
} else if (raw.data?.type === 'Buffer' && Array.isArray(raw.data.data)) {
|
|
32
|
+
data = Buffer.from(raw.data.data);
|
|
33
|
+
} else if (Array.isArray(raw.data)) {
|
|
34
|
+
data = Buffer.from(raw.data);
|
|
35
|
+
} else {
|
|
36
|
+
data = Buffer.alloc(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new TransactionInstruction({ programId, keys, data });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Build a single VersionedTransaction (no signing) ─────────────────────────
|
|
43
|
+
function buildTransaction(rawInstructions, blockhash, payerPublicKey) {
|
|
44
|
+
const instructions = rawInstructions.map(deserialiseInstruction);
|
|
45
|
+
|
|
46
|
+
const message = new TransactionMessage({
|
|
47
|
+
payerKey: payerPublicKey,
|
|
48
|
+
recentBlockhash: blockhash,
|
|
49
|
+
instructions,
|
|
50
|
+
}).compileToV0Message();
|
|
51
|
+
|
|
52
|
+
return new VersionedTransaction(message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Build and sign ALL sub-batch transactions in one pass ─────────────────────
|
|
56
|
+
export function buildAndSignAll(subBatches, blockhash, keypair) {
|
|
57
|
+
// 1. Build all transactions
|
|
58
|
+
const txs = subBatches.map(sub =>
|
|
59
|
+
buildTransaction(sub.instructionsBatch, blockhash, keypair.publicKey)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// 2. Sign all at once — VersionedTransaction.sign accepts an array of signers
|
|
63
|
+
txs.forEach(tx => tx.sign([keypair]));
|
|
64
|
+
|
|
65
|
+
// 3. Serialise to Base64 and zip with intentIDs
|
|
66
|
+
return subBatches.map((sub, i) => ({
|
|
67
|
+
intentID: sub.intentID,
|
|
68
|
+
signedTransaction: Buffer.from(txs[i].serialize()).toString('base64'),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
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 fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import inquirer from 'inquirer';
|
|
14
|
+
import { Keypair } from '@solana/web3.js';
|
|
15
|
+
import bs58 from 'bs58';
|
|
16
|
+
|
|
17
|
+
const STORAGE_DIR = path.join(os.homedir(), '.gp-cli');
|
|
18
|
+
const STORAGE_FILE = path.join(STORAGE_DIR, 'wallets.json');
|
|
19
|
+
|
|
20
|
+
// Ensure storage directory exists
|
|
21
|
+
if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Load wallet file
|
|
24
|
+
function loadWalletFile() {
|
|
25
|
+
if (!fs.existsSync(STORAGE_FILE)) return { wallets: [] };
|
|
26
|
+
return JSON.parse(fs.readFileSync(STORAGE_FILE, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Save wallet file
|
|
30
|
+
function saveWalletFile(data) {
|
|
31
|
+
fs.writeFileSync(STORAGE_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Generate AES-256 key from password
|
|
35
|
+
function deriveKey(password, salt) {
|
|
36
|
+
return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Encrypt private key
|
|
40
|
+
function encryptPrivateKey(privateKey, password) {
|
|
41
|
+
const salt = crypto.randomBytes(16);
|
|
42
|
+
const iv = crypto.randomBytes(12);
|
|
43
|
+
const key = deriveKey(password, salt);
|
|
44
|
+
|
|
45
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
46
|
+
let encrypted = cipher.update(privateKey, 'utf8', 'hex');
|
|
47
|
+
encrypted += cipher.final('hex');
|
|
48
|
+
const tag = cipher.getAuthTag().toString('hex');
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
encrypted,
|
|
52
|
+
iv: iv.toString('hex'),
|
|
53
|
+
salt: salt.toString('hex'),
|
|
54
|
+
tag
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Decrypt private key
|
|
59
|
+
function decryptPrivateKey(encryptedObj, password) {
|
|
60
|
+
const { encrypted, iv, salt, tag } = encryptedObj;
|
|
61
|
+
const key = deriveKey(password, Buffer.from(salt, 'hex'));
|
|
62
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
|
|
63
|
+
decipher.setAuthTag(Buffer.from(tag, 'hex'));
|
|
64
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
65
|
+
decrypted += decipher.final('utf8');
|
|
66
|
+
return decrypted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Detect inquirer's ExitPromptError (thrown on Ctrl+C in v9+) without
|
|
70
|
+
// importing the class directly — checking the name is sufficient.
|
|
71
|
+
function isExitPrompt(err) {
|
|
72
|
+
return err?.name === 'ExitPromptError';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add wallet
|
|
76
|
+
async function addWallet() {
|
|
77
|
+
let answers;
|
|
78
|
+
try {
|
|
79
|
+
answers = await inquirer.prompt([
|
|
80
|
+
{
|
|
81
|
+
type: 'password',
|
|
82
|
+
name: 'privateKey',
|
|
83
|
+
message: 'Enter wallet private key (JSON byte array [1,2,..] or Base58 string):',
|
|
84
|
+
mask: '*',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'description',
|
|
89
|
+
message: 'Enter a description for this wallet (e.g. "Main wallet", "Trading wallet"):',
|
|
90
|
+
validate: (input) => input.trim().length > 0 ? true : 'Description cannot be empty.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'password',
|
|
94
|
+
name: 'password',
|
|
95
|
+
message: 'Enter password to encrypt wallet:',
|
|
96
|
+
mask: '*',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'password',
|
|
100
|
+
name: 'passwordConfirm',
|
|
101
|
+
message: 'Confirm password:',
|
|
102
|
+
mask: '*',
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (isExitPrompt(err)) { console.log('\nAborted.'); process.exit(0); }
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (answers.password !== answers.passwordConfirm) {
|
|
111
|
+
console.log('Passwords do not match.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { description, privateKey, password } = answers;
|
|
116
|
+
|
|
117
|
+
// ── Support JSON byte array [1,2,3...] AND Base58 string ─────────────────
|
|
118
|
+
let keypair;
|
|
119
|
+
let normalizedKey; // always stored as JSON array string
|
|
120
|
+
|
|
121
|
+
const trimmed = privateKey.trim();
|
|
122
|
+
|
|
123
|
+
if (trimmed.startsWith('[')) {
|
|
124
|
+
// JSON byte array — Phantom / Solflare export
|
|
125
|
+
let bytes;
|
|
126
|
+
try {
|
|
127
|
+
bytes = JSON.parse(trimmed);
|
|
128
|
+
} catch {
|
|
129
|
+
console.log('Invalid private key: could not parse JSON byte array.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(bytes) || bytes.length !== 64) {
|
|
133
|
+
console.log('Invalid private key: expected a 64-element byte array.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
keypair = Keypair.fromSecretKey(Uint8Array.from(bytes));
|
|
138
|
+
} catch {
|
|
139
|
+
console.log('Invalid private key bytes.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
normalizedKey = JSON.stringify(bytes);
|
|
143
|
+
} else {
|
|
144
|
+
// Base58 string
|
|
145
|
+
try {
|
|
146
|
+
const decoded = bs58.decode(trimmed);
|
|
147
|
+
if (decoded.length !== 64) {
|
|
148
|
+
console.log('Invalid Base58 private key: expected a 64-byte keypair (88 characters). Some wallets export a 32-byte seed — use the full keypair export instead.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
keypair = Keypair.fromSecretKey(decoded);
|
|
152
|
+
normalizedKey = JSON.stringify(Array.from(decoded));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.log(`Invalid private key: ${err.message}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
160
|
+
const walletFile = loadWalletFile();
|
|
161
|
+
|
|
162
|
+
// Prevent duplicate
|
|
163
|
+
if (walletFile.wallets.find(w => w.publicKey === publicKey)) {
|
|
164
|
+
console.log(`Wallet ${publicKey} is already saved.`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const encryptedObj = encryptPrivateKey(normalizedKey, password);
|
|
169
|
+
|
|
170
|
+
walletFile.wallets.push({
|
|
171
|
+
description: description.trim(),
|
|
172
|
+
publicKey,
|
|
173
|
+
encryptedPrivateKey: encryptedObj,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
saveWalletFile(walletFile);
|
|
177
|
+
console.log(`\nWallet added successfully.`);
|
|
178
|
+
console.log(` Description : ${description.trim()}`);
|
|
179
|
+
console.log(` Public key : ${publicKey}`);
|
|
180
|
+
console.log(`\n⚠ Important: Note down your password. It is required every time you`);
|
|
181
|
+
console.log(` run close-empty command with this wallet. If you forget it, you can remove`);
|
|
182
|
+
console.log(` and re-add this wallet without any impact on your funds or previous history.\n`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Remove wallet
|
|
186
|
+
async function removeWallet() {
|
|
187
|
+
const walletFile = loadWalletFile();
|
|
188
|
+
if (!walletFile.wallets.length) {
|
|
189
|
+
console.log('No wallets found.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let answers;
|
|
194
|
+
try {
|
|
195
|
+
answers = await inquirer.prompt([
|
|
196
|
+
{
|
|
197
|
+
type: 'rawlist',
|
|
198
|
+
name: 'wallet',
|
|
199
|
+
message: 'Select wallet to remove:',
|
|
200
|
+
choices: walletFile.wallets.map((w, i) => ({
|
|
201
|
+
name: `${w.description || 'No description'} (${w.publicKey})`,
|
|
202
|
+
value: w.publicKey,
|
|
203
|
+
})),
|
|
204
|
+
}
|
|
205
|
+
]);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (isExitPrompt(err)) { console.log('\nAborted.'); process.exit(0); }
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
walletFile.wallets = walletFile.wallets.filter(w => w.publicKey !== answers.wallet);
|
|
212
|
+
saveWalletFile(walletFile);
|
|
213
|
+
console.log(`Wallet ${answers.wallet} removed successfully.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Load and decrypt wallet (returns keypair)
|
|
217
|
+
async function loadWallet(publicKey) {
|
|
218
|
+
const walletFile = loadWalletFile();
|
|
219
|
+
const walletEntry = walletFile.wallets.find(w => w.publicKey === publicKey);
|
|
220
|
+
if (!walletEntry) throw new Error('Wallet not found');
|
|
221
|
+
|
|
222
|
+
let answers;
|
|
223
|
+
try {
|
|
224
|
+
answers = await inquirer.prompt([
|
|
225
|
+
{
|
|
226
|
+
type: 'password',
|
|
227
|
+
name: 'password',
|
|
228
|
+
message: `Enter password to unlock wallet "${walletEntry.description || publicKey}":`,
|
|
229
|
+
mask: '*'
|
|
230
|
+
}
|
|
231
|
+
]);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (isExitPrompt(err)) { console.log('\nAborted.'); process.exit(0); }
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const decryptedKey = decryptPrivateKey(walletEntry.encryptedPrivateKey, answers.password);
|
|
238
|
+
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(decryptedKey)));
|
|
239
|
+
return keypair;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// List wallets
|
|
243
|
+
function listWallets() {
|
|
244
|
+
const walletFile = loadWalletFile();
|
|
245
|
+
if (!walletFile.wallets.length) {
|
|
246
|
+
console.log('No wallets stored.');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.log('\nStored wallets:');
|
|
250
|
+
console.log('─'.repeat(60));
|
|
251
|
+
walletFile.wallets.forEach((w, i) => {
|
|
252
|
+
console.log(` ${i + 1}. ${w.description || 'No description'}`);
|
|
253
|
+
console.log(` ${w.publicKey}`);
|
|
254
|
+
});
|
|
255
|
+
console.log('─'.repeat(60));
|
|
256
|
+
console.log('');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
addWallet,
|
|
261
|
+
removeWallet,
|
|
262
|
+
loadWallet,
|
|
263
|
+
loadWalletFile,
|
|
264
|
+
listWallets
|
|
265
|
+
};
|