aether-hub 1.2.5 → 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/rewards.js +187 -4
- package/commands/tx-history.js +508 -0
- package/index.js +10 -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,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli tx-history
|
|
4
|
+
*
|
|
5
|
+
* Fetch and display transaction history for an Aether wallet address.
|
|
6
|
+
* Shows recent transactions with type, amount, timestamp, fee, and status.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* aether tx history --address <addr> [--limit <n>] [--json] [--rpc <url>]
|
|
10
|
+
* aether history --address <addr> [--limit <n>] [--json]
|
|
11
|
+
*
|
|
12
|
+
* Examples:
|
|
13
|
+
* aether tx history --address ATHxxx
|
|
14
|
+
* aether tx history --address ATHxxx --limit 50 --json
|
|
15
|
+
* aether history --address ATHxxx --rpc https://rpc.aether.io
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const https = require('https');
|
|
19
|
+
const http = require('http');
|
|
20
|
+
|
|
21
|
+
// ANSI colours
|
|
22
|
+
const C = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bright: '\x1b[1m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
cyan: '\x1b[36m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const CLI_VERSION = '1.0.0';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Config
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function getDefaultRpc() {
|
|
40
|
+
return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// HTTP helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
function httpRequest(rpcUrl, path, timeoutMs = 10000) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const url = new URL(path, rpcUrl);
|
|
50
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
51
|
+
const req = lib.request({
|
|
52
|
+
hostname: url.hostname,
|
|
53
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
54
|
+
path: url.pathname + url.search,
|
|
55
|
+
method: 'GET',
|
|
56
|
+
timeout: timeoutMs,
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
}, (res) => {
|
|
59
|
+
let data = '';
|
|
60
|
+
res.on('data', (chunk) => data += chunk);
|
|
61
|
+
res.on('end', () => {
|
|
62
|
+
try { resolve(JSON.parse(data)); }
|
|
63
|
+
catch { resolve({ raw: data }); }
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
req.on('error', reject);
|
|
67
|
+
req.on('timeout', () => { req.destroy(); reject(new Error(`Request timeout after ${timeoutMs}ms`)); });
|
|
68
|
+
req.end();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function httpPost(rpcUrl, path, body, timeoutMs = 15000) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const url = new URL(path, rpcUrl);
|
|
75
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
76
|
+
const bodyStr = JSON.stringify(body);
|
|
77
|
+
const req = lib.request({
|
|
78
|
+
hostname: url.hostname,
|
|
79
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
80
|
+
path: url.pathname + url.search,
|
|
81
|
+
method: 'POST',
|
|
82
|
+
timeout: timeoutMs,
|
|
83
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
|
|
84
|
+
}, (res) => {
|
|
85
|
+
let data = '';
|
|
86
|
+
res.on('data', (chunk) => data += chunk);
|
|
87
|
+
res.on('end', () => {
|
|
88
|
+
try { resolve(JSON.parse(data)); }
|
|
89
|
+
catch { resolve({ raw: data }); }
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
req.on('error', reject);
|
|
93
|
+
req.on('timeout', () => { req.destroy(); reject(new Error(`Request timeout after ${timeoutMs}ms`)); });
|
|
94
|
+
req.write(bodyStr);
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Argument parsing
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function parseArgs() {
|
|
104
|
+
const args = process.argv.slice(2);
|
|
105
|
+
const result = { address: null, limit: 20, json: false, rpc: null };
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < args.length; i++) {
|
|
108
|
+
const arg = args[i];
|
|
109
|
+
if ((arg === '--address' || arg === '-a') && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
110
|
+
result.address = args[i + 1];
|
|
111
|
+
i++;
|
|
112
|
+
} else if ((arg === '--limit' || arg === '-l') && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
113
|
+
result.limit = parseInt(args[i + 1], 10);
|
|
114
|
+
i++;
|
|
115
|
+
} else if (arg === '--json' || arg === '--json-output') {
|
|
116
|
+
result.json = true;
|
|
117
|
+
} else if (arg === '--rpc' && args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
118
|
+
result.rpc = args[i + 1];
|
|
119
|
+
i++;
|
|
120
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
121
|
+
result.help = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Formatting helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
function formatAether(lamports) {
|
|
133
|
+
if (lamports == null) return '—';
|
|
134
|
+
const aeth = lamports / 1e9;
|
|
135
|
+
if (aeth === 0) return '0 AETH';
|
|
136
|
+
return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatTimestamp(slot, blockTime) {
|
|
140
|
+
if (!blockTime) return '—';
|
|
141
|
+
try {
|
|
142
|
+
const d = new Date(blockTime * 1000);
|
|
143
|
+
return d.toISOString().replace('T', ' ').slice(0, 19);
|
|
144
|
+
} catch {
|
|
145
|
+
return String(blockTime);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function shortAddress(addr) {
|
|
150
|
+
if (!addr) return '—';
|
|
151
|
+
if (addr.length <= 16) return addr;
|
|
152
|
+
return addr.slice(0, 8) + '…' + addr.slice(-6);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatTxType(type) {
|
|
156
|
+
const t = (type || 'unknown').toLowerCase();
|
|
157
|
+
if (t.includes('transfer') || t.includes('send') || t.includes('payment')) return { label: 'TRANSFER', color: C.cyan };
|
|
158
|
+
if (t.includes('stake') || t.includes('delegate')) return { label: 'STAKE', color: C.green };
|
|
159
|
+
if (t.includes('unstake') || t.includes('deactivate')) return { label: 'UNSTAKE', color: C.yellow };
|
|
160
|
+
if (t.includes('reward') || t.includes('mint')) return { label: 'REWARD', color: C.magenta };
|
|
161
|
+
if (t.includes('vote')) return { label: 'VOTE', color: C.bright };
|
|
162
|
+
if (t.includes('create') || t.includes('initialize')) return { label: 'CREATE', color: C.bright };
|
|
163
|
+
if (t.includes('burn')) return { label: 'BURN', color: C.red };
|
|
164
|
+
return { label: type ? type.toUpperCase().slice(0, 10) : 'TX', color: C.dim };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatStatus(status) {
|
|
168
|
+
if (!status) return { label: '—', color: C.dim };
|
|
169
|
+
const s = status.toLowerCase();
|
|
170
|
+
if (s === 'success' || s === 'finalized' || s === 'confirmed') {
|
|
171
|
+
return { label: '✓ OK', color: C.green };
|
|
172
|
+
}
|
|
173
|
+
if (s === 'pending' || s === 'processing') {
|
|
174
|
+
return { label: '⏳', color: C.yellow };
|
|
175
|
+
}
|
|
176
|
+
if (s === 'failed') {
|
|
177
|
+
return { label: '✗ FAIL', color: C.red };
|
|
178
|
+
}
|
|
179
|
+
return { label: status.slice(0, 8), color: C.dim };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Core RPC calls
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Fetch confirmed transaction signatures for an address using getSignaturesForAddress.
|
|
188
|
+
*/
|
|
189
|
+
async function fetchTxSignatures(rpcUrl, address, limit) {
|
|
190
|
+
const body = {
|
|
191
|
+
jsonrpc: '2.0',
|
|
192
|
+
id: 1,
|
|
193
|
+
method: 'getSignaturesForAddress',
|
|
194
|
+
params: [
|
|
195
|
+
address,
|
|
196
|
+
{ limit },
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
return httpPost(rpcUrl, '/', body);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Fetch a specific confirmed transaction by signature.
|
|
204
|
+
*/
|
|
205
|
+
async function fetchTx(rpcUrl, signature) {
|
|
206
|
+
const body = {
|
|
207
|
+
jsonrpc: '2.0',
|
|
208
|
+
id: 1,
|
|
209
|
+
method: 'getTransaction',
|
|
210
|
+
params: [
|
|
211
|
+
signature,
|
|
212
|
+
{ encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 },
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
return httpPost(rpcUrl, '/', body);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Parse a transaction result into a normalized display object.
|
|
220
|
+
*/
|
|
221
|
+
function parseTransaction(txResult, sigInfo) {
|
|
222
|
+
const blockTime = sigInfo.blockTime || txResult.blockTime;
|
|
223
|
+
const slot = sigInfo.slot;
|
|
224
|
+
const status = sigInfo.err ? 'failed' : (sigInfo.confirmationStatus || 'confirmed');
|
|
225
|
+
|
|
226
|
+
let txType = 'unknown';
|
|
227
|
+
let amount = 0;
|
|
228
|
+
let fee = txResult.meta?.fee || 0;
|
|
229
|
+
let fromAddr = null;
|
|
230
|
+
let toAddr = null;
|
|
231
|
+
let memo = null;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const msg = txResult.transaction?.message;
|
|
235
|
+
if (msg) {
|
|
236
|
+
// Parse instructions for transfer/stake types
|
|
237
|
+
const instructions = msg.instructions || [];
|
|
238
|
+
for (const ix of instructions) {
|
|
239
|
+
const programId = ix.programId || (ix.parsed && ix.parsed.info && ix.parsed.type);
|
|
240
|
+
// Native transfer
|
|
241
|
+
if (ix.parsed && ix.parsed.type === 'transfer') {
|
|
242
|
+
txType = 'transfer';
|
|
243
|
+
const info = ix.parsed.info;
|
|
244
|
+
fromAddr = info.source || info.from;
|
|
245
|
+
toAddr = info.destination || info.to;
|
|
246
|
+
amount = info.lamports || info.amount || 0;
|
|
247
|
+
} else if (ix.parsed && ix.parsed.type === 'stake') {
|
|
248
|
+
txType = 'stake';
|
|
249
|
+
const info = ix.parsed.info;
|
|
250
|
+
fromAddr = info.from || info.funder;
|
|
251
|
+
toAddr = info.validator;
|
|
252
|
+
amount = info.lamports || info.amount || 0;
|
|
253
|
+
} else if (ix.parsed && ix.parsed.type === 'withdrawStake') {
|
|
254
|
+
txType = 'unstake';
|
|
255
|
+
const info = ix.parsed.info;
|
|
256
|
+
toAddr = info.destination || info.withdrawer;
|
|
257
|
+
amount = info.lamports || info.amount || 0;
|
|
258
|
+
} else if (ix.parsed && ix.parsed.type === 'vote') {
|
|
259
|
+
txType = 'vote';
|
|
260
|
+
} else if (ix.parsed && ix.parsed.type === 'initialize') {
|
|
261
|
+
txType = 'initialize';
|
|
262
|
+
} else if (ix.parsed && ix.parsed.type === 'createAccount') {
|
|
263
|
+
txType = 'create';
|
|
264
|
+
} else if (ix.parsed && ix.parsed.type === 'approve') {
|
|
265
|
+
txType = 'stake';
|
|
266
|
+
const info = ix.parsed.info || {};
|
|
267
|
+
fromAddr = info.from || info.owner;
|
|
268
|
+
toAddr = info.stake;
|
|
269
|
+
amount = info.amount || info.lamports || 0;
|
|
270
|
+
} else if (ix.parsed && ix.parsed.type === 'delegate') {
|
|
271
|
+
txType = 'stake';
|
|
272
|
+
const info = ix.parsed.info || {};
|
|
273
|
+
fromAddr = info.stake || info.from;
|
|
274
|
+
toAddr = info.validator;
|
|
275
|
+
amount = info.lamports || 0;
|
|
276
|
+
} else if (ix.parsed && ix.parsed.type === 'withdraw') {
|
|
277
|
+
txType = 'unstake';
|
|
278
|
+
const info = ix.parsed.info || {};
|
|
279
|
+
toAddr = info.destination;
|
|
280
|
+
amount = info.lamports || 0;
|
|
281
|
+
}
|
|
282
|
+
// Check memo
|
|
283
|
+
if (ix.memo) memo = ix.memo;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fallback: try legacy instructions if no parsed instructions
|
|
287
|
+
if (!instructions.length || instructions.every(ix => !ix.parsed)) {
|
|
288
|
+
for (const ix of instructions) {
|
|
289
|
+
if (ix.data === 'AAAA' || ix.data === '2ugJ4ELK3wW9qNXH' || !ix.data) {
|
|
290
|
+
txType = 'transfer';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Compute fee
|
|
296
|
+
if (txResult.meta) {
|
|
297
|
+
fee = txResult.meta.fee || 0;
|
|
298
|
+
if (txResult.meta.postBalances && txResult.meta.preBalances) {
|
|
299
|
+
// Try to detect native transfer from balance changes
|
|
300
|
+
for (let i = 0; i < txResult.meta.postBalances.length; i++) {
|
|
301
|
+
const diff = txResult.meta.postBalances[i] - txResult.meta.preBalances[i];
|
|
302
|
+
if (diff < 0) {
|
|
303
|
+
amount = Math.abs(diff);
|
|
304
|
+
if (!fromAddr) fromAddr = msg.accountKeys?.[i];
|
|
305
|
+
} else if (diff > 0 && amount === 0) {
|
|
306
|
+
if (!toAddr) toAddr = msg.accountKeys?.[i];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// Parsing failed — use defaults
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
signature: sigInfo.signature || sigInfo.signatures?.[0],
|
|
318
|
+
slot,
|
|
319
|
+
blockTime,
|
|
320
|
+
status,
|
|
321
|
+
type: txType,
|
|
322
|
+
amount,
|
|
323
|
+
fee,
|
|
324
|
+
from: fromAddr,
|
|
325
|
+
to: toAddr,
|
|
326
|
+
memo,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Display
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
function displayTxTable(txs) {
|
|
335
|
+
// Header
|
|
336
|
+
console.log(
|
|
337
|
+
`\n${C.bright}${C.cyan} ╔══════════════════════════════════════════════════════════════════════════════════════════╗${C.reset}\n` +
|
|
338
|
+
` ║${C.bright} ${C.cyan}Transaction History${C.reset}${C.bright} ║${C.reset}\n` +
|
|
339
|
+
` ${C.cyan}╚══════════════════════════════════════════════════════════════════════════════════════════${C.reset}`
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (txs.length === 0) {
|
|
343
|
+
console.log(`\n ${C.yellow}No transactions found for this address.${C.reset}\n`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(
|
|
348
|
+
` ${C.dim}┌────┬──────────────────────┬──────────┬───────────────┬────────────┬────────┬───────────┐${C.reset}\n` +
|
|
349
|
+
` ${C.dim}│ # │ ${C.reset}Timestamp ${C.dim}│ ${C.reset}Type ${C.dim}│ ${C.reset}Amount ${C.dim}│ ${C.reset}From ${C.dim}│ ${C.reset}To ${C.dim}│ ${C.reset}Status ${C.dim}│${C.reset}`
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < txs.length; i++) {
|
|
353
|
+
const tx = txs[i];
|
|
354
|
+
const num = String(i + 1).padStart(3);
|
|
355
|
+
const time = formatTimestamp(tx.blockTime).slice(0, 19).padEnd(19);
|
|
356
|
+
const typeInfo = formatTxType(tx.type);
|
|
357
|
+
const type = typeInfo.label.padEnd(10);
|
|
358
|
+
const amt = tx.amount > 0 ? formatAether(tx.amount).padEnd(13) : '—'.padEnd(13);
|
|
359
|
+
const from = shortAddress(tx.from || '').padEnd(11);
|
|
360
|
+
const to = shortAddress(tx.to || '').padEnd(11);
|
|
361
|
+
const statusInfo = formatStatus(tx.status);
|
|
362
|
+
const status = statusInfo.label;
|
|
363
|
+
|
|
364
|
+
const bgAlt = (i % 2 === 0) ? '' : C.dim;
|
|
365
|
+
const reset = C.reset + bgAlt;
|
|
366
|
+
|
|
367
|
+
console.log(
|
|
368
|
+
` ${bgAlt}${C.dim}├────┼──────────────────────┼──────────┼───────────────┼────────────┼────────┼───────────┤${reset}`
|
|
369
|
+
);
|
|
370
|
+
console.log(
|
|
371
|
+
` ${bgAlt}${C.dim}│${reset} ${num} ${bgAlt}${C.dim}│ ${reset}${time} ${bgAlt}${C.dim}│ ${reset}${typeInfo.color}${type}${reset} ${bgAlt}${C.dim}│ ${reset}${amt} ${bgAlt}${C.dim}│ ${reset}${from} ${bgAlt}${C.dim}│ ${reset}${to} ${bgAlt}${C.dim}│ ${reset}${statusInfo.color}${status}${reset}${bgAlt} │${reset}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log(
|
|
376
|
+
` ${C.dim}└────┴──────────────────────┴──────────┴───────────────┴────────────┴────────┴───────────┘${C.reset}`
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// Summary
|
|
380
|
+
const totalVolume = txs.reduce((sum, tx) => sum + tx.amount, 0);
|
|
381
|
+
const successCount = txs.filter(tx => tx.status !== 'failed').length;
|
|
382
|
+
console.log(`\n ${C.dim} ${txs.length} transactions · Total volume: ${C.reset}${C.bright}${formatAether(totalVolume)}${C.reset} ${C.dim}· Success: ${C.reset}${C.green}${successCount}/${txs.length}${C.reset} ${C.dim}· Failed: ${C.reset}${C.red}${(txs.length - successCount)}${C.reset}\n`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function displayJson(txs, meta) {
|
|
386
|
+
console.log(JSON.stringify({
|
|
387
|
+
address: meta.address,
|
|
388
|
+
rpc: meta.rpc,
|
|
389
|
+
limit: meta.limit,
|
|
390
|
+
transaction_count: txs.length,
|
|
391
|
+
total_volume_lamports: txs.reduce((sum, tx) => sum + tx.amount, 0),
|
|
392
|
+
transactions: txs.map(tx => ({
|
|
393
|
+
signature: tx.signature,
|
|
394
|
+
slot: tx.slot,
|
|
395
|
+
timestamp: tx.blockTime ? new Date(tx.blockTime * 1000).toISOString() : null,
|
|
396
|
+
type: tx.type,
|
|
397
|
+
amount_lamports: tx.amount,
|
|
398
|
+
amount_aeth: (tx.amount / 1e9).toFixed(9),
|
|
399
|
+
fee_lamports: tx.fee,
|
|
400
|
+
from: tx.from,
|
|
401
|
+
to: tx.to,
|
|
402
|
+
memo: tx.memo,
|
|
403
|
+
status: tx.status,
|
|
404
|
+
})),
|
|
405
|
+
}, null, 2));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Main
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
async function main() {
|
|
413
|
+
const opts = parseArgs();
|
|
414
|
+
|
|
415
|
+
if (opts.help) {
|
|
416
|
+
console.log(`
|
|
417
|
+
${C.bright}${C.cyan}tx-history${C.reset} — Fetch and display transaction history for an Aether address
|
|
418
|
+
|
|
419
|
+
${C.bright}USAGE${C.reset}
|
|
420
|
+
aether tx history --address <addr> [--limit <n>] [--json] [--rpc <url>]
|
|
421
|
+
aether history --address <addr> [--limit <n>] [--json]
|
|
422
|
+
|
|
423
|
+
${C.bright}OPTIONS${C.reset}
|
|
424
|
+
--address <addr> Aether wallet address (ATH...)
|
|
425
|
+
--limit <n> Max transactions to fetch (default: 20, max: 100)
|
|
426
|
+
--json Output raw JSON for scripting
|
|
427
|
+
--rpc <url> RPC endpoint (default: AETHER_RPC or http://127.0.0.1:8899)
|
|
428
|
+
--help Show this help
|
|
429
|
+
|
|
430
|
+
${C.bright}EXAMPLES${C.reset}
|
|
431
|
+
aether tx history --address ATH3abc... --limit 50
|
|
432
|
+
aether tx history --address ATH3abc... --json
|
|
433
|
+
aether history --address ATH3abc... --rpc https://mainnet.aether.io
|
|
434
|
+
`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!opts.address) {
|
|
439
|
+
console.log(` ${C.red}✗ Missing --address${C.reset}\n`);
|
|
440
|
+
console.log(` Usage: aether tx history --address <addr> [--limit <n>] [--json]\n`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const rpcUrl = opts.rpc || getDefaultRpc();
|
|
445
|
+
const limit = Math.min(Math.max(1, opts.limit || 20), 100);
|
|
446
|
+
|
|
447
|
+
if (!opts.json) {
|
|
448
|
+
console.log(`\n${C.bright}${C.cyan} Tx History${C.reset} · ${C.dim}address:${C.reset} ${opts.address} ${C.dim}· ${C.dim}limit:${C.reset} ${limit} ${C.dim}· ${C.dim}rpc:${C.reset} ${rpcUrl}\n`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Step 1: Get transaction signatures
|
|
453
|
+
const sigsResult = await fetchTxSignatures(rpcUrl, opts.address, limit);
|
|
454
|
+
|
|
455
|
+
if (sigsResult.error) {
|
|
456
|
+
throw new Error(sigsResult.error.message || JSON.stringify(sigsResult.error));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const signatures = Array.isArray(sigsResult.result) ? sigsResult.result : [];
|
|
460
|
+
|
|
461
|
+
if (signatures.length === 0) {
|
|
462
|
+
if (!opts.json) {
|
|
463
|
+
displayTxTable([]);
|
|
464
|
+
} else {
|
|
465
|
+
displayJson([], { address: opts.address, rpc: rpcUrl, limit });
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Step 2: Fetch each transaction in parallel (up to 10 at a time)
|
|
471
|
+
const txResults = [];
|
|
472
|
+
const BATCH = 10;
|
|
473
|
+
|
|
474
|
+
for (let i = 0; i < signatures.length; i += BATCH) {
|
|
475
|
+
const batch = signatures.slice(i, i + BATCH);
|
|
476
|
+
const batchPromises = batch.map(sig => fetchTx(rpcUrl, sig.signature).catch(err => ({ error: err.message })));
|
|
477
|
+
const batchResults = await Promise.all(batchPromises);
|
|
478
|
+
txResults.push(...batchResults);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Step 3: Parse and normalize
|
|
482
|
+
const txs = txResults
|
|
483
|
+
.map((res, idx) => {
|
|
484
|
+
if (res.error) return null;
|
|
485
|
+
try {
|
|
486
|
+
return parseTransaction(res.result || {}, signatures[idx] || {});
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
.filter(Boolean);
|
|
492
|
+
|
|
493
|
+
if (opts.json) {
|
|
494
|
+
displayJson(txs, { address: opts.address, rpc: rpcUrl, limit });
|
|
495
|
+
} else {
|
|
496
|
+
displayTxTable(txs);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.log(`\n ${C.red}✗ Failed to fetch transaction history:${C.reset} ${err.message}\n`);
|
|
501
|
+
if (err.stack && !opts.json) {
|
|
502
|
+
console.log(` ${C.dim}${err.stack.split('\n').slice(0, 3).join('\n ')}${C.reset}\n`);
|
|
503
|
+
}
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
main();
|
package/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const { statusCommand } = require('./commands/status');
|
|
|
29
29
|
const { broadcastCommand } = require('./commands/broadcast');
|
|
30
30
|
const { apyCommand } = require('./commands/apy');
|
|
31
31
|
const { statsCommand } = require('./commands/stats');
|
|
32
|
+
const { txHistoryCommand } = require('./commands/tx-history');
|
|
32
33
|
const readline = require('readline');
|
|
33
34
|
|
|
34
35
|
// CLI version
|
|
@@ -236,11 +237,13 @@ const COMMANDS = {
|
|
|
236
237
|
tx: {
|
|
237
238
|
description: 'Transaction history — aether tx history --address <addr> [--limit 20] [--json]',
|
|
238
239
|
handler: () => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
txHistoryCommand();
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
'tx-history': {
|
|
244
|
+
description: 'Transaction history for an address — aether tx-history --address <addr> [--limit 20] [--json]',
|
|
245
|
+
handler: () => {
|
|
246
|
+
txHistoryCommand();
|
|
244
247
|
},
|
|
245
248
|
},
|
|
246
249
|
network: {
|
|
@@ -253,11 +256,7 @@ const COMMANDS = {
|
|
|
253
256
|
history: {
|
|
254
257
|
description: 'Transaction history for an address — alias for tx history',
|
|
255
258
|
handler: () => {
|
|
256
|
-
|
|
257
|
-
const originalArgv = process.argv;
|
|
258
|
-
process.argv = [...originalArgv.slice(0, 2), 'wallet', 'history', ...originalArgv.slice(3)];
|
|
259
|
-
walletCommand();
|
|
260
|
-
process.argv = originalArgv;
|
|
259
|
+
txHistoryCommand();
|
|
261
260
|
},
|
|
262
261
|
},
|
|
263
262
|
validator: {
|
|
@@ -300,7 +299,7 @@ const COMMANDS = {
|
|
|
300
299
|
},
|
|
301
300
|
},
|
|
302
301
|
rewards: {
|
|
303
|
-
description: 'View staking rewards — aether rewards list |
|
|
302
|
+
description: 'View staking rewards — aether rewards list | summary | pending | claim | compound',
|
|
304
303
|
handler: () => {
|
|
305
304
|
rewardsCommand();
|
|
306
305
|
},
|