@jellylegsai/aether-cli 1.8.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/aether-cli-1.0.0.tgz +0 -0
  4. package/aether-cli-1.8.0.tgz +0 -0
  5. package/aether-hub-1.0.5.tgz +0 -0
  6. package/aether-hub-1.1.8.tgz +0 -0
  7. package/aether-hub-1.2.1.tgz +0 -0
  8. package/commands/account.js +280 -0
  9. package/commands/apy.js +499 -0
  10. package/commands/balance.js +241 -0
  11. package/commands/blockhash.js +181 -0
  12. package/commands/broadcast.js +387 -0
  13. package/commands/claim.js +490 -0
  14. package/commands/config.js +851 -0
  15. package/commands/delegations.js +582 -0
  16. package/commands/doctor.js +769 -0
  17. package/commands/emergency.js +667 -0
  18. package/commands/epoch.js +275 -0
  19. package/commands/fees.js +276 -0
  20. package/commands/index.js +78 -0
  21. package/commands/info.js +495 -0
  22. package/commands/init.js +816 -0
  23. package/commands/install.js +666 -0
  24. package/commands/kyc.js +272 -0
  25. package/commands/logs.js +315 -0
  26. package/commands/monitor.js +431 -0
  27. package/commands/multisig.js +701 -0
  28. package/commands/network.js +429 -0
  29. package/commands/nft.js +857 -0
  30. package/commands/ping.js +266 -0
  31. package/commands/price.js +253 -0
  32. package/commands/rewards.js +931 -0
  33. package/commands/sdk-test.js +477 -0
  34. package/commands/sdk.js +656 -0
  35. package/commands/slot.js +155 -0
  36. package/commands/snapshot.js +470 -0
  37. package/commands/stake-info.js +139 -0
  38. package/commands/stake-positions.js +205 -0
  39. package/commands/stake.js +516 -0
  40. package/commands/stats.js +396 -0
  41. package/commands/status.js +327 -0
  42. package/commands/supply.js +391 -0
  43. package/commands/tps.js +238 -0
  44. package/commands/transfer.js +495 -0
  45. package/commands/tx-history.js +346 -0
  46. package/commands/unstake.js +597 -0
  47. package/commands/validator-info.js +657 -0
  48. package/commands/validator-register.js +593 -0
  49. package/commands/validator-start.js +323 -0
  50. package/commands/validator-status.js +227 -0
  51. package/commands/validators.js +626 -0
  52. package/commands/wallet.js +1570 -0
  53. package/index.js +593 -0
  54. package/lib/errors.js +398 -0
  55. package/package.json +76 -0
  56. package/sdk/README.md +210 -0
  57. package/sdk/index.js +1639 -0
  58. package/sdk/package.json +34 -0
  59. package/sdk/rpc.js +254 -0
  60. package/sdk/test.js +85 -0
  61. package/test/doctor.test.js +76 -0
  62. package/validator-identity.json +4 -0
@@ -0,0 +1,857 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli nft
4
+ *
5
+ * Complete NFT management suite - fully wired to @jellylegsai/aether-sdk.
6
+ * All blockchain calls use real HTTP RPC via AetherClient.
7
+ *
8
+ * Commands:
9
+ * aether nft create --metadata <url> [--royalties <bps>] [--address <wallet>]
10
+ * aether nft list --address <wallet> [--creator <addr>] [--json]
11
+ * aether nft transfer --nft <id> --to <addr> [--from <wallet>]
12
+ * aether nft info --nft <id> [--json]
13
+ * aether nft update --nft <id> --metadata <url> [--address <wallet>]
14
+ * aether nft burn --nft <id> [--address <wallet>]
15
+ *
16
+ * SDK Methods:
17
+ * - client.createNFT() → Creates new NFT with metadata
18
+ * - client.transferNFT() → Transfer NFT ownership
19
+ * - client.updateMetadata()→ Update NFT metadata URL
20
+ * - getTokenAccounts() → List NFTs for address
21
+ * - getAccountInfo() → Get NFT details
22
+ *
23
+ * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const os = require('os');
29
+ const readline = require('readline');
30
+ const nacl = require('tweetnacl');
31
+ const bs58 = require('bs58').default;
32
+ const bip39 = require('bip39');
33
+
34
+ // Import SDK for real blockchain RPC calls
35
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
36
+ const aether = require(sdkPath);
37
+
38
+ // ANSI colours
39
+ const C = {
40
+ reset: '\x1b[0m',
41
+ bright: '\x1b[1m',
42
+ dim: '\x1b[2m',
43
+ red: '\x1b[31m',
44
+ green: '\x1b[32m',
45
+ yellow: '\x1b[33m',
46
+ cyan: '\x1b[36m',
47
+ magenta: '\x1b[35m',
48
+ blue: '\x1b[34m',
49
+ };
50
+
51
+ const CLI_VERSION = '1.0.0';
52
+ const DERIVATION_PATH = "m/44'/7777777'/0'/0'";
53
+
54
+ // ============================================================================
55
+ // SDK Client Setup
56
+ // ============================================================================
57
+
58
+ function getDefaultRpc() {
59
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
60
+ }
61
+
62
+ function createClient(rpcUrl) {
63
+ return new aether.AetherClient({ rpcUrl });
64
+ }
65
+
66
+ // ============================================================================
67
+ // Config & Wallet
68
+ // ============================================================================
69
+
70
+ function getAetherDir() {
71
+ return path.join(os.homedir(), '.aether');
72
+ }
73
+
74
+ function getConfigPath() {
75
+ return path.join(getAetherDir(), 'config.json');
76
+ }
77
+
78
+ function loadConfig() {
79
+ if (!fs.existsSync(getConfigPath())) {
80
+ return { defaultWallet: null };
81
+ }
82
+ try {
83
+ return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
84
+ } catch {
85
+ return { defaultWallet: null };
86
+ }
87
+ }
88
+
89
+ function loadWallet(address) {
90
+ const fp = path.join(getAetherDir(), 'wallets', `${address}.json`);
91
+ if (!fs.existsSync(fp)) return null;
92
+ try {
93
+ return JSON.parse(fs.readFileSync(fp, 'utf8'));
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ // ============================================================================
100
+ // Crypto Helpers
101
+ // ============================================================================
102
+
103
+ function deriveKeypair(mnemonic) {
104
+ if (!bip39.validateMnemonic(mnemonic)) {
105
+ throw new Error('Invalid mnemonic phrase');
106
+ }
107
+ const seedBuffer = bip39.mnemonicToSeedSync(mnemonic, '');
108
+ const seed32 = seedBuffer.slice(0, 32);
109
+ const keyPair = nacl.sign.keyPair.fromSeed(seed32);
110
+ return {
111
+ publicKey: Buffer.from(keyPair.publicKey),
112
+ secretKey: Buffer.from(keyPair.secretKey),
113
+ };
114
+ }
115
+
116
+ function formatAddress(publicKey) {
117
+ return 'ATH' + bs58.encode(publicKey);
118
+ }
119
+
120
+ function signTransaction(tx, secretKey) {
121
+ const txBytes = Buffer.from(JSON.stringify(tx));
122
+ const sig = nacl.sign.detached(txBytes, secretKey);
123
+ return bs58.encode(sig);
124
+ }
125
+
126
+ // ============================================================================
127
+ // Format Helpers
128
+ // ============================================================================
129
+
130
+ function formatAether(lamports) {
131
+ if (!lamports || lamports === '0') return '0 AETH';
132
+ const aeth = Number(lamports) / 1e9;
133
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
134
+ }
135
+
136
+ function shortAddress(addr) {
137
+ if (!addr || addr.length < 16) return addr || 'unknown';
138
+ return addr.slice(0, 8) + '...' + addr.slice(-8);
139
+ }
140
+
141
+ function truncate(str, len) {
142
+ if (!str) return '';
143
+ if (str.length <= len) return str;
144
+ return str.slice(0, len - 3) + '...';
145
+ }
146
+
147
+ // ============================================================================
148
+ // Readline Helpers
149
+ // ============================================================================
150
+
151
+ function createRl() {
152
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
153
+ }
154
+
155
+ function question(rl, q) {
156
+ return new Promise((res) => rl.question(q, res));
157
+ }
158
+
159
+ async function askMnemonic(rl, promptText) {
160
+ console.log(`\n${C.cyan}${promptText}${C.reset}`);
161
+ console.log(`${C.dim}Enter your 12 or 24-word passphrase:${C.reset}`);
162
+ const raw = await question(rl, ` > ${C.reset}`);
163
+ return raw.trim().toLowerCase();
164
+ }
165
+
166
+ // ============================================================================
167
+ // NFT Create Command - Uses SDK createNFT()
168
+ // ============================================================================
169
+
170
+ async function nftCreate(args) {
171
+ const rl = createRl();
172
+ const rpc = args.rpc || getDefaultRpc();
173
+
174
+ // Resolve wallet address
175
+ let address = args.address;
176
+ if (!address) {
177
+ const config = loadConfig();
178
+ address = config.defaultWallet;
179
+ }
180
+
181
+ if (!address) {
182
+ console.log(`\n ${C.red}✗ No wallet address specified.${C.reset}`);
183
+ console.log(` ${C.dim}Usage: aether nft create --metadata <url> --address <wallet>${C.reset}\n`);
184
+ rl.close();
185
+ return;
186
+ }
187
+
188
+ const wallet = loadWallet(address);
189
+ if (!wallet) {
190
+ console.log(`\n ${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
191
+ console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
192
+ rl.close();
193
+ return;
194
+ }
195
+
196
+ // Get metadata URL
197
+ let metadataUrl = args.metadata;
198
+ if (!metadataUrl) {
199
+ console.log(`\n${C.bright}${C.cyan}── NFT Creation ─────────────────────────────────────────${C.reset}\n`);
200
+ console.log(` ${C.dim}Enter the metadata URL for your NFT:${C.reset}`);
201
+ metadataUrl = await question(rl, ` Metadata URL > ${C.reset}`);
202
+ if (!metadataUrl.trim()) {
203
+ console.log(`\n ${C.red}✗ Metadata URL is required.${C.reset}\n`);
204
+ rl.close();
205
+ return;
206
+ }
207
+ }
208
+
209
+ // Get royalties (default 5% = 500 bps)
210
+ let royalties = args.royalties;
211
+ if (!royalties) {
212
+ const royaltyAns = await question(rl, ` ${C.dim}Royalties (basis points, default 500 = 5%):${C.reset} `);
213
+ royalties = parseInt(royaltyAns.trim() || '500', 10);
214
+ } else {
215
+ royalties = parseInt(royalties, 10);
216
+ }
217
+
218
+ if (isNaN(royalties) || royalties < 0 || royalties > 10000) {
219
+ console.log(`\n ${C.red}✗ Invalid royalties. Must be 0-10000 basis points.${C.reset}\n`);
220
+ rl.close();
221
+ return;
222
+ }
223
+
224
+ console.log(`\n ${C.green}★${C.reset} Creator: ${C.bright}${address}${C.reset}`);
225
+ console.log(` ${C.green}★${C.reset} Metadata: ${C.bright}${truncate(metadataUrl, 50)}${C.reset}`);
226
+ console.log(` ${C.green}★${C.reset} Royalties: ${C.bright}${(royalties / 100).toFixed(2)}%${C.reset}`);
227
+ console.log();
228
+
229
+ // Ask for mnemonic for signing
230
+ console.log(`${C.yellow} ⚠ Creating NFT requires your wallet passphrase.${C.reset}`);
231
+ const mnemonic = await askMnemonic(rl, 'Enter passphrase to sign the NFT creation');
232
+ console.log();
233
+
234
+ let keyPair;
235
+ try {
236
+ keyPair = deriveKeypair(mnemonic);
237
+ } catch (e) {
238
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}\n`);
239
+ rl.close();
240
+ return;
241
+ }
242
+
243
+ // Verify derived address matches
244
+ const derivedAddress = formatAddress(keyPair.publicKey);
245
+ if (derivedAddress !== address) {
246
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
247
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
248
+ console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
249
+ rl.close();
250
+ return;
251
+ }
252
+
253
+ // Confirm
254
+ const confirm = await question(rl, ` ${C.yellow}Create NFT? [y/N]${C.reset} > `);
255
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
256
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
257
+ rl.close();
258
+ return;
259
+ }
260
+ rl.close();
261
+
262
+ // Create NFT via SDK
263
+ const client = createClient(rpc);
264
+
265
+ console.log(`\n ${C.dim}Submitting NFT creation via SDK...${C.reset}`);
266
+
267
+ try {
268
+ // SDK call: createNFT with signing function
269
+ const result = await client.createNFT({
270
+ creator: address.startsWith('ATH') ? address.slice(3) : address,
271
+ metadataUrl: metadataUrl.trim(),
272
+ royalties: royalties,
273
+ signFn: async (tx) => signTransaction(tx, keyPair.secretKey),
274
+ });
275
+
276
+ if (result.error) {
277
+ throw new Error(result.error.message || JSON.stringify(result.error));
278
+ }
279
+
280
+ const nftId = result.nft_id || result.id || result.signature;
281
+
282
+ console.log(`\n${C.green}✓ NFT created successfully!${C.reset}`);
283
+ console.log(` ${C.dim}NFT ID:${C.reset} ${C.cyan}${C.bright}${nftId}${C.reset}`);
284
+ console.log(` ${C.dim}Creator:${C.reset} ${address}`);
285
+ console.log(` ${C.dim}Metadata:${C.reset} ${truncate(metadataUrl, 45)}`);
286
+ console.log(` ${C.dim}Royalties:${C.reset} ${(royalties / 100).toFixed(2)}%`);
287
+ if (result.signature) {
288
+ console.log(` ${C.dim}Signature:${C.reset} ${shortAddress(result.signature)}`);
289
+ }
290
+ if (result.slot) {
291
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot}`);
292
+ }
293
+ console.log(` ${C.dim}SDK:${C.reset} createNFT() → POST /v1/transaction`);
294
+ console.log();
295
+ console.log(` ${C.dim}View your NFT:${C.reset}`);
296
+ console.log(` ${C.cyan}aether nft info --nft ${nftId}${C.reset}\n`);
297
+
298
+ return result;
299
+ } catch (err) {
300
+ console.log(`\n ${C.red}✗ NFT creation failed:${C.reset} ${err.message}`);
301
+ console.log(` ${C.dim}Common causes:${C.reset}`);
302
+ console.log(` • Insufficient balance for NFT creation fee (typically 0.01 AETH)`);
303
+ console.log(` • RPC endpoint not accepting transactions`);
304
+ console.log(` • Invalid metadata URL format\n`);
305
+ process.exit(1);
306
+ }
307
+ }
308
+
309
+ // ============================================================================
310
+ // NFT List Command - Uses SDK getTokenAccounts()
311
+ // ============================================================================
312
+
313
+ async function nftList(args) {
314
+ const rpc = args.rpc || getDefaultRpc();
315
+ const isJson = args.json || false;
316
+
317
+ let address = args.address;
318
+ if (!address) {
319
+ const config = loadConfig();
320
+ address = config.defaultWallet;
321
+ }
322
+
323
+ if (!address) {
324
+ if (isJson) {
325
+ console.log(JSON.stringify({ error: 'No address provided' }, null, 2));
326
+ } else {
327
+ console.log(`\n ${C.red}✗ No wallet address specified.${C.reset}`);
328
+ console.log(` ${C.dim}Usage: aether nft list --address <wallet>${C.reset}\n`);
329
+ }
330
+ return;
331
+ }
332
+
333
+ const client = createClient(rpc);
334
+ const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
335
+
336
+ if (!isJson) {
337
+ console.log(`\n${C.bright}${C.cyan}── NFT Collection ───────────────────────────────────────${C.reset}\n`);
338
+ console.log(` ${C.dim}Wallet:${C.reset} ${address}`);
339
+ console.log(` ${C.dim}RPC:${C.reset} ${rpc}\n`);
340
+ console.log(` ${C.dim}Fetching NFTs via SDK...${C.reset}\n`);
341
+ }
342
+
343
+ try {
344
+ // SDK call: getTokenAccounts → GET /v1/tokens/<address>
345
+ const tokenAccounts = await client.getTokenAccounts(rawAddr);
346
+
347
+ // Filter for NFTs (amount = 1, has metadata)
348
+ const nfts = (tokenAccounts || []).filter(t =>
349
+ t.amount === '1' || t.amount === 1 || t.is_nft || t.nft_id
350
+ );
351
+
352
+ if (isJson) {
353
+ console.log(JSON.stringify({
354
+ address,
355
+ rpc,
356
+ nft_count: nfts.length,
357
+ nfts: nfts.map(n => ({
358
+ id: n.mint || n.nft_id || n.id,
359
+ amount: n.amount,
360
+ metadata: n.metadata_url || n.metadata,
361
+ creator: n.creator,
362
+ royalties: n.royalties,
363
+ })),
364
+ cli_version: CLI_VERSION,
365
+ fetched_at: new Date().toISOString(),
366
+ }, null, 2));
367
+ return;
368
+ }
369
+
370
+ if (nfts.length === 0) {
371
+ console.log(` ${C.yellow}⚠ No NFTs found for this wallet.${C.reset}`);
372
+ console.log(` ${C.dim}Create your first NFT:${C.reset}`);
373
+ console.log(` ${C.cyan}aether nft create --metadata <url>${C.reset}\n`);
374
+ return;
375
+ }
376
+
377
+ console.log(` ${C.bright}Found ${nfts.length} NFT${nfts.length === 1 ? '' : 's'}${C.reset}\n`);
378
+
379
+ // Display table
380
+ console.log(` ${C.dim}┌─────────────────────────────────────────────────────────────────┐${C.reset}`);
381
+ console.log(` ${C.dim}│${C.reset} ${C.bright}# NFT ID${C.reset} ${C.bright}Metadata${C.reset} ${C.bright}Royalties${C.reset} ${C.dim}│${C.reset}`);
382
+ console.log(` ${C.dim}├─────────────────────────────────────────────────────────────────┤${C.reset}`);
383
+
384
+ nfts.forEach((nft, i) => {
385
+ const id = shortAddress(nft.mint || nft.nft_id || nft.id, 10).padEnd(24);
386
+ const meta = truncate(nft.metadata_url || nft.metadata || 'N/A', 22).padEnd(22);
387
+ const royalty = nft.royalties !== undefined
388
+ ? `${(nft.royalties / 100).toFixed(0)}%`.padEnd(9)
389
+ : 'N/A ';
390
+ console.log(` ${C.dim}│${C.reset} ${(i + 1).toString().padStart(2)} ${C.cyan}${id}${C.reset} ${meta} ${royalty} ${C.dim}│${C.reset}`);
391
+ });
392
+
393
+ console.log(` ${C.dim}└─────────────────────────────────────────────────────────────────┘${C.reset}`);
394
+ console.log();
395
+ console.log(` ${C.dim}SDK: getTokenAccounts() → GET /v1/tokens/${shortAddress(rawAddr)}${C.reset}\n`);
396
+
397
+ } catch (err) {
398
+ if (isJson) {
399
+ console.log(JSON.stringify({ error: err.message, address }, null, 2));
400
+ } else {
401
+ console.log(` ${C.red}✗ Failed to fetch NFTs:${C.reset} ${err.message}`);
402
+ console.log(` ${C.dim}Is your validator running? RPC: ${rpc}${C.reset}\n`);
403
+ }
404
+ }
405
+ }
406
+
407
+ // ============================================================================
408
+ // NFT Transfer Command - Uses SDK transferNFT()
409
+ // ============================================================================
410
+
411
+ async function nftTransfer(args) {
412
+ const rl = createRl();
413
+ const rpc = args.rpc || getDefaultRpc();
414
+
415
+ let fromAddress = args.from;
416
+ if (!fromAddress) {
417
+ const config = loadConfig();
418
+ fromAddress = config.defaultWallet;
419
+ }
420
+
421
+ if (!fromAddress) {
422
+ console.log(`\n ${C.red}✗ No wallet address specified.${C.reset}`);
423
+ console.log(` ${C.dim}Usage: aether nft transfer --nft <id> --to <addr> --from <wallet>${C.reset}\n`);
424
+ rl.close();
425
+ return;
426
+ }
427
+
428
+ // Get NFT ID
429
+ let nftId = args.nft;
430
+ if (!nftId) {
431
+ console.log(`\n ${C.dim}Enter the NFT ID to transfer:${C.reset}`);
432
+ nftId = await question(rl, ` NFT ID > ${C.reset}`);
433
+ if (!nftId.trim()) {
434
+ console.log(`\n ${C.red}✗ NFT ID is required.${C.reset}\n`);
435
+ rl.close();
436
+ return;
437
+ }
438
+ }
439
+
440
+ // Get recipient
441
+ let toAddress = args.to;
442
+ if (!toAddress) {
443
+ console.log(` ${C.dim}Enter recipient address:${C.reset}`);
444
+ toAddress = await question(rl, ` Recipient > ${C.reset}`);
445
+ if (!toAddress.trim()) {
446
+ console.log(`\n ${C.red}✗ Recipient address is required.${C.reset}\n`);
447
+ rl.close();
448
+ return;
449
+ }
450
+ }
451
+
452
+ console.log(`\n ${C.green}★${C.reset} From: ${C.bright}${fromAddress}${C.reset}`);
453
+ console.log(` ${C.green}★${C.reset} To: ${C.bright}${toAddress}${C.reset}`);
454
+ console.log(` ${C.green}★${C.reset} NFT: ${C.bright}${shortAddress(nftId)}${C.reset}`);
455
+ console.log();
456
+
457
+ // Ask for mnemonic
458
+ console.log(`${C.yellow} ⚠ Transferring NFT requires your wallet passphrase.${C.reset}`);
459
+ const mnemonic = await askMnemonic(rl, 'Enter passphrase to sign the transfer');
460
+ console.log();
461
+
462
+ let keyPair;
463
+ try {
464
+ keyPair = deriveKeypair(mnemonic);
465
+ } catch (e) {
466
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}\n`);
467
+ rl.close();
468
+ return;
469
+ }
470
+
471
+ // Verify derived address
472
+ const derivedAddress = formatAddress(keyPair.publicKey);
473
+ if (derivedAddress !== fromAddress) {
474
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
475
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
476
+ console.log(` ${C.dim} Expected: ${fromAddress}${C.reset}\n`);
477
+ rl.close();
478
+ return;
479
+ }
480
+
481
+ // Confirm
482
+ const confirm = await question(rl, ` ${C.yellow}Confirm NFT transfer? [y/N]${C.reset} > `);
483
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
484
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
485
+ rl.close();
486
+ return;
487
+ }
488
+ rl.close();
489
+
490
+ // Transfer via SDK
491
+ const client = createClient(rpc);
492
+
493
+ console.log(`\n ${C.dim}Submitting NFT transfer via SDK...${C.reset}`);
494
+
495
+ try {
496
+ const result = await client.transferNFT({
497
+ from: fromAddress.startsWith('ATH') ? fromAddress.slice(3) : fromAddress,
498
+ nftId: nftId.trim(),
499
+ to: toAddress.startsWith('ATH') ? toAddress.slice(3) : toAddress,
500
+ signFn: async (tx) => signTransaction(tx, keyPair.secretKey),
501
+ });
502
+
503
+ if (result.error) {
504
+ throw new Error(result.error.message || JSON.stringify(result.error));
505
+ }
506
+
507
+ console.log(`\n${C.green}✓ NFT transferred successfully!${C.reset}`);
508
+ console.log(` ${C.dim}NFT ID:${C.reset} ${C.cyan}${C.bright}${nftId}${C.reset}`);
509
+ console.log(` ${C.dim}From:${C.reset} ${fromAddress}`);
510
+ console.log(` ${C.dim}To:${C.reset} ${C.green}${toAddress}${C.reset}`);
511
+ if (result.signature) {
512
+ console.log(` ${C.dim}Signature:${C.reset} ${shortAddress(result.signature)}`);
513
+ }
514
+ if (result.slot) {
515
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot}`);
516
+ }
517
+ console.log(` ${C.dim}SDK:${C.reset} transferNFT() → POST /v1/transaction`);
518
+ console.log();
519
+ console.log(` ${C.dim}Verify transfer:${C.reset}`);
520
+ console.log(` ${C.cyan}aether nft list --address ${toAddress}${C.reset}\n`);
521
+
522
+ } catch (err) {
523
+ console.log(`\n ${C.red}✗ NFT transfer failed:${C.reset} ${err.message}`);
524
+ console.log(` ${C.dim}Common causes:${C.reset}`);
525
+ console.log(` • You don't own this NFT`);
526
+ console.log(` • Invalid NFT ID`);
527
+ console.log(` • Recipient address is invalid`);
528
+ console.log(` • Insufficient balance for transaction fee\n`);
529
+ process.exit(1);
530
+ }
531
+ }
532
+
533
+ // ============================================================================
534
+ // NFT Info Command - Uses SDK getAccountInfo()
535
+ // ============================================================================
536
+
537
+ async function nftInfo(args) {
538
+ const rpc = args.rpc || getDefaultRpc();
539
+ const isJson = args.json || false;
540
+
541
+ let nftId = args.nft;
542
+ if (!nftId) {
543
+ console.log(`\n ${C.red}✗ NFT ID is required.${C.reset}`);
544
+ console.log(` ${C.dim}Usage: aether nft info --nft <id>${C.reset}\n`);
545
+ return;
546
+ }
547
+
548
+ const client = createClient(rpc);
549
+
550
+ if (!isJson) {
551
+ console.log(`\n${C.bright}${C.cyan}── NFT Details ──────────────────────────────────────────${C.reset}\n`);
552
+ console.log(` ${C.dim}Fetching NFT info via SDK...${C.reset}\n`);
553
+ }
554
+
555
+ try {
556
+ // SDK call: getAccountInfo → GET /v1/account/<nft_id>
557
+ const accountInfo = await client.getAccountInfo(nftId);
558
+
559
+ if (!accountInfo || accountInfo.error) {
560
+ throw new Error(accountInfo?.error || 'NFT not found');
561
+ }
562
+
563
+ // Extract NFT-specific data
564
+ const nftData = {
565
+ id: nftId,
566
+ owner: accountInfo.owner,
567
+ metadata: accountInfo.data?.metadata || accountInfo.metadata_url,
568
+ creator: accountInfo.data?.creator,
569
+ royalties: accountInfo.data?.royalties,
570
+ lamports: accountInfo.lamports,
571
+ rentEpoch: accountInfo.rent_epoch,
572
+ };
573
+
574
+ if (isJson) {
575
+ console.log(JSON.stringify({
576
+ nft: nftData,
577
+ rpc,
578
+ cli_version: CLI_VERSION,
579
+ fetched_at: new Date().toISOString(),
580
+ }, null, 2));
581
+ return;
582
+ }
583
+
584
+ console.log(` ${C.green}★${C.reset} NFT ID: ${C.bright}${C.cyan}${nftId}${C.reset}`);
585
+ console.log(` ${C.green}★${C.reset} Owner: ${nftData.owner || 'Unknown'}`);
586
+ console.log(` ${C.green}★${C.reset} Creator: ${nftData.creator || 'Unknown'}`);
587
+ console.log(` ${C.green}★${C.reset} Metadata: ${truncate(nftData.metadata, 50) || 'N/A'}`);
588
+ if (nftData.royalties !== undefined) {
589
+ console.log(` ${C.green}★${C.reset} Royalties: ${(nftData.royalties / 100).toFixed(2)}%`);
590
+ }
591
+ if (nftData.lamports !== undefined) {
592
+ console.log(` ${C.green}★${C.reset} Lamports: ${nftData.lamports.toLocaleString()}`);
593
+ }
594
+ console.log();
595
+ console.log(` ${C.dim}SDK: getAccountInfo() → GET /v1/account/${shortAddress(nftId)}${C.reset}\n`);
596
+
597
+ } catch (err) {
598
+ if (isJson) {
599
+ console.log(JSON.stringify({ error: err.message, nft_id: nftId }, null, 2));
600
+ } else {
601
+ console.log(` ${C.red}✗ Failed to fetch NFT info:${C.reset} ${err.message}`);
602
+ console.log(` ${C.dim}The NFT may not exist or the RPC endpoint is unavailable.${C.reset}\n`);
603
+ }
604
+ }
605
+ }
606
+
607
+ // ============================================================================
608
+ // NFT Update Command - Uses SDK updateMetadata()
609
+ // ============================================================================
610
+
611
+ async function nftUpdate(args) {
612
+ const rl = createRl();
613
+ const rpc = args.rpc || getDefaultRpc();
614
+
615
+ let address = args.address;
616
+ if (!address) {
617
+ const config = loadConfig();
618
+ address = config.defaultWallet;
619
+ }
620
+
621
+ if (!address) {
622
+ console.log(`\n ${C.red}✗ No wallet address specified.${C.reset}`);
623
+ console.log(` ${C.dim}Usage: aether nft update --nft <id> --metadata <url> --address <wallet>${C.reset}\n`);
624
+ rl.close();
625
+ return;
626
+ }
627
+
628
+ let nftId = args.nft;
629
+ if (!nftId) {
630
+ console.log(`\n ${C.dim}Enter the NFT ID to update:${C.reset}`);
631
+ nftId = await question(rl, ` NFT ID > ${C.reset}`);
632
+ if (!nftId.trim()) {
633
+ console.log(`\n ${C.red}✗ NFT ID is required.${C.reset}\n`);
634
+ rl.close();
635
+ return;
636
+ }
637
+ }
638
+
639
+ let metadataUrl = args.metadata;
640
+ if (!metadataUrl) {
641
+ console.log(` ${C.dim}Enter new metadata URL:${C.reset}`);
642
+ metadataUrl = await question(rl, ` New Metadata URL > ${C.reset}`);
643
+ if (!metadataUrl.trim()) {
644
+ console.log(`\n ${C.red}✗ Metadata URL is required.${C.reset}\n`);
645
+ rl.close();
646
+ return;
647
+ }
648
+ }
649
+
650
+ console.log(`\n ${C.green}★${C.reset} Updater: ${C.bright}${address}${C.reset}`);
651
+ console.log(` ${C.green}★${C.reset} NFT: ${C.bright}${shortAddress(nftId)}${C.reset}`);
652
+ console.log(` ${C.green}★${C.reset} New Meta: ${C.bright}${truncate(metadataUrl, 40)}${C.reset}`);
653
+ console.log();
654
+
655
+ // Ask for mnemonic
656
+ console.log(`${C.yellow} ⚠ Updating metadata requires your wallet passphrase.${C.reset}`);
657
+ const mnemonic = await askMnemonic(rl, 'Enter passphrase to sign the update');
658
+ console.log();
659
+
660
+ let keyPair;
661
+ try {
662
+ keyPair = deriveKeypair(mnemonic);
663
+ } catch (e) {
664
+ console.log(` ${C.red}✗ Failed to derive keypair: ${e.message}${C.reset}\n`);
665
+ rl.close();
666
+ return;
667
+ }
668
+
669
+ // Verify derived address
670
+ const derivedAddress = formatAddress(keyPair.publicKey);
671
+ if (derivedAddress !== address) {
672
+ console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
673
+ console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
674
+ console.log(` ${C.dim} Expected: ${address}${C.reset}\n`);
675
+ rl.close();
676
+ return;
677
+ }
678
+
679
+ // Confirm
680
+ const confirm = await question(rl, ` ${C.yellow}Confirm metadata update? [y/N]${C.reset} > `);
681
+ if (!confirm.trim().toLowerCase().startsWith('y')) {
682
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
683
+ rl.close();
684
+ return;
685
+ }
686
+ rl.close();
687
+
688
+ // Update via SDK
689
+ const client = createClient(rpc);
690
+
691
+ console.log(`\n ${C.dim}Submitting metadata update via SDK...${C.reset}`);
692
+
693
+ try {
694
+ const result = await client.updateMetadata({
695
+ creator: address.startsWith('ATH') ? address.slice(3) : address,
696
+ nftId: nftId.trim(),
697
+ metadataUrl: metadataUrl.trim(),
698
+ signFn: async (tx) => signTransaction(tx, keyPair.secretKey),
699
+ });
700
+
701
+ if (result.error) {
702
+ throw new Error(result.error.message || JSON.stringify(result.error));
703
+ }
704
+
705
+ console.log(`\n${C.green}✓ Metadata updated successfully!${C.reset}`);
706
+ console.log(` ${C.dim}NFT ID:${C.reset} ${C.cyan}${C.bright}${nftId}${C.reset}`);
707
+ console.log(` ${C.dim}New Meta:${C.reset} ${truncate(metadataUrl, 45)}`);
708
+ if (result.signature) {
709
+ console.log(` ${C.dim}Signature:${C.reset} ${shortAddress(result.signature)}`);
710
+ }
711
+ if (result.slot) {
712
+ console.log(` ${C.dim}Slot:${C.reset} ${result.slot}`);
713
+ }
714
+ console.log(` ${C.dim}SDK:${C.reset} updateMetadata() → POST /v1/transaction`);
715
+ console.log();
716
+ console.log(` ${C.dim}Verify update:${C.reset}`);
717
+ console.log(` ${C.cyan}aether nft info --nft ${nftId}${C.reset}\n`);
718
+
719
+ } catch (err) {
720
+ console.log(`\n ${C.red}✗ Metadata update failed:${C.reset} ${err.message}`);
721
+ console.log(` ${C.dim}Common causes:${C.reset}`);
722
+ console.log(` • You are not the NFT creator`);
723
+ console.log(` • Invalid NFT ID`);
724
+ console.log(` • Insufficient balance for transaction fee\n`);
725
+ process.exit(1);
726
+ }
727
+ }
728
+
729
+ // ============================================================================
730
+ // Help
731
+ // ============================================================================
732
+
733
+ function showHelp() {
734
+ console.log(`
735
+ ${C.bright}${C.cyan}aether-cli nft${C.reset} — NFT management suite
736
+
737
+ ${C.bright}COMMANDS${C.reset}
738
+ create Create a new NFT with metadata and royalties
739
+ list List all NFTs owned by a wallet
740
+ transfer Transfer an NFT to another address
741
+ info Show detailed info about an NFT
742
+ update Update NFT metadata (creator only)
743
+
744
+ ${C.bright}USAGE${C.reset}
745
+ aether nft create --metadata <url> [--royalties <bps>] [--address <wallet>]
746
+ aether nft list --address <wallet> [--json]
747
+ aether nft transfer --nft <id> --to <addr> [--from <wallet>]
748
+ aether nft info --nft <id> [--json]
749
+ aether nft update --nft <id> --metadata <url> [--address <wallet>]
750
+
751
+ ${C.bright}SDK METHODS USED${C.reset}
752
+ client.createNFT() → POST /v1/transaction (CreateNFT)
753
+ client.transferNFT() → POST /v1/transaction (TransferNFT)
754
+ client.updateMetadata() → POST /v1/transaction (UpdateMetadata)
755
+ client.getTokenAccounts() → GET /v1/tokens/<addr>
756
+ client.getAccountInfo() → GET /v1/account/<addr>
757
+
758
+ ${C.bright}EXAMPLES${C.reset}
759
+ aether nft create --metadata https://example.com/nft.json --royalties 500
760
+ aether nft list --address ATHxxx... --json
761
+ aether nft transfer --nft NFTxxx... --to ATHyyy...
762
+ aether nft info --nft NFTxxx... --json
763
+
764
+ ${C.bright}NOTES${C.reset}
765
+ • Only the creator can update NFT metadata
766
+ • Royalties are specified in basis points (100 = 1%, 500 = 5%)
767
+ • NFT creation fee is typically 0.01 AETH
768
+ • Metadata must be a valid JSON file hosted at the provided URL
769
+
770
+ ${C.green}✓ Fully wired to @jellylegsai/aether-sdk${C.reset}
771
+ `);
772
+ }
773
+
774
+ // ============================================================================
775
+ // Argument Parser
776
+ // ============================================================================
777
+
778
+ function parseArgs() {
779
+ const rawArgs = process.argv.slice(3);
780
+ const subcmd = rawArgs[0] || 'help';
781
+ const allArgs = rawArgs.slice(1);
782
+
783
+ const rpcIndex = allArgs.findIndex(a => a === '--rpc' || a === '-r');
784
+ const rpc = rpcIndex !== -1 && allArgs[rpcIndex + 1] ? allArgs[rpcIndex + 1] : getDefaultRpc();
785
+
786
+ const parsed = {
787
+ subcmd,
788
+ rpc,
789
+ json: allArgs.includes('--json') || allArgs.includes('-j'),
790
+ address: null,
791
+ from: null,
792
+ to: null,
793
+ nft: null,
794
+ metadata: null,
795
+ royalties: null,
796
+ };
797
+
798
+ const addrIdx = allArgs.findIndex(a => a === '--address' || a === '-a');
799
+ if (addrIdx !== -1 && allArgs[addrIdx + 1]) parsed.address = allArgs[addrIdx + 1];
800
+
801
+ const fromIdx = allArgs.findIndex(a => a === '--from' || a === '-f');
802
+ if (fromIdx !== -1 && allArgs[fromIdx + 1]) parsed.from = allArgs[fromIdx + 1];
803
+
804
+ const toIdx = allArgs.findIndex(a => a === '--to' || a === '-t');
805
+ if (toIdx !== -1 && allArgs[toIdx + 1]) parsed.to = allArgs[toIdx + 1];
806
+
807
+ const nftIdx = allArgs.findIndex(a => a === '--nft' || a === '-n');
808
+ if (nftIdx !== -1 && allArgs[nftIdx + 1]) parsed.nft = allArgs[nftIdx + 1];
809
+
810
+ const metaIdx = allArgs.findIndex(a => a === '--metadata' || a === '-m');
811
+ if (metaIdx !== -1 && allArgs[metaIdx + 1]) parsed.metadata = allArgs[metaIdx + 1];
812
+
813
+ const royaltyIdx = allArgs.findIndex(a => a === '--royalties' || a === '-r');
814
+ if (royaltyIdx !== -1 && allArgs[royaltyIdx + 1]) parsed.royalties = allArgs[royaltyIdx + 1];
815
+
816
+ return parsed;
817
+ }
818
+
819
+ // ============================================================================
820
+ // Main Entry Point
821
+ // ============================================================================
822
+
823
+ async function nftCommand() {
824
+ const args = parseArgs();
825
+
826
+ switch (args.subcmd) {
827
+ case 'create':
828
+ await nftCreate(args);
829
+ break;
830
+ case 'list':
831
+ await nftList(args);
832
+ break;
833
+ case 'transfer':
834
+ await nftTransfer(args);
835
+ break;
836
+ case 'info':
837
+ await nftInfo(args);
838
+ break;
839
+ case 'update':
840
+ await nftUpdate(args);
841
+ break;
842
+ case 'help':
843
+ case '--help':
844
+ case '-h':
845
+ default:
846
+ showHelp();
847
+ }
848
+ }
849
+
850
+ module.exports = { nftCommand };
851
+
852
+ if (require.main === module) {
853
+ nftCommand().catch(err => {
854
+ console.error(`\n${C.red}✗ NFT command failed:${C.reset}`, err.message, '\n');
855
+ process.exit(1);
856
+ });
857
+ }