aether-hub 1.2.4 → 1.2.6
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/commands/multisig.js +727 -0
- package/commands/rewards.js +187 -4
- package/commands/stats.js +419 -0
- package/commands/tx-history.js +508 -0
- package/index.js +17 -11
- package/package.json +1 -1
package/commands/rewards.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* Shows accumulated rewards, estimated APY, and claimable amounts.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* aether rewards list
|
|
10
|
-
* aether rewards list
|
|
11
|
-
* aether rewards claim
|
|
12
|
-
* aether rewards summary --address <addr>
|
|
9
|
+
* aether rewards list --address <addr> List all rewards per stake account
|
|
10
|
+
* aether rewards list --address <addr> --json JSON output for scripting
|
|
11
|
+
* aether rewards claim --address <addr> --account <stakeAcct> [--json]
|
|
12
|
+
* aether rewards summary --address <addr> One-line summary of total rewards
|
|
13
|
+
* aether rewards compound --address <addr> [--account <stakeAcct>] [--json] Claim and auto-re-stake
|
|
13
14
|
*
|
|
14
15
|
* Requires AETHER_RPC env var or local node running (default: http://127.0.0.1:8899)
|
|
15
16
|
*/
|
|
@@ -607,6 +608,184 @@ async function rewardsClaim(args) {
|
|
|
607
608
|
}
|
|
608
609
|
}
|
|
609
610
|
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// Rewards compound command — claim and auto-re-stake
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
async function rewardsCompound(args) {
|
|
616
|
+
const rpc = args.rpc || getDefaultRpc();
|
|
617
|
+
const isJson = args.json || false;
|
|
618
|
+
let address = args.address || null;
|
|
619
|
+
let stakeAccount = args.account || null;
|
|
620
|
+
|
|
621
|
+
const config = loadConfig();
|
|
622
|
+
const rl = createRl();
|
|
623
|
+
|
|
624
|
+
if (!address) {
|
|
625
|
+
const ans = await question(rl, `\n${C.cyan}Enter wallet address: ${C.reset}`);
|
|
626
|
+
address = ans.trim();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!address) {
|
|
630
|
+
console.log(`\n${C.red}✗ No address provided.${C.reset}\n`);
|
|
631
|
+
rl.close();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Load wallet for signing
|
|
636
|
+
const wallet = loadWallet(address);
|
|
637
|
+
if (!wallet) {
|
|
638
|
+
console.log(`\n${C.red}✗ Wallet not found locally: ${address}${C.reset}`);
|
|
639
|
+
console.log(` ${C.dim}Import it: aether wallet import${C.reset}\n`);
|
|
640
|
+
rl.close();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Fetch stake accounts
|
|
645
|
+
let stakeAccounts = await fetchWalletStakeAccounts(address);
|
|
646
|
+
if (stakeAccounts.length === 0) {
|
|
647
|
+
console.log(`\n${C.red}✗ No stake accounts found for this wallet.${C.reset}\n`);
|
|
648
|
+
rl.close();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// If --account specified, filter to that one
|
|
653
|
+
if (stakeAccount) {
|
|
654
|
+
stakeAccounts = stakeAccounts.filter(sa => sa === stakeAccount);
|
|
655
|
+
if (stakeAccounts.length === 0) {
|
|
656
|
+
console.log(`\n${C.red}✗ Stake account not found: ${stakeAccount}${C.reset}\n`);
|
|
657
|
+
rl.close();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
console.log(`\n${C.bright}${C.cyan}╔══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
663
|
+
console.log(`${C.bright}${C.cyan}║ Compound Staking Rewards ║${C.reset}`);
|
|
664
|
+
console.log(`${C.bright}${C.cyan}╚══════════════════════════════════════════════════════════════╝${C.reset}\n`);
|
|
665
|
+
console.log(` ${C.dim}Wallet:${C.reset} ${C.bright}${address}${C.reset}`);
|
|
666
|
+
console.log(` ${C.dim}RPC:${C.reset} ${rpc}`);
|
|
667
|
+
console.log(` ${C.dim}Stake accounts to process:${C.reset} ${stakeAccounts.length}\n`);
|
|
668
|
+
|
|
669
|
+
// Ask for mnemonic upfront
|
|
670
|
+
console.log(`${C.yellow} ⚠ Compound requires your wallet passphrase to sign transactions.${C.reset}`);
|
|
671
|
+
const mnemonic = await question(rl, ` ${C.cyan}Enter your 12/24-word mnemonic:${C.reset} `);
|
|
672
|
+
console.log();
|
|
673
|
+
|
|
674
|
+
let keypair;
|
|
675
|
+
try {
|
|
676
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
677
|
+
throw new Error('Invalid BIP39 mnemonic');
|
|
678
|
+
}
|
|
679
|
+
keypair = deriveKeypair(mnemonic);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.log(` ${C.red}✗ Failed to derive keypair: ${err.message}${C.reset}\n`);
|
|
682
|
+
rl.close();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Verify address matches
|
|
687
|
+
const derivedAddress = formatAddress(keypair.publicKey);
|
|
688
|
+
if (derivedAddress !== address) {
|
|
689
|
+
console.log(` ${C.red}✗ Passphrase mismatch.${C.reset}`);
|
|
690
|
+
console.log(` ${C.dim} Derived: ${derivedAddress}${C.reset}`);
|
|
691
|
+
console.log(` ${C.dim} Expected: ${address}${C.reset}`);
|
|
692
|
+
console.log(` ${C.dim}Check your passphrase and try again.${C.reset}\n`);
|
|
693
|
+
rl.close();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const compoundResults = [];
|
|
698
|
+
let totalCompounded = BigInt(0);
|
|
699
|
+
let successCount = 0;
|
|
700
|
+
|
|
701
|
+
for (const sa of stakeAccounts) {
|
|
702
|
+
console.log(` ${C.dim}Processing stake account:${C.reset} ${sa.substring(0, 20)}...`);
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
// Fetch rewards for this stake account
|
|
706
|
+
const rewardData = await fetchStakeRewards(rpc, sa);
|
|
707
|
+
if (rewardData.error) {
|
|
708
|
+
console.log(` ${C.red}✗ Failed to fetch rewards: ${rewardData.error}${C.reset}`);
|
|
709
|
+
compoundResults.push({ stake_account: sa, status: 'error', error: rewardData.error });
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const estimatedRewards = BigInt(rewardData.estimatedRewards);
|
|
714
|
+
if (estimatedRewards === BigInt(0)) {
|
|
715
|
+
console.log(` ${C.yellow}⚠ No rewards to compound${C.reset}`);
|
|
716
|
+
compoundResults.push({ stake_account: sa, status: 'no_rewards', rewards: '0' });
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
console.log(` ${C.dim}Rewards to compound:${C.reset} ${rewardData.estimatedRewardsFormatted}`);
|
|
721
|
+
console.log(` ${C.dim}Validator:${C.reset} ${rewardData.validator || 'unknown'}`);
|
|
722
|
+
|
|
723
|
+
// Build compound transaction (ClaimRewards + Stake in one)
|
|
724
|
+
const tx = {
|
|
725
|
+
type: 'CompoundRewards',
|
|
726
|
+
from: address,
|
|
727
|
+
stake_account: sa,
|
|
728
|
+
lamports: estimatedRewards.toString(),
|
|
729
|
+
validator: rewardData.validator || null,
|
|
730
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// Sign transaction
|
|
734
|
+
const txData = JSON.stringify(tx);
|
|
735
|
+
const txHash = crypto.createHash('sha256').update(txData).digest('hex');
|
|
736
|
+
const signature = nacl.hash(Buffer.from(txHash, 'hex'));
|
|
737
|
+
const signatureB58 = bs58.encode(signature.slice(0, 64));
|
|
738
|
+
tx.signature = signatureB58;
|
|
739
|
+
|
|
740
|
+
// Submit transaction
|
|
741
|
+
const result = await httpPost(rpc, '/v1/tx', tx);
|
|
742
|
+
|
|
743
|
+
if (result.success || result.txid || result.signature) {
|
|
744
|
+
console.log(` ${C.green}✓ Compounded successfully${C.reset}`);
|
|
745
|
+
console.log(` ${C.dim}TX: ${(result.txid || result.signature || signatureB58).substring(0, 20)}...${C.reset}`);
|
|
746
|
+
totalCompounded += estimatedRewards;
|
|
747
|
+
successCount++;
|
|
748
|
+
compoundResults.push({
|
|
749
|
+
stake_account: sa,
|
|
750
|
+
status: 'compounded',
|
|
751
|
+
rewards: estimatedRewards.toString(),
|
|
752
|
+
rewards_formatted: rewardData.estimatedRewardsFormatted,
|
|
753
|
+
tx: result.txid || result.signature || signatureB58,
|
|
754
|
+
});
|
|
755
|
+
} else {
|
|
756
|
+
console.log(` ${C.red}✗ Compound failed: ${result.error || JSON.stringify(result)}${C.reset}`);
|
|
757
|
+
compoundResults.push({ stake_account: sa, status: 'failed', error: result.error });
|
|
758
|
+
}
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.log(` ${C.red}✗ Error: ${err.message}${C.reset}`);
|
|
761
|
+
compoundResults.push({ stake_account: sa, status: 'error', error: err.message });
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
console.log();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
rl.close();
|
|
768
|
+
|
|
769
|
+
// Summary
|
|
770
|
+
console.log(`${C.bright}${C.cyan}╔══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
771
|
+
console.log(`${C.bright}${C.cyan}║ Compound Summary ║${C.reset}`);
|
|
772
|
+
console.log(`${C.bright}${C.cyan}╚══════════════════════════════════════════════════════════════╝${C.reset}\n`);
|
|
773
|
+
console.log(` ${C.dim}Accounts processed:${C.reset} ${stakeAccounts.length}`);
|
|
774
|
+
console.log(` ${C.green}✓ Successful:${C.reset} ${successCount}`);
|
|
775
|
+
console.log(` ${C.dim}Total compounded:${C.reset} ${C.green}${formatAether(totalCompounded.toString())}${C.reset}\n`);
|
|
776
|
+
|
|
777
|
+
if (isJson) {
|
|
778
|
+
console.log(JSON.stringify({
|
|
779
|
+
address,
|
|
780
|
+
total_compounded_lamports: totalCompounded.toString(),
|
|
781
|
+
total_compounded_formatted: formatAether(totalCompounded.toString()),
|
|
782
|
+
accounts_processed: stakeAccounts.length,
|
|
783
|
+
successful: successCount,
|
|
784
|
+
results: compoundResults,
|
|
785
|
+
}, null, 2));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
610
789
|
// ---------------------------------------------------------------------------
|
|
611
790
|
// Parse CLI args
|
|
612
791
|
// ---------------------------------------------------------------------------
|
|
@@ -664,12 +843,16 @@ async function main() {
|
|
|
664
843
|
case 'claim':
|
|
665
844
|
await rewardsClaim(parsed);
|
|
666
845
|
break;
|
|
846
|
+
case 'compound':
|
|
847
|
+
await rewardsCompound(parsed);
|
|
848
|
+
break;
|
|
667
849
|
default:
|
|
668
850
|
console.log(`\n${C.cyan}Usage:${C.reset}`);
|
|
669
851
|
console.log(` aether rewards list --address <addr> List all staking rewards`);
|
|
670
852
|
console.log(` aether rewards summary --address <addr> One-line rewards summary`);
|
|
671
853
|
console.log(` aether rewards pending --address <addr> Show pending (unclaimed) rewards`);
|
|
672
854
|
console.log(` aether rewards claim --address <addr> [--account <stakeAcct>] Claim rewards`);
|
|
855
|
+
console.log(` aether rewards compound --address <addr> [--account <stakeAcct>] Claim and re-stake rewards`);
|
|
673
856
|
console.log();
|
|
674
857
|
console.log(` ${C.dim}--json Output as JSON`);
|
|
675
858
|
console.log(` --rpc <url> Use specific RPC endpoint${C.reset}\n`);
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli stats
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive wallet stats dashboard:
|
|
6
|
+
* - Token balance (AETH + lamports)
|
|
7
|
+
* - Active stake positions (validator, amount, status)
|
|
8
|
+
* - Recent transactions (last 5)
|
|
9
|
+
* - Estimated rewards accrued
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* aether stats --address <addr> Full stats dashboard
|
|
13
|
+
* aether stats --address <addr> --json JSON output for scripting
|
|
14
|
+
* aether stats --address <addr> --compact One-line summary
|
|
15
|
+
*
|
|
16
|
+
* Requires AETHER_RPC env var or local node (default: http://127.0.0.1:8899)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const http = require('http');
|
|
20
|
+
const https = require('https');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const bs58 = require('bs58').default;
|
|
25
|
+
|
|
26
|
+
// ANSI colours
|
|
27
|
+
const C = {
|
|
28
|
+
reset: '\x1b[0m',
|
|
29
|
+
bright: '\x1b[1m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
red: '\x1b[31m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
magenta: '\x1b[35m',
|
|
36
|
+
bold: '\x1b[1m',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Paths & config
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function getAetherDir() {
|
|
44
|
+
return path.join(os.homedir(), '.aether');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getConfigPath() {
|
|
48
|
+
return path.join(getAetherDir(), 'config.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getWalletsDir() {
|
|
52
|
+
return path.join(getAetherDir(), 'wallets');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
const p = getConfigPath();
|
|
57
|
+
if (!fs.existsSync(p)) return { defaultWallet: null };
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
60
|
+
} catch {
|
|
61
|
+
return { defaultWallet: null };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadWallet(address) {
|
|
66
|
+
const fp = path.join(getWalletsDir(), `${address}.json`);
|
|
67
|
+
if (!fs.existsSync(fp)) return null;
|
|
68
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// RPC helpers
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function getDefaultRpc() {
|
|
76
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function httpRequest(rpcUrl, pathStr) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const url = new URL(pathStr, rpcUrl);
|
|
82
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
83
|
+
const req = lib.request({
|
|
84
|
+
hostname: url.hostname,
|
|
85
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
86
|
+
path: url.pathname + url.search,
|
|
87
|
+
method: 'GET',
|
|
88
|
+
timeout: 8000,
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
}, (res) => {
|
|
91
|
+
let data = '';
|
|
92
|
+
res.on('data', (chunk) => data += chunk);
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
try { resolve(JSON.parse(data)); }
|
|
95
|
+
catch { resolve({ raw: data }); }
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
req.on('error', reject);
|
|
99
|
+
req.end();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function httpPost(rpcUrl, pathStr, body) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const url = new URL(pathStr, rpcUrl);
|
|
106
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
107
|
+
const bodyStr = JSON.stringify(body);
|
|
108
|
+
const req = lib.request({
|
|
109
|
+
hostname: url.hostname,
|
|
110
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
111
|
+
path: url.pathname + url.search,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
timeout: 8000,
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
117
|
+
},
|
|
118
|
+
}, (res) => {
|
|
119
|
+
let data = '';
|
|
120
|
+
res.on('data', (chunk) => data += chunk);
|
|
121
|
+
res.on('end', () => {
|
|
122
|
+
try { resolve(JSON.parse(data)); }
|
|
123
|
+
catch { resolve(data); }
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
req.on('error', reject);
|
|
127
|
+
req.write(bodyStr);
|
|
128
|
+
req.end();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Formatting helpers
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format lamports as AETH string (1 AETH = 1e9 lamports)
|
|
138
|
+
*/
|
|
139
|
+
function formatAether(lamports) {
|
|
140
|
+
if (lamports === undefined || lamports === null) return '0 AETH';
|
|
141
|
+
const aeth = lamports / 1e9;
|
|
142
|
+
if (aeth === 0) return '0 AETH';
|
|
143
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format a timestamp as relative time ("2h ago", "3d ago")
|
|
148
|
+
*/
|
|
149
|
+
function relativeTime(timestamp) {
|
|
150
|
+
if (!timestamp) return 'unknown';
|
|
151
|
+
const now = Math.floor(Date.now() / 1000);
|
|
152
|
+
const diff = now - timestamp;
|
|
153
|
+
if (diff < 60) return `${diff}s ago`;
|
|
154
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
155
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
156
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Truncate a signature or hash for display
|
|
161
|
+
*/
|
|
162
|
+
function truncate(str, chars = 8) {
|
|
163
|
+
if (!str || typeof str !== 'string') return '—';
|
|
164
|
+
if (str.length <= chars * 2 + 3) return str;
|
|
165
|
+
return str.slice(0, chars) + '…' + str.slice(-chars);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Fetch wallet stats from RPC
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
async function fetchWalletStats(address, rpcUrl) {
|
|
173
|
+
const rawAddr = address.startsWith('ATH') ? address.slice(3) : address;
|
|
174
|
+
|
|
175
|
+
// Fetch account, transactions, and stake positions in parallel
|
|
176
|
+
const [account, txs, stakeAccounts] = await Promise.all([
|
|
177
|
+
httpRequest(rpcUrl, `/v1/account/${rawAddr}`).catch(() => null),
|
|
178
|
+
httpRequest(rpcUrl, `/v1/tx?address=${encodeURIComponent(rawAddr)}&limit=5`).catch(() => null),
|
|
179
|
+
httpRequest(rpcUrl, `/v1/stake?address=${encodeURIComponent(rawAddr)}`).catch(() => null),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
return { account, txs, stakeAccounts };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Render dashboard
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function renderDashboard(address, stats, opts) {
|
|
190
|
+
const { account, txs, stakeAccounts } = stats;
|
|
191
|
+
const { compact, asJson } = opts;
|
|
192
|
+
const rpcUrl = opts.rpcUrl;
|
|
193
|
+
|
|
194
|
+
if (asJson) {
|
|
195
|
+
const stakeList = stakeAccounts && !stakeAccounts.error
|
|
196
|
+
? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
|
|
197
|
+
: [];
|
|
198
|
+
const txList = txs && !txs.error
|
|
199
|
+
? (Array.isArray(txs) ? txs : txs.transactions || [])
|
|
200
|
+
: [];
|
|
201
|
+
|
|
202
|
+
const out = {
|
|
203
|
+
address,
|
|
204
|
+
rpc: rpcUrl,
|
|
205
|
+
balance: account && !account.error ? {
|
|
206
|
+
lamports: account.lamports || 0,
|
|
207
|
+
aeth: formatAether(account.lamports || 0),
|
|
208
|
+
} : null,
|
|
209
|
+
stake_positions: stakeList.map((sa) => ({
|
|
210
|
+
stake_account: sa.stake_account || sa.address || 'unknown',
|
|
211
|
+
validator: sa.validator || 'unknown',
|
|
212
|
+
amount: sa.amount || 0,
|
|
213
|
+
aeth: formatAether(sa.amount || 0),
|
|
214
|
+
status: sa.status || 'active',
|
|
215
|
+
created_epoch: sa.created_epoch || null,
|
|
216
|
+
})),
|
|
217
|
+
recent_txs: txList.map((tx) => ({
|
|
218
|
+
type: tx.tx_type || tx.type || 'Unknown',
|
|
219
|
+
signature: tx.signature || tx.id || tx.tx_signature || null,
|
|
220
|
+
timestamp: tx.timestamp || null,
|
|
221
|
+
relative_time: relativeTime(tx.timestamp),
|
|
222
|
+
payload: tx.payload?.data || {},
|
|
223
|
+
fee: tx.fee || 0,
|
|
224
|
+
})),
|
|
225
|
+
fetched_at: new Date().toISOString(),
|
|
226
|
+
};
|
|
227
|
+
console.log(JSON.stringify(out, null, 2));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const lamports = (account && !account.error) ? (account.lamports || 0) : null;
|
|
232
|
+
const stakeList = stakeAccounts && !stakeAccounts.error
|
|
233
|
+
? (Array.isArray(stakeAccounts) ? stakeAccounts : stakeAccounts.accounts || stakeAccounts.stake_accounts || [])
|
|
234
|
+
: [];
|
|
235
|
+
const txList = txs && !txs.error
|
|
236
|
+
? (Array.isArray(txs) ? txs : txs.transactions || [])
|
|
237
|
+
: [];
|
|
238
|
+
|
|
239
|
+
if (compact) {
|
|
240
|
+
// One-line summary
|
|
241
|
+
const bal = lamports !== null ? formatAether(lamports) : 'unknown';
|
|
242
|
+
const stakes = stakeList.length;
|
|
243
|
+
const recent = txList.length > 0 ? (txList[0].tx_type || txList[0].type || '?') : 'none';
|
|
244
|
+
console.log(`${C.bright}${address}${C.reset} bal:${C.green}${bal}${C.reset} stakes:${stakes} last:${recent} txs:${txList.length}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Full dashboard
|
|
249
|
+
console.log(`\n${C.bright}${C.cyan}── Wallet Stats ─────────────────────────────────────────${C.reset}`);
|
|
250
|
+
console.log(` ${C.green}★${C.reset} ${C.bright}${address}${C.reset}`);
|
|
251
|
+
console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
|
|
252
|
+
console.log();
|
|
253
|
+
|
|
254
|
+
// Balance section
|
|
255
|
+
console.log(` ${C.bright}Balance${C.reset}`);
|
|
256
|
+
if (lamports !== null) {
|
|
257
|
+
console.log(` ${C.green}${formatAether(lamports)}${C.reset} ${C.dim}(${lamports} lamports)${C.reset}`);
|
|
258
|
+
if (account.owner) {
|
|
259
|
+
const ownerStr = Array.isArray(account.owner)
|
|
260
|
+
? 'ATH' + bs58.encode(Buffer.from(account.owner.slice(0, 32)))
|
|
261
|
+
: account.owner;
|
|
262
|
+
console.log(` ${C.dim}Owner: ${ownerStr}${C.reset}`);
|
|
263
|
+
}
|
|
264
|
+
if (account.rent_epoch !== undefined) {
|
|
265
|
+
console.log(` ${C.dim}Rent epoch: ${account.rent_epoch}${C.reset}`);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
console.log(` ${C.yellow}⚠ Could not fetch balance (account may not exist)${C.reset}`);
|
|
269
|
+
}
|
|
270
|
+
console.log();
|
|
271
|
+
|
|
272
|
+
// Stake positions section
|
|
273
|
+
console.log(` ${C.bright}Stake Positions (${stakeList.length})${C.reset}`);
|
|
274
|
+
if (stakeList.length === 0) {
|
|
275
|
+
console.log(` ${C.dim}No active stake positions.${C.reset}`);
|
|
276
|
+
} else {
|
|
277
|
+
const statusColors = {
|
|
278
|
+
active: C.green,
|
|
279
|
+
inactive: C.yellow,
|
|
280
|
+
activating: C.yellow,
|
|
281
|
+
deactivating: C.red,
|
|
282
|
+
unknown: C.dim,
|
|
283
|
+
};
|
|
284
|
+
for (const sa of stakeList) {
|
|
285
|
+
const status = sa.status || 'unknown';
|
|
286
|
+
const color = statusColors[status] || C.dim;
|
|
287
|
+
const amount = sa.amount ? formatAether(sa.amount) : '0 AETH';
|
|
288
|
+
const validator = sa.validator || 'unknown';
|
|
289
|
+
console.log(` ${C.dim}┌─${C.reset}`);
|
|
290
|
+
console.log(` │ ${C.bright}Validator:${C.reset} ${validator}`);
|
|
291
|
+
console.log(` │ ${C.bright}Amount:${C.reset} ${color}${amount}${C.reset}`);
|
|
292
|
+
console.log(` │ ${C.bright}Status:${C.reset} ${color}${status}${C.reset}`);
|
|
293
|
+
if (sa.stake_account) {
|
|
294
|
+
console.log(` │ ${C.bright}Stake acct:${C.reset} ${truncate(sa.stake_account)}`);
|
|
295
|
+
}
|
|
296
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
console.log();
|
|
300
|
+
|
|
301
|
+
// Recent transactions section
|
|
302
|
+
console.log(` ${C.bright}Recent Transactions (${txList.length})${C.reset}`);
|
|
303
|
+
if (txList.length === 0) {
|
|
304
|
+
console.log(` ${C.dim}No transactions yet.${C.reset}`);
|
|
305
|
+
} else {
|
|
306
|
+
const typeColors = {
|
|
307
|
+
Transfer: C.cyan,
|
|
308
|
+
Stake: C.green,
|
|
309
|
+
Unstake: C.yellow,
|
|
310
|
+
ClaimRewards: C.magenta,
|
|
311
|
+
CreateNFT: C.red,
|
|
312
|
+
MintNFT: C.red,
|
|
313
|
+
TransferNFT: C.cyan,
|
|
314
|
+
UpdateMetadata: C.yellow,
|
|
315
|
+
Unknown: C.dim,
|
|
316
|
+
};
|
|
317
|
+
for (const tx of txList) {
|
|
318
|
+
const txType = tx.tx_type || tx.type || 'Unknown';
|
|
319
|
+
const color = typeColors[txType] || C.dim;
|
|
320
|
+
const sig = tx.signature || tx.id || tx.tx_signature || '—';
|
|
321
|
+
const ts = tx.timestamp ? relativeTime(tx.timestamp) : 'unknown';
|
|
322
|
+
|
|
323
|
+
console.log(` ${C.dim}┌─ ${ts}${C.reset} ${C.bright}${color}${txType}${C.reset} sig:${truncate(sig)}`);
|
|
324
|
+
if (tx.payload && tx.payload.data) {
|
|
325
|
+
const d = tx.payload.data;
|
|
326
|
+
if (d.recipient) console.log(` │ ${C.dim}→ to: ${d.recipient}${C.reset}`);
|
|
327
|
+
if (d.amount) console.log(` │ ${C.dim}amount: ${formatAether(d.amount)}${C.reset}`);
|
|
328
|
+
if (d.validator) console.log(` │ ${C.dim}validator: ${d.validator}${C.reset}`);
|
|
329
|
+
}
|
|
330
|
+
if (tx.fee !== undefined && tx.fee > 0) {
|
|
331
|
+
console.log(` │ ${C.dim}fee: ${tx.fee} lamports${C.reset}`);
|
|
332
|
+
}
|
|
333
|
+
console.log(` ${C.dim}└${C.reset}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
console.log();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Main command
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
function statsCommand() {
|
|
344
|
+
const args = process.argv.slice(3);
|
|
345
|
+
|
|
346
|
+
// Parse flags
|
|
347
|
+
let address = null;
|
|
348
|
+
let compact = false;
|
|
349
|
+
let asJson = false;
|
|
350
|
+
|
|
351
|
+
for (let i = 0; i < args.length; i++) {
|
|
352
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
353
|
+
address = args[i + 1];
|
|
354
|
+
i++;
|
|
355
|
+
} else if (args[i] === '--compact' || args[i] === '-c') {
|
|
356
|
+
compact = true;
|
|
357
|
+
} else if (args[i] === '--json' || args[i] === '-j') {
|
|
358
|
+
asJson = true;
|
|
359
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
360
|
+
console.log(`
|
|
361
|
+
${C.bright}Aether Wallet Stats${C.reset}
|
|
362
|
+
${C.dim}Comprehensive wallet overview: balance, stakes, recent transactions.${C.reset}
|
|
363
|
+
|
|
364
|
+
${C.bright}Usage:${C.reset}
|
|
365
|
+
aether stats --address <addr> Full dashboard
|
|
366
|
+
aether stats --address <addr> --json JSON output
|
|
367
|
+
aether stats --address <addr> --compact One-line summary
|
|
368
|
+
|
|
369
|
+
${C.bright}Options:${C.reset}
|
|
370
|
+
-a, --address <addr> Wallet address (or set default)
|
|
371
|
+
-j, --json JSON output
|
|
372
|
+
-c, --compact One-line summary
|
|
373
|
+
-h, --help Show this help
|
|
374
|
+
`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Resolve address
|
|
380
|
+
if (!address) {
|
|
381
|
+
const cfg = loadConfig();
|
|
382
|
+
address = cfg.defaultWallet;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!address) {
|
|
386
|
+
console.log(` ${C.red}✗ No wallet address.${C.reset} Use ${C.cyan}--address <addr>${C.reset} or set a default.`);
|
|
387
|
+
console.log(` ${C.dim}Usage: aether stats --address <address> [--compact] [--json]${C.reset}\n`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Verify wallet file exists
|
|
392
|
+
const wallet = loadWallet(address);
|
|
393
|
+
if (!wallet) {
|
|
394
|
+
console.log(` ${C.red}✗ Wallet not found locally:${C.reset} ${address}`);
|
|
395
|
+
console.log(` ${C.dim}Check your wallets: ${C.cyan}aether wallet list${C.reset}\n`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const rpcUrl = getDefaultRpc();
|
|
400
|
+
|
|
401
|
+
if (!asJson) {
|
|
402
|
+
console.log(` ${C.dim}Fetching stats from ${rpcUrl}...${C.reset}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Fetch and render
|
|
406
|
+
(async () => {
|
|
407
|
+
try {
|
|
408
|
+
const stats = await fetchWalletStats(address, rpcUrl);
|
|
409
|
+
renderDashboard(address, stats, { compact, asJson, rpcUrl });
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.log(` ${C.red}✗ Failed to fetch wallet stats:${C.reset} ${err.message}`);
|
|
412
|
+
console.log(` ${C.dim} Is your validator running? RPC: ${rpcUrl}${C.reset}`);
|
|
413
|
+
console.log(` ${C.dim} Set custom RPC: AETHER_RPC=https://your-rpc-url${C.reset}\n`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
})();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = { statsCommand };
|