Files
salvium-rs/test/analyze-stakes.js
T
Matt Hess 56ab4ec3fe Convert debug-full-carrot-scan.js and verify-real-carrot.js to use
environment variables instead of hardcoded SPEND_SECRET values.

  Required env vars: SPEND_SECRET, TX_HASH
  Optional: DAEMON_URL (defaults to seed01.salvium.io:19081)
2026-01-22 03:24:00 +00:00

318 lines
11 KiB
JavaScript

#!/usr/bin/env bun
/**
* Analyze stakes, returns, and yields from wallet sync
*
* Based on Salvium C++ wallet yield_info implementation:
* - STAKE transactions (type 6) lock coins for STAKE_LOCK_PERIOD (21600 blocks)
* - PROTOCOL transactions (type 2) return staked coins + yield
* - Yield = return_amount - staked_amount
*/
import { createDaemonRPC } from '../src/rpc/index.js';
import { MemoryStorage } from '../src/wallet-store.js';
import { WalletSync } from '../src/wallet-sync.js';
import { deriveKeys, deriveCarrotKeys } from '../src/carrot.js';
import { hexToBytes, bytesToHex } from '../src/address.js';
import { generateCNSubaddressMap, generateCarrotSubaddressMap } from '../src/subaddress.js';
import { TX_TYPE } from '../src/wallet.js';
const MASTER_KEY = process.env.MASTER_KEY;
const START_HEIGHT = parseInt(process.env.START_HEIGHT || '0', 10);
const DAEMON_URL = process.env.DAEMON_URL || 'http://seed01.salvium.io:19081';
const STAKE_LOCK_PERIOD = 21600; // 30*24*30 blocks on mainnet
async function analyzeStakes() {
console.log('=== Stake/Return/Yield Analysis ===\n');
if (!MASTER_KEY) {
console.error('ERROR: MASTER_KEY environment variable required.\n');
console.log('Usage:');
console.log(' MASTER_KEY="64-char-hex" bun test/analyze-stakes.js');
console.log(' MASTER_KEY="hex" START_HEIGHT=190000 bun test/analyze-stakes.js');
process.exit(1);
}
console.log(`Stake lock period: ${STAKE_LOCK_PERIOD} blocks\n`);
// Derive keys
const keys = deriveKeys(hexToBytes(MASTER_KEY));
const carrotKeys = deriveCarrotKeys(keys.spendSecretKey);
// Connect to daemon
const daemon = createDaemonRPC({ url: DAEMON_URL, timeout: 30000 });
const info = await daemon.getInfo();
if (!info.success) {
console.error('Failed to connect to daemon');
process.exit(1);
}
const currentHeight = info.result.height;
console.log(`Daemon height: ${currentHeight}\n`);
// Generate subaddress maps
console.log('Generating subaddress maps...');
const cnSubaddresses = generateCNSubaddressMap(keys.spendPublicKey, keys.viewSecretKey, 50, 200);
const carrotSubaddresses = generateCarrotSubaddressMap(
hexToBytes(carrotKeys.accountSpendPubkey),
hexToBytes(carrotKeys.accountViewPubkey),
hexToBytes(carrotKeys.generateAddressSecret),
50, 200
);
console.log(` CN: ${cnSubaddresses.size}, CARROT: ${carrotSubaddresses.size}\n`);
// Setup storage and sync
const storage = new MemoryStorage();
await storage.open();
const sync = new WalletSync({
storage,
daemon,
keys: {
viewSecretKey: keys.viewSecretKey,
spendPublicKey: keys.spendPublicKey,
spendSecretKey: keys.spendSecretKey
},
carrotKeys: {
viewIncomingKey: hexToBytes(carrotKeys.viewIncomingKey),
accountSpendPubkey: hexToBytes(carrotKeys.accountSpendPubkey),
generateImageKey: hexToBytes(carrotKeys.generateImageKey),
generateAddressSecret: hexToBytes(carrotKeys.generateAddressSecret)
},
subaddresses: cnSubaddresses,
carrotSubaddresses: carrotSubaddresses,
batchSize: 200
});
// Progress reporting
let lastHeight = START_HEIGHT;
sync.on('syncProgress', (data) => {
if (data.currentHeight - lastHeight >= 10000) {
console.log(` Syncing... height ${data.currentHeight}`);
lastHeight = data.currentHeight;
}
});
console.log(`Starting sync from height ${START_HEIGHT}...`);
const startTime = Date.now();
await sync.start(START_HEIGHT);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`Sync completed in ${elapsed}s\n`);
// Get all outputs and transactions
const outputs = await storage.getOutputs();
const transactions = await storage.getTransactions();
console.log(`Total outputs: ${outputs.length}`);
console.log(`Total transactions: ${transactions.length}\n`);
// Categorize transactions by type
const txByType = {
miner: [],
protocol: [],
transfer: [],
stake: [],
return: [],
convert: [],
burn: [],
audit: [],
other: []
};
for (const tx of transactions) {
const type = tx.txType;
if (type === TX_TYPE.MINER || type === 'miner' || tx.isMinerTx) {
txByType.miner.push(tx);
} else if (type === TX_TYPE.PROTOCOL || type === 'protocol' || tx.isProtocolTx) {
txByType.protocol.push(tx);
} else if (type === TX_TYPE.STAKE || type === 6) {
txByType.stake.push(tx);
} else if (type === TX_TYPE.RETURN || type === 7) {
txByType.return.push(tx);
} else if (type === TX_TYPE.CONVERT || type === 4) {
txByType.convert.push(tx);
} else if (type === TX_TYPE.BURN || type === 5) {
txByType.burn.push(tx);
} else if (type === TX_TYPE.AUDIT || type === 8) {
txByType.audit.push(tx);
} else if (type === TX_TYPE.TRANSFER || type === 3) {
txByType.transfer.push(tx);
} else {
txByType.other.push(tx);
}
}
console.log('=== TRANSACTION BREAKDOWN ===\n');
console.log(` Miner (coinbase): ${txByType.miner.length}`);
console.log(` Protocol (yields): ${txByType.protocol.length}`);
console.log(` Transfer: ${txByType.transfer.length}`);
console.log(` Stake: ${txByType.stake.length}`);
console.log(` Return: ${txByType.return.length}`);
console.log(` Convert: ${txByType.convert.length}`);
console.log(` Burn: ${txByType.burn.length}`);
console.log(` Audit: ${txByType.audit.length}`);
console.log(` Other/Unknown: ${txByType.other.length}`);
console.log('');
// Group outputs by transaction
const outputsByTx = new Map();
for (const output of outputs) {
if (!outputsByTx.has(output.txHash)) {
outputsByTx.set(output.txHash, []);
}
outputsByTx.get(output.txHash).push(output);
}
// Find stake outputs - outputs with unlock_time = height + STAKE_LOCK_PERIOD
const stakeOutputs = [];
for (const output of outputs) {
const unlockTime = Number(output.unlockTime || 0);
if (unlockTime > 0) {
const lockDuration = unlockTime - output.blockHeight;
// Allow some tolerance (within 100 blocks of expected lock period)
if (Math.abs(lockDuration - STAKE_LOCK_PERIOD) <= 100) {
stakeOutputs.push({
...output,
expectedUnlock: output.blockHeight + STAKE_LOCK_PERIOD,
actualUnlock: unlockTime
});
}
}
}
// Group outputs by height for return matching
const outputsByHeight = new Map();
for (const output of outputs) {
if (!outputsByHeight.has(output.blockHeight)) {
outputsByHeight.set(output.blockHeight, []);
}
outputsByHeight.get(output.blockHeight).push(output);
}
// Match stakes with returns
console.log('=== STAKE ANALYSIS ===\n');
console.log(`Found ${stakeOutputs.length} stake output(s)\n`);
const stakeRecords = [];
for (let i = 0; i < stakeOutputs.length; i++) {
const stake = stakeOutputs[i];
const stakeAmt = Number(stake.amount) / 1e8;
const unlockHeight = stake.actualUnlock;
// Look for return in protocol outputs at or near unlock height
let returnOutput = null;
let yieldAmount = 0n;
// Check unlock height and a few blocks after
for (let h = unlockHeight; h <= unlockHeight + 5; h++) {
const candidates = outputsByHeight.get(h) || [];
for (const candidate of candidates) {
// Check if it's from a protocol tx
const tx = transactions.find(t => t.txHash === candidate.txHash);
if (tx && (tx.isProtocolTx || tx.txType === TX_TYPE.PROTOCOL || tx.txType === 'protocol')) {
// Return should be >= stake amount (stake + yield)
if (candidate.amount >= stake.amount) {
returnOutput = candidate;
yieldAmount = candidate.amount - stake.amount;
break;
}
}
}
if (returnOutput) break;
}
const record = {
index: i + 1,
stakeHeight: stake.blockHeight,
stakeAmount: stake.amount,
unlockHeight: unlockHeight,
stakeSpent: stake.isSpent,
returnFound: !!returnOutput,
returnHeight: returnOutput?.blockHeight || null,
returnAmount: returnOutput?.amount || 0n,
yieldAmount: yieldAmount,
matured: currentHeight >= unlockHeight
};
stakeRecords.push(record);
// Print stake info
console.log(`Stake #${record.index}:`);
console.log(` Staked at block: ${record.stakeHeight}`);
console.log(` Stake amount: ${stakeAmt.toFixed(8)} SAL`);
console.log(` Unlocks at block: ${record.unlockHeight}`);
console.log(` Maturity status: ${record.matured ? 'MATURED' : `Matures in ${record.unlockHeight - currentHeight} blocks`}`);
console.log(` Stake output spent: ${record.stakeSpent ? 'YES' : 'NO'}`);
if (record.returnFound) {
const returnAmt = Number(record.returnAmount) / 1e8;
const yieldAmt = Number(record.yieldAmount) / 1e8;
console.log(` Return found: YES (block ${record.returnHeight})`);
console.log(` Return amount: ${returnAmt.toFixed(8)} SAL`);
console.log(` Yield earned: ${yieldAmt.toFixed(8)} SAL`);
} else {
console.log(` Return found: NO`);
}
console.log('');
}
// Protocol transaction analysis (yields from staking network)
console.log('=== PROTOCOL TRANSACTION OUTPUTS ===\n');
const protocolOutputs = [];
for (const output of outputs) {
const tx = transactions.find(t => t.txHash === output.txHash);
if (tx && (tx.isProtocolTx || tx.txType === TX_TYPE.PROTOCOL || tx.txType === 'protocol')) {
protocolOutputs.push({ output, tx });
}
}
console.log(`Total protocol tx outputs: ${protocolOutputs.length}\n`);
let totalProtocolAmount = 0n;
for (const { output } of protocolOutputs) {
const amt = Number(output.amount) / 1e8;
console.log(` Height ${output.blockHeight}: ${amt.toFixed(8)} SAL (${output.isSpent ? 'SPENT' : 'UNSPENT'})`);
totalProtocolAmount += output.amount;
}
// Summary
console.log('\n=== SUMMARY ===\n');
let totalStaked = 0n;
let totalReturned = 0n;
let totalYield = 0n;
let activeStakes = 0;
for (const record of stakeRecords) {
totalStaked += record.stakeAmount;
if (record.returnFound) {
totalReturned += record.returnAmount;
totalYield += record.yieldAmount;
}
if (!record.matured) {
activeStakes++;
}
}
console.log(`Stakes found: ${stakeRecords.length}`);
console.log(`Active (not matured): ${activeStakes}`);
console.log(`Total staked: ${(Number(totalStaked) / 1e8).toFixed(8)} SAL`);
console.log(`Total returned: ${(Number(totalReturned) / 1e8).toFixed(8)} SAL`);
console.log(`Total yield earned: ${(Number(totalYield) / 1e8).toFixed(8)} SAL`);
console.log(`Protocol tx total: ${(Number(totalProtocolAmount) / 1e8).toFixed(8)} SAL`);
// Current balance
let unspentBalance = 0n;
let unspentCount = 0;
for (const o of outputs) {
if (!o.isSpent) {
unspentBalance += o.amount;
unspentCount++;
}
}
console.log(`\nCurrent balance: ${(Number(unspentBalance) / 1e8).toFixed(8)} SAL (${unspentCount} unspent outputs)`);
await storage.close();
}
analyzeStakes().catch(console.error);