@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/lib/errors.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-cli lib/errors.js
|
|
3
|
+
*
|
|
4
|
+
* Centralized error handling for production-grade CLI experience.
|
|
5
|
+
* Provides consistent error formatting, retry logic, and user-friendly messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ANSI colours
|
|
9
|
+
const C = {
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
bright: '\x1b[1m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
red: '\x1b[31m',
|
|
14
|
+
green: '\x1b[32m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
cyan: '\x1b[36m',
|
|
17
|
+
magenta: '\x1b[35m',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error categories for user-friendly messaging
|
|
22
|
+
*/
|
|
23
|
+
const ERROR_CATEGORIES = {
|
|
24
|
+
NETWORK: 'network',
|
|
25
|
+
RPC: 'rpc',
|
|
26
|
+
VALIDATION: 'validation',
|
|
27
|
+
CONFIG: 'config',
|
|
28
|
+
WALLET: 'wallet',
|
|
29
|
+
PERMISSION: 'permission',
|
|
30
|
+
TIMEOUT: 'timeout',
|
|
31
|
+
UNKNOWN: 'unknown',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* User-friendly error messages by category
|
|
36
|
+
*/
|
|
37
|
+
const ERROR_MESSAGES = {
|
|
38
|
+
[ERROR_CATEGORIES.NETWORK]: {
|
|
39
|
+
title: 'Network Error',
|
|
40
|
+
description: 'Unable to reach the Aether network.',
|
|
41
|
+
suggestions: [
|
|
42
|
+
'Check your internet connection',
|
|
43
|
+
'Verify the RPC endpoint is accessible',
|
|
44
|
+
'Try using --rpc to specify a different endpoint',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
[ERROR_CATEGORIES.RPC]: {
|
|
48
|
+
title: 'RPC Error',
|
|
49
|
+
description: 'The Aether node returned an error.',
|
|
50
|
+
suggestions: [
|
|
51
|
+
'The node may be syncing - try again in a few moments',
|
|
52
|
+
'Check if the RPC endpoint is correct',
|
|
53
|
+
'Try a different RPC endpoint with --rpc <url>',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
[ERROR_CATEGORIES.VALIDATION]: {
|
|
57
|
+
title: 'Validation Error',
|
|
58
|
+
description: 'Invalid input provided.',
|
|
59
|
+
suggestions: [
|
|
60
|
+
'Check the command syntax with --help',
|
|
61
|
+
'Verify addresses are valid base58 format',
|
|
62
|
+
'Ensure amounts are positive numbers',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
[ERROR_CATEGORIES.CONFIG]: {
|
|
66
|
+
title: 'Configuration Error',
|
|
67
|
+
description: 'CLI configuration issue detected.',
|
|
68
|
+
suggestions: [
|
|
69
|
+
'Run "aether config init" to create default config',
|
|
70
|
+
'Check ~/.aether/config.json exists and is valid JSON',
|
|
71
|
+
'Use "aether config validate" to check settings',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
[ERROR_CATEGORIES.WALLET]: {
|
|
75
|
+
title: 'Wallet Error',
|
|
76
|
+
description: 'Unable to access wallet.',
|
|
77
|
+
suggestions: [
|
|
78
|
+
'Ensure you have created a wallet: aether wallet create',
|
|
79
|
+
'Check the wallet address is correct',
|
|
80
|
+
'Verify wallet files exist in ~/.aether/wallets/',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
[ERROR_CATEGORIES.PERMISSION]: {
|
|
84
|
+
title: 'Permission Error',
|
|
85
|
+
description: 'Insufficient permissions.',
|
|
86
|
+
suggestions: [
|
|
87
|
+
'Check file permissions in ~/.aether/',
|
|
88
|
+
'Ensure you own the wallet files',
|
|
89
|
+
'Try running with appropriate privileges',
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
[ERROR_CATEGORIES.TIMEOUT]: {
|
|
93
|
+
title: 'Request Timeout',
|
|
94
|
+
description: 'The request took too long to complete.',
|
|
95
|
+
suggestions: [
|
|
96
|
+
'The network may be congested - try again',
|
|
97
|
+
'Increase timeout with AETHER_RPC_TIMEOUT env var',
|
|
98
|
+
'Check if the RPC endpoint is responsive',
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
[ERROR_CATEGORIES.UNKNOWN]: {
|
|
102
|
+
title: 'Unexpected Error',
|
|
103
|
+
description: 'An unexpected error occurred.',
|
|
104
|
+
suggestions: [
|
|
105
|
+
'Try the command again',
|
|
106
|
+
'Check logs with --verbose flag',
|
|
107
|
+
'Report this issue with the error details',
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Categorize an error based on its properties
|
|
114
|
+
*/
|
|
115
|
+
function categorizeError(error) {
|
|
116
|
+
if (!error) return ERROR_CATEGORIES.UNKNOWN;
|
|
117
|
+
|
|
118
|
+
const message = (error.message || '').toLowerCase();
|
|
119
|
+
const code = error.code || '';
|
|
120
|
+
|
|
121
|
+
// Network errors
|
|
122
|
+
if (message.includes('enetunreach') ||
|
|
123
|
+
message.includes('econnrefused') ||
|
|
124
|
+
message.includes('econnreset') ||
|
|
125
|
+
message.includes('socket') ||
|
|
126
|
+
message.includes('network') ||
|
|
127
|
+
code === 'ECONNREFUSED' ||
|
|
128
|
+
code === 'ENOTFOUND' ||
|
|
129
|
+
code === 'ENETUNREACH') {
|
|
130
|
+
return ERROR_CATEGORIES.NETWORK;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// RPC errors
|
|
134
|
+
if (message.includes('rpc') ||
|
|
135
|
+
message.includes('json-rpc') ||
|
|
136
|
+
message.includes('-32000') ||
|
|
137
|
+
message.includes('method not found') ||
|
|
138
|
+
message.includes('invalid params') ||
|
|
139
|
+
error.statusCode >= 500) {
|
|
140
|
+
return ERROR_CATEGORIES.RPC;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Validation errors
|
|
144
|
+
if (message.includes('invalid') ||
|
|
145
|
+
message.includes('required') ||
|
|
146
|
+
message.includes('must be') ||
|
|
147
|
+
message.includes('cannot be') ||
|
|
148
|
+
message.includes('bad request') ||
|
|
149
|
+
error.statusCode === 400) {
|
|
150
|
+
return ERROR_CATEGORIES.VALIDATION;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Config errors
|
|
154
|
+
if (message.includes('config') ||
|
|
155
|
+
message.includes('enoent') && message.includes('config')) {
|
|
156
|
+
return ERROR_CATEGORIES.CONFIG;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Wallet errors
|
|
160
|
+
if (message.includes('wallet') ||
|
|
161
|
+
message.includes('keypair') ||
|
|
162
|
+
message.includes('mnemonic') ||
|
|
163
|
+
message.includes('signature')) {
|
|
164
|
+
return ERROR_CATEGORIES.WALLET;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Permission errors
|
|
168
|
+
if (message.includes('eperm') ||
|
|
169
|
+
message.includes('eacces') ||
|
|
170
|
+
code === 'EACCES' ||
|
|
171
|
+
code === 'EPERM') {
|
|
172
|
+
return ERROR_CATEGORIES.PERMISSION;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Timeout errors
|
|
176
|
+
if (message.includes('timeout') ||
|
|
177
|
+
message.includes('etimedout') ||
|
|
178
|
+
code === 'ETIMEDOUT') {
|
|
179
|
+
return ERROR_CATEGORIES.TIMEOUT;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return ERROR_CATEGORIES.UNKNOWN;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format error for display
|
|
187
|
+
*/
|
|
188
|
+
function formatError(error, options = {}) {
|
|
189
|
+
const { verbose = false, exit = true } = options;
|
|
190
|
+
const category = categorizeError(error);
|
|
191
|
+
const template = ERROR_MESSAGES[category];
|
|
192
|
+
|
|
193
|
+
let output = '\n';
|
|
194
|
+
output += `${C.red}${C.bright}✖ ${template.title}${C.reset}\n`;
|
|
195
|
+
output += `${C.dim}${template.description}${C.reset}\n\n`;
|
|
196
|
+
|
|
197
|
+
if (error.message) {
|
|
198
|
+
output += `${C.yellow}Error: ${error.message}${C.reset}\n`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (verbose && error.stack) {
|
|
202
|
+
output += `\n${C.dim}Stack trace:${C.reset}\n`;
|
|
203
|
+
output += `${C.dim}${error.stack}${C.reset}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
output += `\n${C.bright}Suggestions:${C.reset}\n`;
|
|
207
|
+
template.suggestions.forEach(suggestion => {
|
|
208
|
+
output += ` ${C.cyan}•${C.reset} ${suggestion}\n`;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return output;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Display error and optionally exit
|
|
216
|
+
*/
|
|
217
|
+
function displayError(error, options = {}) {
|
|
218
|
+
const { exit = true, exitCode = 1, verbose = process.env.AETHER_VERBOSE === '1' } = options;
|
|
219
|
+
|
|
220
|
+
console.error(formatError(error, { verbose }));
|
|
221
|
+
|
|
222
|
+
if (exit) {
|
|
223
|
+
process.exit(exitCode);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Wrap an async function with error handling
|
|
229
|
+
*/
|
|
230
|
+
function withErrorHandling(fn, options = {}) {
|
|
231
|
+
return async function(...args) {
|
|
232
|
+
try {
|
|
233
|
+
return await fn(...args);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
displayError(error, options);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Retry an async operation with exponential backoff
|
|
243
|
+
*/
|
|
244
|
+
async function withRetry(operation, options = {}) {
|
|
245
|
+
const {
|
|
246
|
+
maxRetries = 3,
|
|
247
|
+
initialDelay = 1000,
|
|
248
|
+
maxDelay = 30000,
|
|
249
|
+
backoffMultiplier = 2,
|
|
250
|
+
retryableErrors = null, // null = retry all
|
|
251
|
+
onRetry = null,
|
|
252
|
+
} = options;
|
|
253
|
+
|
|
254
|
+
let lastError;
|
|
255
|
+
let delay = initialDelay;
|
|
256
|
+
|
|
257
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
return await operation();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
lastError = error;
|
|
262
|
+
|
|
263
|
+
// Check if we should retry this error
|
|
264
|
+
if (retryableErrors && !retryableErrors.some(e => error instanceof e)) {
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Don't retry on validation errors
|
|
269
|
+
const category = categorizeError(error);
|
|
270
|
+
if (category === ERROR_CATEGORIES.VALIDATION) {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (attempt < maxRetries) {
|
|
275
|
+
if (onRetry) {
|
|
276
|
+
onRetry(error, attempt + 1, maxRetries);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await sleep(delay);
|
|
280
|
+
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw lastError;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Sleep utility
|
|
290
|
+
*/
|
|
291
|
+
function sleep(ms) {
|
|
292
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create a RPC call wrapper with retry logic
|
|
297
|
+
*/
|
|
298
|
+
function createRpcCaller(clientMethod, options = {}) {
|
|
299
|
+
return async function(...args) {
|
|
300
|
+
return withRetry(
|
|
301
|
+
() => clientMethod(...args),
|
|
302
|
+
{
|
|
303
|
+
maxRetries: 3,
|
|
304
|
+
initialDelay: 1000,
|
|
305
|
+
onRetry: (error, attempt, max) => {
|
|
306
|
+
console.log(
|
|
307
|
+
`${C.yellow}⚠ RPC call failed (attempt ${attempt}/${max}): ${error.message}${C.reset}`
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
...options,
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Validate required arguments
|
|
318
|
+
*/
|
|
319
|
+
function validateRequired(args, requirements) {
|
|
320
|
+
const missing = [];
|
|
321
|
+
|
|
322
|
+
for (const requirement of requirements) {
|
|
323
|
+
if (requirement.validator ? !requirement.validator(args) : !args[requirement.name]) {
|
|
324
|
+
missing.push(requirement.displayName || requirement.name);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (missing.length > 0) {
|
|
329
|
+
const error = new Error(`Missing required arguments: ${missing.join(', ')}`);
|
|
330
|
+
error.isValidationError = true;
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Validate address format (base58)
|
|
337
|
+
*/
|
|
338
|
+
function validateAddress(address, fieldName = 'address') {
|
|
339
|
+
if (!address) {
|
|
340
|
+
throw new Error(`${fieldName} is required`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Base58 regex (simplified)
|
|
344
|
+
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]+$/;
|
|
345
|
+
if (!base58Regex.test(address)) {
|
|
346
|
+
throw new Error(`Invalid ${fieldName} format: must be base58 encoded`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Typical Solana/Aether addresses are 32-44 chars
|
|
350
|
+
if (address.length < 32 || address.length > 44) {
|
|
351
|
+
throw new Error(`Invalid ${fieldName} length: expected 32-44 characters, got ${address.length}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Validate amount (positive number)
|
|
359
|
+
*/
|
|
360
|
+
function validateAmount(amount, fieldName = 'amount') {
|
|
361
|
+
if (amount === undefined || amount === null || amount === '') {
|
|
362
|
+
throw new Error(`${fieldName} is required`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const num = Number(amount);
|
|
366
|
+
if (isNaN(num) || !isFinite(num)) {
|
|
367
|
+
throw new Error(`Invalid ${fieldName}: must be a valid number`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (num <= 0) {
|
|
371
|
+
throw new Error(`Invalid ${fieldName}: must be greater than 0`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return num;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
// Constants
|
|
379
|
+
ERROR_CATEGORIES,
|
|
380
|
+
ERROR_MESSAGES,
|
|
381
|
+
|
|
382
|
+
// Core functions
|
|
383
|
+
categorizeError,
|
|
384
|
+
formatError,
|
|
385
|
+
displayError,
|
|
386
|
+
withErrorHandling,
|
|
387
|
+
withRetry,
|
|
388
|
+
createRpcCaller,
|
|
389
|
+
|
|
390
|
+
// Validation
|
|
391
|
+
validateRequired,
|
|
392
|
+
validateAddress,
|
|
393
|
+
validateAmount,
|
|
394
|
+
|
|
395
|
+
// Utilities
|
|
396
|
+
sleep,
|
|
397
|
+
C,
|
|
398
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jellylegsai/aether-cli",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Aether CLI - Validator Onboarding, Staking, and Blockchain Operations",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aether-cli": "./index.js",
|
|
8
|
+
"aether": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node test.js && cd sdk && node test.js",
|
|
12
|
+
"lint": "echo 'No linting configured'",
|
|
13
|
+
"doctor": "node index.js doctor",
|
|
14
|
+
"init": "node index.js init",
|
|
15
|
+
"monitor": "node index.js monitor",
|
|
16
|
+
"logs": "node index.js logs",
|
|
17
|
+
"sdk": "node index.js sdk",
|
|
18
|
+
"sdk-test": "node index.js sdk-test",
|
|
19
|
+
"wallet": "node index.js wallet",
|
|
20
|
+
"stake": "node index.js stake",
|
|
21
|
+
"stake-positions": "node index.js stake-positions",
|
|
22
|
+
"unstake": "node index.js unstake",
|
|
23
|
+
"claim": "node index.js claim",
|
|
24
|
+
"delegations": "node index.js delegations",
|
|
25
|
+
"rewards": "node index.js rewards",
|
|
26
|
+
"validators": "node index.js validators",
|
|
27
|
+
"validator": "node index.js validator",
|
|
28
|
+
"validator-register": "node index.js validator-register",
|
|
29
|
+
"validator-start": "node index.js validator-start",
|
|
30
|
+
"validator-status": "node index.js validator-status",
|
|
31
|
+
"validator-info": "node index.js validator-info",
|
|
32
|
+
"multisig": "node index.js multisig",
|
|
33
|
+
"transfer": "node index.js transfer",
|
|
34
|
+
"balance": "node index.js balance",
|
|
35
|
+
"tx-history": "node index.js tx-history",
|
|
36
|
+
"tx": "node index.js tx-history",
|
|
37
|
+
"network": "node index.js network",
|
|
38
|
+
"epoch": "node index.js epoch",
|
|
39
|
+
"supply": "node index.js supply",
|
|
40
|
+
"status": "node index.js status",
|
|
41
|
+
"blockhash": "node index.js blockhash",
|
|
42
|
+
"tps": "node index.js tps",
|
|
43
|
+
"fees": "node index.js fees",
|
|
44
|
+
"apy": "node index.js apy",
|
|
45
|
+
"account": "node index.js account",
|
|
46
|
+
"price": "node index.js price",
|
|
47
|
+
"emergency": "node index.js emergency",
|
|
48
|
+
"snapshot": "node index.js snapshot",
|
|
49
|
+
"nft": "node index.js nft",
|
|
50
|
+
"prepare-publish": "npm pack && echo 'Package ready for publishing'",
|
|
51
|
+
"version-bump": "node -e \"const fs=require('fs');let p=JSON.parse(fs.readFileSync('package.json'));let v=p.version.split('.');v[2]=parseInt(v[2])+1;p.version=v.join('.');fs.writeFileSync('package.json',JSON.stringify(p,null,2));console.log('Version bumped to',p.version);\""
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"aether",
|
|
55
|
+
"blockchain",
|
|
56
|
+
"validator",
|
|
57
|
+
"staking",
|
|
58
|
+
"cryptocurrency",
|
|
59
|
+
"multi-signature",
|
|
60
|
+
"cli"
|
|
61
|
+
],
|
|
62
|
+
"author": "Jelly-legs AI Team",
|
|
63
|
+
"license": "MIT",
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "https://github.com/jelly-legs-ai/Jelly-legs-unsteady-workshop"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=14.0.0"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"bip39": "^3.0.4",
|
|
73
|
+
"tweetnacl": "^1.0.3",
|
|
74
|
+
"bs58": "^5.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/sdk/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @jellylegsai/aether-sdk
|
|
2
|
+
|
|
3
|
+
Official Aether Blockchain SDK for Node.js. Every function makes **REAL HTTP RPC calls** to the Aether blockchain at `http://127.0.0.1:8899`. No stubs, no mocks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @jellylegsai/aether-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use locally from the `sdk/` folder in this repo:
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const aether = require('./sdk');
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const aether = require('@jellylegsai/aether-sdk');
|
|
21
|
+
|
|
22
|
+
// Get current slot
|
|
23
|
+
const slot = await aether.getSlot();
|
|
24
|
+
console.log('Current slot:', slot);
|
|
25
|
+
|
|
26
|
+
// Get account balance
|
|
27
|
+
const balance = await aether.getBalance('ATH...');
|
|
28
|
+
console.log('Balance:', balance);
|
|
29
|
+
|
|
30
|
+
// Use custom RPC endpoint
|
|
31
|
+
const client = new aether.AetherClient({ rpcUrl: 'http://127.0.0.1:8899' });
|
|
32
|
+
const epoch = await client.getEpochInfo();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## SDK Architecture
|
|
36
|
+
|
|
37
|
+
- **`AetherClient`** — Full-featured blockchain client class. All methods make real HTTP calls.
|
|
38
|
+
- **Convenience functions** — Top-level exports for one-off queries using the default RPC.
|
|
39
|
+
- **Low-level RPC helpers** — `rpcGet` / `rpcPost` for custom requests.
|
|
40
|
+
|
|
41
|
+
Default RPC: `http://127.0.0.1:8899` (override via `AETHER_RPC` env or constructor option).
|
|
42
|
+
|
|
43
|
+
## AetherClient
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const aether = require('@jellylegsai/aether-sdk');
|
|
47
|
+
|
|
48
|
+
const client = new aether.AetherClient({
|
|
49
|
+
rpcUrl: 'http://127.0.0.1:8899', // default
|
|
50
|
+
timeoutMs: 10000, // default: 10s
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API Reference
|
|
55
|
+
|
|
56
|
+
### Chain Queries
|
|
57
|
+
|
|
58
|
+
| Method | Returns | RPC Endpoint |
|
|
59
|
+
|--------|---------|--------------|
|
|
60
|
+
| `getSlot()` | `number` — current slot | `GET /v1/slot` |
|
|
61
|
+
| `getBlockHeight()` | `number` — block height | `GET /v1/blockheight` |
|
|
62
|
+
| `getEpochInfo()` | `object` — epoch + slot info | `GET /v1/epoch` |
|
|
63
|
+
| `getAccountInfo(address)` | `object` — lamports, owner, data | `GET /v1/account/:address` |
|
|
64
|
+
| `getBalance(address)` | `number` — lamports | `GET /v1/account/:address` |
|
|
65
|
+
| `getTransaction(signature)` | `object` — tx details | `GET /v1/transaction/:sig` |
|
|
66
|
+
| `getRecentTransactions(address, limit?)` | `array` — recent txs | `GET /v1/transactions/:address` |
|
|
67
|
+
| `getTokenAccounts(address)` | `array` — SPL token accounts | `GET /v1/tokens/:address` |
|
|
68
|
+
| `getStakeAccounts(address)` | `array` — stake accounts | `GET /v1/stake-accounts/:address` |
|
|
69
|
+
| `getStakePositions(address)` | `array` — delegations | `GET /v1/stake/:address` |
|
|
70
|
+
| `getRewards(address)` | `object` — staking rewards | `GET /v1/rewards/:address` |
|
|
71
|
+
| `getValidators()` | `array` — validator list | `GET /v1/validators` |
|
|
72
|
+
| `getValidatorAPY(validatorAddr)` | `object` — APY data | `GET /v1/validator/:addr/apy` |
|
|
73
|
+
| `getTPS()` | `number` — TPS | `GET /v1/tps` |
|
|
74
|
+
| `getSupply()` | `object` — total/circulating/non-circulating | `GET /v1/supply` |
|
|
75
|
+
| `getFees()` | `object` — fee estimates | `GET /v1/fees` |
|
|
76
|
+
| `getSlotProduction()` | `object` — slot production | `POST /v1/slot_production` |
|
|
77
|
+
| `getClusterPeers()` | `array` — peer list | `GET /v1/peers` |
|
|
78
|
+
| `getHealth()` | `string` — "ok" if healthy | `GET /v1/health` |
|
|
79
|
+
| `getVersion()` | `object` — node version | `GET /v1/version` |
|
|
80
|
+
|
|
81
|
+
### Blockhash (required for signing)
|
|
82
|
+
|
|
83
|
+
| Method | Returns | RPC Endpoint |
|
|
84
|
+
|--------|---------|--------------|
|
|
85
|
+
| `getRecentBlockhash()` | `{ blockhash, lastValidBlockHeight }` | `GET /v1/recent-blockhash` |
|
|
86
|
+
|
|
87
|
+
### Transaction Submission
|
|
88
|
+
|
|
89
|
+
| Method | Returns | RPC Endpoint |
|
|
90
|
+
|--------|---------|--------------|
|
|
91
|
+
| `sendTransaction(tx)` | `object` — receipt | `POST /v1/transaction` |
|
|
92
|
+
|
|
93
|
+
### Transaction Builders (build + submit)
|
|
94
|
+
|
|
95
|
+
| Method | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| `transfer({ from, to, amount, nonce, signFn })` | Build + send a Transfer TX |
|
|
98
|
+
| `stake({ staker, validator, amount, signFn })` | Build + send a Stake TX |
|
|
99
|
+
| `unstake({ stakeAccount, amount, signFn })` | Build + send an Unstake TX |
|
|
100
|
+
| `claimRewards({ stakeAccount, signFn })` | Build + send a ClaimRewards TX |
|
|
101
|
+
| `createNFT({ creator, metadataUrl, royalties, signFn })` | Build + send a CreateNFT TX |
|
|
102
|
+
| `transferNFT({ from, nftId, to, signFn })` | Build + send a TransferNFT TX |
|
|
103
|
+
| `updateMetadata({ creator, nftId, metadataUrl, signFn })` | Build + send an UpdateMetadata TX |
|
|
104
|
+
|
|
105
|
+
Each builder fetches a fresh `blockhash` + current `slot` from the chain, then submits the signed transaction via `sendTransaction()`. All steps are real RPC calls.
|
|
106
|
+
|
|
107
|
+
## Convenience Functions
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// All of these use the default RPC (http://127.0.0.1:8899)
|
|
111
|
+
const slot = await aether.getSlot();
|
|
112
|
+
const balance = await aether.getBalance('ATH...');
|
|
113
|
+
const epoch = await aether.getEpoch();
|
|
114
|
+
const tps = await aether.getTPS();
|
|
115
|
+
const supply = await aether.getSupply();
|
|
116
|
+
const fees = await aether.getFees();
|
|
117
|
+
const validators = await aether.getValidators();
|
|
118
|
+
const peers = await aether.getPeers();
|
|
119
|
+
const health = await aether.getHealth();
|
|
120
|
+
const { blockhash } = await aether.getRecentBlockhash();
|
|
121
|
+
const tx = await aether.getTransaction('sig...');
|
|
122
|
+
const txs = await aether.getRecentTransactions('ATH...', 20);
|
|
123
|
+
const stakePos = await aether.getStakePositions('ATH...');
|
|
124
|
+
const rewards = await aether.getRewards('ATH...');
|
|
125
|
+
const tokenAccts = await aether.getTokenAccounts('ATH...');
|
|
126
|
+
const stakeAccts = await aether.getStakeAccounts('ATH...');
|
|
127
|
+
const apy = await aether.getValidatorAPY('validatorAddr');
|
|
128
|
+
const pingResult = await aether.ping(); // { ok, latency, rpc }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Utilities
|
|
132
|
+
|
|
133
|
+
| Function | Description |
|
|
134
|
+
|----------|-------------|
|
|
135
|
+
| `createClient(options)` | Factory: create an `AetherClient` instance |
|
|
136
|
+
| `ping(rpcUrl?)` | Ping RPC, returns `{ ok, latency, rpc }` |
|
|
137
|
+
| `rpcGet(rpcUrl, path, timeout?)` | Low-level GET |
|
|
138
|
+
| `rpcPost(rpcUrl, path, body, timeout?)` | Low-level POST |
|
|
139
|
+
| `DEFAULT_RPC_URL` | `"http://127.0.0.1:8899"` |
|
|
140
|
+
| `DEFAULT_TIMEOUT_MS` | `10000` |
|
|
141
|
+
|
|
142
|
+
## CLI Integration
|
|
143
|
+
|
|
144
|
+
The SDK is wired into these CLI commands in `aether-cli`:
|
|
145
|
+
|
|
146
|
+
| CLI Command | SDK Method Used |
|
|
147
|
+
|-------------|----------------|
|
|
148
|
+
| `aether status` | `getEpochInfo`, `getSupply`, `getSlot`, `getBlockHeight`, `getClusterPeers`, `getVersion`, `getStakePositions`, `getRewards` |
|
|
149
|
+
| `aether network` | `getSlot`, `getBlockHeight`, `getClusterPeers`, `getValidators`, `getEpochInfo`, `getSupply`, `getTPS` |
|
|
150
|
+
| `aether blockhash` | `getRecentBlockhash` |
|
|
151
|
+
| `aether fees` | `getFees`, `getRecentBlockhash` |
|
|
152
|
+
| `aether tps` | `getTPS`, `getSlot` |
|
|
153
|
+
| `aether account` | `getAccountInfo`, `getBalance`, `getRecentTransactions`, `getStakePositions`, `getTokenAccounts`, `getStakeAccounts` |
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Environment variable
|
|
159
|
+
export AETHER_RPC=http://127.0.0.1:8899
|
|
160
|
+
|
|
161
|
+
# Or per-call
|
|
162
|
+
const client = new aether.AetherClient({ rpcUrl: 'http://custom:8899' });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Example: Full Dashboard
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
const aether = require('@jellylegsai/aether-sdk');
|
|
169
|
+
|
|
170
|
+
async function dashboard() {
|
|
171
|
+
const [slot, blockHeight, tps, supply, { blockhash }] = await Promise.all([
|
|
172
|
+
aether.getSlot(),
|
|
173
|
+
aether.getBlockHeight(),
|
|
174
|
+
aether.getTPS(),
|
|
175
|
+
aether.getSupply(),
|
|
176
|
+
aether.getRecentBlockhash(),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
console.log('=== Aether Network Dashboard ===');
|
|
180
|
+
console.log(`Slot: ${slot}`);
|
|
181
|
+
console.log(`Block: ${blockHeight}`);
|
|
182
|
+
console.log(`TPS: ${tps}`);
|
|
183
|
+
console.log(`Total Supply: ${supply.total}`);
|
|
184
|
+
console.log(`Blockhash: ${blockhash}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
dashboard();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Example: Check Staking Rewards
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const aether = require('@jellylegsai/aether-sdk');
|
|
194
|
+
|
|
195
|
+
async function checkRewards(walletAddress) {
|
|
196
|
+
const [stakePositions, rewards] = await Promise.all([
|
|
197
|
+
aether.getStakePositions(walletAddress),
|
|
198
|
+
aether.getRewards(walletAddress),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
console.log(`Stake positions: ${stakePositions.length}`);
|
|
202
|
+
console.log(`Total rewards: ${rewards.total || rewards.amount} lamports`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
checkRewards('ATH...');
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT © Jelly-legs AI Team
|