iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

lightweight-wallet.test.cjs (6524B)


      1 const { test } = require('node:test');
      2 const assert = require('node:assert/strict');
      3 const { readFileSync } = require('node:fs');
      4 const { pathToFileURL } = require('node:url');
      5 
      6 let core;
      7 
      8 test.before(async () => {
      9   const source = readFileSync(require.resolve('../wallet/wallet-core.js'), 'utf8');
     10   core = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`);
     11 });
     12 
     13 test('migrates the single-wallet record into a named wallet collection', () => {
     14   const legacy = { address: 'ab'.repeat(32), ciphertext: 'encrypted' };
     15   const store = core.normalizeWalletStore(null, legacy);
     16 
     17   assert.equal(store.version, 2);
     18   assert.equal(store.wallets.length, 1);
     19   assert.equal(store.wallets[0].type, 'signing');
     20   assert.equal(store.wallets[0].record, legacy);
     21   assert.equal(store.activeId, store.wallets[0].id);
     22 });
     23 
     24 test('adds, selects, and removes multiple wallet types', () => {
     25   const signing = { id: 'signing', name: 'Daily', type: 'signing', publicKeyHex: '11'.repeat(32), record: { ciphertext: 'secret' } };
     26   const readonly = { id: 'readonly', name: 'Savings', type: 'readonly', publicKeyHex: '22'.repeat(32) };
     27   let store = core.upsertWallet(core.emptyWalletStore(), signing);
     28   store = core.upsertWallet(store, readonly);
     29 
     30   assert.deepEqual(store.wallets.map((wallet) => wallet.name), ['Daily', 'Savings']);
     31   assert.equal(store.activeId, 'readonly');
     32   assert.equal('record' in store.wallets[1], false);
     33 
     34   store = core.removeWallet(store, 'readonly');
     35   assert.equal(store.activeId, 'signing');
     36   assert.deepEqual(store.wallets.map((wallet) => wallet.id), ['signing']);
     37 });
     38 
     39 test('refuses to sign with a watch-only wallet', async () => {
     40   await assert.rejects(
     41     core.buildSignedTransfer({ wallet: { publicKeyHex: '22'.repeat(32) } }),
     42     /watch-only wallet cannot sign/,
     43   );
     44 });
     45 
     46 test('explains oversized transactions before submission', () => {
     47   const signature = 'aa'.repeat(64);
     48   const transaction = {
     49     kind: 'transfer',
     50     inputs: Array.from({ length: 205 }, (_, index) => ({
     51       outpoint: { txid: 'bb'.repeat(32), index },
     52       owner: 'cc'.repeat(32),
     53       signature,
     54     })),
     55     outputs: [
     56       { address: 'dd'.repeat(32), amount: 1 },
     57       { address: 'cc'.repeat(32), amount: 1 },
     58     ],
     59     fee: 1,
     60     signature,
     61   };
     62 
     63   assert.throws(
     64     () => core.ensureTransactionBodySize(transaction),
     65     /205 inputs, 65 KiB; maximum 64 KiB.*smaller amount.*consolidate/s,
     66   );
     67 });
     68 
     69 test('validates a configurable fee rate with a default minimum of one', () => {
     70   assert.equal(core.parseFeeRate('1'), 1n);
     71   assert.equal(core.parseFeeRate('25'), 25n);
     72   assert.throws(() => core.parseFeeRate('0'), /at least 1/);
     73   assert.throws(() => core.parseFeeRate('1.5'), /whole number/);
     74 });
     75 
     76 test('uses the selected fee rate when calculating a transfer', () => {
     77   const utxos = [{
     78     outpoint: { txid: 'aa'.repeat(32), index: 0 },
     79     output: { address: 'bb'.repeat(32), amount: 1_000_000 },
     80   }];
     81   const owner = 'bb'.repeat(32);
     82   const recipient = 'cc'.repeat(32);
     83   const standard = core.selectInputs(utxos, 100_000n, 1n, owner, recipient);
     84   const priority = core.selectInputs(utxos, 100_000n, 5n, owner, recipient);
     85 
     86   assert.equal(priority.fee, standard.fee * 5n);
     87 });
     88 
     89 test('recognizes hybrid Bech32m addresses without treating them as legacy keys', () => {
     90   const address = 'iuna1py9qlrnw6cm3mpz26hwwkww9spaa96zrw2g34gu0p4y3ea5cqhg0qa82x9s';
     91   const decoded = core.decodeVersionedAddress(address, 'iuna');
     92   assert.equal(decoded.version, 1);
     93   assert.equal(decoded.payloadHex.length, 64);
     94   assert.throws(() => core.decodeAddress(address, 'iuna'), /requires a legacy address/);
     95 });
     96 
     97 test('browser crypto derives the same rotating hybrid address vectors as the node', async () => {
     98   const modulePath = require.resolve('../wallet/crypto/iuna_wallet_crypto.js');
     99   const wasmPath = require.resolve('../wallet/crypto/iuna_wallet_crypto_bg.wasm');
    100   const quantum = await import(pathToFileURL(modulePath));
    101   await quantum.default({ module_or_path: readFileSync(wasmPath) });
    102   const addresses = JSON.parse(quantum.derive_external_addresses('hybrid-wallet-seed', 2, 'iuna-mainnet-v1'));
    103   assert.deepEqual(addresses, [
    104     { index: 0, address: 'iuna1py9qlrnw6cm3mpz26hwwkww9spaa96zrw2g34gu0p4y3ea5cqhg0qa82x9s' },
    105     { index: 1, address: 'iuna1pkxdcktlzf5tc2p59xns2nccq7rg5zjhwg2zn5r9u73c5j9jxgk8s0e5l4e' },
    106   ]);
    107   const rewardAddresses = JSON.parse(quantum.derive_reward_addresses('hybrid-wallet-seed', 2, 'iuna-mainnet-v1'));
    108   assert.deepEqual(rewardAddresses, [
    109     { index: 0, address: 'iuna1pvxpc35gavamxw0tvqj3ms82gwtvchh7mdyzqdttx7ktx7pfkgz4qkaq24k' },
    110     { index: 1, address: 'iuna1p6cj9c3ths75p7vnsdx9tgkvu4jlzs8cxpnh2lc4klsu5fvd0cpxq8ypw45' },
    111   ]);
    112 
    113   const built = JSON.parse(quantum.build_transfer(JSON.stringify({
    114     seed: 'hybrid-wallet-seed',
    115     chainId: 'iuna-mainnet-candidate',
    116     genesisHash: '11'.repeat(32),
    117     recipientAddress: addresses[1].address,
    118     amount: '1000000',
    119     feeRate: '1',
    120     changeIndex: 2,
    121     utxos: [{
    122       addressIndex: 0,
    123       outpoint: { txid: '22'.repeat(32), index: 7 },
    124       output: { amount: 2000000 },
    125     }],
    126   })));
    127   assert.equal(built.fee, '4079');
    128   assert.equal(built.envelope.length / 2, 4079);
    129   assert.equal(built.transactionId, 'bda748b6ba9fd6550ca47d580f980a1c00d0050a20992750265a62d23b0b590d');
    130 
    131   const rewardBuilt = JSON.parse(quantum.build_transfer(JSON.stringify({
    132     seed: 'hybrid-wallet-seed',
    133     chainId: 'iuna-mainnet-candidate',
    134     genesisHash: '11'.repeat(32),
    135     recipientAddress: addresses[1].address,
    136     amount: '1000000',
    137     feeRate: '1',
    138     changeIndex: 2,
    139     utxos: [{
    140       addressBranch: 'reward',
    141       addressIndex: 0,
    142       outpoint: { txid: '44'.repeat(32), index: 8 },
    143       output: { amount: 2000000 },
    144     }],
    145   })));
    146   assert.equal(rewardBuilt.fee, '4079');
    147   assert.equal(rewardBuilt.inputCount, 1);
    148   assert.equal(rewardBuilt.transactionId, '558cee1dfa1425e0b214681445e3d0641a898e15245f80d4524d2b98d762df88');
    149 
    150   const migration = JSON.parse(quantum.build_migration(JSON.stringify({
    151     seed: 'hybrid-wallet-seed',
    152     chainId: 'iuna-mainnet-candidate',
    153     genesisHash: '11'.repeat(32),
    154     destinationIndex: 2,
    155     feeRate: '1',
    156     utxos: [{
    157       outpoint: { txid: '33'.repeat(32), index: 4 },
    158       output: { amount: 2000000 },
    159     }],
    160   })));
    161   assert.equal(migration.fee, '307');
    162   assert.equal(migration.envelope.length / 2, 307);
    163   assert.equal(migration.transactionId, '37539c9d89a391c599b838c3f415e7adc93a36c22cd4fb5fe44a79433f336075');
    164 });