Files
Matt Hess 733ecd2681 Migrate all JS tests to Rust: 9-crate workspace, 703 tests, 0 JS remaining
Add root Cargo workspace with 9 crates: salvium-crypto (extended),
  salvium-types, salvium-consensus, salvium-wallet, salvium-tx,
  salvium-rpc, salvium-miner (extended), salvium-cli, salvium-multisig.

  New modules: chain_state, block_weight, alt_chain, validation,
  offline signing, stake lifecycle, wallet sync/query/encryption/utxo,
  randomx utilities, and full multisig crate with CARROT support.

  Delete 188 JS test/helper/debug files; archive integration test
  scripts to test/legacy-js/ for live testnet use. Testnet integration
  tests (transfer, stake, burn, convert, sweep) remain as #[ignore]-
  gated Rust tests runnable with --ignored against a live daemon.
2026-02-17 23:09:35 +00:00

199 lines
6.8 KiB
JavaScript

#!/usr/bin/env bun
// Debug transaction with EXACT Salvium format parsing
import { createDaemonRPC } from '../src/rpc/index.js';
import { hexToBytes, bytesToHex } from '../src/address.js';
const daemon = createDaemonRPC({ url: 'http://seed01.salvium.io:19081', timeout: 30000 });
function decodeVarint(data, offset) {
let value = 0n;
let shift = 0n;
let bytesRead = 0;
while (offset + bytesRead < data.length) {
const byte = data[offset + bytesRead];
bytesRead++;
value |= BigInt(byte & 0x7f) << shift;
if ((byte & 0x80) === 0) break;
shift += 7n;
if (shift >= 70n) throw new Error('Varint overflow');
}
return { value: Number(value), bytesRead };
}
async function test() {
const txHash = 'a5e85d03e9200229a72fc3cfe94e5b139c8c2ad982dc3fd174898be63269e670';
const resp = await daemon.getTransactions([txHash], { decode_as_json: true });
const txData = resp.result?.txs?.[0];
const data = hexToBytes(txData.as_hex);
const jsonTx = JSON.parse(txData.as_json);
console.log('TX size:', data.length, 'bytes');
console.log('Daemon JSON: inputs=%d, outputs=%d, rct_type=%d\n',
jsonTx.vin?.length, jsonTx.vout?.length, jsonTx.rct_signatures?.type);
let offset = 0;
// Helper to read varint
const readVarint = () => {
const { value, bytesRead } = decodeVarint(data, offset);
offset += bytesRead;
return value;
};
// Helper to read bytes
const readBytes = (count) => {
const result = data.slice(offset, offset + count);
offset += count;
return result;
};
// Helper to read string (length-prefixed)
const readString = () => {
const len = readVarint();
if (len === 0) return '';
const str = new TextDecoder().decode(data.slice(offset, offset + len));
offset += len;
return str;
};
// Version
const version = readVarint();
console.log(`Version: ${version} (offset 0->${offset})`);
// Unlock time
const unlockTime = readVarint();
console.log(`Unlock time: ${unlockTime} (offset->${offset})`);
// Number of inputs
const numInputs = readVarint();
console.log(`\nInputs: ${numInputs} (expected ${jsonTx.vin?.length})`);
// Parse inputs (Salvium txin_to_key: amount, asset_type, key_offsets, k_image)
for (let i = 0; i < numInputs; i++) {
const startOffset = offset;
const inputType = data[offset++];
if (inputType === 0xff) {
// Coinbase
const height = readVarint();
console.log(` Input ${i}: GEN height=${height}`);
} else if (inputType === 0x02) {
// txin_to_key
const amount = readVarint();
const assetType = readString();
const numOffsets = readVarint();
const offsets = [];
for (let j = 0; j < numOffsets; j++) {
offsets.push(readVarint());
}
const keyImage = readBytes(32);
console.log(` Input ${i}: amount=${amount}, asset="${assetType}", offsets=${numOffsets}, ki=${bytesToHex(keyImage).slice(0, 8)}... (${startOffset}->${offset})`);
} else {
console.log(` Input ${i}: UNKNOWN type 0x${inputType.toString(16)} at offset ${offset-1}`);
break;
}
}
console.log(`After inputs: offset ${offset}`);
// Number of outputs
const numOutputs = readVarint();
console.log(`\nOutputs: ${numOutputs} (expected ${jsonTx.vout?.length})`);
// Parse outputs (Salvium txout_to_key/tagged_key)
for (let i = 0; i < numOutputs; i++) {
const startOffset = offset;
const amount = readVarint();
const outputType = data[offset++];
if (outputType === 0x02) {
// txout_to_key: key + asset_type + unlock_time
const key = readBytes(32);
const assetType = readString();
const outUnlock = readVarint();
console.log(` Output ${i}: amount=${amount}, type=key, asset="${assetType}", unlock=${outUnlock} (${startOffset}->${offset})`);
} else if (outputType === 0x03) {
// txout_to_tagged_key: key + asset_type + unlock_time + view_tag
const key = readBytes(32);
const assetType = readString();
const outUnlock = readVarint();
const viewTag = data[offset++];
console.log(` Output ${i}: amount=${amount}, type=tagged, asset="${assetType}", unlock=${outUnlock}, vtag=${viewTag} (${startOffset}->${offset})`);
} else {
console.log(` Output ${i}: UNKNOWN type 0x${outputType.toString(16)} at offset ${offset-1}`);
break;
}
}
console.log(`After outputs: offset ${offset}`);
// Extra
const extraLen = readVarint();
console.log(`\nExtra length: ${extraLen}`);
const extra = readBytes(extraLen);
console.log(`After extra: offset ${offset}`);
// Salvium tx prefix fields (txType, etc.)
console.log('\n=== Salvium TX prefix fields ===');
const txType = readVarint();
console.log(`txType: ${txType}`);
// TX_TYPE: UNSET=0, PROTOCOL=1, MINER=2, TRANSFER=3, BURN=4, CONVERT=5, STAKE=6
if (txType !== 0 && txType !== 1) {
const amountBurnt = readVarint();
console.log(`amount_burnt: ${amountBurnt}`);
if (txType !== 2) {
// Not MINER
if (txType === 3 && version >= 5) {
// TRANSFER with N outputs
const listCount = readVarint();
console.log(`return_address_list count: ${listCount}`);
for (let i = 0; i < listCount; i++) {
readBytes(32);
}
const maskCount = readVarint();
readBytes(maskCount);
} else if (txType === 6 && version >= 6) {
// STAKE with CARROT
console.log('protocol_tx_data:');
console.log(' return_address:', bytesToHex(readBytes(32)).slice(0, 16) + '...');
console.log(' return_pubkey:', bytesToHex(readBytes(32)).slice(0, 16) + '...');
console.log(' return_view_tag:', bytesToHex(readBytes(3)));
console.log(' return_anchor_enc:', bytesToHex(readBytes(16)));
} else {
const returnAddr = readBytes(32);
const returnPubkey = readBytes(32);
console.log(`return_address: ${bytesToHex(returnAddr).slice(0, 16)}...`);
console.log(`return_pubkey: ${bytesToHex(returnPubkey).slice(0, 16)}...`);
}
const srcType = readString();
const dstType = readString();
const slippage = readVarint();
console.log(`source_asset_type: "${srcType}"`);
console.log(`destination_asset_type: "${dstType}"`);
console.log(`amount_slippage_limit: ${slippage}`);
}
}
console.log(`\n=== RCT section starts at offset ${offset} ===`);
console.log(`Remaining bytes: ${data.length - offset}`);
// Check RCT structure
console.log(`\nFirst 100 RCT bytes: ${bytesToHex(data.slice(offset, offset + 100))}`);
// Check if asset type marker exists after offset
for (let i = offset; i < Math.min(offset + 100, data.length - 4); i++) {
if (data[i] === 0x03 && data[i+1] === 0x53 && data[i+2] === 0x41 && data[i+3] === 0x4c) {
console.log(`\nFound SAL marker at offset ${i} (${i - offset} bytes into RCT)`);
break;
}
}
}
test().catch(console.error);