Add transaction sending to testnet with full CLSAG ring signature pipeline
Exercises the complete transfer flow on the self-contained testnet: UTXO selection, gamma-distribution decoy selection, ring member fetching, buildTransaction with CLSAG signing, mempool submission, block mining, and recipient wallet scanning. Key changes: - TestnetNode: global output index, spent key image tracking, getOuts/getOutputDistribution/sendRawTransaction/isKeyImageSpent RPCs - TestnetMiner: includes mempool user transactions in mined blocks - Testnet.transfer(): end-to-end send from wallet to wallet - WalletSync: fallback to JSON-based tx processing when as_hex unavailable - 5 new tests covering transfer, change outputs, double-spend rejection
This commit is contained in:
+73
-2
@@ -195,6 +195,77 @@ describe('Testnet Integration', () => {
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('Testnet Transactions', () => {
|
||||
let testnet;
|
||||
|
||||
beforeAll(async () => {
|
||||
testnet = new Testnet();
|
||||
await testnet.init();
|
||||
// Mine 70+ blocks for enough outputs for ring size 16,
|
||||
// plus 60 for maturity window = 130 total needed
|
||||
await testnet.mineBlocks(130);
|
||||
}, 60000);
|
||||
|
||||
test('chain has enough outputs for ring selection', () => {
|
||||
expect(testnet.node.globalOutputs.length).toBeGreaterThanOrEqual(70);
|
||||
});
|
||||
|
||||
test('miner wallet has unlocked balance', async () => {
|
||||
const wallet = testnet.getMinerWallet();
|
||||
await testnet.syncWallet(wallet);
|
||||
const { unlockedBalance } = await testnet.getBalance(wallet);
|
||||
expect(unlockedBalance).toBeGreaterThan(0n);
|
||||
}, 30000);
|
||||
|
||||
test('basic transfer: miner → new wallet', async () => {
|
||||
const minerWallet = testnet.getMinerWallet();
|
||||
const recipient = testnet.createWallet();
|
||||
|
||||
const transferAmount = 1000000000n; // 1 SAL
|
||||
|
||||
const { tx, fee } = await testnet.transfer(
|
||||
minerWallet,
|
||||
{
|
||||
viewPublicKey: recipient.keys.viewPublicKey,
|
||||
spendPublicKey: recipient.keys.spendPublicKey,
|
||||
},
|
||||
transferAmount,
|
||||
);
|
||||
|
||||
expect(tx).toBeDefined();
|
||||
expect(fee).toBeGreaterThan(0n);
|
||||
|
||||
// Sync recipient and verify balance
|
||||
await testnet.syncWallet(recipient);
|
||||
const { balance } = await testnet.getBalance(recipient);
|
||||
expect(balance).toBe(transferAmount);
|
||||
}, 30000);
|
||||
|
||||
test('sender has change output after transfer', async () => {
|
||||
const minerWallet = testnet.getMinerWallet();
|
||||
await testnet.syncWallet(minerWallet);
|
||||
|
||||
const { balance } = await testnet.getBalance(minerWallet);
|
||||
// Miner should still have funds (change from previous transfer + new coinbase)
|
||||
expect(balance).toBeGreaterThan(0n);
|
||||
}, 30000);
|
||||
|
||||
test('double-spend is rejected', async () => {
|
||||
// Key images from the first transfer should be marked as spent
|
||||
expect(testnet.node.spentKeyImages.size).toBeGreaterThan(0);
|
||||
|
||||
const spent = await testnet.node.isKeyImageSpent(
|
||||
[...testnet.node.spentKeyImages].slice(0, 1)
|
||||
);
|
||||
expect(spent.spent_status[0]).toBe(1);
|
||||
|
||||
// Attempting to resubmit a TX with the same key image should fail
|
||||
const fakeTx = { _meta: { keyImages: [...testnet.node.spentKeyImages].slice(0, 1) } };
|
||||
const result = await testnet.node.sendRawTransaction(fakeTx);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testnet CARROT Hard Fork', () => {
|
||||
let testnet;
|
||||
|
||||
@@ -240,7 +311,7 @@ describe('Testnet CARROT Hard Fork', () => {
|
||||
for (const o of carrotOutputs) {
|
||||
expect(o.amount).toBeGreaterThan(0n);
|
||||
}
|
||||
});
|
||||
}, 120000);
|
||||
|
||||
test('post-fork blocks use carrot_v1 output format', async () => {
|
||||
const block = await testnet.node.getBlock({ height: 1100 });
|
||||
@@ -260,5 +331,5 @@ describe('Testnet CARROT Hard Fork', () => {
|
||||
expect(balance).toBeGreaterThan(0n);
|
||||
// With 1100+ blocks mined, most should be unlocked (60-block window)
|
||||
expect(unlockedBalance).toBeGreaterThan(0n);
|
||||
});
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user