naracli 0.1.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/README.md +155 -0
- package/bin/nara-cli.ts +32 -0
- package/dist/nara-cli.mjs +2631 -0
- package/dist/quest/nara_quest.json +534 -0
- package/dist/zk/answer_proof.wasm +0 -0
- package/dist/zk/answer_proof_final.zkey +0 -0
- package/index.ts +76 -0
- package/package.json +54 -0
- package/src/cli/commands/config.ts +125 -0
- package/src/cli/commands/migrate.ts +270 -0
- package/src/cli/commands/pool.ts +364 -0
- package/src/cli/commands/quest.ts +312 -0
- package/src/cli/commands/swap.ts +349 -0
- package/src/cli/commands/wallet.ts +719 -0
- package/src/cli/index.ts +25 -0
- package/src/cli/quest/nara_quest.json +534 -0
- package/src/cli/quest/nara_quest_types.ts +540 -0
- package/src/cli/types.ts +207 -0
- package/src/cli/utils/output.ts +110 -0
- package/src/cli/utils/transaction.ts +146 -0
- package/src/cli/utils/validation.ts +120 -0
- package/src/cli/utils/wallet.ts +72 -0
- package/src/cli/zk/answer_proof.wasm +0 -0
- package/src/cli/zk/answer_proof_final.zkey +0 -0
- package/src/client.ts +96 -0
- package/src/config.ts +132 -0
- package/src/constants.ts +29 -0
- package/src/migrate.ts +222 -0
- package/src/pool.ts +259 -0
- package/src/quest.ts +379 -0
- package/src/swap.ts +608 -0
- package/src/types/snarkjs.d.ts +9 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import {
|
|
7
|
+
Keypair,
|
|
8
|
+
LAMPORTS_PER_SOL,
|
|
9
|
+
PublicKey,
|
|
10
|
+
SystemProgram,
|
|
11
|
+
Transaction,
|
|
12
|
+
} from "@solana/web3.js";
|
|
13
|
+
import {
|
|
14
|
+
getAssociatedTokenAddress,
|
|
15
|
+
createTransferInstruction,
|
|
16
|
+
TOKEN_PROGRAM_ID,
|
|
17
|
+
} from "@solana/spl-token";
|
|
18
|
+
import * as bip39 from "bip39";
|
|
19
|
+
import { derivePath } from "ed25519-hd-key";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { mkdir } from "node:fs/promises";
|
|
23
|
+
import bs58 from "bs58";
|
|
24
|
+
import { NaraSDK } from "../../client";
|
|
25
|
+
import { DEFAULT_WALLET_PATH as _DEFAULT_WALLET_PATH } from "../../constants";
|
|
26
|
+
import { loadWallet, getRpcUrl } from "../utils/wallet";
|
|
27
|
+
import { validatePublicKey, validatePositiveNumber } from "../utils/validation";
|
|
28
|
+
import {
|
|
29
|
+
handleTransaction,
|
|
30
|
+
printTransactionResult,
|
|
31
|
+
} from "../utils/transaction";
|
|
32
|
+
import { formatOutput, printError, printInfo, printSuccess, printWarning } from "../utils/output";
|
|
33
|
+
import type {
|
|
34
|
+
GlobalOptions,
|
|
35
|
+
WalletBalanceOptions,
|
|
36
|
+
TokenBalanceOptions,
|
|
37
|
+
TxStatusOptions,
|
|
38
|
+
TransferSolOptions,
|
|
39
|
+
TransferTokenOptions,
|
|
40
|
+
} from "../types";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve wallet path (expand ~ to home directory)
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_WALLET_PATH = _DEFAULT_WALLET_PATH.startsWith("~")
|
|
46
|
+
? join(homedir(), _DEFAULT_WALLET_PATH.slice(1))
|
|
47
|
+
: _DEFAULT_WALLET_PATH;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register wallet commands
|
|
51
|
+
* @param program Commander program
|
|
52
|
+
*/
|
|
53
|
+
export function registerWalletCommands(program: Command): void {
|
|
54
|
+
const wallet = program
|
|
55
|
+
.command("wallet")
|
|
56
|
+
.description("Wallet management commands");
|
|
57
|
+
|
|
58
|
+
// wallet create
|
|
59
|
+
wallet
|
|
60
|
+
.command("create")
|
|
61
|
+
.description("Create a new wallet")
|
|
62
|
+
.option("-o, --output <path>", "Output path for wallet file (default: ~/.config/nara/id.json)")
|
|
63
|
+
.action(async (options: { output?: string }) => {
|
|
64
|
+
try {
|
|
65
|
+
await handleWalletCreate(options);
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
printError(error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// wallet import
|
|
73
|
+
wallet
|
|
74
|
+
.command("import")
|
|
75
|
+
.description("Import a wallet from mnemonic or private key")
|
|
76
|
+
.option("-m, --mnemonic <phrase>", "Mnemonic phrase (12 or 24 words)")
|
|
77
|
+
.option("-k, --private-key <key>", "Private key (base58 or JSON array)")
|
|
78
|
+
.option("-o, --output <path>", "Output path for wallet file (default: ~/.config/nara/id.json)")
|
|
79
|
+
.action(async (options: { mnemonic?: string; privateKey?: string; output?: string }) => {
|
|
80
|
+
try {
|
|
81
|
+
await handleWalletImport(options);
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
printError(error.message);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// wallet address
|
|
89
|
+
wallet
|
|
90
|
+
.command("address")
|
|
91
|
+
.description("Show wallet address")
|
|
92
|
+
.action(async (options: GlobalOptions) => {
|
|
93
|
+
try {
|
|
94
|
+
await handleWalletAddress(options);
|
|
95
|
+
} catch (error: any) {
|
|
96
|
+
printError(error.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// wallet balance
|
|
102
|
+
wallet
|
|
103
|
+
.command("balance")
|
|
104
|
+
.description("Check NSO balance")
|
|
105
|
+
.argument("[address]", "Wallet address (optional, defaults to current wallet)")
|
|
106
|
+
.action(async (address: string | undefined, options: WalletBalanceOptions) => {
|
|
107
|
+
try {
|
|
108
|
+
await handleWalletBalance(address, options);
|
|
109
|
+
} catch (error: any) {
|
|
110
|
+
printError(error.message);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// wallet token-balance
|
|
116
|
+
wallet
|
|
117
|
+
.command("token-balance <token-address>")
|
|
118
|
+
.description("Check token balance")
|
|
119
|
+
.option("--owner <address>", "Owner address (optional, defaults to current wallet)")
|
|
120
|
+
.action(
|
|
121
|
+
async (
|
|
122
|
+
tokenAddress: string,
|
|
123
|
+
options: Omit<TokenBalanceOptions, "tokenAddress">
|
|
124
|
+
) => {
|
|
125
|
+
try {
|
|
126
|
+
await handleTokenBalance(tokenAddress, options);
|
|
127
|
+
} catch (error: any) {
|
|
128
|
+
printError(error.message);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// wallet tx-status
|
|
135
|
+
wallet
|
|
136
|
+
.command("tx-status <signature>")
|
|
137
|
+
.description("Check transaction status")
|
|
138
|
+
.action(
|
|
139
|
+
async (signature: string, options: Omit<TxStatusOptions, "signature">) => {
|
|
140
|
+
try {
|
|
141
|
+
await handleTxStatus(signature, options);
|
|
142
|
+
} catch (error: any) {
|
|
143
|
+
printError(error.message);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// wallet transfer
|
|
150
|
+
wallet
|
|
151
|
+
.command("transfer <to> <amount>")
|
|
152
|
+
.description("Transfer NSO to another wallet")
|
|
153
|
+
.option("-e, --export-tx", "Export unsigned transaction", false)
|
|
154
|
+
.action(
|
|
155
|
+
async (
|
|
156
|
+
to: string,
|
|
157
|
+
amount: string,
|
|
158
|
+
options: Omit<TransferSolOptions, "to" | "amount">
|
|
159
|
+
) => {
|
|
160
|
+
try {
|
|
161
|
+
await handleTransferSol(to, amount, options);
|
|
162
|
+
} catch (error: any) {
|
|
163
|
+
printError(error.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// wallet transfer-token
|
|
170
|
+
wallet
|
|
171
|
+
.command("transfer-token <token-address> <to> <amount>")
|
|
172
|
+
.description("Transfer tokens to another wallet")
|
|
173
|
+
.option("--decimals <number>", "Token decimals", "6")
|
|
174
|
+
.option("-e, --export-tx", "Export unsigned transaction", false)
|
|
175
|
+
.action(
|
|
176
|
+
async (
|
|
177
|
+
tokenAddress: string,
|
|
178
|
+
to: string,
|
|
179
|
+
amount: string,
|
|
180
|
+
options: Omit<TransferTokenOptions, "tokenAddress" | "to" | "amount">
|
|
181
|
+
) => {
|
|
182
|
+
try {
|
|
183
|
+
await handleTransferToken(tokenAddress, to, amount, options);
|
|
184
|
+
} catch (error: any) {
|
|
185
|
+
printError(error.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handle wallet balance command
|
|
194
|
+
* @param address Wallet address
|
|
195
|
+
* @param options Command options
|
|
196
|
+
*/
|
|
197
|
+
async function handleWalletBalance(
|
|
198
|
+
address: string | undefined,
|
|
199
|
+
options: WalletBalanceOptions
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
202
|
+
|
|
203
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
204
|
+
|
|
205
|
+
// Initialize SDK
|
|
206
|
+
const sdk = new NaraSDK({
|
|
207
|
+
rpcUrl,
|
|
208
|
+
commitment: "confirmed",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const connection = sdk.getConnection();
|
|
212
|
+
|
|
213
|
+
// Determine which address to query
|
|
214
|
+
let pubkey: PublicKey;
|
|
215
|
+
if (address) {
|
|
216
|
+
pubkey = validatePublicKey(address);
|
|
217
|
+
} else {
|
|
218
|
+
// Load wallet to get address
|
|
219
|
+
const wallet = await loadWallet(options.wallet);
|
|
220
|
+
pubkey = wallet.publicKey;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
printInfo(`Checking balance for: ${pubkey.toBase58()}`);
|
|
224
|
+
|
|
225
|
+
// Get balance
|
|
226
|
+
const balance = await connection.getBalance(pubkey);
|
|
227
|
+
const balanceSOL = balance / LAMPORTS_PER_SOL;
|
|
228
|
+
|
|
229
|
+
// Output result
|
|
230
|
+
if (options.json) {
|
|
231
|
+
const output = {
|
|
232
|
+
address: pubkey.toBase58(),
|
|
233
|
+
balance: balanceSOL,
|
|
234
|
+
lamports: balance,
|
|
235
|
+
};
|
|
236
|
+
console.log(JSON.stringify(output, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
console.log(`\nWallet: ${pubkey.toBase58()}`);
|
|
239
|
+
console.log(`Balance: ${balanceSOL.toFixed(4)} NSO (${balance.toLocaleString()} lamports)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle token balance command
|
|
245
|
+
* @param tokenAddress Token address
|
|
246
|
+
* @param options Command options
|
|
247
|
+
*/
|
|
248
|
+
async function handleTokenBalance(
|
|
249
|
+
tokenAddress: string,
|
|
250
|
+
options: Omit<TokenBalanceOptions, "tokenAddress">
|
|
251
|
+
): Promise<void> {
|
|
252
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
253
|
+
|
|
254
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
255
|
+
|
|
256
|
+
// Validate token address
|
|
257
|
+
const tokenMint = validatePublicKey(tokenAddress);
|
|
258
|
+
|
|
259
|
+
// Initialize SDK
|
|
260
|
+
const sdk = new NaraSDK({
|
|
261
|
+
rpcUrl,
|
|
262
|
+
commitment: "confirmed",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const connection = sdk.getConnection();
|
|
266
|
+
|
|
267
|
+
// Determine owner address
|
|
268
|
+
let owner: PublicKey;
|
|
269
|
+
if (options.owner) {
|
|
270
|
+
owner = validatePublicKey(options.owner);
|
|
271
|
+
} else {
|
|
272
|
+
// Load wallet to get owner
|
|
273
|
+
const wallet = await loadWallet(options.wallet);
|
|
274
|
+
owner = wallet.publicKey;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
printInfo(`Owner: ${owner.toBase58()}`);
|
|
278
|
+
printInfo(`Token: ${tokenAddress}`);
|
|
279
|
+
|
|
280
|
+
// Get associated token account
|
|
281
|
+
const tokenAccount = await getAssociatedTokenAddress(tokenMint, owner);
|
|
282
|
+
|
|
283
|
+
// Get token account balance
|
|
284
|
+
try {
|
|
285
|
+
const accountInfo = await connection.getTokenAccountBalance(tokenAccount);
|
|
286
|
+
const balance = accountInfo.value;
|
|
287
|
+
|
|
288
|
+
// Output result
|
|
289
|
+
if (options.json) {
|
|
290
|
+
const output = {
|
|
291
|
+
owner: owner.toBase58(),
|
|
292
|
+
tokenAddress,
|
|
293
|
+
tokenAccount: tokenAccount.toBase58(),
|
|
294
|
+
balance: balance.uiAmountString,
|
|
295
|
+
amount: balance.amount,
|
|
296
|
+
decimals: balance.decimals,
|
|
297
|
+
};
|
|
298
|
+
console.log(JSON.stringify(output, null, 2));
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`\nToken Account: ${tokenAccount.toBase58()}`);
|
|
301
|
+
console.log(`Balance: ${balance.uiAmountString || "0"} tokens`);
|
|
302
|
+
console.log(`Amount: ${balance.amount} (smallest unit)`);
|
|
303
|
+
console.log(`Decimals: ${balance.decimals}`);
|
|
304
|
+
}
|
|
305
|
+
} catch (error: any) {
|
|
306
|
+
if (error.message?.includes("could not find account")) {
|
|
307
|
+
if (options.json) {
|
|
308
|
+
const output = {
|
|
309
|
+
owner: owner.toBase58(),
|
|
310
|
+
tokenAddress,
|
|
311
|
+
tokenAccount: tokenAccount.toBase58(),
|
|
312
|
+
balance: "0",
|
|
313
|
+
amount: "0",
|
|
314
|
+
};
|
|
315
|
+
console.log(JSON.stringify(output, null, 2));
|
|
316
|
+
} else {
|
|
317
|
+
printInfo("\nToken account does not exist yet.");
|
|
318
|
+
console.log(`Balance: 0 tokens`);
|
|
319
|
+
console.log(`Token Account (will be created on first transfer): ${tokenAccount.toBase58()}`);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Handle transaction status command
|
|
329
|
+
* @param signature Transaction signature
|
|
330
|
+
* @param options Command options
|
|
331
|
+
*/
|
|
332
|
+
async function handleTxStatus(
|
|
333
|
+
signature: string,
|
|
334
|
+
options: Omit<TxStatusOptions, "signature">
|
|
335
|
+
): Promise<void> {
|
|
336
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
337
|
+
|
|
338
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
339
|
+
printInfo(`Checking transaction: ${signature}`);
|
|
340
|
+
|
|
341
|
+
// Initialize SDK
|
|
342
|
+
const sdk = new NaraSDK({
|
|
343
|
+
rpcUrl,
|
|
344
|
+
commitment: "confirmed",
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const connection = sdk.getConnection();
|
|
348
|
+
|
|
349
|
+
// Get transaction status
|
|
350
|
+
const status = await connection.getSignatureStatus(signature);
|
|
351
|
+
|
|
352
|
+
if (!status || !status.value) {
|
|
353
|
+
if (options.json) {
|
|
354
|
+
console.log(JSON.stringify({ signature, status: "not_found" }, null, 2));
|
|
355
|
+
} else {
|
|
356
|
+
printError("Transaction not found");
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Get transaction details
|
|
362
|
+
const transaction = await connection.getTransaction(signature, {
|
|
363
|
+
maxSupportedTransactionVersion: 0,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Build output
|
|
367
|
+
const output: any = {
|
|
368
|
+
signature,
|
|
369
|
+
status: status.value.confirmationStatus || "unknown",
|
|
370
|
+
slot: status.value.slot,
|
|
371
|
+
confirmations: status.value.confirmations,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (status.value.err) {
|
|
375
|
+
output.error = status.value.err;
|
|
376
|
+
output.success = false;
|
|
377
|
+
} else {
|
|
378
|
+
output.success = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (transaction) {
|
|
382
|
+
output.blockTime = transaction.blockTime;
|
|
383
|
+
output.fee = transaction.meta?.fee;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Output result
|
|
387
|
+
if (options.json) {
|
|
388
|
+
console.log(JSON.stringify(output, null, 2));
|
|
389
|
+
} else {
|
|
390
|
+
console.log(`\nTransaction: ${signature}`);
|
|
391
|
+
console.log(`Status: ${output.status}`);
|
|
392
|
+
console.log(`Success: ${output.success ? "Yes" : "No"}`);
|
|
393
|
+
console.log(`Slot: ${output.slot}`);
|
|
394
|
+
if (output.confirmations !== null) {
|
|
395
|
+
console.log(`Confirmations: ${output.confirmations}`);
|
|
396
|
+
}
|
|
397
|
+
if (output.blockTime) {
|
|
398
|
+
const date = new Date(output.blockTime * 1000);
|
|
399
|
+
console.log(`Time: ${date.toISOString()}`);
|
|
400
|
+
}
|
|
401
|
+
if (output.fee) {
|
|
402
|
+
console.log(`Fee: ${output.fee / LAMPORTS_PER_SOL} NSO`);
|
|
403
|
+
}
|
|
404
|
+
if (output.error) {
|
|
405
|
+
console.log(`Error: ${JSON.stringify(output.error)}`);
|
|
406
|
+
}
|
|
407
|
+
console.log(
|
|
408
|
+
`\nView on Solscan: https://solscan.io/tx/${signature}?cluster=devnet`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Handle transfer SOL command
|
|
415
|
+
* @param to Recipient address
|
|
416
|
+
* @param amount Amount in SOL
|
|
417
|
+
* @param options Command options
|
|
418
|
+
*/
|
|
419
|
+
async function handleTransferSol(
|
|
420
|
+
to: string,
|
|
421
|
+
amount: string,
|
|
422
|
+
options: Omit<TransferSolOptions, "to" | "amount">
|
|
423
|
+
): Promise<void> {
|
|
424
|
+
// Load wallet
|
|
425
|
+
const wallet = await loadWallet(options.wallet);
|
|
426
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
427
|
+
|
|
428
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
429
|
+
printInfo(`From: ${wallet.publicKey.toBase58()}`);
|
|
430
|
+
|
|
431
|
+
// Validate inputs
|
|
432
|
+
const recipient = validatePublicKey(to);
|
|
433
|
+
const amountSOL = validatePositiveNumber(amount, "amount");
|
|
434
|
+
const lamports = Math.floor(amountSOL * LAMPORTS_PER_SOL);
|
|
435
|
+
|
|
436
|
+
printInfo(`To: ${recipient.toBase58()}`);
|
|
437
|
+
printInfo(`Amount: ${amountSOL} NSO`);
|
|
438
|
+
|
|
439
|
+
// Initialize SDK
|
|
440
|
+
const sdk = new NaraSDK({
|
|
441
|
+
rpcUrl,
|
|
442
|
+
commitment: "confirmed",
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const connection = sdk.getConnection();
|
|
446
|
+
|
|
447
|
+
// Create transfer instruction
|
|
448
|
+
const transferInstruction = SystemProgram.transfer({
|
|
449
|
+
fromPubkey: wallet.publicKey,
|
|
450
|
+
toPubkey: recipient,
|
|
451
|
+
lamports,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Create transaction
|
|
455
|
+
const transaction = new Transaction().add(transferInstruction);
|
|
456
|
+
|
|
457
|
+
// Get latest blockhash
|
|
458
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
459
|
+
transaction.recentBlockhash = blockhash;
|
|
460
|
+
transaction.feePayer = wallet.publicKey;
|
|
461
|
+
|
|
462
|
+
// Handle transaction
|
|
463
|
+
const txResult = await handleTransaction(
|
|
464
|
+
sdk,
|
|
465
|
+
transaction,
|
|
466
|
+
[wallet],
|
|
467
|
+
options.exportTx || false
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// Output result
|
|
471
|
+
if (options.json) {
|
|
472
|
+
const output = {
|
|
473
|
+
from: wallet.publicKey.toBase58(),
|
|
474
|
+
to: recipient.toBase58(),
|
|
475
|
+
amount: amountSOL,
|
|
476
|
+
lamports,
|
|
477
|
+
...(txResult.signature && { signature: txResult.signature }),
|
|
478
|
+
...(txResult.base64 && { transaction: txResult.base64 }),
|
|
479
|
+
};
|
|
480
|
+
console.log(JSON.stringify(output, null, 2));
|
|
481
|
+
} else {
|
|
482
|
+
console.log(`\nTransfer Details:`);
|
|
483
|
+
console.log(` From: ${wallet.publicKey.toBase58()}`);
|
|
484
|
+
console.log(` To: ${recipient.toBase58()}`);
|
|
485
|
+
console.log(` Amount: ${amountSOL} NSO`);
|
|
486
|
+
printTransactionResult(txResult, false);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Handle transfer token command
|
|
492
|
+
* @param tokenAddress Token address
|
|
493
|
+
* @param to Recipient address
|
|
494
|
+
* @param amount Amount in tokens
|
|
495
|
+
* @param options Command options
|
|
496
|
+
*/
|
|
497
|
+
async function handleTransferToken(
|
|
498
|
+
tokenAddress: string,
|
|
499
|
+
to: string,
|
|
500
|
+
amount: string,
|
|
501
|
+
options: Omit<TransferTokenOptions, "tokenAddress" | "to" | "amount">
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
// Load wallet
|
|
504
|
+
const wallet = await loadWallet(options.wallet);
|
|
505
|
+
const rpcUrl = getRpcUrl(options.rpcUrl);
|
|
506
|
+
|
|
507
|
+
printInfo(`Using RPC: ${rpcUrl}`);
|
|
508
|
+
printInfo(`From: ${wallet.publicKey.toBase58()}`);
|
|
509
|
+
|
|
510
|
+
// Validate inputs
|
|
511
|
+
const tokenMint = validatePublicKey(tokenAddress);
|
|
512
|
+
const recipient = validatePublicKey(to);
|
|
513
|
+
const amountInToken = validatePositiveNumber(amount, "amount");
|
|
514
|
+
const decimals = parseInt(String(options.decimals || "6"));
|
|
515
|
+
const amountInSmallestUnit = Math.floor(amountInToken * 10 ** decimals);
|
|
516
|
+
|
|
517
|
+
printInfo(`To: ${recipient.toBase58()}`);
|
|
518
|
+
printInfo(`Token: ${tokenAddress}`);
|
|
519
|
+
printInfo(`Amount: ${amountInToken} tokens`);
|
|
520
|
+
|
|
521
|
+
// Initialize SDK
|
|
522
|
+
const sdk = new NaraSDK({
|
|
523
|
+
rpcUrl,
|
|
524
|
+
commitment: "confirmed",
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const connection = sdk.getConnection();
|
|
528
|
+
|
|
529
|
+
// Get source and destination token accounts
|
|
530
|
+
const sourceAccount = await getAssociatedTokenAddress(
|
|
531
|
+
tokenMint,
|
|
532
|
+
wallet.publicKey
|
|
533
|
+
);
|
|
534
|
+
const destinationAccount = await getAssociatedTokenAddress(
|
|
535
|
+
tokenMint,
|
|
536
|
+
recipient
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Create transfer instruction
|
|
540
|
+
const transferInstruction = createTransferInstruction(
|
|
541
|
+
sourceAccount,
|
|
542
|
+
destinationAccount,
|
|
543
|
+
wallet.publicKey,
|
|
544
|
+
amountInSmallestUnit,
|
|
545
|
+
[],
|
|
546
|
+
TOKEN_PROGRAM_ID
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Create transaction
|
|
550
|
+
const transaction = new Transaction().add(transferInstruction);
|
|
551
|
+
|
|
552
|
+
// Get latest blockhash
|
|
553
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
554
|
+
transaction.recentBlockhash = blockhash;
|
|
555
|
+
transaction.feePayer = wallet.publicKey;
|
|
556
|
+
|
|
557
|
+
// Handle transaction
|
|
558
|
+
const txResult = await handleTransaction(
|
|
559
|
+
sdk,
|
|
560
|
+
transaction,
|
|
561
|
+
[wallet],
|
|
562
|
+
options.exportTx || false
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Output result
|
|
566
|
+
if (options.json) {
|
|
567
|
+
const output = {
|
|
568
|
+
from: wallet.publicKey.toBase58(),
|
|
569
|
+
to: recipient.toBase58(),
|
|
570
|
+
tokenAddress,
|
|
571
|
+
amount: amountInToken,
|
|
572
|
+
amountSmallestUnit: amountInSmallestUnit.toString(),
|
|
573
|
+
decimals,
|
|
574
|
+
...(txResult.signature && { signature: txResult.signature }),
|
|
575
|
+
...(txResult.base64 && { transaction: txResult.base64 }),
|
|
576
|
+
};
|
|
577
|
+
console.log(JSON.stringify(output, null, 2));
|
|
578
|
+
} else {
|
|
579
|
+
console.log(`\nToken Transfer Details:`);
|
|
580
|
+
console.log(` From: ${wallet.publicKey.toBase58()}`);
|
|
581
|
+
console.log(` To: ${recipient.toBase58()}`);
|
|
582
|
+
console.log(` Token: ${tokenAddress}`);
|
|
583
|
+
console.log(` Amount: ${amountInToken} tokens`);
|
|
584
|
+
printTransactionResult(txResult, false);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Handle wallet create command
|
|
590
|
+
* @param options Command options
|
|
591
|
+
*/
|
|
592
|
+
async function handleWalletCreate(options: { output?: string }): Promise<void> {
|
|
593
|
+
const outputPath = options.output || DEFAULT_WALLET_PATH;
|
|
594
|
+
|
|
595
|
+
// Check if wallet file already exists
|
|
596
|
+
if (await Bun.file(outputPath).exists()) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Wallet file already exists at ${outputPath}. Please use a different path or remove the existing file first.`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Generate mnemonic (12 words by default, can be changed to 24)
|
|
603
|
+
const mnemonic = bip39.generateMnemonic(128); // 128 bits = 12 words, 256 bits = 24 words
|
|
604
|
+
|
|
605
|
+
// Derive keypair from mnemonic using Solana's derivation path
|
|
606
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
607
|
+
const derivedSeed = derivePath("m/44'/501'/0'/0'", seed.toString("hex")).key;
|
|
608
|
+
const keypair = Keypair.fromSeed(derivedSeed);
|
|
609
|
+
|
|
610
|
+
// Ensure directory exists
|
|
611
|
+
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
|
|
612
|
+
await mkdir(dir, { recursive: true });
|
|
613
|
+
|
|
614
|
+
// Save wallet to file
|
|
615
|
+
const walletData = Array.from(keypair.secretKey);
|
|
616
|
+
await Bun.write(outputPath, JSON.stringify(walletData, null, 2));
|
|
617
|
+
|
|
618
|
+
// Display results
|
|
619
|
+
console.log("\nā
Wallet created successfully!");
|
|
620
|
+
console.log(`\nš Wallet saved to: ${outputPath}`);
|
|
621
|
+
console.log(`š Public Key: ${keypair.publicKey.toBase58()}`);
|
|
622
|
+
|
|
623
|
+
printWarning("\nā ļø IMPORTANT: Save your mnemonic phrase securely!");
|
|
624
|
+
printWarning("ā ļø You will need it to recover your wallet.");
|
|
625
|
+
console.log("\nš Mnemonic phrase (12 words):");
|
|
626
|
+
console.log(`\n${mnemonic}\n`);
|
|
627
|
+
|
|
628
|
+
printWarning("ā ļø Never share your mnemonic phrase with anyone!");
|
|
629
|
+
printWarning("ā ļø Anyone with your mnemonic can access your funds.\n");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Handle wallet import command
|
|
634
|
+
* @param options Command options
|
|
635
|
+
*/
|
|
636
|
+
async function handleWalletImport(options: {
|
|
637
|
+
mnemonic?: string;
|
|
638
|
+
privateKey?: string;
|
|
639
|
+
output?: string;
|
|
640
|
+
}): Promise<void> {
|
|
641
|
+
const outputPath = options.output || DEFAULT_WALLET_PATH;
|
|
642
|
+
|
|
643
|
+
// Check if wallet file already exists
|
|
644
|
+
if (await Bun.file(outputPath).exists()) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Wallet file already exists at ${outputPath}. Please use a different path or remove the existing file first.`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let keypair: Keypair;
|
|
651
|
+
|
|
652
|
+
if (options.mnemonic) {
|
|
653
|
+
// Import from mnemonic
|
|
654
|
+
const mnemonic = options.mnemonic.trim();
|
|
655
|
+
|
|
656
|
+
// Validate mnemonic
|
|
657
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
658
|
+
throw new Error("Invalid mnemonic phrase. Please check your words and try again.");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Derive keypair from mnemonic
|
|
662
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
663
|
+
const derivedSeed = derivePath("m/44'/501'/0'/0'", seed.toString("hex")).key;
|
|
664
|
+
keypair = Keypair.fromSeed(derivedSeed);
|
|
665
|
+
|
|
666
|
+
printInfo("Importing wallet from mnemonic...");
|
|
667
|
+
} else if (options.privateKey) {
|
|
668
|
+
// Import from private key
|
|
669
|
+
const privateKey = options.privateKey.trim();
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
if (privateKey.startsWith("[")) {
|
|
673
|
+
// JSON array format
|
|
674
|
+
const data = JSON.parse(privateKey);
|
|
675
|
+
keypair = Keypair.fromSecretKey(new Uint8Array(data));
|
|
676
|
+
} else {
|
|
677
|
+
// Base58 format
|
|
678
|
+
keypair = Keypair.fromSecretKey(bs58.decode(privateKey));
|
|
679
|
+
}
|
|
680
|
+
printInfo("Importing wallet from private key...");
|
|
681
|
+
} catch (error: any) {
|
|
682
|
+
throw new Error(`Invalid private key format: ${error.message}`);
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
throw new Error("Please provide either --mnemonic or --private-key option.");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Ensure directory exists
|
|
689
|
+
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
|
|
690
|
+
await mkdir(dir, { recursive: true });
|
|
691
|
+
|
|
692
|
+
// Save wallet to file
|
|
693
|
+
const walletData = Array.from(keypair.secretKey);
|
|
694
|
+
await Bun.write(outputPath, JSON.stringify(walletData, null, 2));
|
|
695
|
+
|
|
696
|
+
// Display results
|
|
697
|
+
console.log("\nā
Wallet imported successfully!");
|
|
698
|
+
console.log(`\nš Wallet saved to: ${outputPath}`);
|
|
699
|
+
console.log(`š Public Key: ${keypair.publicKey.toBase58()}\n`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Handle wallet address command
|
|
704
|
+
* @param options Command options
|
|
705
|
+
*/
|
|
706
|
+
async function handleWalletAddress(options: GlobalOptions): Promise<void> {
|
|
707
|
+
// Load wallet
|
|
708
|
+
const wallet = await loadWallet(options.wallet);
|
|
709
|
+
|
|
710
|
+
// Output result
|
|
711
|
+
if (options.json) {
|
|
712
|
+
const output = {
|
|
713
|
+
address: wallet.publicKey.toBase58(),
|
|
714
|
+
};
|
|
715
|
+
console.log(JSON.stringify(output, null, 2));
|
|
716
|
+
} else {
|
|
717
|
+
console.log(`\nš Wallet Address: ${wallet.publicKey.toBase58()}\n`);
|
|
718
|
+
}
|
|
719
|
+
}
|