@jellylegsai/aether-cli 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- package/validator-identity.json +4 -0
package/sdk/index.js
ADDED
|
@@ -0,0 +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
|
+
};
|