@jellylegsai/aether-cli 1.8.0 → 1.9.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 CHANGED
@@ -1,1639 +1,1639 @@
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
- * Features:
8
- * - Retry logic with exponential backoff
9
- * - Rate limiting with token bucket algorithm
10
- * - Enhanced error handling for network timeouts and RPC failures
11
- * - Circuit breaker for repeated failures
12
- *
13
- * Default RPC: http://127.0.0.1:8899 (configurable via constructor or AETHER_RPC env)
14
- */
15
-
16
- const http = require('http');
17
- const https = require('https');
18
- const { rpcGet, rpcPost } = require('./rpc');
19
-
20
- // Default configuration
21
- const DEFAULT_RPC_URL = 'http://127.0.0.1:8899';
22
- const DEFAULT_TIMEOUT_MS = 10000;
23
-
24
- // Retry configuration
25
- const DEFAULT_RETRY_ATTEMPTS = 3;
26
- const DEFAULT_RETRY_DELAY_MS = 1000;
27
- const DEFAULT_BACKOFF_MULTIPLIER = 2;
28
- const DEFAULT_MAX_RETRY_DELAY_MS = 30000;
29
-
30
- // Rate limiting configuration
31
- const DEFAULT_RATE_LIMIT_RPS = 10; // Requests per second
32
- const DEFAULT_RATE_LIMIT_BURST = 20; // Burst capacity
33
-
34
- // Circuit breaker configuration
35
- const DEFAULT_CIRCUIT_BREAKER_THRESHOLD = 5; // Failures before opening
36
- const DEFAULT_CIRCUIT_BREAKER_RESET_MS = 60000; // Reset after 60s
37
-
38
- /**
39
- * Custom error types for better error handling
40
- */
41
- class AetherSDKError extends Error {
42
- constructor(message, code, details = {}) {
43
- super(message);
44
- this.name = 'AetherSDKError';
45
- this.code = code;
46
- this.details = details;
47
- this.timestamp = new Date().toISOString();
48
- }
49
- }
50
-
51
- class NetworkTimeoutError extends AetherSDKError {
52
- constructor(message, details = {}) {
53
- super(message, 'NETWORK_TIMEOUT', details);
54
- this.name = 'NetworkTimeoutError';
55
- }
56
- }
57
-
58
- class RPCError extends AetherSDKError {
59
- constructor(message, details = {}) {
60
- super(message, 'RPC_ERROR', details);
61
- this.name = 'RPCError';
62
- }
63
- }
64
-
65
- class RateLimitError extends AetherSDKError {
66
- constructor(message, details = {}) {
67
- super(message, 'RATE_LIMIT', details);
68
- this.name = 'RateLimitError';
69
- }
70
- }
71
-
72
- class CircuitBreakerOpenError extends AetherSDKError {
73
- constructor(message, details = {}) {
74
- super(message, 'CIRCUIT_BREAKER_OPEN', details);
75
- this.name = 'CircuitBreakerOpenError';
76
- }
77
- }
78
-
79
- /**
80
- * Token bucket rate limiter
81
- */
82
- class TokenBucketRateLimiter {
83
- constructor(rps = DEFAULT_RATE_LIMIT_RPS, burst = DEFAULT_RATE_LIMIT_BURST) {
84
- this.rps = rps;
85
- this.burst = burst;
86
- this.tokens = burst;
87
- this.lastRefill = Date.now();
88
- this.queue = [];
89
- this.refillInterval = setInterval(() => this.refill(), 1000 / rps);
90
- }
91
-
92
- refill() {
93
- const now = Date.now();
94
- const timePassed = (now - this.lastRefill) / 1000;
95
- const tokensToAdd = timePassed * this.rps;
96
- this.tokens = Math.min(this.burst, this.tokens + tokensToAdd);
97
- this.lastRefill = now;
98
- this.processQueue();
99
- }
100
-
101
- processQueue() {
102
- while (this.queue.length > 0 && this.tokens >= 1) {
103
- const { resolve, reject, tokens } = this.queue.shift();
104
- if (this.tokens >= tokens) {
105
- this.tokens -= tokens;
106
- resolve();
107
- } else {
108
- this.queue.unshift({ resolve, reject, tokens });
109
- break;
110
- }
111
- }
112
- }
113
-
114
- async acquire(tokens = 1) {
115
- return new Promise((resolve, reject) => {
116
- if (this.tokens >= tokens) {
117
- this.tokens -= tokens;
118
- resolve();
119
- } else {
120
- this.queue.push({ resolve, reject, tokens });
121
- }
122
- });
123
- }
124
-
125
- destroy() {
126
- if (this.refillInterval) {
127
- clearInterval(this.refillInterval);
128
- this.refillInterval = null;
129
- }
130
- }
131
- }
132
-
133
- /**
134
- * Circuit breaker for handling repeated failures
135
- */
136
- class CircuitBreaker {
137
- constructor(threshold = DEFAULT_CIRCUIT_BREAKER_THRESHOLD, resetTimeoutMs = DEFAULT_CIRCUIT_BREAKER_RESET_MS) {
138
- this.threshold = threshold;
139
- this.resetTimeoutMs = resetTimeoutMs;
140
- this.failureCount = 0;
141
- this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
142
- this.nextAttempt = 0;
143
- }
144
-
145
- canExecute() {
146
- if (this.state === 'CLOSED') return true;
147
- if (this.state === 'OPEN') {
148
- if (Date.now() >= this.nextAttempt) {
149
- this.state = 'HALF_OPEN';
150
- return true;
151
- }
152
- return false;
153
- }
154
- return this.state === 'HALF_OPEN';
155
- }
156
-
157
- recordSuccess() {
158
- this.failureCount = 0;
159
- this.state = 'CLOSED';
160
- }
161
-
162
- recordFailure() {
163
- this.failureCount++;
164
- if (this.failureCount >= this.threshold) {
165
- this.state = 'OPEN';
166
- this.nextAttempt = Date.now() + this.resetTimeoutMs;
167
- }
168
- }
169
-
170
- getState() {
171
- return {
172
- state: this.state,
173
- failureCount: this.failureCount,
174
- nextAttempt: this.state === 'OPEN' ? this.nextAttempt : null,
175
- };
176
- }
177
- }
178
-
179
- /**
180
- * Aether SDK Client
181
- * Real blockchain interface layer - every method makes actual HTTP RPC calls
182
- *
183
- * Includes:
184
- * - Retry logic with exponential backoff
185
- * - Rate limiting
186
- * - Circuit breaker for resilience
187
- * - Enhanced error handling
188
- */
189
- class AetherClient {
190
- constructor(options = {}) {
191
- this.rpcUrl = options.rpcUrl || process.env.AETHER_RPC || DEFAULT_RPC_URL;
192
- this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
193
-
194
- // Retry configuration
195
- this.retryAttempts = options.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
196
- this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
197
- this.backoffMultiplier = options.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER;
198
- this.maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
199
-
200
- // Rate limiting
201
- this.rateLimiter = new TokenBucketRateLimiter(
202
- options.rateLimitRps ?? DEFAULT_RATE_LIMIT_RPS,
203
- options.rateLimitBurst ?? DEFAULT_RATE_LIMIT_BURST
204
- );
205
-
206
- // Circuit breaker
207
- this.circuitBreaker = new CircuitBreaker(
208
- options.circuitBreakerThreshold ?? DEFAULT_CIRCUIT_BREAKER_THRESHOLD,
209
- options.circuitBreakerResetMs ?? DEFAULT_CIRCUIT_BREAKER_RESET_MS
210
- );
211
-
212
- // Parse RPC URL
213
- const url = new URL(this.rpcUrl);
214
- this.protocol = url.protocol;
215
- this.hostname = url.hostname;
216
- this.port = url.port || (this.protocol === 'https:' ? 443 : 80);
217
-
218
- // Request stats
219
- this.stats = {
220
- totalRequests: 0,
221
- successfulRequests: 0,
222
- failedRequests: 0,
223
- retriedRequests: 0,
224
- rateLimitedRequests: 0,
225
- circuitBreakerBlocked: 0,
226
- };
227
- }
228
-
229
- /**
230
- * Calculate delay for exponential backoff with jitter
231
- */
232
- _calculateDelay(attempt) {
233
- const baseDelay = this.retryDelayMs * Math.pow(this.backoffMultiplier, attempt);
234
- const jitter = Math.random() * 100; // Add up to 100ms jitter
235
- const delay = Math.min(baseDelay + jitter, this.maxRetryDelayMs);
236
- return delay;
237
- }
238
-
239
- /**
240
- * Check if error is retryable
241
- */
242
- _isRetryableError(error) {
243
- if (!error) return false;
244
-
245
- // Network errors
246
- if (error.code === 'ECONNREFUSED') return true;
247
- if (error.code === 'ENOTFOUND') return true;
248
- if (error.code === 'ETIMEDOUT') return true;
249
- if (error.code === 'ECONNRESET') return true;
250
- if (error.code === 'EPIPE') return true;
251
-
252
- // Timeout errors
253
- if (error.message && error.message.includes('timeout')) return true;
254
-
255
- // HTTP 5xx errors (server errors)
256
- if (error.statusCode >= 500) return true;
257
- if (error.statusCode === 429) return true; // Rate limit - retry with backoff
258
-
259
- // RPC errors that might be transient
260
- if (error.message && (
261
- error.message.includes('rate limit') ||
262
- error.message.includes('rate_limit') ||
263
- error.message.includes('too many requests') ||
264
- error.message.includes('temporarily unavailable') ||
265
- error.message.includes('service unavailable')
266
- )) return true;
267
-
268
- return false;
269
- }
270
-
271
- /**
272
- * Execute function with retry logic and rate limiting
273
- */
274
- async _executeWithRetry(operation, operationName) {
275
- // Check circuit breaker
276
- if (!this.circuitBreaker.canExecute()) {
277
- this.stats.circuitBreakerBlocked++;
278
- const state = this.circuitBreaker.getState();
279
- const waitTime = Math.ceil((state.nextAttempt - Date.now()) / 1000);
280
- throw new CircuitBreakerOpenError(
281
- `Circuit breaker is OPEN. Too many failures. Retry in ${waitTime}s.`,
282
- { circuitBreakerState: state, operation: operationName }
283
- );
284
- }
285
-
286
- // Wait for rate limit token
287
- await this.rateLimiter.acquire();
288
-
289
- let lastError = null;
290
-
291
- for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
292
- this.stats.totalRequests++;
293
-
294
- try {
295
- const result = await operation();
296
- this.circuitBreaker.recordSuccess();
297
- this.stats.successfulRequests++;
298
- return result;
299
- } catch (error) {
300
- lastError = error;
301
-
302
- // Don't retry if it's not a retryable error
303
- if (!this._isRetryableError(error)) {
304
- this.circuitBreaker.recordFailure();
305
- this.stats.failedRequests++;
306
- break;
307
- }
308
-
309
- this.stats.retriedRequests++;
310
- this.circuitBreaker.recordFailure();
311
-
312
- // If this was the last attempt, throw the error
313
- if (attempt === this.retryAttempts - 1) {
314
- this.stats.failedRequests++;
315
- break;
316
- }
317
-
318
- // Calculate and apply backoff delay
319
- const delay = this._calculateDelay(attempt);
320
- await new Promise(resolve => setTimeout(resolve, delay));
321
- }
322
- }
323
-
324
- // All retries exhausted - classify and throw error
325
- throw this._classifyError(lastError, operationName);
326
- }
327
-
328
- /**
329
- * Classify error into specific error types
330
- */
331
- _classifyError(error, operationName) {
332
- if (!error) {
333
- return new AetherSDKError('Unknown error occurred', 'UNKNOWN_ERROR', { operation: operationName });
334
- }
335
-
336
- // Already classified
337
- if (error instanceof AetherSDKError) {
338
- return error;
339
- }
340
-
341
- // Timeout errors
342
- if (error.message && (
343
- error.message.includes('timeout') ||
344
- error.code === 'ETIMEDOUT'
345
- )) {
346
- return new NetworkTimeoutError(
347
- `Network timeout during ${operationName}: ${error.message}`,
348
- {
349
- originalError: error.message,
350
- code: error.code,
351
- operation: operationName,
352
- rpcUrl: this.rpcUrl,
353
- }
354
- );
355
- }
356
-
357
- // Connection errors
358
- if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
359
- return new AetherSDKError(
360
- `Cannot connect to RPC endpoint during ${operationName}: ${error.message}`,
361
- 'CONNECTION_ERROR',
362
- {
363
- originalError: error.message,
364
- code: error.code,
365
- operation: operationName,
366
- rpcUrl: this.rpcUrl,
367
- }
368
- );
369
- }
370
-
371
- // RPC-specific errors
372
- if (error.message && (
373
- error.message.includes('RPC') ||
374
- error.message.includes('rpc') ||
375
- error.statusCode
376
- )) {
377
- return new RPCError(
378
- `RPC error during ${operationName}: ${error.message}`,
379
- {
380
- originalError: error.message,
381
- code: error.code || error.statusCode,
382
- operation: operationName,
383
- rpcUrl: this.rpcUrl,
384
- }
385
- );
386
- }
387
-
388
- // Generic error
389
- return new AetherSDKError(
390
- `Error during ${operationName}: ${error.message}`,
391
- 'SDK_ERROR',
392
- {
393
- originalError: error.message,
394
- code: error.code,
395
- operation: operationName,
396
- rpcUrl: this.rpcUrl,
397
- }
398
- );
399
- }
400
-
401
- /**
402
- * Internal: Make HTTP GET request to RPC endpoint
403
- */
404
- _httpGet(path, timeoutMs = this.timeoutMs) {
405
- return new Promise((resolve, reject) => {
406
- const lib = this.protocol === 'https:' ? https : http;
407
- const req = lib.request({
408
- hostname: this.hostname,
409
- port: this.port,
410
- path: path,
411
- method: 'GET',
412
- timeout: timeoutMs,
413
- headers: { 'Content-Type': 'application/json' },
414
- }, (res) => {
415
- let data = '';
416
- res.on('data', (chunk) => data += chunk);
417
- res.on('end', () => {
418
- try {
419
- const parsed = JSON.parse(data);
420
- if (parsed.error) {
421
- const err = new Error(parsed.error.message || JSON.stringify(parsed.error));
422
- err.statusCode = res.statusCode;
423
- err.responseData = parsed;
424
- reject(err);
425
- } else {
426
- resolve(parsed);
427
- }
428
- } catch (e) {
429
- resolve({ raw: data });
430
- }
431
- });
432
- });
433
- req.on('error', reject);
434
- req.on('timeout', () => {
435
- req.destroy();
436
- const err = new Error(`Request timeout after ${timeoutMs}ms`);
437
- err.code = 'ETIMEDOUT';
438
- reject(err);
439
- });
440
- req.end();
441
- });
442
- }
443
-
444
- /**
445
- * Internal: Make HTTP POST request to RPC endpoint
446
- */
447
- _httpPost(path, body = {}, timeoutMs = this.timeoutMs) {
448
- return new Promise((resolve, reject) => {
449
- const lib = this.protocol === 'https:' ? https : http;
450
- const bodyStr = JSON.stringify(body);
451
- const req = lib.request({
452
- hostname: this.hostname,
453
- port: this.port,
454
- path: path,
455
- method: 'POST',
456
- timeout: timeoutMs,
457
- headers: {
458
- 'Content-Type': 'application/json',
459
- 'Content-Length': Buffer.byteLength(bodyStr),
460
- },
461
- }, (res) => {
462
- let data = '';
463
- res.on('data', (chunk) => data += chunk);
464
- res.on('end', () => {
465
- try {
466
- const parsed = JSON.parse(data);
467
- if (parsed.error) {
468
- const err = new Error(parsed.error.message || JSON.stringify(parsed.error));
469
- err.statusCode = res.statusCode;
470
- err.responseData = parsed;
471
- reject(err);
472
- } else {
473
- resolve(parsed);
474
- }
475
- } catch (e) {
476
- resolve({ raw: data });
477
- }
478
- });
479
- });
480
- req.on('error', reject);
481
- req.on('timeout', () => {
482
- req.destroy();
483
- const err = new Error(`Request timeout after ${timeoutMs}ms`);
484
- err.code = 'ETIMEDOUT';
485
- reject(err);
486
- });
487
- req.write(bodyStr);
488
- req.end();
489
- });
490
- }
491
-
492
- // ============================================================
493
- // Core RPC Methods - Real blockchain calls with retry & rate limiting
494
- // ============================================================
495
-
496
- /**
497
- * Get current slot number
498
- * RPC: GET /v1/slot
499
- *
500
- * @returns {Promise<number>} Current slot number
501
- */
502
- async getSlot() {
503
- return this._executeWithRetry(
504
- async () => {
505
- const result = await this._httpGet('/v1/slot');
506
- return result.slot !== undefined ? result.slot : result;
507
- },
508
- 'getSlot'
509
- );
510
- }
511
-
512
- /**
513
- * Get current block height
514
- * RPC: GET /v1/blockheight
515
- *
516
- * @returns {Promise<number>} Current block height
517
- */
518
- async getBlockHeight() {
519
- return this._executeWithRetry(
520
- async () => {
521
- const result = await this._httpGet('/v1/blockheight');
522
- return result.blockHeight !== undefined ? result.blockHeight : result;
523
- },
524
- 'getBlockHeight'
525
- );
526
- }
527
-
528
- /**
529
- * Get account info including balance
530
- * RPC: GET /v1/account/<address>
531
- *
532
- * @param {string} address - Account public key (base58)
533
- * @returns {Promise<Object>} Account info: { lamports, owner, data, rent_epoch }
534
- */
535
- async getAccountInfo(address) {
536
- if (!address) {
537
- throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
538
- }
539
- return this._executeWithRetry(
540
- async () => {
541
- const result = await this._httpGet(`/v1/account/${address}`);
542
- return result;
543
- },
544
- 'getAccountInfo'
545
- );
546
- }
547
-
548
- /**
549
- * Alias for getAccountInfo
550
- * @param {string} address - Account address
551
- * @returns {Promise<Object>} Account info
552
- */
553
- async getAccount(address) {
554
- return this.getAccountInfo(address);
555
- }
556
-
557
- /**
558
- * Get balance in lamports
559
- * RPC: GET /v1/account/<address>
560
- *
561
- * @param {string} address - Account public key (base58)
562
- * @returns {Promise<number>} Balance in lamports
563
- */
564
- async getBalance(address) {
565
- const account = await this.getAccountInfo(address);
566
- return account.lamports !== undefined ? account.lamports : 0;
567
- }
568
-
569
- /**
570
- * Get epoch info
571
- * RPC: GET /v1/epoch
572
- *
573
- * @returns {Promise<Object>} Epoch info: { epoch, slotIndex, slotsInEpoch, absoluteSlot }
574
- */
575
- async getEpochInfo() {
576
- return this._executeWithRetry(
577
- async () => {
578
- const result = await this._httpGet('/v1/epoch');
579
- return result;
580
- },
581
- 'getEpochInfo'
582
- );
583
- }
584
-
585
- /**
586
- * Get transaction by signature
587
- * RPC: GET /v1/transaction/<signature>
588
- *
589
- * @param {string} signature - Transaction signature (base58)
590
- * @returns {Promise<Object>} Transaction details
591
- */
592
- async getTransaction(signature) {
593
- if (!signature) {
594
- throw new AetherSDKError('Transaction signature is required', 'VALIDATION_ERROR');
595
- }
596
- return this._executeWithRetry(
597
- async () => {
598
- const result = await this._httpGet(`/v1/transaction/${signature}`);
599
- return result;
600
- },
601
- 'getTransaction'
602
- );
603
- }
604
-
605
- /**
606
- * Submit a signed transaction
607
- * RPC: POST /v1/transaction
608
- *
609
- * @param {Object} tx - Signed transaction object
610
- * @param {string} tx.signature - Transaction signature (base58)
611
- * @param {string} tx.signer - Signer public key (base58)
612
- * @param {string} tx.tx_type - Transaction type
613
- * @param {Object} tx.payload - Transaction payload
614
- * @returns {Promise<Object>} Transaction receipt: { signature, slot, confirmed }
615
- */
616
- async sendTransaction(tx) {
617
- if (!tx || !tx.signature) {
618
- throw new AetherSDKError('Transaction with signature is required', 'VALIDATION_ERROR');
619
- }
620
- return this._executeWithRetry(
621
- async () => {
622
- const result = await this._httpPost('/v1/transaction', tx);
623
- return result;
624
- },
625
- 'sendTransaction'
626
- );
627
- }
628
-
629
- /**
630
- * Get recent blockhash for transaction signing
631
- * RPC: GET /v1/recent-blockhash
632
- *
633
- * @returns {Promise<Object>} { blockhash, lastValidBlockHeight }
634
- */
635
- async getRecentBlockhash() {
636
- return this._executeWithRetry(
637
- async () => {
638
- const result = await this._httpGet('/v1/recent-blockhash');
639
- return result;
640
- },
641
- 'getRecentBlockhash'
642
- );
643
- }
644
-
645
- /**
646
- * Get network peers
647
- * RPC: GET /v1/peers
648
- *
649
- * @returns {Promise<Array>} List of peer node addresses
650
- */
651
- async getClusterPeers() {
652
- return this._executeWithRetry(
653
- async () => {
654
- const result = await this._httpGet('/v1/peers');
655
- return Array.isArray(result) ? result : (result.peers || []);
656
- },
657
- 'getClusterPeers'
658
- );
659
- }
660
-
661
- /**
662
- * Get validator info
663
- * RPC: GET /v1/validators
664
- *
665
- * @returns {Promise<Array>} List of validators with stake, commission, etc.
666
- */
667
- async getValidators() {
668
- return this._executeWithRetry(
669
- async () => {
670
- const result = await this._httpGet('/v1/validators');
671
- return Array.isArray(result) ? result : (result.validators || []);
672
- },
673
- 'getValidators'
674
- );
675
- }
676
-
677
- /**
678
- * Get supply info
679
- * RPC: GET /v1/supply
680
- *
681
- * @returns {Promise<Object>} Supply info: { total, circulating, nonCirculating }
682
- */
683
- async getSupply() {
684
- return this._executeWithRetry(
685
- async () => {
686
- const result = await this._httpGet('/v1/supply');
687
- return result;
688
- },
689
- 'getSupply'
690
- );
691
- }
692
-
693
- /**
694
- * Get health status
695
- * RPC: GET /v1/health
696
- *
697
- * @returns {Promise<string>} 'ok' if node is healthy
698
- */
699
- async getHealth() {
700
- return this._executeWithRetry(
701
- async () => {
702
- const result = await this._httpGet('/v1/health');
703
- return result.status || result;
704
- },
705
- 'getHealth'
706
- );
707
- }
708
-
709
- /**
710
- * Get version info
711
- * RPC: GET /v1/version
712
- *
713
- * @returns {Promise<Object>} Version info: { aetherCore, featureSet }
714
- */
715
- async getVersion() {
716
- return this._executeWithRetry(
717
- async () => {
718
- const result = await this._httpGet('/v1/version');
719
- return result;
720
- },
721
- 'getVersion'
722
- );
723
- }
724
-
725
- /**
726
- * Get TPS (transactions per second)
727
- * RPC: GET /v1/tps
728
- *
729
- * @returns {Promise<number>} Current TPS
730
- */
731
- async getTPS() {
732
- return this._executeWithRetry(
733
- async () => {
734
- const result = await this._httpGet('/v1/tps');
735
- return result.tps ?? result.tps_avg ?? result.transactions_per_second ?? null;
736
- },
737
- 'getTPS'
738
- );
739
- }
740
-
741
- /**
742
- * Get fee estimates
743
- * RPC: GET /v1/fees
744
- *
745
- * @returns {Promise<Object>} Fee info
746
- */
747
- async getFees() {
748
- return this._executeWithRetry(
749
- async () => {
750
- const result = await this._httpGet('/v1/fees');
751
- return result;
752
- },
753
- 'getFees'
754
- );
755
- }
756
-
757
- /**
758
- * Get slot production stats
759
- * RPC: POST /v1/slot_production
760
- *
761
- * @returns {Promise<Object>} Slot production stats
762
- */
763
- async getSlotProduction() {
764
- return this._executeWithRetry(
765
- async () => {
766
- const result = await this._httpPost('/v1/slot_production', {});
767
- return result;
768
- },
769
- 'getSlotProduction'
770
- );
771
- }
772
-
773
- /**
774
- * Get stake positions for an address
775
- * RPC: GET /v1/stake/<address>
776
- *
777
- * @param {string} address - Account address
778
- * @returns {Promise<Array>} List of stake positions
779
- */
780
- async getStakePositions(address) {
781
- if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
782
- return this._executeWithRetry(
783
- async () => {
784
- const result = await this._httpGet(`/v1/stake/${address}`);
785
- return result.delegations ?? result.stakes ?? result ?? [];
786
- },
787
- 'getStakePositions'
788
- );
789
- }
790
-
791
- /**
792
- * Get rewards for an address
793
- * RPC: GET /v1/rewards/<address>
794
- *
795
- * @param {string} address - Account address
796
- * @returns {Promise<Object>} Rewards info
797
- */
798
- async getRewards(address) {
799
- if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
800
- return this._executeWithRetry(
801
- async () => {
802
- const result = await this._httpGet(`/v1/rewards/${address}`);
803
- return result;
804
- },
805
- 'getRewards'
806
- );
807
- }
808
-
809
- /**
810
- * Get validator APY
811
- * RPC: GET /v1/validator/<address>/apy
812
- *
813
- * @param {string} validatorAddr - Validator address
814
- * @returns {Promise<Object>} APY info
815
- */
816
- async getValidatorAPY(validatorAddr) {
817
- if (!validatorAddr) throw new AetherSDKError('Validator address is required', 'VALIDATION_ERROR');
818
- return this._executeWithRetry(
819
- async () => {
820
- const result = await this._httpGet(`/v1/validator/${validatorAddr}/apy`);
821
- return result;
822
- },
823
- 'getValidatorAPY'
824
- );
825
- }
826
-
827
- /**
828
- * Get recent transactions for an address
829
- * RPC: GET /v1/transactions/<address>?limit=<n>
830
- *
831
- * @param {string} address - Account address
832
- * @param {number} limit - Max transactions to return
833
- * @returns {Promise<Array>} List of recent transactions
834
- */
835
- async getRecentTransactions(address, limit = 20) {
836
- if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
837
- return this._executeWithRetry(
838
- async () => {
839
- const result = await this._httpGet(`/v1/transactions/${address}?limit=${limit}`);
840
- return result.transactions ?? result ?? [];
841
- },
842
- 'getRecentTransactions'
843
- );
844
- }
845
-
846
- /**
847
- * Get transaction history with signatures for an address
848
- * RPC: POST /v1/transactions/history (or GET /v1/transactions/<address>?limit=<n>)
849
- *
850
- * @param {string} address - Account address
851
- * @param {number} limit - Max transactions to return
852
- * @returns {Promise<Object>} Transaction history with signatures and details
853
- */
854
- async getTransactionHistory(address, limit = 20) {
855
- if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
856
-
857
- // First get signatures
858
- const sigResult = await this._executeWithRetry(
859
- async () => {
860
- const result = await this._httpPost('/v1/transactions/history', { address, limit });
861
- if (result.error) {
862
- throw new RPCError(result.error.message || result.error, { result });
863
- }
864
- return result;
865
- },
866
- 'getTransactionHistory.signatures'
867
- );
868
-
869
- const signatures = sigResult.signatures || sigResult.result || [];
870
-
871
- // Fetch full transaction details for each signature (up to 10 at a time)
872
- const BATCH = 10;
873
- const txs = [];
874
- for (let i = 0; i < signatures.length; i += BATCH) {
875
- const batch = signatures.slice(i, i + BATCH);
876
- const batchPromises = batch.map(sig =>
877
- this.getTransaction(sig.signature || sig).catch(() => null)
878
- );
879
- const batchResults = await Promise.all(batchPromises);
880
- txs.push(...batchResults.filter(Boolean));
881
- }
882
-
883
- return {
884
- signatures: signatures,
885
- transactions: txs,
886
- address: address,
887
- };
888
- }
889
-
890
- /**
891
- * Get all SPL token accounts for a wallet address
892
- * RPC: GET /v1/tokens/<address>
893
- *
894
- * @param {string} address - Account public key (base58)
895
- * @returns {Promise<Array>} List of token accounts with mint, amount, decimals
896
- */
897
- async getTokenAccounts(address) {
898
- if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
899
- return this._executeWithRetry(
900
- async () => {
901
- const result = await this._httpGet(`/v1/tokens/${address}`);
902
- return result.tokens ?? result.accounts ?? result ?? [];
903
- },
904
- 'getTokenAccounts'
905
- );
906
- }
907
-
908
- /**
909
- * Get all stake accounts for a wallet address
910
- * RPC: GET /v1/stake-accounts/<address>
911
- *
912
- * @param {string} address - Account public key (base58)
913
- * @returns {Promise<Array>} List of stake accounts
914
- */
915
- async getStakeAccounts(address) {
916
- if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
917
- return this._executeWithRetry(
918
- async () => {
919
- const result = await this._httpGet(`/v1/stake-accounts/${address}`);
920
- return result.stake_accounts ?? result.delegations ?? result ?? [];
921
- },
922
- 'getStakeAccounts'
923
- );
924
- }
925
-
926
- // ============================================================
927
- // Transaction Helpers - Build and send real transactions
928
- // ============================================================
929
-
930
- /**
931
- * Build and send a transfer transaction
932
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
933
- *
934
- * @param {Object} params
935
- * @param {string} params.from - Sender address (base58)
936
- * @param {string} params.to - Recipient address (base58)
937
- * @param {number} params.amount - Amount in lamports
938
- * @param {number} params.nonce - Nonce for replay protection
939
- * @param {Function} params.signFn - Function to sign the transaction (receives tx object, returns signature)
940
- * @returns {Promise<Object>} Transaction receipt
941
- */
942
- async transfer({ from, to, amount, nonce, signFn }) {
943
- if (!from || !to || !amount === undefined || nonce === undefined) {
944
- throw new AetherSDKError('from, to, amount, and nonce are required', 'VALIDATION_ERROR');
945
- }
946
- if (!signFn || typeof signFn !== 'function') {
947
- throw new AetherSDKError('signFn is required (function to sign the transaction)', 'VALIDATION_ERROR');
948
- }
949
-
950
- // Get recent blockhash (real RPC call)
951
- const { blockhash } = await this.getRecentBlockhash();
952
-
953
- // Build transaction payload
954
- const tx = {
955
- signature: '', // Will be filled after signing
956
- signer: from,
957
- tx_type: 'Transfer',
958
- payload: {
959
- recipient: to,
960
- amount: BigInt(amount),
961
- nonce: BigInt(nonce),
962
- },
963
- fee: 5000, // 5000 lamports fee
964
- slot: await this.getSlot(),
965
- timestamp: Date.now(),
966
- };
967
-
968
- // Sign transaction (user provides signing function)
969
- const signature = await signFn(tx, blockhash);
970
- tx.signature = signature;
971
-
972
- // Send to blockchain (real RPC call)
973
- const receipt = await this.sendTransaction(tx);
974
- return receipt;
975
- }
976
-
977
- /**
978
- * Build and send a stake delegation transaction
979
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
980
- *
981
- * @param {Object} params
982
- * @param {string} params.staker - Staker address (base58)
983
- * @param {string} params.validator - Validator address (base58)
984
- * @param {number} params.amount - Amount to stake in lamports
985
- * @param {Function} params.signFn - Function to sign the transaction
986
- * @returns {Promise<Object>} Transaction receipt
987
- */
988
- async stake({ staker, validator, amount, signFn }) {
989
- if (!staker || !validator || !amount === undefined) {
990
- throw new AetherSDKError('staker, validator, and amount are required', 'VALIDATION_ERROR');
991
- }
992
- if (!signFn || typeof signFn !== 'function') {
993
- throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
994
- }
995
-
996
- const { blockhash } = await this.getRecentBlockhash();
997
-
998
- const tx = {
999
- signature: '',
1000
- signer: staker,
1001
- tx_type: 'Stake',
1002
- payload: {
1003
- validator: validator,
1004
- amount: BigInt(amount),
1005
- },
1006
- fee: 5000,
1007
- slot: await this.getSlot(),
1008
- timestamp: Date.now(),
1009
- };
1010
-
1011
- const signature = await signFn(tx, blockhash);
1012
- tx.signature = signature;
1013
-
1014
- const receipt = await this.sendTransaction(tx);
1015
- return receipt;
1016
- }
1017
-
1018
- /**
1019
- * Build and send an unstake (withdraw) transaction
1020
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1021
- *
1022
- * @param {Object} params
1023
- * @param {string} params.stakeAccount - Stake account address (base58)
1024
- * @param {number} params.amount - Amount to unstake in lamports
1025
- * @param {Function} params.signFn - Function to sign the transaction
1026
- * @returns {Promise<Object>} Transaction receipt
1027
- */
1028
- async unstake({ stakeAccount, amount, signFn }) {
1029
- if (!stakeAccount || !amount === undefined) {
1030
- throw new AetherSDKError('stakeAccount and amount are required', 'VALIDATION_ERROR');
1031
- }
1032
- if (!signFn || typeof signFn !== 'function') {
1033
- throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1034
- }
1035
-
1036
- const { blockhash } = await this.getRecentBlockhash();
1037
-
1038
- const tx = {
1039
- signature: '',
1040
- signer: stakeAccount,
1041
- tx_type: 'Unstake',
1042
- payload: {
1043
- stake_account: stakeAccount,
1044
- amount: BigInt(amount),
1045
- },
1046
- fee: 5000,
1047
- slot: await this.getSlot(),
1048
- timestamp: Date.now(),
1049
- };
1050
-
1051
- const signature = await signFn(tx, blockhash);
1052
- tx.signature = signature;
1053
-
1054
- const receipt = await this.sendTransaction(tx);
1055
- return receipt;
1056
- }
1057
-
1058
- /**
1059
- * Build and send a claim rewards transaction
1060
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1061
- *
1062
- * @param {Object} params
1063
- * @param {string} params.stakeAccount - Stake account address (base58)
1064
- * @param {Function} params.signFn - Function to sign the transaction
1065
- * @returns {Promise<Object>} Transaction receipt
1066
- */
1067
- async claimRewards({ stakeAccount, signFn }) {
1068
- if (!stakeAccount) {
1069
- throw new AetherSDKError('stakeAccount is required', 'VALIDATION_ERROR');
1070
- }
1071
- if (!signFn || typeof signFn !== 'function') {
1072
- throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1073
- }
1074
-
1075
- const { blockhash } = await this.getRecentBlockhash();
1076
-
1077
- const tx = {
1078
- signature: '',
1079
- signer: stakeAccount,
1080
- tx_type: 'ClaimRewards',
1081
- payload: {
1082
- stake_account: stakeAccount,
1083
- },
1084
- fee: 5000,
1085
- slot: await this.getSlot(),
1086
- timestamp: Date.now(),
1087
- };
1088
-
1089
- const signature = await signFn(tx, blockhash);
1090
- tx.signature = signature;
1091
-
1092
- const receipt = await this.sendTransaction(tx);
1093
- return receipt;
1094
- }
1095
-
1096
- // ============================================================
1097
- // NFT Methods - Real blockchain calls for NFT operations
1098
- // ============================================================
1099
-
1100
- /**
1101
- * Create a new NFT
1102
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1103
- *
1104
- * @param {Object} params
1105
- * @param {string} params.creator - Creator address (base58)
1106
- * @param {string} params.metadataUrl - URL to NFT metadata (JSON)
1107
- * @param {number} params.royalties - Royalty basis points (e.g., 500 = 5%)
1108
- * @param {Function} params.signFn - Function to sign the transaction
1109
- * @returns {Promise<Object>} Transaction receipt with NFT ID
1110
- */
1111
- async createNFT({ creator, metadataUrl, royalties, signFn }) {
1112
- if (!creator || !metadataUrl || royalties === undefined) {
1113
- throw new AetherSDKError('creator, metadataUrl, and royalties are required', 'VALIDATION_ERROR');
1114
- }
1115
- if (!signFn || typeof signFn !== 'function') {
1116
- throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1117
- }
1118
-
1119
- const { blockhash } = await this.getRecentBlockhash();
1120
-
1121
- const tx = {
1122
- signature: '',
1123
- signer: creator,
1124
- tx_type: 'CreateNFT',
1125
- payload: {
1126
- metadata_url: metadataUrl,
1127
- royalties: royalties,
1128
- },
1129
- fee: 10000, // Higher fee for NFT creation
1130
- slot: await this.getSlot(),
1131
- timestamp: Date.now(),
1132
- };
1133
-
1134
- const signature = await signFn(tx, blockhash);
1135
- tx.signature = signature;
1136
-
1137
- const receipt = await this.sendTransaction(tx);
1138
- return receipt;
1139
- }
1140
-
1141
- /**
1142
- * Transfer an NFT to another address
1143
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1144
- *
1145
- * @param {Object} params
1146
- * @param {string} params.from - Current owner address (base58)
1147
- * @param {string} params.nftId - NFT ID
1148
- * @param {string} params.to - Recipient address (base58)
1149
- * @param {Function} params.signFn - Function to sign the transaction
1150
- * @returns {Promise<Object>} Transaction receipt
1151
- */
1152
- async transferNFT({ from, nftId, to, signFn }) {
1153
- if (!from || !nftId || !to) {
1154
- throw new AetherSDKError('from, nftId, and to are required', 'VALIDATION_ERROR');
1155
- }
1156
- if (!signFn || typeof signFn !== 'function') {
1157
- throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1158
- }
1159
-
1160
- const { blockhash } = await this.getRecentBlockhash();
1161
-
1162
- const tx = {
1163
- signature: '',
1164
- signer: from,
1165
- tx_type: 'TransferNFT',
1166
- payload: {
1167
- nft_id: nftId,
1168
- recipient: to,
1169
- },
1170
- fee: 5000,
1171
- slot: await this.getSlot(),
1172
- timestamp: Date.now(),
1173
- };
1174
-
1175
- const signature = await signFn(tx, blockhash);
1176
- tx.signature = signature;
1177
-
1178
- const receipt = await this.sendTransaction(tx);
1179
- return receipt;
1180
- }
1181
-
1182
- /**
1183
- * Update NFT metadata URL
1184
- * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1185
- *
1186
- * @param {Object} params
1187
- * @param {string} params.creator - NFT creator/owner address (base58)
1188
- * @param {string} params.nftId - NFT ID
1189
- * @param {string} params.metadataUrl - New metadata URL
1190
- * @param {Function} params.signFn - Function to sign the transaction
1191
- * @returns {Promise<Object>} Transaction receipt
1192
- */
1193
- async updateMetadata({ creator, nftId, metadataUrl, signFn }) {
1194
- if (!creator || !nftId || !metadataUrl) {
1195
- throw new AetherSDKError('creator, nftId, and metadataUrl are required', 'VALIDATION_ERROR');
1196
- }
1197
- if (!signFn || typeof signFn !== 'function') {
1198
- throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1199
- }
1200
-
1201
- const { blockhash } = await this.getRecentBlockhash();
1202
-
1203
- const tx = {
1204
- signature: '',
1205
- signer: creator,
1206
- tx_type: 'UpdateMetadata',
1207
- payload: {
1208
- nft_id: nftId,
1209
- metadata_url: metadataUrl,
1210
- },
1211
- fee: 5000,
1212
- slot: await this.getSlot(),
1213
- timestamp: Date.now(),
1214
- };
1215
-
1216
- const signature = await signFn(tx, blockhash);
1217
- tx.signature = signature;
1218
-
1219
- const receipt = await this.sendTransaction(tx);
1220
- return receipt;
1221
- }
1222
-
1223
- // ============================================================
1224
- // Utilities
1225
- // ============================================================
1226
-
1227
- /**
1228
- * Get client statistics
1229
- * @returns {Object} Request statistics
1230
- */
1231
- getStats() {
1232
- return {
1233
- ...this.stats,
1234
- circuitBreaker: this.circuitBreaker.getState(),
1235
- rateLimiter: {
1236
- rps: this.rateLimiter.rps,
1237
- burst: this.rateLimiter.burst,
1238
- tokens: this.rateLimiter.tokens,
1239
- },
1240
- };
1241
- }
1242
-
1243
- /**
1244
- * Reset circuit breaker
1245
- */
1246
- resetCircuitBreaker() {
1247
- this.circuitBreaker = new CircuitBreaker(
1248
- this.circuitBreaker.threshold,
1249
- this.circuitBreaker.resetTimeoutMs
1250
- );
1251
- }
1252
-
1253
- /**
1254
- * Close the client and cleanup resources
1255
- */
1256
- destroy() {
1257
- this.rateLimiter.destroy();
1258
- }
1259
- }
1260
-
1261
- // ============================================================
1262
- // Convenience Functions (for quick one-off calls)
1263
- // ============================================================
1264
-
1265
- /**
1266
- * Create a new AetherClient instance
1267
- * @param {Object} options - Client options
1268
- * @returns {AetherClient}
1269
- */
1270
- function createClient(options = {}) {
1271
- return new AetherClient(options);
1272
- }
1273
-
1274
- /**
1275
- * Quick slot check (uses default RPC)
1276
- * @returns {Promise<number>} Current slot
1277
- */
1278
- async function getSlot() {
1279
- const client = new AetherClient();
1280
- try {
1281
- return await client.getSlot();
1282
- } finally {
1283
- client.destroy();
1284
- }
1285
- }
1286
-
1287
- /**
1288
- * Quick balance check (uses default RPC)
1289
- * @param {string} address - Account address
1290
- * @returns {Promise<number>} Balance in lamports
1291
- */
1292
- async function getBalance(address) {
1293
- const client = new AetherClient();
1294
- try {
1295
- return await client.getBalance(address);
1296
- } finally {
1297
- client.destroy();
1298
- }
1299
- }
1300
-
1301
- /**
1302
- * Quick health check (uses default RPC)
1303
- * @returns {Promise<string>} 'ok' if healthy
1304
- */
1305
- async function getHealth() {
1306
- const client = new AetherClient();
1307
- try {
1308
- return await client.getHealth();
1309
- } finally {
1310
- client.destroy();
1311
- }
1312
- }
1313
-
1314
- /**
1315
- * Get current block height (uses default RPC)
1316
- * @returns {Promise<number>} Block height
1317
- */
1318
- async function getBlockHeight() {
1319
- const client = new AetherClient();
1320
- try {
1321
- return await client.getBlockHeight();
1322
- } finally {
1323
- client.destroy();
1324
- }
1325
- }
1326
-
1327
- /**
1328
- * Get epoch info (uses default RPC)
1329
- * @returns {Promise<Object>} Epoch info
1330
- */
1331
- async function getEpoch() {
1332
- const client = new AetherClient();
1333
- try {
1334
- return await client.getEpochInfo();
1335
- } finally {
1336
- client.destroy();
1337
- }
1338
- }
1339
-
1340
- /**
1341
- * Get TPS (uses default RPC)
1342
- * @returns {Promise<number>} Transactions per second
1343
- */
1344
- async function getTPS() {
1345
- const client = new AetherClient();
1346
- try {
1347
- return await client.getTPS();
1348
- } finally {
1349
- client.destroy();
1350
- }
1351
- }
1352
-
1353
- /**
1354
- * Get supply info (uses default RPC)
1355
- * @returns {Promise<Object>} Supply info
1356
- */
1357
- async function getSupply() {
1358
- const client = new AetherClient();
1359
- try {
1360
- return await client.getSupply();
1361
- } finally {
1362
- client.destroy();
1363
- }
1364
- }
1365
-
1366
- /**
1367
- * Get fees info (uses default RPC)
1368
- * @returns {Promise<Object>} Fee info
1369
- */
1370
- async function getFees() {
1371
- const client = new AetherClient();
1372
- try {
1373
- return await client.getFees();
1374
- } finally {
1375
- client.destroy();
1376
- }
1377
- }
1378
-
1379
- /**
1380
- * Get validators list (uses default RPC)
1381
- * @returns {Promise<Array>} List of validators
1382
- */
1383
- async function getValidators() {
1384
- const client = new AetherClient();
1385
- try {
1386
- return await client.getValidators();
1387
- } finally {
1388
- client.destroy();
1389
- }
1390
- }
1391
-
1392
- /**
1393
- * Get peers list (uses default RPC)
1394
- * @returns {Promise<Array>} List of peers
1395
- */
1396
- async function getPeers() {
1397
- const client = new AetherClient();
1398
- try {
1399
- return await client.getClusterPeers();
1400
- } finally {
1401
- client.destroy();
1402
- }
1403
- }
1404
-
1405
- /**
1406
- * Get slot production stats (uses default RPC)
1407
- * @returns {Promise<Object>} Slot production stats
1408
- */
1409
- async function getSlotProduction() {
1410
- const client = new AetherClient();
1411
- try {
1412
- return await client.getSlotProduction();
1413
- } finally {
1414
- client.destroy();
1415
- }
1416
- }
1417
-
1418
- /**
1419
- * Get account info (uses default RPC)
1420
- * @param {string} address - Account address
1421
- * @returns {Promise<Object>} Account info
1422
- */
1423
- async function getAccount(address) {
1424
- const client = new AetherClient();
1425
- try {
1426
- return await client.getAccount(address);
1427
- } finally {
1428
- client.destroy();
1429
- }
1430
- }
1431
-
1432
- /**
1433
- * Get stake positions (uses default RPC)
1434
- * @param {string} address - Account address
1435
- * @returns {Promise<Array>} Stake positions
1436
- */
1437
- async function getStakePositions(address) {
1438
- const client = new AetherClient();
1439
- try {
1440
- return await client.getStakePositions(address);
1441
- } finally {
1442
- client.destroy();
1443
- }
1444
- }
1445
-
1446
- /**
1447
- * Get rewards info (uses default RPC)
1448
- * @param {string} address - Account address
1449
- * @returns {Promise<Object>} Rewards info
1450
- */
1451
- async function getRewards(address) {
1452
- const client = new AetherClient();
1453
- try {
1454
- return await client.getRewards(address);
1455
- } finally {
1456
- client.destroy();
1457
- }
1458
- }
1459
-
1460
- /**
1461
- * Get transaction by signature (uses default RPC)
1462
- * @param {string} signature - Transaction signature
1463
- * @returns {Promise<Object>} Transaction info
1464
- */
1465
- async function getTransaction(signature) {
1466
- const client = new AetherClient();
1467
- try {
1468
- return await client.getTransaction(signature);
1469
- } finally {
1470
- client.destroy();
1471
- }
1472
- }
1473
-
1474
- /**
1475
- * Get recent transactions (uses default RPC)
1476
- * @param {string} address - Account address
1477
- * @param {number} limit - Max transactions
1478
- * @returns {Promise<Array>} Recent transactions
1479
- */
1480
- async function getRecentTransactions(address, limit = 20) {
1481
- const client = new AetherClient();
1482
- try {
1483
- return await client.getRecentTransactions(address, limit);
1484
- } finally {
1485
- client.destroy();
1486
- }
1487
- }
1488
-
1489
- /**
1490
- * Get transaction history with signatures for an address (uses default RPC)
1491
- * @param {string} address - Account address
1492
- * @param {number} limit - Max transactions
1493
- * @returns {Promise<Object>} Transaction history with signatures and details
1494
- */
1495
- async function getTransactionHistory(address, limit = 20) {
1496
- const client = new AetherClient();
1497
- try {
1498
- return await client.getTransactionHistory(address, limit);
1499
- } finally {
1500
- client.destroy();
1501
- }
1502
- }
1503
-
1504
- /**
1505
- * Get all SPL token accounts for a wallet (uses default RPC)
1506
- * @param {string} address - Account address
1507
- * @returns {Promise<Array>} Token accounts with mint, amount, decimals
1508
- */
1509
- async function getTokenAccounts(address) {
1510
- const client = new AetherClient();
1511
- try {
1512
- return await client.getTokenAccounts(address);
1513
- } finally {
1514
- client.destroy();
1515
- }
1516
- }
1517
-
1518
- /**
1519
- * Get all stake accounts for a wallet (uses default RPC)
1520
- * @param {string} address - Account address
1521
- * @returns {Promise<Array>} Stake accounts list
1522
- */
1523
- async function getStakeAccounts(address) {
1524
- const client = new AetherClient();
1525
- try {
1526
- return await client.getStakeAccounts(address);
1527
- } finally {
1528
- client.destroy();
1529
- }
1530
- }
1531
-
1532
- /**
1533
- * Get validator APY (uses default RPC)
1534
- * @param {string} validatorAddr - Validator address
1535
- * @returns {Promise<Object>} APY info
1536
- */
1537
- async function getValidatorAPY(validatorAddr) {
1538
- const client = new AetherClient();
1539
- try {
1540
- return await client.getValidatorAPY(validatorAddr);
1541
- } finally {
1542
- client.destroy();
1543
- }
1544
- }
1545
-
1546
- /**
1547
- * Send transaction (uses default RPC)
1548
- * @param {Object} tx - Signed transaction
1549
- * @returns {Promise<Object>} Transaction receipt
1550
- */
1551
- async function sendTransaction(tx) {
1552
- const client = new AetherClient();
1553
- try {
1554
- return await client.sendTransaction(tx);
1555
- } finally {
1556
- client.destroy();
1557
- }
1558
- }
1559
-
1560
- /**
1561
- * Ping RPC endpoint
1562
- * @param {string} rpcUrl - RPC URL to ping
1563
- * @returns {Promise<Object>} Ping result with latency
1564
- */
1565
- async function ping(rpcUrl) {
1566
- const client = new AetherClient({ rpcUrl });
1567
- const start = Date.now();
1568
- try {
1569
- await client.getSlot();
1570
- return { ok: true, latency: Date.now() - start, rpc: rpcUrl || DEFAULT_RPC_URL };
1571
- } catch (err) {
1572
- return { ok: false, error: err.message, rpc: rpcUrl || DEFAULT_RPC_URL };
1573
- } finally {
1574
- client.destroy();
1575
- }
1576
- }
1577
-
1578
- // ============================================================
1579
- // Exports
1580
- // ============================================================
1581
-
1582
- module.exports = {
1583
- // Main class
1584
- AetherClient,
1585
-
1586
- // Error classes
1587
- AetherSDKError,
1588
- NetworkTimeoutError,
1589
- RPCError,
1590
- RateLimitError,
1591
- CircuitBreakerOpenError,
1592
-
1593
- // Factory function
1594
- createClient,
1595
-
1596
- // Convenience functions (all chain queries)
1597
- getSlot,
1598
- getBlockHeight,
1599
- getEpoch,
1600
- getAccount,
1601
- getBalance,
1602
- getTransaction,
1603
- getRecentTransactions,
1604
- getTransactionHistory,
1605
- getTokenAccounts,
1606
- getStakeAccounts,
1607
- getValidators,
1608
- getTPS,
1609
- getSupply,
1610
- getSlotProduction,
1611
- getFees,
1612
- getStakePositions,
1613
- getRewards,
1614
- getValidatorAPY,
1615
- getPeers,
1616
- getHealth,
1617
-
1618
- // Transactions
1619
- sendTransaction,
1620
-
1621
- // Utilities
1622
- ping,
1623
-
1624
- // Low-level RPC
1625
- rpcGet,
1626
- rpcPost,
1627
-
1628
- // Constants
1629
- DEFAULT_RPC_URL,
1630
- DEFAULT_TIMEOUT_MS,
1631
- DEFAULT_RETRY_ATTEMPTS,
1632
- DEFAULT_RETRY_DELAY_MS,
1633
- DEFAULT_BACKOFF_MULTIPLIER,
1634
- DEFAULT_MAX_RETRY_DELAY_MS,
1635
- DEFAULT_RATE_LIMIT_RPS,
1636
- DEFAULT_RATE_LIMIT_BURST,
1637
- DEFAULT_CIRCUIT_BREAKER_THRESHOLD,
1638
- DEFAULT_CIRCUIT_BREAKER_RESET_MS,
1639
- };
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
+ * Features:
8
+ * - Retry logic with exponential backoff
9
+ * - Rate limiting with token bucket algorithm
10
+ * - Enhanced error handling for network timeouts and RPC failures
11
+ * - Circuit breaker for repeated failures
12
+ *
13
+ * Default RPC: http://127.0.0.1:8899 (configurable via constructor or AETHER_RPC env)
14
+ */
15
+
16
+ const http = require('http');
17
+ const https = require('https');
18
+ const { rpcGet, rpcPost } = require('./rpc');
19
+
20
+ // Default configuration
21
+ const DEFAULT_RPC_URL = 'http://127.0.0.1:8899';
22
+ const DEFAULT_TIMEOUT_MS = 10000;
23
+
24
+ // Retry configuration
25
+ const DEFAULT_RETRY_ATTEMPTS = 3;
26
+ const DEFAULT_RETRY_DELAY_MS = 1000;
27
+ const DEFAULT_BACKOFF_MULTIPLIER = 2;
28
+ const DEFAULT_MAX_RETRY_DELAY_MS = 30000;
29
+
30
+ // Rate limiting configuration
31
+ const DEFAULT_RATE_LIMIT_RPS = 10; // Requests per second
32
+ const DEFAULT_RATE_LIMIT_BURST = 20; // Burst capacity
33
+
34
+ // Circuit breaker configuration
35
+ const DEFAULT_CIRCUIT_BREAKER_THRESHOLD = 5; // Failures before opening
36
+ const DEFAULT_CIRCUIT_BREAKER_RESET_MS = 60000; // Reset after 60s
37
+
38
+ /**
39
+ * Custom error types for better error handling
40
+ */
41
+ class AetherSDKError extends Error {
42
+ constructor(message, code, details = {}) {
43
+ super(message);
44
+ this.name = 'AetherSDKError';
45
+ this.code = code;
46
+ this.details = details;
47
+ this.timestamp = new Date().toISOString();
48
+ }
49
+ }
50
+
51
+ class NetworkTimeoutError extends AetherSDKError {
52
+ constructor(message, details = {}) {
53
+ super(message, 'NETWORK_TIMEOUT', details);
54
+ this.name = 'NetworkTimeoutError';
55
+ }
56
+ }
57
+
58
+ class RPCError extends AetherSDKError {
59
+ constructor(message, details = {}) {
60
+ super(message, 'RPC_ERROR', details);
61
+ this.name = 'RPCError';
62
+ }
63
+ }
64
+
65
+ class RateLimitError extends AetherSDKError {
66
+ constructor(message, details = {}) {
67
+ super(message, 'RATE_LIMIT', details);
68
+ this.name = 'RateLimitError';
69
+ }
70
+ }
71
+
72
+ class CircuitBreakerOpenError extends AetherSDKError {
73
+ constructor(message, details = {}) {
74
+ super(message, 'CIRCUIT_BREAKER_OPEN', details);
75
+ this.name = 'CircuitBreakerOpenError';
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Token bucket rate limiter
81
+ */
82
+ class TokenBucketRateLimiter {
83
+ constructor(rps = DEFAULT_RATE_LIMIT_RPS, burst = DEFAULT_RATE_LIMIT_BURST) {
84
+ this.rps = rps;
85
+ this.burst = burst;
86
+ this.tokens = burst;
87
+ this.lastRefill = Date.now();
88
+ this.queue = [];
89
+ this.refillInterval = setInterval(() => this.refill(), 1000 / rps);
90
+ }
91
+
92
+ refill() {
93
+ const now = Date.now();
94
+ const timePassed = (now - this.lastRefill) / 1000;
95
+ const tokensToAdd = timePassed * this.rps;
96
+ this.tokens = Math.min(this.burst, this.tokens + tokensToAdd);
97
+ this.lastRefill = now;
98
+ this.processQueue();
99
+ }
100
+
101
+ processQueue() {
102
+ while (this.queue.length > 0 && this.tokens >= 1) {
103
+ const { resolve, reject, tokens } = this.queue.shift();
104
+ if (this.tokens >= tokens) {
105
+ this.tokens -= tokens;
106
+ resolve();
107
+ } else {
108
+ this.queue.unshift({ resolve, reject, tokens });
109
+ break;
110
+ }
111
+ }
112
+ }
113
+
114
+ async acquire(tokens = 1) {
115
+ return new Promise((resolve, reject) => {
116
+ if (this.tokens >= tokens) {
117
+ this.tokens -= tokens;
118
+ resolve();
119
+ } else {
120
+ this.queue.push({ resolve, reject, tokens });
121
+ }
122
+ });
123
+ }
124
+
125
+ destroy() {
126
+ if (this.refillInterval) {
127
+ clearInterval(this.refillInterval);
128
+ this.refillInterval = null;
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Circuit breaker for handling repeated failures
135
+ */
136
+ class CircuitBreaker {
137
+ constructor(threshold = DEFAULT_CIRCUIT_BREAKER_THRESHOLD, resetTimeoutMs = DEFAULT_CIRCUIT_BREAKER_RESET_MS) {
138
+ this.threshold = threshold;
139
+ this.resetTimeoutMs = resetTimeoutMs;
140
+ this.failureCount = 0;
141
+ this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
142
+ this.nextAttempt = 0;
143
+ }
144
+
145
+ canExecute() {
146
+ if (this.state === 'CLOSED') return true;
147
+ if (this.state === 'OPEN') {
148
+ if (Date.now() >= this.nextAttempt) {
149
+ this.state = 'HALF_OPEN';
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+ return this.state === 'HALF_OPEN';
155
+ }
156
+
157
+ recordSuccess() {
158
+ this.failureCount = 0;
159
+ this.state = 'CLOSED';
160
+ }
161
+
162
+ recordFailure() {
163
+ this.failureCount++;
164
+ if (this.failureCount >= this.threshold) {
165
+ this.state = 'OPEN';
166
+ this.nextAttempt = Date.now() + this.resetTimeoutMs;
167
+ }
168
+ }
169
+
170
+ getState() {
171
+ return {
172
+ state: this.state,
173
+ failureCount: this.failureCount,
174
+ nextAttempt: this.state === 'OPEN' ? this.nextAttempt : null,
175
+ };
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Aether SDK Client
181
+ * Real blockchain interface layer - every method makes actual HTTP RPC calls
182
+ *
183
+ * Includes:
184
+ * - Retry logic with exponential backoff
185
+ * - Rate limiting
186
+ * - Circuit breaker for resilience
187
+ * - Enhanced error handling
188
+ */
189
+ class AetherClient {
190
+ constructor(options = {}) {
191
+ this.rpcUrl = options.rpcUrl || process.env.AETHER_RPC || DEFAULT_RPC_URL;
192
+ this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
193
+
194
+ // Retry configuration
195
+ this.retryAttempts = options.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
196
+ this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
197
+ this.backoffMultiplier = options.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER;
198
+ this.maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
199
+
200
+ // Rate limiting
201
+ this.rateLimiter = new TokenBucketRateLimiter(
202
+ options.rateLimitRps ?? DEFAULT_RATE_LIMIT_RPS,
203
+ options.rateLimitBurst ?? DEFAULT_RATE_LIMIT_BURST
204
+ );
205
+
206
+ // Circuit breaker
207
+ this.circuitBreaker = new CircuitBreaker(
208
+ options.circuitBreakerThreshold ?? DEFAULT_CIRCUIT_BREAKER_THRESHOLD,
209
+ options.circuitBreakerResetMs ?? DEFAULT_CIRCUIT_BREAKER_RESET_MS
210
+ );
211
+
212
+ // Parse RPC URL
213
+ const url = new URL(this.rpcUrl);
214
+ this.protocol = url.protocol;
215
+ this.hostname = url.hostname;
216
+ this.port = url.port || (this.protocol === 'https:' ? 443 : 80);
217
+
218
+ // Request stats
219
+ this.stats = {
220
+ totalRequests: 0,
221
+ successfulRequests: 0,
222
+ failedRequests: 0,
223
+ retriedRequests: 0,
224
+ rateLimitedRequests: 0,
225
+ circuitBreakerBlocked: 0,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Calculate delay for exponential backoff with jitter
231
+ */
232
+ _calculateDelay(attempt) {
233
+ const baseDelay = this.retryDelayMs * Math.pow(this.backoffMultiplier, attempt);
234
+ const jitter = Math.random() * 100; // Add up to 100ms jitter
235
+ const delay = Math.min(baseDelay + jitter, this.maxRetryDelayMs);
236
+ return delay;
237
+ }
238
+
239
+ /**
240
+ * Check if error is retryable
241
+ */
242
+ _isRetryableError(error) {
243
+ if (!error) return false;
244
+
245
+ // Network errors
246
+ if (error.code === 'ECONNREFUSED') return true;
247
+ if (error.code === 'ENOTFOUND') return true;
248
+ if (error.code === 'ETIMEDOUT') return true;
249
+ if (error.code === 'ECONNRESET') return true;
250
+ if (error.code === 'EPIPE') return true;
251
+
252
+ // Timeout errors
253
+ if (error.message && error.message.includes('timeout')) return true;
254
+
255
+ // HTTP 5xx errors (server errors)
256
+ if (error.statusCode >= 500) return true;
257
+ if (error.statusCode === 429) return true; // Rate limit - retry with backoff
258
+
259
+ // RPC errors that might be transient
260
+ if (error.message && (
261
+ error.message.includes('rate limit') ||
262
+ error.message.includes('rate_limit') ||
263
+ error.message.includes('too many requests') ||
264
+ error.message.includes('temporarily unavailable') ||
265
+ error.message.includes('service unavailable')
266
+ )) return true;
267
+
268
+ return false;
269
+ }
270
+
271
+ /**
272
+ * Execute function with retry logic and rate limiting
273
+ */
274
+ async _executeWithRetry(operation, operationName) {
275
+ // Check circuit breaker
276
+ if (!this.circuitBreaker.canExecute()) {
277
+ this.stats.circuitBreakerBlocked++;
278
+ const state = this.circuitBreaker.getState();
279
+ const waitTime = Math.ceil((state.nextAttempt - Date.now()) / 1000);
280
+ throw new CircuitBreakerOpenError(
281
+ `Circuit breaker is OPEN. Too many failures. Retry in ${waitTime}s.`,
282
+ { circuitBreakerState: state, operation: operationName }
283
+ );
284
+ }
285
+
286
+ // Wait for rate limit token
287
+ await this.rateLimiter.acquire();
288
+
289
+ let lastError = null;
290
+
291
+ for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
292
+ this.stats.totalRequests++;
293
+
294
+ try {
295
+ const result = await operation();
296
+ this.circuitBreaker.recordSuccess();
297
+ this.stats.successfulRequests++;
298
+ return result;
299
+ } catch (error) {
300
+ lastError = error;
301
+
302
+ // Don't retry if it's not a retryable error
303
+ if (!this._isRetryableError(error)) {
304
+ this.circuitBreaker.recordFailure();
305
+ this.stats.failedRequests++;
306
+ break;
307
+ }
308
+
309
+ this.stats.retriedRequests++;
310
+ this.circuitBreaker.recordFailure();
311
+
312
+ // If this was the last attempt, throw the error
313
+ if (attempt === this.retryAttempts - 1) {
314
+ this.stats.failedRequests++;
315
+ break;
316
+ }
317
+
318
+ // Calculate and apply backoff delay
319
+ const delay = this._calculateDelay(attempt);
320
+ await new Promise(resolve => setTimeout(resolve, delay));
321
+ }
322
+ }
323
+
324
+ // All retries exhausted - classify and throw error
325
+ throw this._classifyError(lastError, operationName);
326
+ }
327
+
328
+ /**
329
+ * Classify error into specific error types
330
+ */
331
+ _classifyError(error, operationName) {
332
+ if (!error) {
333
+ return new AetherSDKError('Unknown error occurred', 'UNKNOWN_ERROR', { operation: operationName });
334
+ }
335
+
336
+ // Already classified
337
+ if (error instanceof AetherSDKError) {
338
+ return error;
339
+ }
340
+
341
+ // Timeout errors
342
+ if (error.message && (
343
+ error.message.includes('timeout') ||
344
+ error.code === 'ETIMEDOUT'
345
+ )) {
346
+ return new NetworkTimeoutError(
347
+ `Network timeout during ${operationName}: ${error.message}`,
348
+ {
349
+ originalError: error.message,
350
+ code: error.code,
351
+ operation: operationName,
352
+ rpcUrl: this.rpcUrl,
353
+ }
354
+ );
355
+ }
356
+
357
+ // Connection errors
358
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
359
+ return new AetherSDKError(
360
+ `Cannot connect to RPC endpoint during ${operationName}: ${error.message}`,
361
+ 'CONNECTION_ERROR',
362
+ {
363
+ originalError: error.message,
364
+ code: error.code,
365
+ operation: operationName,
366
+ rpcUrl: this.rpcUrl,
367
+ }
368
+ );
369
+ }
370
+
371
+ // RPC-specific errors
372
+ if (error.message && (
373
+ error.message.includes('RPC') ||
374
+ error.message.includes('rpc') ||
375
+ error.statusCode
376
+ )) {
377
+ return new RPCError(
378
+ `RPC error during ${operationName}: ${error.message}`,
379
+ {
380
+ originalError: error.message,
381
+ code: error.code || error.statusCode,
382
+ operation: operationName,
383
+ rpcUrl: this.rpcUrl,
384
+ }
385
+ );
386
+ }
387
+
388
+ // Generic error
389
+ return new AetherSDKError(
390
+ `Error during ${operationName}: ${error.message}`,
391
+ 'SDK_ERROR',
392
+ {
393
+ originalError: error.message,
394
+ code: error.code,
395
+ operation: operationName,
396
+ rpcUrl: this.rpcUrl,
397
+ }
398
+ );
399
+ }
400
+
401
+ /**
402
+ * Internal: Make HTTP GET request to RPC endpoint
403
+ */
404
+ _httpGet(path, timeoutMs = this.timeoutMs) {
405
+ return new Promise((resolve, reject) => {
406
+ const lib = this.protocol === 'https:' ? https : http;
407
+ const req = lib.request({
408
+ hostname: this.hostname,
409
+ port: this.port,
410
+ path: path,
411
+ method: 'GET',
412
+ timeout: timeoutMs,
413
+ headers: { 'Content-Type': 'application/json' },
414
+ }, (res) => {
415
+ let data = '';
416
+ res.on('data', (chunk) => data += chunk);
417
+ res.on('end', () => {
418
+ try {
419
+ const parsed = JSON.parse(data);
420
+ if (parsed.error) {
421
+ const err = new Error(parsed.error.message || JSON.stringify(parsed.error));
422
+ err.statusCode = res.statusCode;
423
+ err.responseData = parsed;
424
+ reject(err);
425
+ } else {
426
+ resolve(parsed);
427
+ }
428
+ } catch (e) {
429
+ resolve({ raw: data });
430
+ }
431
+ });
432
+ });
433
+ req.on('error', reject);
434
+ req.on('timeout', () => {
435
+ req.destroy();
436
+ const err = new Error(`Request timeout after ${timeoutMs}ms`);
437
+ err.code = 'ETIMEDOUT';
438
+ reject(err);
439
+ });
440
+ req.end();
441
+ });
442
+ }
443
+
444
+ /**
445
+ * Internal: Make HTTP POST request to RPC endpoint
446
+ */
447
+ _httpPost(path, body = {}, timeoutMs = this.timeoutMs) {
448
+ return new Promise((resolve, reject) => {
449
+ const lib = this.protocol === 'https:' ? https : http;
450
+ const bodyStr = JSON.stringify(body);
451
+ const req = lib.request({
452
+ hostname: this.hostname,
453
+ port: this.port,
454
+ path: path,
455
+ method: 'POST',
456
+ timeout: timeoutMs,
457
+ headers: {
458
+ 'Content-Type': 'application/json',
459
+ 'Content-Length': Buffer.byteLength(bodyStr),
460
+ },
461
+ }, (res) => {
462
+ let data = '';
463
+ res.on('data', (chunk) => data += chunk);
464
+ res.on('end', () => {
465
+ try {
466
+ const parsed = JSON.parse(data);
467
+ if (parsed.error) {
468
+ const err = new Error(parsed.error.message || JSON.stringify(parsed.error));
469
+ err.statusCode = res.statusCode;
470
+ err.responseData = parsed;
471
+ reject(err);
472
+ } else {
473
+ resolve(parsed);
474
+ }
475
+ } catch (e) {
476
+ resolve({ raw: data });
477
+ }
478
+ });
479
+ });
480
+ req.on('error', reject);
481
+ req.on('timeout', () => {
482
+ req.destroy();
483
+ const err = new Error(`Request timeout after ${timeoutMs}ms`);
484
+ err.code = 'ETIMEDOUT';
485
+ reject(err);
486
+ });
487
+ req.write(bodyStr);
488
+ req.end();
489
+ });
490
+ }
491
+
492
+ // ============================================================
493
+ // Core RPC Methods - Real blockchain calls with retry & rate limiting
494
+ // ============================================================
495
+
496
+ /**
497
+ * Get current slot number
498
+ * RPC: GET /v1/slot
499
+ *
500
+ * @returns {Promise<number>} Current slot number
501
+ */
502
+ async getSlot() {
503
+ return this._executeWithRetry(
504
+ async () => {
505
+ const result = await this._httpGet('/v1/slot');
506
+ return result.slot !== undefined ? result.slot : result;
507
+ },
508
+ 'getSlot'
509
+ );
510
+ }
511
+
512
+ /**
513
+ * Get current block height
514
+ * RPC: GET /v1/blockheight
515
+ *
516
+ * @returns {Promise<number>} Current block height
517
+ */
518
+ async getBlockHeight() {
519
+ return this._executeWithRetry(
520
+ async () => {
521
+ const result = await this._httpGet('/v1/blockheight');
522
+ return result.blockHeight !== undefined ? result.blockHeight : result;
523
+ },
524
+ 'getBlockHeight'
525
+ );
526
+ }
527
+
528
+ /**
529
+ * Get account info including balance
530
+ * RPC: GET /v1/account/<address>
531
+ *
532
+ * @param {string} address - Account public key (base58)
533
+ * @returns {Promise<Object>} Account info: { lamports, owner, data, rent_epoch }
534
+ */
535
+ async getAccountInfo(address) {
536
+ if (!address) {
537
+ throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
538
+ }
539
+ return this._executeWithRetry(
540
+ async () => {
541
+ const result = await this._httpGet(`/v1/account/${address}`);
542
+ return result;
543
+ },
544
+ 'getAccountInfo'
545
+ );
546
+ }
547
+
548
+ /**
549
+ * Alias for getAccountInfo
550
+ * @param {string} address - Account address
551
+ * @returns {Promise<Object>} Account info
552
+ */
553
+ async getAccount(address) {
554
+ return this.getAccountInfo(address);
555
+ }
556
+
557
+ /**
558
+ * Get balance in lamports
559
+ * RPC: GET /v1/account/<address>
560
+ *
561
+ * @param {string} address - Account public key (base58)
562
+ * @returns {Promise<number>} Balance in lamports
563
+ */
564
+ async getBalance(address) {
565
+ const account = await this.getAccountInfo(address);
566
+ return account.lamports !== undefined ? account.lamports : 0;
567
+ }
568
+
569
+ /**
570
+ * Get epoch info
571
+ * RPC: GET /v1/epoch
572
+ *
573
+ * @returns {Promise<Object>} Epoch info: { epoch, slotIndex, slotsInEpoch, absoluteSlot }
574
+ */
575
+ async getEpochInfo() {
576
+ return this._executeWithRetry(
577
+ async () => {
578
+ const result = await this._httpGet('/v1/epoch');
579
+ return result;
580
+ },
581
+ 'getEpochInfo'
582
+ );
583
+ }
584
+
585
+ /**
586
+ * Get transaction by signature
587
+ * RPC: GET /v1/transaction/<signature>
588
+ *
589
+ * @param {string} signature - Transaction signature (base58)
590
+ * @returns {Promise<Object>} Transaction details
591
+ */
592
+ async getTransaction(signature) {
593
+ if (!signature) {
594
+ throw new AetherSDKError('Transaction signature is required', 'VALIDATION_ERROR');
595
+ }
596
+ return this._executeWithRetry(
597
+ async () => {
598
+ const result = await this._httpGet(`/v1/transaction/${signature}`);
599
+ return result;
600
+ },
601
+ 'getTransaction'
602
+ );
603
+ }
604
+
605
+ /**
606
+ * Submit a signed transaction
607
+ * RPC: POST /v1/transaction
608
+ *
609
+ * @param {Object} tx - Signed transaction object
610
+ * @param {string} tx.signature - Transaction signature (base58)
611
+ * @param {string} tx.signer - Signer public key (base58)
612
+ * @param {string} tx.tx_type - Transaction type
613
+ * @param {Object} tx.payload - Transaction payload
614
+ * @returns {Promise<Object>} Transaction receipt: { signature, slot, confirmed }
615
+ */
616
+ async sendTransaction(tx) {
617
+ if (!tx || !tx.signature) {
618
+ throw new AetherSDKError('Transaction with signature is required', 'VALIDATION_ERROR');
619
+ }
620
+ return this._executeWithRetry(
621
+ async () => {
622
+ const result = await this._httpPost('/v1/transaction', tx);
623
+ return result;
624
+ },
625
+ 'sendTransaction'
626
+ );
627
+ }
628
+
629
+ /**
630
+ * Get recent blockhash for transaction signing
631
+ * RPC: GET /v1/recent-blockhash
632
+ *
633
+ * @returns {Promise<Object>} { blockhash, lastValidBlockHeight }
634
+ */
635
+ async getRecentBlockhash() {
636
+ return this._executeWithRetry(
637
+ async () => {
638
+ const result = await this._httpGet('/v1/recent-blockhash');
639
+ return result;
640
+ },
641
+ 'getRecentBlockhash'
642
+ );
643
+ }
644
+
645
+ /**
646
+ * Get network peers
647
+ * RPC: GET /v1/peers
648
+ *
649
+ * @returns {Promise<Array>} List of peer node addresses
650
+ */
651
+ async getClusterPeers() {
652
+ return this._executeWithRetry(
653
+ async () => {
654
+ const result = await this._httpGet('/v1/peers');
655
+ return Array.isArray(result) ? result : (result.peers || []);
656
+ },
657
+ 'getClusterPeers'
658
+ );
659
+ }
660
+
661
+ /**
662
+ * Get validator info
663
+ * RPC: GET /v1/validators
664
+ *
665
+ * @returns {Promise<Array>} List of validators with stake, commission, etc.
666
+ */
667
+ async getValidators() {
668
+ return this._executeWithRetry(
669
+ async () => {
670
+ const result = await this._httpGet('/v1/validators');
671
+ return Array.isArray(result) ? result : (result.validators || []);
672
+ },
673
+ 'getValidators'
674
+ );
675
+ }
676
+
677
+ /**
678
+ * Get supply info
679
+ * RPC: GET /v1/supply
680
+ *
681
+ * @returns {Promise<Object>} Supply info: { total, circulating, nonCirculating }
682
+ */
683
+ async getSupply() {
684
+ return this._executeWithRetry(
685
+ async () => {
686
+ const result = await this._httpGet('/v1/supply');
687
+ return result;
688
+ },
689
+ 'getSupply'
690
+ );
691
+ }
692
+
693
+ /**
694
+ * Get health status
695
+ * RPC: GET /v1/health
696
+ *
697
+ * @returns {Promise<string>} 'ok' if node is healthy
698
+ */
699
+ async getHealth() {
700
+ return this._executeWithRetry(
701
+ async () => {
702
+ const result = await this._httpGet('/v1/health');
703
+ return result.status || result;
704
+ },
705
+ 'getHealth'
706
+ );
707
+ }
708
+
709
+ /**
710
+ * Get version info
711
+ * RPC: GET /v1/version
712
+ *
713
+ * @returns {Promise<Object>} Version info: { aetherCore, featureSet }
714
+ */
715
+ async getVersion() {
716
+ return this._executeWithRetry(
717
+ async () => {
718
+ const result = await this._httpGet('/v1/version');
719
+ return result;
720
+ },
721
+ 'getVersion'
722
+ );
723
+ }
724
+
725
+ /**
726
+ * Get TPS (transactions per second)
727
+ * RPC: GET /v1/tps
728
+ *
729
+ * @returns {Promise<number>} Current TPS
730
+ */
731
+ async getTPS() {
732
+ return this._executeWithRetry(
733
+ async () => {
734
+ const result = await this._httpGet('/v1/tps');
735
+ return result.tps ?? result.tps_avg ?? result.transactions_per_second ?? null;
736
+ },
737
+ 'getTPS'
738
+ );
739
+ }
740
+
741
+ /**
742
+ * Get fee estimates
743
+ * RPC: GET /v1/fees
744
+ *
745
+ * @returns {Promise<Object>} Fee info
746
+ */
747
+ async getFees() {
748
+ return this._executeWithRetry(
749
+ async () => {
750
+ const result = await this._httpGet('/v1/fees');
751
+ return result;
752
+ },
753
+ 'getFees'
754
+ );
755
+ }
756
+
757
+ /**
758
+ * Get slot production stats
759
+ * RPC: POST /v1/slot_production
760
+ *
761
+ * @returns {Promise<Object>} Slot production stats
762
+ */
763
+ async getSlotProduction() {
764
+ return this._executeWithRetry(
765
+ async () => {
766
+ const result = await this._httpPost('/v1/slot_production', {});
767
+ return result;
768
+ },
769
+ 'getSlotProduction'
770
+ );
771
+ }
772
+
773
+ /**
774
+ * Get stake positions for an address
775
+ * RPC: GET /v1/stake/<address>
776
+ *
777
+ * @param {string} address - Account address
778
+ * @returns {Promise<Array>} List of stake positions
779
+ */
780
+ async getStakePositions(address) {
781
+ if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
782
+ return this._executeWithRetry(
783
+ async () => {
784
+ const result = await this._httpGet(`/v1/stake/${address}`);
785
+ return result.delegations ?? result.stakes ?? result ?? [];
786
+ },
787
+ 'getStakePositions'
788
+ );
789
+ }
790
+
791
+ /**
792
+ * Get rewards for an address
793
+ * RPC: GET /v1/rewards/<address>
794
+ *
795
+ * @param {string} address - Account address
796
+ * @returns {Promise<Object>} Rewards info
797
+ */
798
+ async getRewards(address) {
799
+ if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
800
+ return this._executeWithRetry(
801
+ async () => {
802
+ const result = await this._httpGet(`/v1/rewards/${address}`);
803
+ return result;
804
+ },
805
+ 'getRewards'
806
+ );
807
+ }
808
+
809
+ /**
810
+ * Get validator APY
811
+ * RPC: GET /v1/validator/<address>/apy
812
+ *
813
+ * @param {string} validatorAddr - Validator address
814
+ * @returns {Promise<Object>} APY info
815
+ */
816
+ async getValidatorAPY(validatorAddr) {
817
+ if (!validatorAddr) throw new AetherSDKError('Validator address is required', 'VALIDATION_ERROR');
818
+ return this._executeWithRetry(
819
+ async () => {
820
+ const result = await this._httpGet(`/v1/validator/${validatorAddr}/apy`);
821
+ return result;
822
+ },
823
+ 'getValidatorAPY'
824
+ );
825
+ }
826
+
827
+ /**
828
+ * Get recent transactions for an address
829
+ * RPC: GET /v1/transactions/<address>?limit=<n>
830
+ *
831
+ * @param {string} address - Account address
832
+ * @param {number} limit - Max transactions to return
833
+ * @returns {Promise<Array>} List of recent transactions
834
+ */
835
+ async getRecentTransactions(address, limit = 20) {
836
+ if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
837
+ return this._executeWithRetry(
838
+ async () => {
839
+ const result = await this._httpGet(`/v1/transactions/${address}?limit=${limit}`);
840
+ return result.transactions ?? result ?? [];
841
+ },
842
+ 'getRecentTransactions'
843
+ );
844
+ }
845
+
846
+ /**
847
+ * Get transaction history with signatures for an address
848
+ * RPC: POST /v1/transactions/history (or GET /v1/transactions/<address>?limit=<n>)
849
+ *
850
+ * @param {string} address - Account address
851
+ * @param {number} limit - Max transactions to return
852
+ * @returns {Promise<Object>} Transaction history with signatures and details
853
+ */
854
+ async getTransactionHistory(address, limit = 20) {
855
+ if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
856
+
857
+ // First get signatures
858
+ const sigResult = await this._executeWithRetry(
859
+ async () => {
860
+ const result = await this._httpPost('/v1/transactions/history', { address, limit });
861
+ if (result.error) {
862
+ throw new RPCError(result.error.message || result.error, { result });
863
+ }
864
+ return result;
865
+ },
866
+ 'getTransactionHistory.signatures'
867
+ );
868
+
869
+ const signatures = sigResult.signatures || sigResult.result || [];
870
+
871
+ // Fetch full transaction details for each signature (up to 10 at a time)
872
+ const BATCH = 10;
873
+ const txs = [];
874
+ for (let i = 0; i < signatures.length; i += BATCH) {
875
+ const batch = signatures.slice(i, i + BATCH);
876
+ const batchPromises = batch.map(sig =>
877
+ this.getTransaction(sig.signature || sig).catch(() => null)
878
+ );
879
+ const batchResults = await Promise.all(batchPromises);
880
+ txs.push(...batchResults.filter(Boolean));
881
+ }
882
+
883
+ return {
884
+ signatures: signatures,
885
+ transactions: txs,
886
+ address: address,
887
+ };
888
+ }
889
+
890
+ /**
891
+ * Get all SPL token accounts for a wallet address
892
+ * RPC: GET /v1/tokens/<address>
893
+ *
894
+ * @param {string} address - Account public key (base58)
895
+ * @returns {Promise<Array>} List of token accounts with mint, amount, decimals
896
+ */
897
+ async getTokenAccounts(address) {
898
+ if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
899
+ return this._executeWithRetry(
900
+ async () => {
901
+ const result = await this._httpGet(`/v1/tokens/${address}`);
902
+ return result.tokens ?? result.accounts ?? result ?? [];
903
+ },
904
+ 'getTokenAccounts'
905
+ );
906
+ }
907
+
908
+ /**
909
+ * Get all stake accounts for a wallet address
910
+ * RPC: GET /v1/stake-accounts/<address>
911
+ *
912
+ * @param {string} address - Account public key (base58)
913
+ * @returns {Promise<Array>} List of stake accounts
914
+ */
915
+ async getStakeAccounts(address) {
916
+ if (!address) throw new AetherSDKError('Address is required', 'VALIDATION_ERROR');
917
+ return this._executeWithRetry(
918
+ async () => {
919
+ const result = await this._httpGet(`/v1/stake-accounts/${address}`);
920
+ return result.stake_accounts ?? result.delegations ?? result ?? [];
921
+ },
922
+ 'getStakeAccounts'
923
+ );
924
+ }
925
+
926
+ // ============================================================
927
+ // Transaction Helpers - Build and send real transactions
928
+ // ============================================================
929
+
930
+ /**
931
+ * Build and send a transfer transaction
932
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
933
+ *
934
+ * @param {Object} params
935
+ * @param {string} params.from - Sender address (base58)
936
+ * @param {string} params.to - Recipient address (base58)
937
+ * @param {number} params.amount - Amount in lamports
938
+ * @param {number} params.nonce - Nonce for replay protection
939
+ * @param {Function} params.signFn - Function to sign the transaction (receives tx object, returns signature)
940
+ * @returns {Promise<Object>} Transaction receipt
941
+ */
942
+ async transfer({ from, to, amount, nonce, signFn }) {
943
+ if (!from || !to || !amount === undefined || nonce === undefined) {
944
+ throw new AetherSDKError('from, to, amount, and nonce are required', 'VALIDATION_ERROR');
945
+ }
946
+ if (!signFn || typeof signFn !== 'function') {
947
+ throw new AetherSDKError('signFn is required (function to sign the transaction)', 'VALIDATION_ERROR');
948
+ }
949
+
950
+ // Get recent blockhash (real RPC call)
951
+ const { blockhash } = await this.getRecentBlockhash();
952
+
953
+ // Build transaction payload
954
+ const tx = {
955
+ signature: '', // Will be filled after signing
956
+ signer: from,
957
+ tx_type: 'Transfer',
958
+ payload: {
959
+ recipient: to,
960
+ amount: BigInt(amount),
961
+ nonce: BigInt(nonce),
962
+ },
963
+ fee: 5000, // 5000 lamports fee
964
+ slot: await this.getSlot(),
965
+ timestamp: Date.now(),
966
+ };
967
+
968
+ // Sign transaction (user provides signing function)
969
+ const signature = await signFn(tx, blockhash);
970
+ tx.signature = signature;
971
+
972
+ // Send to blockchain (real RPC call)
973
+ const receipt = await this.sendTransaction(tx);
974
+ return receipt;
975
+ }
976
+
977
+ /**
978
+ * Build and send a stake delegation transaction
979
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
980
+ *
981
+ * @param {Object} params
982
+ * @param {string} params.staker - Staker address (base58)
983
+ * @param {string} params.validator - Validator address (base58)
984
+ * @param {number} params.amount - Amount to stake in lamports
985
+ * @param {Function} params.signFn - Function to sign the transaction
986
+ * @returns {Promise<Object>} Transaction receipt
987
+ */
988
+ async stake({ staker, validator, amount, signFn }) {
989
+ if (!staker || !validator || !amount === undefined) {
990
+ throw new AetherSDKError('staker, validator, and amount are required', 'VALIDATION_ERROR');
991
+ }
992
+ if (!signFn || typeof signFn !== 'function') {
993
+ throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
994
+ }
995
+
996
+ const { blockhash } = await this.getRecentBlockhash();
997
+
998
+ const tx = {
999
+ signature: '',
1000
+ signer: staker,
1001
+ tx_type: 'Stake',
1002
+ payload: {
1003
+ validator: validator,
1004
+ amount: BigInt(amount),
1005
+ },
1006
+ fee: 5000,
1007
+ slot: await this.getSlot(),
1008
+ timestamp: Date.now(),
1009
+ };
1010
+
1011
+ const signature = await signFn(tx, blockhash);
1012
+ tx.signature = signature;
1013
+
1014
+ const receipt = await this.sendTransaction(tx);
1015
+ return receipt;
1016
+ }
1017
+
1018
+ /**
1019
+ * Build and send an unstake (withdraw) transaction
1020
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1021
+ *
1022
+ * @param {Object} params
1023
+ * @param {string} params.stakeAccount - Stake account address (base58)
1024
+ * @param {number} params.amount - Amount to unstake in lamports
1025
+ * @param {Function} params.signFn - Function to sign the transaction
1026
+ * @returns {Promise<Object>} Transaction receipt
1027
+ */
1028
+ async unstake({ stakeAccount, amount, signFn }) {
1029
+ if (!stakeAccount || !amount === undefined) {
1030
+ throw new AetherSDKError('stakeAccount and amount are required', 'VALIDATION_ERROR');
1031
+ }
1032
+ if (!signFn || typeof signFn !== 'function') {
1033
+ throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1034
+ }
1035
+
1036
+ const { blockhash } = await this.getRecentBlockhash();
1037
+
1038
+ const tx = {
1039
+ signature: '',
1040
+ signer: stakeAccount,
1041
+ tx_type: 'Unstake',
1042
+ payload: {
1043
+ stake_account: stakeAccount,
1044
+ amount: BigInt(amount),
1045
+ },
1046
+ fee: 5000,
1047
+ slot: await this.getSlot(),
1048
+ timestamp: Date.now(),
1049
+ };
1050
+
1051
+ const signature = await signFn(tx, blockhash);
1052
+ tx.signature = signature;
1053
+
1054
+ const receipt = await this.sendTransaction(tx);
1055
+ return receipt;
1056
+ }
1057
+
1058
+ /**
1059
+ * Build and send a claim rewards transaction
1060
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1061
+ *
1062
+ * @param {Object} params
1063
+ * @param {string} params.stakeAccount - Stake account address (base58)
1064
+ * @param {Function} params.signFn - Function to sign the transaction
1065
+ * @returns {Promise<Object>} Transaction receipt
1066
+ */
1067
+ async claimRewards({ stakeAccount, signFn }) {
1068
+ if (!stakeAccount) {
1069
+ throw new AetherSDKError('stakeAccount is required', 'VALIDATION_ERROR');
1070
+ }
1071
+ if (!signFn || typeof signFn !== 'function') {
1072
+ throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1073
+ }
1074
+
1075
+ const { blockhash } = await this.getRecentBlockhash();
1076
+
1077
+ const tx = {
1078
+ signature: '',
1079
+ signer: stakeAccount,
1080
+ tx_type: 'ClaimRewards',
1081
+ payload: {
1082
+ stake_account: stakeAccount,
1083
+ },
1084
+ fee: 5000,
1085
+ slot: await this.getSlot(),
1086
+ timestamp: Date.now(),
1087
+ };
1088
+
1089
+ const signature = await signFn(tx, blockhash);
1090
+ tx.signature = signature;
1091
+
1092
+ const receipt = await this.sendTransaction(tx);
1093
+ return receipt;
1094
+ }
1095
+
1096
+ // ============================================================
1097
+ // NFT Methods - Real blockchain calls for NFT operations
1098
+ // ============================================================
1099
+
1100
+ /**
1101
+ * Create a new NFT
1102
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1103
+ *
1104
+ * @param {Object} params
1105
+ * @param {string} params.creator - Creator address (base58)
1106
+ * @param {string} params.metadataUrl - URL to NFT metadata (JSON)
1107
+ * @param {number} params.royalties - Royalty basis points (e.g., 500 = 5%)
1108
+ * @param {Function} params.signFn - Function to sign the transaction
1109
+ * @returns {Promise<Object>} Transaction receipt with NFT ID
1110
+ */
1111
+ async createNFT({ creator, metadataUrl, royalties, signFn }) {
1112
+ if (!creator || !metadataUrl || royalties === undefined) {
1113
+ throw new AetherSDKError('creator, metadataUrl, and royalties are required', 'VALIDATION_ERROR');
1114
+ }
1115
+ if (!signFn || typeof signFn !== 'function') {
1116
+ throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1117
+ }
1118
+
1119
+ const { blockhash } = await this.getRecentBlockhash();
1120
+
1121
+ const tx = {
1122
+ signature: '',
1123
+ signer: creator,
1124
+ tx_type: 'CreateNFT',
1125
+ payload: {
1126
+ metadata_url: metadataUrl,
1127
+ royalties: royalties,
1128
+ },
1129
+ fee: 10000, // Higher fee for NFT creation
1130
+ slot: await this.getSlot(),
1131
+ timestamp: Date.now(),
1132
+ };
1133
+
1134
+ const signature = await signFn(tx, blockhash);
1135
+ tx.signature = signature;
1136
+
1137
+ const receipt = await this.sendTransaction(tx);
1138
+ return receipt;
1139
+ }
1140
+
1141
+ /**
1142
+ * Transfer an NFT to another address
1143
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1144
+ *
1145
+ * @param {Object} params
1146
+ * @param {string} params.from - Current owner address (base58)
1147
+ * @param {string} params.nftId - NFT ID
1148
+ * @param {string} params.to - Recipient address (base58)
1149
+ * @param {Function} params.signFn - Function to sign the transaction
1150
+ * @returns {Promise<Object>} Transaction receipt
1151
+ */
1152
+ async transferNFT({ from, nftId, to, signFn }) {
1153
+ if (!from || !nftId || !to) {
1154
+ throw new AetherSDKError('from, nftId, and to are required', 'VALIDATION_ERROR');
1155
+ }
1156
+ if (!signFn || typeof signFn !== 'function') {
1157
+ throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1158
+ }
1159
+
1160
+ const { blockhash } = await this.getRecentBlockhash();
1161
+
1162
+ const tx = {
1163
+ signature: '',
1164
+ signer: from,
1165
+ tx_type: 'TransferNFT',
1166
+ payload: {
1167
+ nft_id: nftId,
1168
+ recipient: to,
1169
+ },
1170
+ fee: 5000,
1171
+ slot: await this.getSlot(),
1172
+ timestamp: Date.now(),
1173
+ };
1174
+
1175
+ const signature = await signFn(tx, blockhash);
1176
+ tx.signature = signature;
1177
+
1178
+ const receipt = await this.sendTransaction(tx);
1179
+ return receipt;
1180
+ }
1181
+
1182
+ /**
1183
+ * Update NFT metadata URL
1184
+ * Makes real RPC calls: getRecentBlockhash() + sendTransaction()
1185
+ *
1186
+ * @param {Object} params
1187
+ * @param {string} params.creator - NFT creator/owner address (base58)
1188
+ * @param {string} params.nftId - NFT ID
1189
+ * @param {string} params.metadataUrl - New metadata URL
1190
+ * @param {Function} params.signFn - Function to sign the transaction
1191
+ * @returns {Promise<Object>} Transaction receipt
1192
+ */
1193
+ async updateMetadata({ creator, nftId, metadataUrl, signFn }) {
1194
+ if (!creator || !nftId || !metadataUrl) {
1195
+ throw new AetherSDKError('creator, nftId, and metadataUrl are required', 'VALIDATION_ERROR');
1196
+ }
1197
+ if (!signFn || typeof signFn !== 'function') {
1198
+ throw new AetherSDKError('signFn is required', 'VALIDATION_ERROR');
1199
+ }
1200
+
1201
+ const { blockhash } = await this.getRecentBlockhash();
1202
+
1203
+ const tx = {
1204
+ signature: '',
1205
+ signer: creator,
1206
+ tx_type: 'UpdateMetadata',
1207
+ payload: {
1208
+ nft_id: nftId,
1209
+ metadata_url: metadataUrl,
1210
+ },
1211
+ fee: 5000,
1212
+ slot: await this.getSlot(),
1213
+ timestamp: Date.now(),
1214
+ };
1215
+
1216
+ const signature = await signFn(tx, blockhash);
1217
+ tx.signature = signature;
1218
+
1219
+ const receipt = await this.sendTransaction(tx);
1220
+ return receipt;
1221
+ }
1222
+
1223
+ // ============================================================
1224
+ // Utilities
1225
+ // ============================================================
1226
+
1227
+ /**
1228
+ * Get client statistics
1229
+ * @returns {Object} Request statistics
1230
+ */
1231
+ getStats() {
1232
+ return {
1233
+ ...this.stats,
1234
+ circuitBreaker: this.circuitBreaker.getState(),
1235
+ rateLimiter: {
1236
+ rps: this.rateLimiter.rps,
1237
+ burst: this.rateLimiter.burst,
1238
+ tokens: this.rateLimiter.tokens,
1239
+ },
1240
+ };
1241
+ }
1242
+
1243
+ /**
1244
+ * Reset circuit breaker
1245
+ */
1246
+ resetCircuitBreaker() {
1247
+ this.circuitBreaker = new CircuitBreaker(
1248
+ this.circuitBreaker.threshold,
1249
+ this.circuitBreaker.resetTimeoutMs
1250
+ );
1251
+ }
1252
+
1253
+ /**
1254
+ * Close the client and cleanup resources
1255
+ */
1256
+ destroy() {
1257
+ this.rateLimiter.destroy();
1258
+ }
1259
+ }
1260
+
1261
+ // ============================================================
1262
+ // Convenience Functions (for quick one-off calls)
1263
+ // ============================================================
1264
+
1265
+ /**
1266
+ * Create a new AetherClient instance
1267
+ * @param {Object} options - Client options
1268
+ * @returns {AetherClient}
1269
+ */
1270
+ function createClient(options = {}) {
1271
+ return new AetherClient(options);
1272
+ }
1273
+
1274
+ /**
1275
+ * Quick slot check (uses default RPC)
1276
+ * @returns {Promise<number>} Current slot
1277
+ */
1278
+ async function getSlot() {
1279
+ const client = new AetherClient();
1280
+ try {
1281
+ return await client.getSlot();
1282
+ } finally {
1283
+ client.destroy();
1284
+ }
1285
+ }
1286
+
1287
+ /**
1288
+ * Quick balance check (uses default RPC)
1289
+ * @param {string} address - Account address
1290
+ * @returns {Promise<number>} Balance in lamports
1291
+ */
1292
+ async function getBalance(address) {
1293
+ const client = new AetherClient();
1294
+ try {
1295
+ return await client.getBalance(address);
1296
+ } finally {
1297
+ client.destroy();
1298
+ }
1299
+ }
1300
+
1301
+ /**
1302
+ * Quick health check (uses default RPC)
1303
+ * @returns {Promise<string>} 'ok' if healthy
1304
+ */
1305
+ async function getHealth() {
1306
+ const client = new AetherClient();
1307
+ try {
1308
+ return await client.getHealth();
1309
+ } finally {
1310
+ client.destroy();
1311
+ }
1312
+ }
1313
+
1314
+ /**
1315
+ * Get current block height (uses default RPC)
1316
+ * @returns {Promise<number>} Block height
1317
+ */
1318
+ async function getBlockHeight() {
1319
+ const client = new AetherClient();
1320
+ try {
1321
+ return await client.getBlockHeight();
1322
+ } finally {
1323
+ client.destroy();
1324
+ }
1325
+ }
1326
+
1327
+ /**
1328
+ * Get epoch info (uses default RPC)
1329
+ * @returns {Promise<Object>} Epoch info
1330
+ */
1331
+ async function getEpoch() {
1332
+ const client = new AetherClient();
1333
+ try {
1334
+ return await client.getEpochInfo();
1335
+ } finally {
1336
+ client.destroy();
1337
+ }
1338
+ }
1339
+
1340
+ /**
1341
+ * Get TPS (uses default RPC)
1342
+ * @returns {Promise<number>} Transactions per second
1343
+ */
1344
+ async function getTPS() {
1345
+ const client = new AetherClient();
1346
+ try {
1347
+ return await client.getTPS();
1348
+ } finally {
1349
+ client.destroy();
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * Get supply info (uses default RPC)
1355
+ * @returns {Promise<Object>} Supply info
1356
+ */
1357
+ async function getSupply() {
1358
+ const client = new AetherClient();
1359
+ try {
1360
+ return await client.getSupply();
1361
+ } finally {
1362
+ client.destroy();
1363
+ }
1364
+ }
1365
+
1366
+ /**
1367
+ * Get fees info (uses default RPC)
1368
+ * @returns {Promise<Object>} Fee info
1369
+ */
1370
+ async function getFees() {
1371
+ const client = new AetherClient();
1372
+ try {
1373
+ return await client.getFees();
1374
+ } finally {
1375
+ client.destroy();
1376
+ }
1377
+ }
1378
+
1379
+ /**
1380
+ * Get validators list (uses default RPC)
1381
+ * @returns {Promise<Array>} List of validators
1382
+ */
1383
+ async function getValidators() {
1384
+ const client = new AetherClient();
1385
+ try {
1386
+ return await client.getValidators();
1387
+ } finally {
1388
+ client.destroy();
1389
+ }
1390
+ }
1391
+
1392
+ /**
1393
+ * Get peers list (uses default RPC)
1394
+ * @returns {Promise<Array>} List of peers
1395
+ */
1396
+ async function getPeers() {
1397
+ const client = new AetherClient();
1398
+ try {
1399
+ return await client.getClusterPeers();
1400
+ } finally {
1401
+ client.destroy();
1402
+ }
1403
+ }
1404
+
1405
+ /**
1406
+ * Get slot production stats (uses default RPC)
1407
+ * @returns {Promise<Object>} Slot production stats
1408
+ */
1409
+ async function getSlotProduction() {
1410
+ const client = new AetherClient();
1411
+ try {
1412
+ return await client.getSlotProduction();
1413
+ } finally {
1414
+ client.destroy();
1415
+ }
1416
+ }
1417
+
1418
+ /**
1419
+ * Get account info (uses default RPC)
1420
+ * @param {string} address - Account address
1421
+ * @returns {Promise<Object>} Account info
1422
+ */
1423
+ async function getAccount(address) {
1424
+ const client = new AetherClient();
1425
+ try {
1426
+ return await client.getAccount(address);
1427
+ } finally {
1428
+ client.destroy();
1429
+ }
1430
+ }
1431
+
1432
+ /**
1433
+ * Get stake positions (uses default RPC)
1434
+ * @param {string} address - Account address
1435
+ * @returns {Promise<Array>} Stake positions
1436
+ */
1437
+ async function getStakePositions(address) {
1438
+ const client = new AetherClient();
1439
+ try {
1440
+ return await client.getStakePositions(address);
1441
+ } finally {
1442
+ client.destroy();
1443
+ }
1444
+ }
1445
+
1446
+ /**
1447
+ * Get rewards info (uses default RPC)
1448
+ * @param {string} address - Account address
1449
+ * @returns {Promise<Object>} Rewards info
1450
+ */
1451
+ async function getRewards(address) {
1452
+ const client = new AetherClient();
1453
+ try {
1454
+ return await client.getRewards(address);
1455
+ } finally {
1456
+ client.destroy();
1457
+ }
1458
+ }
1459
+
1460
+ /**
1461
+ * Get validator APY (uses default RPC)
1462
+ * @param {string} validatorAddr - Validator address
1463
+ * @returns {Promise<Object>} APY info
1464
+ */
1465
+ async function getValidatorAPY(validatorAddr) {
1466
+ const client = new AetherClient();
1467
+ try {
1468
+ return await client.getValidatorAPY(validatorAddr);
1469
+ } finally {
1470
+ client.destroy();
1471
+ }
1472
+ }
1473
+
1474
+ /**
1475
+ * Get transaction by signature (uses default RPC)
1476
+ * @param {string} signature - Transaction signature
1477
+ * @returns {Promise<Object>} Transaction info
1478
+ */
1479
+ async function getTransaction(signature) {
1480
+ const client = new AetherClient();
1481
+ try {
1482
+ return await client.getTransaction(signature);
1483
+ } finally {
1484
+ client.destroy();
1485
+ }
1486
+ }
1487
+
1488
+ /**
1489
+ * Get recent transactions (uses default RPC)
1490
+ * @param {string} address - Account address
1491
+ * @param {number} limit - Max transactions
1492
+ * @returns {Promise<Array>} Recent transactions
1493
+ */
1494
+ async function getRecentTransactions(address, limit = 20) {
1495
+ const client = new AetherClient();
1496
+ try {
1497
+ return await client.getRecentTransactions(address, limit);
1498
+ } finally {
1499
+ client.destroy();
1500
+ }
1501
+ }
1502
+
1503
+ /**
1504
+ * Get transaction history with signatures for an address (uses default RPC)
1505
+ * @param {string} address - Account address
1506
+ * @param {number} limit - Max transactions
1507
+ * @returns {Promise<Object>} Transaction history with signatures and details
1508
+ */
1509
+ async function getTransactionHistory(address, limit = 20) {
1510
+ const client = new AetherClient();
1511
+ try {
1512
+ return await client.getTransactionHistory(address, limit);
1513
+ } finally {
1514
+ client.destroy();
1515
+ }
1516
+ }
1517
+
1518
+ /**
1519
+ * Get all SPL token accounts for a wallet (uses default RPC)
1520
+ * @param {string} address - Account address
1521
+ * @returns {Promise<Array>} Token accounts with mint, amount, decimals
1522
+ */
1523
+ async function getTokenAccounts(address) {
1524
+ const client = new AetherClient();
1525
+ try {
1526
+ return await client.getTokenAccounts(address);
1527
+ } finally {
1528
+ client.destroy();
1529
+ }
1530
+ }
1531
+
1532
+ /**
1533
+ * Get all stake accounts for a wallet (uses default RPC)
1534
+ * @param {string} address - Account address
1535
+ * @returns {Promise<Array>} Stake accounts list
1536
+ */
1537
+ async function getStakeAccounts(address) {
1538
+ const client = new AetherClient();
1539
+ try {
1540
+ return await client.getStakeAccounts(address);
1541
+ } finally {
1542
+ client.destroy();
1543
+ }
1544
+ }
1545
+
1546
+ /**
1547
+ * Send transaction (uses default RPC)
1548
+ * @param {Object} tx - Signed transaction
1549
+ * @returns {Promise<Object>} Transaction receipt
1550
+ */
1551
+ async function sendTransaction(tx) {
1552
+ const client = new AetherClient();
1553
+ try {
1554
+ return await client.sendTransaction(tx);
1555
+ } finally {
1556
+ client.destroy();
1557
+ }
1558
+ }
1559
+
1560
+ /**
1561
+ * Ping RPC endpoint
1562
+ * @param {string} rpcUrl - RPC URL to ping
1563
+ * @returns {Promise<Object>} Ping result with latency
1564
+ */
1565
+ async function ping(rpcUrl) {
1566
+ const client = new AetherClient({ rpcUrl });
1567
+ const start = Date.now();
1568
+ try {
1569
+ await client.getSlot();
1570
+ return { ok: true, latency: Date.now() - start, rpc: rpcUrl || DEFAULT_RPC_URL };
1571
+ } catch (err) {
1572
+ return { ok: false, error: err.message, rpc: rpcUrl || DEFAULT_RPC_URL };
1573
+ } finally {
1574
+ client.destroy();
1575
+ }
1576
+ }
1577
+
1578
+ // ============================================================
1579
+ // Exports
1580
+ // ============================================================
1581
+
1582
+ module.exports = {
1583
+ // Main class
1584
+ AetherClient,
1585
+
1586
+ // Error classes
1587
+ AetherSDKError,
1588
+ NetworkTimeoutError,
1589
+ RPCError,
1590
+ RateLimitError,
1591
+ CircuitBreakerOpenError,
1592
+
1593
+ // Factory function
1594
+ createClient,
1595
+
1596
+ // Convenience functions (all chain queries)
1597
+ getSlot,
1598
+ getBlockHeight,
1599
+ getEpoch,
1600
+ getAccount,
1601
+ getBalance,
1602
+ getTransaction,
1603
+ getRecentTransactions,
1604
+ getTransactionHistory,
1605
+ getTokenAccounts,
1606
+ getStakeAccounts,
1607
+ getValidators,
1608
+ getTPS,
1609
+ getSupply,
1610
+ getSlotProduction,
1611
+ getFees,
1612
+ getStakePositions,
1613
+ getRewards,
1614
+ getValidatorAPY,
1615
+ getPeers,
1616
+ getHealth,
1617
+
1618
+ // Transactions
1619
+ sendTransaction,
1620
+
1621
+ // Utilities
1622
+ ping,
1623
+
1624
+ // Low-level RPC
1625
+ rpcGet,
1626
+ rpcPost,
1627
+
1628
+ // Constants
1629
+ DEFAULT_RPC_URL,
1630
+ DEFAULT_TIMEOUT_MS,
1631
+ DEFAULT_RETRY_ATTEMPTS,
1632
+ DEFAULT_RETRY_DELAY_MS,
1633
+ DEFAULT_BACKOFF_MULTIPLIER,
1634
+ DEFAULT_MAX_RETRY_DELAY_MS,
1635
+ DEFAULT_RATE_LIMIT_RPS,
1636
+ DEFAULT_RATE_LIMIT_BURST,
1637
+ DEFAULT_CIRCUIT_BREAKER_THRESHOLD,
1638
+ DEFAULT_CIRCUIT_BREAKER_RESET_MS,
1639
+ };