733ecd2681
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.
199 lines
6.8 KiB
JavaScript
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);
|