aether-hub 1.2.8 → 1.3.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/sdk/index.js ADDED
@@ -0,0 +1,1013 @@
1
+ /**
2
+ * @jellylegsai/aether-sdk
3
+ *
4
+ * Official Aether Blockchain SDK - Real HTTP RPC calls to Aether nodes
5
+ * No stubs, no mocks - every function makes actual blockchain calls
6
+ *
7
+ * Default RPC: http://127.0.0.1:8899 (configurable via constructor or AETHER_RPC env)
8
+ */
9
+
10
+ const http = require('http');
11
+ const https = require('https');
12
+
13
+ // Default configuration
14
+ const DEFAULT_RPC_URL = 'http://127.0.0.1:8899';
15
+ const DEFAULT_TIMEOUT_MS = 10000;
16
+
17
+ /**
18
+ * Aether SDK Client
19
+ * Real blockchain interface layer - every method makes actual HTTP RPC calls
20
+ */
21
+ class AetherClient {
22
+ constructor(options = {}) {
23
+ this.rpcUrl = options.rpcUrl || process.env.AETHER_RPC || DEFAULT_RPC_URL;
24
+ this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
25
+
26
+ // Parse RPC URL
27
+ const url = new URL(this.rpcUrl);
28
+ this.protocol = url.protocol;
29
+ this.hostname = url.hostname;
30
+ this.port = url.port || (this.protocol === 'https:' ? 443 : 80);
31
+ }
32
+
33
+ /**
34
+ * Internal: Make HTTP GET request to RPC endpoint
35
+ */
36
+ _httpGet(path, timeoutMs = this.timeoutMs) {
37
+ return new Promise((resolve, reject) => {
38
+ const lib = this.protocol === 'https:' ? https : http;
39
+ const req = lib.request({
40
+ hostname: this.hostname,
41
+ port: this.port,
42
+ path: path,
43
+ method: 'GET',
44
+ timeout: timeoutMs,
45
+ headers: { 'Content-Type': 'application/json' },
46
+ }, (res) => {
47
+ let data = '';
48
+ res.on('data', (chunk) => data += chunk);
49
+ res.on('end', () => {
50
+ try {
51
+ const parsed = JSON.parse(data);
52
+ if (parsed.error) {
53
+ reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
54
+ } else {
55
+ resolve(parsed);
56
+ }
57
+ } catch (e) {
58
+ resolve({ raw: data });
59
+ }
60
+ });
61
+ });
62
+ req.on('error', reject);
63
+ req.on('timeout', () => {
64
+ req.destroy();
65
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
66
+ });
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Internal: Make HTTP POST request to RPC endpoint
73
+ */
74
+ _httpPost(path, body = {}, timeoutMs = this.timeoutMs) {
75
+ return new Promise((resolve, reject) => {
76
+ const lib = this.protocol === 'https:' ? https : http;
77
+ const bodyStr = JSON.stringify(body);
78
+ const req = lib.request({
79
+ hostname: this.hostname,
80
+ port: this.port,
81
+ path: path,
82
+ method: 'POST',
83
+ timeout: timeoutMs,
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ 'Content-Length': Buffer.byteLength(bodyStr),
87
+ },
88
+ }, (res) => {
89
+ let data = '';
90
+ res.on('data', (chunk) => data += chunk);
91
+ res.on('end', () => {
92
+ try {
93
+ const parsed = JSON.parse(data);
94
+ if (parsed.error) {
95
+ reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
96
+ } else {
97
+ resolve(parsed);
98
+ }
99
+ } catch (e) {
100
+ resolve({ raw: data });
101
+ }
102
+ });
103
+ });
104
+ req.on('error', reject);
105
+ req.on('timeout', () => {
106
+ req.destroy();
107
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
108
+ });
109
+ req.write(bodyStr);
110
+ req.end();
111
+ });
112
+ }
113
+
114
+ // ============================================================
115
+ // Core RPC Methods - Real blockchain calls
116
+ // ============================================================
117
+
118
+ /**
119
+ * Get current slot number
120
+ * RPC: GET /v1/slot
121
+ *
122
+ * @returns {Promise<number>} Current slot number
123
+ */
124
+ async getSlot() {
125
+ const result = await this._httpGet('/v1/slot');
126
+ return result.slot !== undefined ? result.slot : result;
127
+ }
128
+
129
+ /**
130
+ * Get current block height
131
+ * RPC: GET /v1/blockheight
132
+ *
133
+ * @returns {Promise<number>} Current block height
134
+ */
135
+ async getBlockHeight() {
136
+ const result = await this._httpGet('/v1/blockheight');
137
+ return result.blockHeight !== undefined ? result.blockHeight : result;
138
+ }
139
+
140
+ /**
141
+ * Get account info including balance
142
+ * RPC: GET /v1/account/<address>
143
+ *
144
+ * @param {string} address - Account public key (base58)
145
+ * @returns {Promise<Object>} Account info: { lamports, owner, data, rent_epoch }
146
+ */
147
+ async getAccountInfo(address) {
148
+ if (!address) {
149
+ throw new Error('Address is required');
150
+ }
151
+ const result = await this._httpGet(`/v1/account/${address}`);
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Alias for getAccountInfo
157
+ * @param {string} address - Account address
158
+ * @returns {Promise<Object>} Account info
159
+ */
160
+ async getAccount(address) {
161
+ return this.getAccountInfo(address);
162
+ }
163
+
164
+ /**
165
+ * Get balance in lamports
166
+ * RPC: GET /v1/account/<address>
167
+ *
168
+ * @param {string} address - Account public key (base58)
169
+ * @returns {Promise<number>} Balance in lamports
170
+ */
171
+ async getBalance(address) {
172
+ const account = await this.getAccountInfo(address);
173
+ return account.lamports !== undefined ? account.lamports : 0;
174
+ }
175
+
176
+ /**
177
+ * Get epoch info
178
+ * RPC: GET /v1/epoch
179
+ *
180
+ * @returns {Promise<Object>} Epoch info: { epoch, slotIndex, slotsInEpoch, absoluteSlot }
181
+ */
182
+ async getEpochInfo() {
183
+ const result = await this._httpGet('/v1/epoch');
184
+ return result;
185
+ }
186
+
187
+ /**
188
+ * Get transaction by signature
189
+ * RPC: GET /v1/transaction/<signature>
190
+ *
191
+ * @param {string} signature - Transaction signature (base58)
192
+ * @returns {Promise<Object>} Transaction details
193
+ */
194
+ async getTransaction(signature) {
195
+ if (!signature) {
196
+ throw new Error('Transaction signature is required');
197
+ }
198
+ const result = await this._httpGet(`/v1/transaction/${signature}`);
199
+ return result;
200
+ }
201
+
202
+ /**
203
+ * Submit a signed transaction
204
+ * RPC: POST /v1/transaction
205
+ *
206
+ * @param {Object} tx - Signed transaction object
207
+ * @param {string} tx.signature - Transaction signature (base58)
208
+ * @param {string} tx.signer - Signer public key (base58)
209
+ * @param {string} tx.tx_type - Transaction type
210
+ * @param {Object} tx.payload - Transaction payload
211
+ * @returns {Promise<Object>} Transaction receipt: { signature, slot, confirmed }
212
+ */
213
+ async sendTransaction(tx) {
214
+ if (!tx || !tx.signature) {
215
+ throw new Error('Transaction with signature is required');
216
+ }
217
+ const result = await this._httpPost('/v1/transaction', tx);
218
+ return result;
219
+ }
220
+
221
+ /**
222
+ * Get recent blockhash for transaction signing
223
+ * RPC: GET /v1/recent-blockhash
224
+ *
225
+ * @returns {Promise<Object>} { blockhash, lastValidBlockHeight }
226
+ */
227
+ async getRecentBlockhash() {
228
+ const result = await this._httpGet('/v1/recent-blockhash');
229
+ return result;
230
+ }
231
+
232
+ /**
233
+ * Get network peers
234
+ * RPC: GET /v1/peers
235
+ *
236
+ * @returns {Promise<Array>} List of peer node addresses
237
+ */
238
+ async getClusterPeers() {
239
+ const result = await this._httpGet('/v1/peers');
240
+ return Array.isArray(result) ? result : (result.peers || []);
241
+ }
242
+
243
+ /**
244
+ * Get validator info
245
+ * RPC: GET /v1/validators
246
+ *
247
+ * @returns {Promise<Array>} List of validators with stake, commission, etc.
248
+ */
249
+ async getValidators() {
250
+ const result = await this._httpGet('/v1/validators');
251
+ return Array.isArray(result) ? result : (result.validators || []);
252
+ }
253
+
254
+ /**
255
+ * Get supply info
256
+ * RPC: GET /v1/supply
257
+ *
258
+ * @returns {Promise<Object>} Supply info: { total, circulating, nonCirculating }
259
+ */
260
+ async getSupply() {
261
+ const result = await this._httpGet('/v1/supply');
262
+ return result;
263
+ }
264
+
265
+ /**
266
+ * Get health status
267
+ * RPC: GET /v1/health
268
+ *
269
+ * @returns {Promise<string>} 'ok' if node is healthy
270
+ */
271
+ async getHealth() {
272
+ const result = await this._httpGet('/v1/health');
273
+ return result.status || result;
274
+ }
275
+
276
+ /**
277
+ * Get version info
278
+ * RPC: GET /v1/version
279
+ *
280
+ * @returns {Promise<Object>} Version info: { aetherCore, featureSet }
281
+ */
282
+ async getVersion() {
283
+ const result = await this._httpGet('/v1/version');
284
+ return result;
285
+ }
286
+
287
+ /**
288
+ * Get TPS (transactions per second)
289
+ * RPC: GET /v1/tps
290
+ *
291
+ * @returns {Promise<number>} Current TPS
292
+ */
293
+ async getTPS() {
294
+ const result = await this._httpGet('/v1/tps');
295
+ return result.tps ?? result.tps_avg ?? result.transactions_per_second ?? null;
296
+ }
297
+
298
+ /**
299
+ * Get fee estimates
300
+ * RPC: GET /v1/fees
301
+ *
302
+ * @returns {Promise<Object>} Fee info
303
+ */
304
+ async getFees() {
305
+ const result = await this._httpGet('/v1/fees');
306
+ return result;
307
+ }
308
+
309
+ /**
310
+ * Get slot production stats
311
+ * RPC: POST /v1/slot_production
312
+ *
313
+ * @returns {Promise<Object>} Slot production stats
314
+ */
315
+ async getSlotProduction() {
316
+ const result = await this._httpPost('/v1/slot_production', {});
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Get stake positions for an address
322
+ * RPC: GET /v1/stake/<address>
323
+ *
324
+ * @param {string} address - Account address
325
+ * @returns {Promise<Array>} List of stake positions
326
+ */
327
+ async getStakePositions(address) {
328
+ if (!address) throw new Error('Address is required');
329
+ const result = await this._httpGet(`/v1/stake/${address}`);
330
+ return result.delegations ?? result.stakes ?? result ?? [];
331
+ }
332
+
333
+ /**
334
+ * Get rewards for an address
335
+ * RPC: GET /v1/rewards/<address>
336
+ *
337
+ * @param {string} address - Account address
338
+ * @returns {Promise<Object>} Rewards info
339
+ */
340
+ async getRewards(address) {
341
+ if (!address) throw new Error('Address is required');
342
+ const result = await this._httpGet(`/v1/rewards/${address}`);
343
+ return result;
344
+ }
345
+
346
+ /**
347
+ * Get validator APY
348
+ * RPC: GET /v1/validator/<address>/apy
349
+ *
350
+ * @param {string} validatorAddr - Validator address
351
+ * @returns {Promise<Object>} APY info
352
+ */
353
+ async getValidatorAPY(validatorAddr) {
354
+ if (!validatorAddr) throw new Error('Validator address is required');
355
+ const result = await this._httpGet(`/v1/validator/${validatorAddr}/apy`);
356
+ return result;
357
+ }
358
+
359
+ /**
360
+ * Get recent transactions for an address
361
+ * RPC: GET /v1/transactions/<address>?limit=<n>
362
+ *
363
+ * @param {string} address - Account address
364
+ * @param {number} limit - Max transactions to return
365
+ * @returns {Promise<Array>} List of recent transactions
366
+ */
367
+ async getRecentTransactions(address, limit = 20) {
368
+ if (!address) throw new Error('Address is required');
369
+ const result = await this._httpGet(`/v1/transactions/${address}?limit=${limit}`);
370
+ return result.transactions ?? result ?? [];
371
+ }
372
+
373
+ /**
374
+ * Get transaction history with signatures for an address
375
+ * RPC: POST /v1/transactions/history (or GET /v1/transactions/<address>?limit=<n>)
376
+ *
377
+ * @param {string} address - Account address
378
+ * @param {number} limit - Max transactions to return
379
+ * @returns {Promise<Object>} Transaction history with signatures and details
380
+ */
381
+ async getTransactionHistory(address, limit = 20) {
382
+ if (!address) throw new Error('Address is required');
383
+ // First get signatures
384
+ const sigResult = await this._httpPost('/v1/transactions/history', { address, limit });
385
+ if (sigResult.error) {
386
+ throw new Error(sigResult.error.message || sigResult.error);
387
+ }
388
+ const signatures = sigResult.signatures || sigResult.result || [];
389
+
390
+ // Fetch full transaction details for each signature (up to 10 at a time)
391
+ const BATCH = 10;
392
+ const txs = [];
393
+ for (let i = 0; i < signatures.length; i += BATCH) {
394
+ const batch = signatures.slice(i, i + BATCH);
395
+ const batchPromises = batch.map(sig =>
396
+ this.getTransaction(sig.signature || sig).catch(() => null)
397
+ );
398
+ const batchResults = await Promise.all(batchPromises);
399
+ txs.push(...batchResults.filter(Boolean));
400
+ }
401
+
402
+ return {
403
+ signatures: signatures,
404
+ transactions: txs,
405
+ address: address,
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Get all SPL token accounts for a wallet address
411
+ * RPC: GET /v1/tokens/<address>
412
+ *
413
+ * @param {string} address - Account public key (base58)
414
+ * @returns {Promise<Array>} List of token accounts with mint, amount, decimals
415
+ */
416
+ async getTokenAccounts(address) {
417
+ if (!address) throw new Error('Address is required');
418
+ const result = await this._httpGet(`/v1/tokens/${address}`);
419
+ return result.tokens ?? result.accounts ?? result ?? [];
420
+ }
421
+
422
+ /**
423
+ * Get all stake accounts for a wallet address
424
+ * RPC: GET /v1/stake-accounts/<address>
425
+ *
426
+ * @param {string} address - Account public key (base58)
427
+ * @returns {Promise<Array>} List of stake accounts
428
+ */
429
+ async getStakeAccounts(address) {
430
+ if (!address) throw new Error('Address is required');
431
+ const result = await this._httpGet(`/v1/stake-accounts/${address}`);
432
+ return result.stake_accounts ?? result.delegations ?? result ?? [];
433
+ }
434
+
435
+ // ============================================================
436
+ // Transaction Helpers - Build and send real transactions
437
+ // ============================================================
438
+
439
+ /**
440
+ * Build and send a transfer transaction
441
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
442
+ *
443
+ * @param {Object} params
444
+ * @param {string} params.from - Sender address (base58)
445
+ * @param {string} params.to - Recipient address (base58)
446
+ * @param {number} params.amount - Amount in lamports
447
+ * @param {number} params.nonce - Nonce for replay protection
448
+ * @param {Function} params.signFn - Function to sign the transaction (receives tx object, returns signature)
449
+ * @returns {Promise<Object>} Transaction receipt
450
+ */
451
+ async transfer({ from, to, amount, nonce, signFn }) {
452
+ if (!from || !to || !amount === undefined || nonce === undefined) {
453
+ throw new Error('from, to, amount, and nonce are required');
454
+ }
455
+ if (!signFn || typeof signFn !== 'function') {
456
+ throw new Error('signFn is required (function to sign the transaction)');
457
+ }
458
+
459
+ // Get recent blockhash (real RPC call)
460
+ const { blockhash } = await this.getRecentBlockhash();
461
+
462
+ // Build transaction payload
463
+ const tx = {
464
+ signature: '', // Will be filled after signing
465
+ signer: from,
466
+ tx_type: 'Transfer',
467
+ payload: {
468
+ recipient: to,
469
+ amount: BigInt(amount),
470
+ nonce: BigInt(nonce),
471
+ },
472
+ fee: 5000, // 5000 lamports fee
473
+ slot: await this.getSlot(),
474
+ timestamp: Date.now(),
475
+ };
476
+
477
+ // Sign transaction (user provides signing function)
478
+ const signature = await signFn(tx, blockhash);
479
+ tx.signature = signature;
480
+
481
+ // Send to blockchain (real RPC call)
482
+ const receipt = await this.sendTransaction(tx);
483
+ return receipt;
484
+ }
485
+
486
+ /**
487
+ * Build and send a stake delegation transaction
488
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
489
+ *
490
+ * @param {Object} params
491
+ * @param {string} params.staker - Staker address (base58)
492
+ * @param {string} params.validator - Validator address (base58)
493
+ * @param {number} params.amount - Amount to stake in lamports
494
+ * @param {Function} params.signFn - Function to sign the transaction
495
+ * @returns {Promise<Object>} Transaction receipt
496
+ */
497
+ async stake({ staker, validator, amount, signFn }) {
498
+ if (!staker || !validator || !amount === undefined) {
499
+ throw new Error('staker, validator, and amount are required');
500
+ }
501
+ if (!signFn || typeof signFn !== 'function') {
502
+ throw new Error('signFn is required');
503
+ }
504
+
505
+ const { blockhash } = await this.getRecentBlockhash();
506
+
507
+ const tx = {
508
+ signature: '',
509
+ signer: staker,
510
+ tx_type: 'Stake',
511
+ payload: {
512
+ validator: validator,
513
+ amount: BigInt(amount),
514
+ },
515
+ fee: 5000,
516
+ slot: await this.getSlot(),
517
+ timestamp: Date.now(),
518
+ };
519
+
520
+ const signature = await signFn(tx, blockhash);
521
+ tx.signature = signature;
522
+
523
+ const receipt = await this.sendTransaction(tx);
524
+ return receipt;
525
+ }
526
+
527
+ /**
528
+ * Build and send an unstake (withdraw) transaction
529
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
530
+ *
531
+ * @param {Object} params
532
+ * @param {string} params.stakeAccount - Stake account address (base58)
533
+ * @param {number} params.amount - Amount to unstake in lamports
534
+ * @param {Function} params.signFn - Function to sign the transaction
535
+ * @returns {Promise<Object>} Transaction receipt
536
+ */
537
+ async unstake({ stakeAccount, amount, signFn }) {
538
+ if (!stakeAccount || !amount === undefined) {
539
+ throw new Error('stakeAccount and amount are required');
540
+ }
541
+ if (!signFn || typeof signFn !== 'function') {
542
+ throw new Error('signFn is required');
543
+ }
544
+
545
+ const { blockhash } = await this.getRecentBlockhash();
546
+
547
+ const tx = {
548
+ signature: '',
549
+ signer: stakeAccount,
550
+ tx_type: 'Unstake',
551
+ payload: {
552
+ stake_account: stakeAccount,
553
+ amount: BigInt(amount),
554
+ },
555
+ fee: 5000,
556
+ slot: await this.getSlot(),
557
+ timestamp: Date.now(),
558
+ };
559
+
560
+ const signature = await signFn(tx, blockhash);
561
+ tx.signature = signature;
562
+
563
+ const receipt = await this.sendTransaction(tx);
564
+ return receipt;
565
+ }
566
+
567
+ /**
568
+ * Build and send a claim rewards transaction
569
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
570
+ *
571
+ * @param {Object} params
572
+ * @param {string} params.stakeAccount - Stake account address (base58)
573
+ * @param {Function} params.signFn - Function to sign the transaction
574
+ * @returns {Promise<Object>} Transaction receipt
575
+ */
576
+ async claimRewards({ stakeAccount, signFn }) {
577
+ if (!stakeAccount) {
578
+ throw new Error('stakeAccount is required');
579
+ }
580
+ if (!signFn || typeof signFn !== 'function') {
581
+ throw new Error('signFn is required');
582
+ }
583
+
584
+ const { blockhash } = await this.getRecentBlockhash();
585
+
586
+ const tx = {
587
+ signature: '',
588
+ signer: stakeAccount,
589
+ tx_type: 'ClaimRewards',
590
+ payload: {
591
+ stake_account: stakeAccount,
592
+ },
593
+ fee: 5000,
594
+ slot: await this.getSlot(),
595
+ timestamp: Date.now(),
596
+ };
597
+
598
+ const signature = await signFn(tx, blockhash);
599
+ tx.signature = signature;
600
+
601
+ const receipt = await this.sendTransaction(tx);
602
+ return receipt;
603
+ }
604
+
605
+ // ============================================================
606
+ // NFT Methods - Real blockchain calls for NFT operations
607
+ // ============================================================
608
+
609
+ /**
610
+ * Create a new NFT
611
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
612
+ *
613
+ * @param {Object} params
614
+ * @param {string} params.creator - Creator address (base58)
615
+ * @param {string} params.metadataUrl - URL to NFT metadata (JSON)
616
+ * @param {number} params.royalties - Royalty basis points (e.g., 500 = 5%)
617
+ * @param {Function} params.signFn - Function to sign the transaction
618
+ * @returns {Promise<Object>} Transaction receipt with NFT ID
619
+ */
620
+ async createNFT({ creator, metadataUrl, royalties, signFn }) {
621
+ if (!creator || !metadataUrl || royalties === undefined) {
622
+ throw new Error('creator, metadataUrl, and royalties are required');
623
+ }
624
+ if (!signFn || typeof signFn !== 'function') {
625
+ throw new Error('signFn is required');
626
+ }
627
+
628
+ const { blockhash } = await this.getRecentBlockhash();
629
+
630
+ const tx = {
631
+ signature: '',
632
+ signer: creator,
633
+ tx_type: 'CreateNFT',
634
+ payload: {
635
+ metadata_url: metadataUrl,
636
+ royalties: royalties,
637
+ },
638
+ fee: 10000, // Higher fee for NFT creation
639
+ slot: await this.getSlot(),
640
+ timestamp: Date.now(),
641
+ };
642
+
643
+ const signature = await signFn(tx, blockhash);
644
+ tx.signature = signature;
645
+
646
+ const receipt = await this.sendTransaction(tx);
647
+ return receipt;
648
+ }
649
+
650
+ /**
651
+ * Transfer an NFT to another address
652
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
653
+ *
654
+ * @param {Object} params
655
+ * @param {string} params.from - Current owner address (base58)
656
+ * @param {string} params.nftId - NFT ID
657
+ * @param {string} params.to - Recipient address (base58)
658
+ * @param {Function} params.signFn - Function to sign the transaction
659
+ * @returns {Promise<Object>} Transaction receipt
660
+ */
661
+ async transferNFT({ from, nftId, to, signFn }) {
662
+ if (!from || !nftId || !to) {
663
+ throw new Error('from, nftId, and to are required');
664
+ }
665
+ if (!signFn || typeof signFn !== 'function') {
666
+ throw new Error('signFn is required');
667
+ }
668
+
669
+ const { blockhash } = await this.getRecentBlockhash();
670
+
671
+ const tx = {
672
+ signature: '',
673
+ signer: from,
674
+ tx_type: 'TransferNFT',
675
+ payload: {
676
+ nft_id: nftId,
677
+ recipient: to,
678
+ },
679
+ fee: 5000,
680
+ slot: await this.getSlot(),
681
+ timestamp: Date.now(),
682
+ };
683
+
684
+ const signature = await signFn(tx, blockhash);
685
+ tx.signature = signature;
686
+
687
+ const receipt = await this.sendTransaction(tx);
688
+ return receipt;
689
+ }
690
+
691
+ /**
692
+ * Update NFT metadata URL
693
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
694
+ *
695
+ * @param {Object} params
696
+ * @param {string} params.creator - NFT creator/owner address (base58)
697
+ * @param {string} params.nftId - NFT ID
698
+ * @param {string} params.metadataUrl - New metadata URL
699
+ * @param {Function} params.signFn - Function to sign the transaction
700
+ * @returns {Promise<Object>} Transaction receipt
701
+ */
702
+ async updateMetadata({ creator, nftId, metadataUrl, signFn }) {
703
+ if (!creator || !nftId || !metadataUrl) {
704
+ throw new Error('creator, nftId, and metadataUrl are required');
705
+ }
706
+ if (!signFn || typeof signFn !== 'function') {
707
+ throw new Error('signFn is required');
708
+ }
709
+
710
+ const { blockhash } = await this.getRecentBlockhash();
711
+
712
+ const tx = {
713
+ signature: '',
714
+ signer: creator,
715
+ tx_type: 'UpdateMetadata',
716
+ payload: {
717
+ nft_id: nftId,
718
+ metadata_url: metadataUrl,
719
+ },
720
+ fee: 5000,
721
+ slot: await this.getSlot(),
722
+ timestamp: Date.now(),
723
+ };
724
+
725
+ const signature = await signFn(tx, blockhash);
726
+ tx.signature = signature;
727
+
728
+ const receipt = await this.sendTransaction(tx);
729
+ return receipt;
730
+ }
731
+ }
732
+
733
+ // ============================================================
734
+ // Convenience Functions (for quick one-off calls)
735
+ // ============================================================
736
+
737
+ /**
738
+ * Create a new AetherClient instance
739
+ * @param {Object} options - Client options
740
+ * @returns {AetherClient}
741
+ */
742
+ function createClient(options = {}) {
743
+ return new AetherClient(options);
744
+ }
745
+
746
+ /**
747
+ * Quick slot check (uses default RPC)
748
+ * @returns {Promise<number>} Current slot
749
+ */
750
+ async function getSlot() {
751
+ const client = new AetherClient();
752
+ return client.getSlot();
753
+ }
754
+
755
+ /**
756
+ * Quick balance check (uses default RPC)
757
+ * @param {string} address - Account address
758
+ * @returns {Promise<number>} Balance in lamports
759
+ */
760
+ async function getBalance(address) {
761
+ const client = new AetherClient();
762
+ return client.getBalance(address);
763
+ }
764
+
765
+ /**
766
+ * Quick health check (uses default RPC)
767
+ * @returns {Promise<string>} 'ok' if healthy
768
+ */
769
+ async function getHealth() {
770
+ const client = new AetherClient();
771
+ return client.getHealth();
772
+ }
773
+
774
+ /**
775
+ * Get current block height (uses default RPC)
776
+ * @returns {Promise<number>} Block height
777
+ */
778
+ async function getBlockHeight() {
779
+ const client = new AetherClient();
780
+ return client.getBlockHeight();
781
+ }
782
+
783
+ /**
784
+ * Get epoch info (uses default RPC)
785
+ * @returns {Promise<Object>} Epoch info
786
+ */
787
+ async function getEpoch() {
788
+ const client = new AetherClient();
789
+ return client.getEpochInfo();
790
+ }
791
+
792
+ /**
793
+ * Get TPS (uses default RPC)
794
+ * @returns {Promise<number>} Transactions per second
795
+ */
796
+ async function getTPS() {
797
+ const client = new AetherClient();
798
+ return client.getTPS();
799
+ }
800
+
801
+ /**
802
+ * Get supply info (uses default RPC)
803
+ * @returns {Promise<Object>} Supply info
804
+ */
805
+ async function getSupply() {
806
+ const client = new AetherClient();
807
+ return client.getSupply();
808
+ }
809
+
810
+ /**
811
+ * Get fees info (uses default RPC)
812
+ * @returns {Promise<Object>} Fee info
813
+ */
814
+ async function getFees() {
815
+ const client = new AetherClient();
816
+ return client.getFees();
817
+ }
818
+
819
+ /**
820
+ * Get validators list (uses default RPC)
821
+ * @returns {Promise<Array>} List of validators
822
+ */
823
+ async function getValidators() {
824
+ const client = new AetherClient();
825
+ return client.getValidators();
826
+ }
827
+
828
+ /**
829
+ * Get peers list (uses default RPC)
830
+ * @returns {Promise<Array>} List of peers
831
+ */
832
+ async function getPeers() {
833
+ const client = new AetherClient();
834
+ return client.getClusterPeers();
835
+ }
836
+
837
+ /**
838
+ * Get slot production stats (uses default RPC)
839
+ * @returns {Promise<Object>} Slot production stats
840
+ */
841
+ async function getSlotProduction() {
842
+ const client = new AetherClient();
843
+ return client.getSlotProduction();
844
+ }
845
+
846
+ /**
847
+ * Get account info (uses default RPC)
848
+ * @param {string} address - Account address
849
+ * @returns {Promise<Object>} Account info
850
+ */
851
+ async function getAccount(address) {
852
+ const client = new AetherClient();
853
+ return client.getAccount(address);
854
+ }
855
+
856
+ /**
857
+ * Get stake positions (uses default RPC)
858
+ * @param {string} address - Account address
859
+ * @returns {Promise<Array>} Stake positions
860
+ */
861
+ async function getStakePositions(address) {
862
+ const client = new AetherClient();
863
+ return client.getStakePositions(address);
864
+ }
865
+
866
+ /**
867
+ * Get rewards info (uses default RPC)
868
+ * @param {string} address - Account address
869
+ * @returns {Promise<Object>} Rewards info
870
+ */
871
+ async function getRewards(address) {
872
+ const client = new AetherClient();
873
+ return client.getRewards(address);
874
+ }
875
+
876
+ /**
877
+ * Get transaction by signature (uses default RPC)
878
+ * @param {string} signature - Transaction signature
879
+ * @returns {Promise<Object>} Transaction info
880
+ */
881
+ async function getTransaction(signature) {
882
+ const client = new AetherClient();
883
+ return client.getTransaction(signature);
884
+ }
885
+
886
+ /**
887
+ * Get recent transactions (uses default RPC)
888
+ * @param {string} address - Account address
889
+ * @param {number} limit - Max transactions
890
+ * @returns {Promise<Array>} Recent transactions
891
+ */
892
+ async function getRecentTransactions(address, limit = 20) {
893
+ const client = new AetherClient();
894
+ return client.getRecentTransactions(address, limit);
895
+ }
896
+
897
+ /**
898
+ * Get transaction history with signatures for an address (uses default RPC)
899
+ * @param {string} address - Account address
900
+ * @param {number} limit - Max transactions
901
+ * @returns {Promise<Object>} Transaction history with signatures and details
902
+ */
903
+ async function getTransactionHistory(address, limit = 20) {
904
+ const client = new AetherClient();
905
+ return client.getTransactionHistory(address, limit);
906
+ }
907
+
908
+ /**
909
+ * Get all SPL token accounts for a wallet (uses default RPC)
910
+ * @param {string} address - Account address
911
+ * @returns {Promise<Array>} Token accounts with mint, amount, decimals
912
+ */
913
+ async function getTokenAccounts(address) {
914
+ const client = new AetherClient();
915
+ return client.getTokenAccounts(address);
916
+ }
917
+
918
+ /**
919
+ * Get all stake accounts for a wallet (uses default RPC)
920
+ * @param {string} address - Account address
921
+ * @returns {Promise<Array>} Stake accounts list
922
+ */
923
+ async function getStakeAccounts(address) {
924
+ const client = new AetherClient();
925
+ return client.getStakeAccounts(address);
926
+ }
927
+
928
+ /**
929
+ * Get validator APY (uses default RPC)
930
+ * @param {string} validatorAddr - Validator address
931
+ * @returns {Promise<Object>} APY info
932
+ */
933
+ async function getValidatorAPY(validatorAddr) {
934
+ const client = new AetherClient();
935
+ return client.getValidatorAPY(validatorAddr);
936
+ }
937
+
938
+ /**
939
+ * Send transaction (uses default RPC)
940
+ * @param {Object} tx - Signed transaction
941
+ * @returns {Promise<Object>} Transaction receipt
942
+ */
943
+ async function sendTransaction(tx) {
944
+ const client = new AetherClient();
945
+ return client.sendTransaction(tx);
946
+ }
947
+
948
+ /**
949
+ * Ping RPC endpoint
950
+ * @param {string} rpcUrl - RPC URL to ping
951
+ * @returns {Promise<Object>} Ping result with latency
952
+ */
953
+ async function ping(rpcUrl) {
954
+ const client = new AetherClient({ rpcUrl });
955
+ const start = Date.now();
956
+ try {
957
+ await client.getSlot();
958
+ return { ok: true, latency: Date.now() - start, rpc: rpcUrl || DEFAULT_RPC_URL };
959
+ } catch (err) {
960
+ return { ok: false, error: err.message, rpc: rpcUrl || DEFAULT_RPC_URL };
961
+ }
962
+ }
963
+
964
+ // Low-level RPC helpers (from rpc.js)
965
+ const { rpcGet, rpcPost } = require('./rpc');
966
+
967
+ // ============================================================
968
+ // Exports
969
+ // ============================================================
970
+
971
+ module.exports = {
972
+ // Main class
973
+ AetherClient,
974
+
975
+ // Factory function
976
+ createClient,
977
+
978
+ // Convenience functions (all chain queries)
979
+ getSlot,
980
+ getBlockHeight,
981
+ getEpoch,
982
+ getAccount,
983
+ getBalance,
984
+ getTransaction,
985
+ getRecentTransactions,
986
+ getTransactionHistory,
987
+ getTokenAccounts,
988
+ getStakeAccounts,
989
+ getValidators,
990
+ getTPS,
991
+ getSupply,
992
+ getSlotProduction,
993
+ getFees,
994
+ getStakePositions,
995
+ getRewards,
996
+ getValidatorAPY,
997
+ getPeers,
998
+ getHealth,
999
+
1000
+ // Transactions
1001
+ sendTransaction,
1002
+
1003
+ // Utilities
1004
+ ping,
1005
+
1006
+ // Low-level RPC
1007
+ rpcGet,
1008
+ rpcPost,
1009
+
1010
+ // Constants
1011
+ DEFAULT_RPC_URL,
1012
+ DEFAULT_TIMEOUT_MS,
1013
+ };