/* eslint-env mocha */ /* eslint prefer-arrow-callback: "off" */ 'use strict'; const assert = require('./util/assert'); const consensus = require('../lib/protocol/consensus'); const util = require('../lib/utils/util'); const encoding = require('../lib/utils/encoding'); const digest = require('../lib/crypto/digest'); const random = require('../lib/crypto/random'); const WalletDB = require('../lib/wallet/walletdb'); const WorkerPool = require('../lib/workers/workerpool'); const Address = require('../lib/primitives/address'); const MTX = require('../lib/primitives/mtx'); const Coin = require('../lib/primitives/coin'); const KeyRing = require('../lib/primitives/keyring'); const Input = require('../lib/primitives/input'); const Outpoint = require('../lib/primitives/outpoint'); const Script = require('../lib/script/script'); const HD = require('../lib/hd'); const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; const KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp' + 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa'; const workers = new WorkerPool({ enabled: false }); const wdb = new WalletDB({ db: 'memory', verify: true, workers }); let currentWallet = null; let importedWallet = null; let importedKey = null; let doubleSpendWallet = null; let doubleSpendCoin = null; let globalTime = util.now(); let globalHeight = 1; function nextBlock() { const height = globalHeight++; const time = globalTime++; const prevHead = encoding.U32(height - 1); const prevHash = digest.hash256(prevHead); const head = encoding.U32(height); const hash = digest.hash256(head); return { hash: hash.toString('hex'), height: height, prevBlock: prevHash.toString('hex'), time: time, merkleRoot: encoding.NULL_HASH, nonce: 0, bits: 0 }; } function dummyInput() { const hash = random.randomBytes(32).toString('hex'); return Input.fromOutpoint(new Outpoint(hash, 0)); } async function testP2PKH(witness, nesting) { const flags = Script.flags.STANDARD_VERIFY_FLAGS; const wallet = await wdb.create({ witness }); const addr = Address.fromString(wallet.getAddress('string')); const type = witness ? Address.types.WITNESS : Address.types.PUBKEYHASH; assert.strictEqual(addr.type, type); const src = new MTX(); src.addInput(dummyInput()); src.addOutput(nesting ? wallet.getNested() : wallet.getAddress(), 5460 * 2); src.addOutput(new Address(), 2 * 5460); const mtx = new MTX(); mtx.addTX(src, 0); mtx.addOutput(wallet.getAddress(), 5460); await wallet.sign(mtx); const [tx, view] = mtx.commit(); assert(tx.verify(view, flags)); } async function testP2SH(witness, nesting) { const flags = Script.flags.STANDARD_VERIFY_FLAGS; const receive = nesting ? 'nested' : 'receive'; const receiveDepth = nesting ? 'nestedDepth' : 'receiveDepth'; const vector = witness ? 'witness' : 'script'; // Create 3 2-of-3 wallets with our pubkeys as "shared keys" const options = { witness, type: 'multisig', m: 2, n: 3 }; const alice = await wdb.create(options); const bob = await wdb.create(options); const carol = await wdb.create(options); const recipient = await wdb.create(); await alice.addSharedKey(bob.account.accountKey); await alice.addSharedKey(carol.account.accountKey); await bob.addSharedKey(alice.account.accountKey); await bob.addSharedKey(carol.account.accountKey); await carol.addSharedKey(alice.account.accountKey); await carol.addSharedKey(bob.account.accountKey); // Our p2sh address const addr1 = alice.account[receive].getAddress(); if (witness) { const type = nesting ? Address.types.SCRIPTHASH : Address.types.WITNESS; assert.strictEqual(addr1.type, type); } else { assert.strictEqual(addr1.type, Address.types.SCRIPTHASH); } assert(alice.account[receive].getAddress().equals(addr1)); assert(bob.account[receive].getAddress().equals(addr1)); assert(carol.account[receive].getAddress().equals(addr1)); const nestedAddr1 = alice.getNested(); if (witness) { assert(nestedAddr1); assert(alice.getNested().equals(nestedAddr1)); assert(bob.getNested().equals(nestedAddr1)); assert(carol.getNested().equals(nestedAddr1)); } { // Add a shared unspent transaction to our wallets const fund = new MTX(); fund.addInput(dummyInput()); fund.addOutput(nesting ? nestedAddr1 : addr1, 5460 * 10); // Simulate a confirmation const block = nextBlock(); assert.strictEqual(alice.account[receiveDepth], 1); await wdb.addBlock(block, [fund.toTX()]); assert.strictEqual(alice.account[receiveDepth], 2); assert.strictEqual(alice.account.changeDepth, 1); } const addr2 = alice.account[receive].getAddress(); assert(!addr2.equals(addr1)); assert(alice.account[receive].getAddress().equals(addr2)); assert(bob.account[receive].getAddress().equals(addr2)); assert(carol.account[receive].getAddress().equals(addr2)); // Create a tx requiring 2 signatures const send = new MTX(); send.addOutput(recipient.getAddress(), 5460); assert(!send.verify(flags)); await alice.fund(send, { rate: 10000, round: true }); await alice.sign(send); assert(!send.verify(flags)); await bob.sign(send); const [tx, view] = send.commit(); assert(tx.verify(view, flags)); assert.strictEqual(alice.account.changeDepth, 1); const change = alice.account.change.getAddress(); assert(alice.account.change.getAddress().equals(change)); assert(bob.account.change.getAddress().equals(change)); assert(carol.account.change.getAddress().equals(change)); // Simulate a confirmation { const block = nextBlock(); await wdb.addBlock(block, [tx]); assert.strictEqual(alice.account[receiveDepth], 2); assert.strictEqual(alice.account.changeDepth, 2); assert(alice.account[receive].getAddress().equals(addr2)); assert(!alice.account.change.getAddress().equals(change)); } const change2 = alice.account.change.getAddress(); assert(alice.account.change.getAddress().equals(change2)); assert(bob.account.change.getAddress().equals(change2)); assert(carol.account.change.getAddress().equals(change2)); const input = tx.inputs[0]; const stack = input[vector].toStack(); stack.set(2, encoding.ZERO_SIG); input[vector].fromStack(stack); assert(!tx.verify(view, flags)); assert.strictEqual(tx.getFee(view), 10000); } describe('Wallet', function() { this.timeout(5000); it('should open walletdb', async () => { consensus.COINBASE_MATURITY = 0; await wdb.open(); }); it('should generate new key and address', async () => { const wallet = await wdb.create(); const addr1 = wallet.getAddress(); assert(addr1); const str = addr1.toString(); const addr2 = Address.fromString(str); assert(addr2.equals(addr1)); }); it('should validate existing address', () => { assert(Address.fromString('1KQ1wMNwXHUYj1nV2xzsRcKUH8gVFpTFUc')); }); it('should fail to validate invalid address', () => { assert.throws(() => { Address.fromString('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc'); }); }); it('should create and get wallet', async () => { const wallet1 = await wdb.create(); await wallet1.destroy(); const wallet2 = await wdb.get(wallet1.id); assert(wallet1 !== wallet2); assert(wallet1.master !== wallet2.master); assert(wallet1.master.key.equals(wallet2.master.key)); assert(wallet1.account.accountKey.equals(wallet2.account.accountKey)); }); it('should sign/verify p2pkh tx', async () => { await testP2PKH(false, false); }); it('should sign/verify p2wpkh tx', async () => { await testP2PKH(true, false); }); it('should sign/verify p2wpkh tx w/ nested bullshit', async () => { await testP2PKH(true, true); }); it('should multisign/verify TX', async () => { const wallet = await wdb.create({ type: 'multisig', m: 1, n: 2 }); const xpriv = HD.PrivateKey.generate(); const key = xpriv.deriveAccount(44, 0).toPublic(); await wallet.addSharedKey(key); const script = Script.fromMultisig(1, 2, [ wallet.account.receive.getPublicKey(), key.derivePath('m/0/0').publicKey ]); // Input transaction (bare 1-of-2 multisig) const src = new MTX(); src.addInput(dummyInput()); src.addOutput(script, 5460 * 2); src.addOutput(new Address(), 5460 * 2); const tx = new MTX(); tx.addTX(src, 0); tx.addOutput(wallet.getAddress(), 5460); const maxSize = await tx.estimateSize(); await wallet.sign(tx); assert(tx.toRaw().length <= maxSize); assert(tx.verify()); }); it('should handle missed and invalid txs', async () => { const alice = await wdb.create(); const bob = await wdb.create(); // Coinbase // balance: 51000 const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 50000); t1.addOutput(alice.getAddress(), 1000); const t2 = new MTX(); t2.addTX(t1, 0); // 50000 t2.addOutput(alice.getAddress(), 24000); t2.addOutput(alice.getAddress(), 24000); // Save for later. doubleSpendWallet = alice; doubleSpendCoin = Coin.fromTX(t1, 0, -1); // balance: 49000 await alice.sign(t2); const t3 = new MTX(); t3.addTX(t1, 1); // 1000 t3.addTX(t2, 0); // 24000 t3.addOutput(alice.getAddress(), 23000); // balance: 47000 await alice.sign(t3); const t4 = new MTX(); t4.addTX(t2, 1); // 24000 t4.addTX(t3, 0); // 23000 t4.addOutput(alice.getAddress(), 11000); t4.addOutput(alice.getAddress(), 11000); // balance: 22000 await alice.sign(t4); const f1 = new MTX(); f1.addTX(t4, 1); // 11000 f1.addOutput(bob.getAddress(), 10000); // balance: 11000 await alice.sign(f1); const fake = new MTX(); fake.addTX(t1, 1); // 1000 (already redeemed) fake.addOutput(alice.getAddress(), 500); // Script inputs but do not sign await alice.template(fake); // Fake signature const input = fake.inputs[0]; const script = input.script.write(); script.setData(0, encoding.ZERO_SIG); script.compile(); // balance: 11000 // Fake TX should temporarily change output. { await wdb.addTX(fake.toTX()); await wdb.addTX(t4.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 22500); } { await wdb.addTX(t1.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 72500); } { await wdb.addTX(t2.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 46500); } { await wdb.addTX(t3.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 22000); } { await wdb.addTX(f1.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 11000); const txs = await alice.getHistory(); assert(txs.some((wtx) => { return wtx.hash === f1.hash('hex'); })); } { const balance = await bob.getBalance(); assert.strictEqual(balance.unconfirmed, 10000); const txs = await bob.getHistory(); assert(txs.some((wtx) => { return wtx.tx.hash('hex') === f1.hash('hex'); })); } }); it('should cleanup spenders after double-spend', async () => { const wallet = doubleSpendWallet; { const txs = await wallet.getHistory(); assert.strictEqual(txs.length, 5); const total = txs.reduce((t, wtx) => { return t + wtx.tx.getOutputValue(); }, 0); assert.strictEqual(total, 154000); } { const balance = await wallet.getBalance(); assert.strictEqual(balance.unconfirmed, 11000); } { const tx = new MTX(); tx.addCoin(doubleSpendCoin); tx.addOutput(wallet.getAddress(), 5000); await wallet.sign(tx); await wdb.addTX(tx.toTX()); const balance = await wallet.getBalance(); assert.strictEqual(balance.unconfirmed, 6000); } { const txs = await wallet.getHistory(); assert.strictEqual(txs.length, 2); const total = txs.reduce((t, wtx) => { return t + wtx.tx.getOutputValue(); }, 0); assert.strictEqual(total, 56000); } }); it('should handle missed txs without resolution', async () => { const wdb = new WalletDB({ name: 'wallet-test', db: 'memory', verify: false }); await wdb.open(); const alice = await wdb.create(); const bob = await wdb.create(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 50000); t1.addOutput(alice.getAddress(), 1000); // balance: 51000 const t2 = new MTX(); t2.addTX(t1, 0); // 50000 t2.addOutput(alice.getAddress(), 24000); t2.addOutput(alice.getAddress(), 24000); // balance: 49000 await alice.sign(t2); const t3 = new MTX(); t3.addTX(t1, 1); // 1000 t3.addTX(t2, 0); // 24000 t3.addOutput(alice.getAddress(), 23000); // balance: 47000 await alice.sign(t3); const t4 = new MTX(); t4.addTX(t2, 1); // 24000 t4.addTX(t3, 0); // 23000 t4.addOutput(alice.getAddress(), 11000); t4.addOutput(alice.getAddress(), 11000); // balance: 22000 await alice.sign(t4); const f1 = new MTX(); f1.addTX(t4, 1); // 11000 f1.addOutput(bob.getAddress(), 10000); // balance: 11000 await alice.sign(f1); { await wdb.addTX(t4.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 22000); } { await wdb.addTX(t1.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 73000); } { await wdb.addTX(t2.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 47000); } { await wdb.addTX(t3.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 22000); } { await wdb.addTX(f1.toTX()); const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 11000); const txs = await alice.getHistory(); assert(txs.some((wtx) => { return wtx.tx.hash('hex') === f1.hash('hex'); })); } { const balance = await bob.getBalance(); assert.strictEqual(balance.unconfirmed, 10000); const txs = await bob.getHistory(); assert(txs.some((wtx) => { return wtx.tx.hash('hex') === f1.hash('hex'); })); } await wdb.addTX(t2.toTX()); await wdb.addTX(t3.toTX()); await wdb.addTX(t4.toTX()); await wdb.addTX(f1.toTX()); { const balance = await alice.getBalance(); assert.strictEqual(balance.unconfirmed, 11000); } { const balance = await bob.getBalance(); assert.strictEqual(balance.unconfirmed, 10000); } }); it('should fill tx with inputs', async () => { const alice = await wdb.create(); const bob = await wdb.create(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Create new transaction const m2 = new MTX(); m2.addOutput(bob.getAddress(), 5460); await alice.fund(m2, { rate: 10000, round: true }); await alice.sign(m2); const [t2, v2] = m2.commit(); assert(t2.verify(v2)); assert.strictEqual(t2.getInputValue(v2), 16380); assert.strictEqual(t2.getOutputValue(), 6380); assert.strictEqual(t2.getFee(v2), 10000); // Create new transaction const t3 = new MTX(); t3.addOutput(bob.getAddress(), 15000); let err; try { await alice.fund(t3, { rate: 10000, round: true }); } catch (e) { err = e; } assert(err); assert.strictEqual(err.requiredFunds, 25000); }); it('should fill tx with inputs with accurate fee', async () => { const alice = await wdb.create({ master: KEY1 }); const bob = await wdb.create({ master: KEY2 }); // Coinbase const t1 = new MTX(); t1.addOutpoint(new Outpoint(encoding.NULL_HASH, 0)); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Create new transaction const m2 = new MTX(); m2.addOutput(bob.getAddress(), 5460); await alice.fund(m2, { rate: 10000 }); await alice.sign(m2); const [t2, v2] = m2.commit(); assert(t2.verify(v2)); assert.strictEqual(t2.getInputValue(v2), 16380); // Should now have a change output: assert.strictEqual(t2.getOutputValue(), 11130); assert.strictEqual(t2.getFee(v2), 5250); assert.strictEqual(t2.getWeight(), 2084); assert.strictEqual(t2.getBaseSize(), 521); assert.strictEqual(t2.getSize(), 521); assert.strictEqual(t2.getVirtualSize(), 521); let balance; bob.once('balance', (b) => { balance = b; }); await wdb.addTX(t2); // Create new transaction const t3 = new MTX(); t3.addOutput(bob.getAddress(), 15000); let err; try { await alice.fund(t3, { rate: 10000 }); } catch (e) { err = e; } assert(err); assert(balance); assert.strictEqual(balance.unconfirmed, 5460); }); it('should sign multiple inputs using different keys', async () => { const alice = await wdb.create(); const bob = await wdb.create(); const carol = await wdb.create(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); // Coinbase const t2 = new MTX(); t2.addInput(dummyInput()); t2.addOutput(bob.getAddress(), 5460); t2.addOutput(bob.getAddress(), 5460); t2.addOutput(bob.getAddress(), 5460); t2.addOutput(bob.getAddress(), 5460); await wdb.addTX(t1.toTX()); await wdb.addTX(t2.toTX()); // Create our tx with an output const tx = new MTX(); tx.addOutput(carol.getAddress(), 5460); const coins1 = await alice.getCoins(); const coins2 = await bob.getCoins(); // Add our unspent inputs to sign tx.addCoin(coins1[0]); tx.addCoin(coins1[1]); tx.addCoin(coins2[0]); // Sign transaction assert.strictEqual(await alice.sign(tx), 2); assert.strictEqual(await bob.sign(tx), 1); // Verify assert.strictEqual(tx.verify(), true); tx.inputs.length = 0; tx.addCoin(coins1[1]); tx.addCoin(coins1[2]); tx.addCoin(coins2[1]); assert.strictEqual(await alice.sign(tx), 2); assert.strictEqual(await bob.sign(tx), 1); // Verify assert.strictEqual(tx.verify(), true); }); it('should verify 2-of-3 p2sh tx', async () => { await testP2SH(false, false); }); it('should verify 2-of-3 p2wsh tx', async () => { await testP2SH(true, false); }); it('should verify 2-of-3 p2wsh tx w/ nested bullshit', async () => { await testP2SH(true, true); }); it('should fill tx with account 1', async () => { const alice = await wdb.create(); const bob = await wdb.create(); { const account = await alice.createAccount({ name: 'foo' }); assert.strictEqual(account.name, 'foo'); assert.strictEqual(account.accountIndex, 1); } const account = await alice.getAccount('foo'); assert.strictEqual(account.name, 'foo'); assert.strictEqual(account.accountIndex, 1); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(account.receive.getAddress(), 5460); t1.addOutput(account.receive.getAddress(), 5460); t1.addOutput(account.receive.getAddress(), 5460); t1.addOutput(account.receive.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Create new transaction const t2 = new MTX(); t2.addOutput(bob.getAddress(), 5460); await alice.fund(t2, { rate: 10000, round: true }); await alice.sign(t2); assert(t2.verify()); assert.strictEqual(t2.getInputValue(), 16380); assert.strictEqual(t2.getOutputValue(), 6380); assert.strictEqual(t2.getFee(), 10000); // Create new transaction const t3 = new MTX(); t3.addOutput(bob.getAddress(), 15000); let err; try { await alice.fund(t3, { rate: 10000, round: true }); } catch (e) { err = e; } assert(err); assert.strictEqual(err.requiredFunds, 25000); const accounts = await alice.getAccounts(); assert.deepStrictEqual(accounts, ['default', 'foo']); }); it('should fail to fill tx with account 1', async () => { const wallet = await wdb.create(); { const account = await wallet.createAccount({ name: 'foo' }); assert.strictEqual(account.name, 'foo'); assert.strictEqual(account.accountIndex, 1); } const account = await wallet.getAccount('foo'); assert.strictEqual(account.name, 'foo'); assert.strictEqual(account.accountIndex, 1); assert.strictEqual(wallet.account.accountIndex, 0); assert(!account.receive.getAddress().equals( wallet.account.receive.getAddress())); assert(wallet.getAddress().equals(wallet.account.receive.getAddress())); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(wallet.getAddress(), 5460); t1.addOutput(wallet.getAddress(), 5460); t1.addOutput(wallet.getAddress(), 5460); t1.addOutput(account.receive.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Should fill from `foo` and fail const t2 = new MTX(); t2.addOutput(wallet.getAddress(), 5460); let err; try { await wallet.fund(t2, { rate: 10000, round: true, account: 'foo' }); } catch (e) { err = e; } assert(err); // Should fill from whole wallet and succeed const t3 = new MTX(); t3.addOutput(wallet.getAddress(), 5460); await wallet.fund(t3, { rate: 10000, round: true }); // Coinbase const t4 = new MTX(); t4.addInput(dummyInput()); t4.addOutput(account.receive.getAddress(), 5460); t4.addOutput(account.receive.getAddress(), 5460); t4.addOutput(account.receive.getAddress(), 5460); await wdb.addTX(t4.toTX()); // Should fill from `foo` and succeed const t5 = new MTX(); t5.addOutput(wallet.getAddress(), 5460); await wallet.fund(t5, { rate: 10000, round: true, account: 'foo' }); currentWallet = wallet; }); it('should create two accounts (multiple encryption)', async () => { { const wallet = await wdb.create({ id: 'foobar', passphrase: 'foo' }); await wallet.destroy(); } const wallet = await wdb.get('foobar'); assert(wallet); const options = { name: 'foo1' }; const account = await wallet.createAccount(options, 'foo'); assert(account); await wallet.lock(); }); it('should fill tx with inputs when encrypted', async () => { const wallet = await wdb.create({ passphrase: 'foo' }); wallet.master.stop(); wallet.master.key = null; // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(wallet.getAddress(), 5460); t1.addOutput(wallet.getAddress(), 5460); t1.addOutput(wallet.getAddress(), 5460); t1.addOutput(wallet.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Create new transaction const t2 = new MTX(); t2.addOutput(wallet.getAddress(), 5460); await wallet.fund(t2, { rate: 10000, round: true }); // Should fail let err; try { await wallet.sign(t2, 'bar'); } catch (e) { err = e; } assert(err); assert(!t2.verify()); // Should succeed await wallet.sign(t2, 'foo'); assert(t2.verify()); }); it('should fill tx with inputs with subtract fee (1)', async () => { const alice = await wdb.create(); const bob = await wdb.create(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Create new transaction const t2 = new MTX(); t2.addOutput(bob.getAddress(), 21840); await alice.fund(t2, { rate: 10000, round: true, subtractFee: true }); await alice.sign(t2); assert(t2.verify()); assert.strictEqual(t2.getInputValue(), 5460 * 4); assert.strictEqual(t2.getOutputValue(), 21840 - 10000); assert.strictEqual(t2.getFee(), 10000); }); it('should fill tx with inputs with subtract fee (2)', async () => { const alice = await wdb.create(); const bob = await wdb.create(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); await wdb.addTX(t1.toTX()); const options = { subtractFee: true, rate: 10000, round: true, outputs: [{ address: bob.getAddress(), value: 21840 }] }; // Create new transaction const t2 = await alice.createTX(options); await alice.sign(t2); assert(t2.verify()); assert.strictEqual(t2.getInputValue(), 5460 * 4); assert.strictEqual(t2.getOutputValue(), 21840 - 10000); assert.strictEqual(t2.getFee(), 10000); }); it('should fill tx with smart coin selection', async () => { const alice = await wdb.create(); const bob = await wdb.create(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); t1.addOutput(alice.getAddress(), 5460); await wdb.addTX(t1.toTX()); // Coinbase const t2 = new MTX(); t2.addInput(dummyInput()); t2.addOutput(alice.getAddress(), 5460); t2.addOutput(alice.getAddress(), 5460); t2.addOutput(alice.getAddress(), 5460); t2.addOutput(alice.getAddress(), 5460); const block = nextBlock(); await wdb.addBlock(block, [t2.toTX()]); { const coins = await alice.getSmartCoins(); assert.strictEqual(coins.length, 4); for (let i = 0; i < coins.length; i++) { const coin = coins[i]; assert.strictEqual(coin.height, block.height); } } // Create a change output for ourselves. await alice.send({ subtractFee: true, rate: 1000, depth: 1, outputs: [{ address: bob.getAddress(), value: 1461 }] }); const coins = await alice.getSmartCoins(); assert.strictEqual(coins.length, 4); let total = 0; { let found = false; for (let i = 0; i < coins.length; i++) { const coin = coins[i]; if (coin.height === -1) { assert(!found); assert(coin.value < 5460); found = true; } else { assert.strictEqual(coin.height, block.height); } total += coin.value; } assert(found); } // Use smart selection const options = { subtractFee: true, smart: true, rate: 10000, outputs: [{ address: bob.getAddress(), value: total }] }; const t3 = await alice.createTX(options); assert.strictEqual(t3.inputs.length, 4); { let found = false; for (let i = 0; i < t3.inputs.length; i++) { const coin = t3.view.getCoinFor(t3.inputs[i]); if (coin.height === -1) { assert(!found); assert(coin.value < 5460); found = true; } else { assert.strictEqual(coin.height, block.height); } } assert(found); } await alice.sign(t3); assert(t3.verify()); }); it('should get range of txs', async () => { const wallet = currentWallet; const txs = await wallet.getRange({ start: util.now() - 1000 }); assert.strictEqual(txs.length, 2); }); it('should get range of txs from account', async () => { const wallet = currentWallet; const txs = await wallet.getRange('foo', { start: util.now() - 1000 }); assert.strictEqual(txs.length, 2); }); it('should not get range of txs from non-existent account', async () => { const wallet = currentWallet; let txs, err; try { txs = await wallet.getRange('bad', { start: 0xdeadbeef - 1000 }); } catch (e) { err = e; } assert(!txs); assert(err); assert.strictEqual(err.message, 'Account not found.'); }); it('should get account balance', async () => { const wallet = currentWallet; const balance = await wallet.getBalance('foo'); assert.strictEqual(balance.unconfirmed, 21840); }); it('should import privkey', async () => { const key = KeyRing.generate(); const wallet = await wdb.create({ passphrase: 'test' }); await wallet.importKey('default', key, 'test'); const wkey = await wallet.getKey(key.getHash('hex')); assert.strictEqual(wkey.getHash('hex'), key.getHash('hex')); // Coinbase const t1 = new MTX(); t1.addOutput(key.getAddress(), 5460); t1.addOutput(key.getAddress(), 5460); t1.addOutput(key.getAddress(), 5460); t1.addOutput(key.getAddress(), 5460); t1.addInput(dummyInput()); await wdb.addTX(t1.toTX()); const wtx = await wallet.getTX(t1.hash('hex')); assert(wtx); assert.strictEqual(t1.hash('hex'), wtx.hash); const options = { rate: 10000, round: true, outputs: [{ address: wallet.getAddress(), value: 7000 }] }; // Create new transaction const t2 = await wallet.createTX(options); await wallet.sign(t2); assert(t2.verify()); assert.strictEqual(t2.inputs[0].prevout.hash, wtx.hash); importedWallet = wallet; importedKey = key; }); it('should import pubkey', async () => { const key = KeyRing.generate(); const pub = new KeyRing(key.publicKey); const wallet = await wdb.create({ watchOnly: true }); await wallet.importKey('default', pub); const path = await wallet.getPath(pub.getHash('hex')); assert.strictEqual(path.hash, pub.getHash('hex')); const wkey = await wallet.getKey(pub.getHash('hex')); assert(wkey); }); it('should import address', async () => { const key = KeyRing.generate(); const wallet = await wdb.create({ watchOnly: true }); await wallet.importAddress('default', key.getAddress()); const path = await wallet.getPath(key.getHash('hex')); assert(path); assert.strictEqual(path.hash, key.getHash('hex')); const wkey = await wallet.getKey(key.getHash('hex')); assert(!wkey); }); it('should get details', async () => { const wallet = currentWallet; const txs = await wallet.getRange('foo', { start: util.now() - 1000 }); const details = await wallet.toDetails(txs); assert(details.some((tx) => { return tx.toJSON().outputs[0].path.name === 'foo'; })); }); it('should rename wallet', async () => { const wallet = currentWallet; await wallet.rename('test'); const txs = await wallet.getRange('foo', { start: util.now() - 1000 }); const details = await wallet.toDetails(txs); assert.strictEqual(details[0].toJSON().id, 'test'); }); it('should change passphrase with encrypted imports', async () => { const wallet = importedWallet; const addr = importedKey.getAddress(); assert(wallet.master.encrypted); let data; { const path = await wallet.getPath(addr); assert(path); assert(path.data && path.encrypted); data = path.data; } await wallet.decrypt('test'); { const path = await wallet.getPath(addr); assert(path); assert(path.data && !path.encrypted); assert(await wallet.getKey(addr)); } await wallet.encrypt('foo'); { const path = await wallet.getPath(addr); assert(path); assert(path.data && path.encrypted); assert(!data.equals(path.data)); assert(!await wallet.getKey(addr)); } await wallet.unlock('foo'); const key = await wallet.getKey(addr); assert(key); assert.strictEqual(key.getHash('hex'), addr.getHash('hex')); }); it('should recover from a missed tx', async () => { const wdb = new WalletDB({ name: 'wallet-test', db: 'memory', verify: false }); await wdb.open(); const alice = await wdb.create({ master: KEY1 }); const bob = await wdb.create({ master: KEY1 }); const addr = alice.getAddress(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(addr, 50000); await alice.add(t1.toTX()); await bob.add(t1.toTX()); // Bob misses this tx! const t2 = new MTX(); t2.addTX(t1, 0); t2.addOutput(addr, 24000); t2.addOutput(addr, 24000); await alice.sign(t2); await alice.add(t2.toTX()); assert.notStrictEqual( (await alice.getBalance()).unconfirmed, (await bob.getBalance()).unconfirmed); // Bob sees this one. const t3 = new MTX(); t3.addTX(t2, 0); t3.addTX(t2, 1); t3.addOutput(addr, 30000); await alice.sign(t3); assert.strictEqual((await bob.getBalance()).unconfirmed, 50000); await alice.add(t3.toTX()); await bob.add(t3.toTX()); assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); // Bob sees t2 on the chain. await bob.add(t2.toTX()); // Bob sees t3 on the chain. await bob.add(t3.toTX()); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000); }); it('should recover from a missed tx and double spend', async () => { const wdb = new WalletDB({ name: 'wallet-test', db: 'memory', verify: false }); await wdb.open(); const alice = await wdb.create({ master: KEY1 }); const bob = await wdb.create({ master: KEY1 }); const addr = alice.getAddress(); // Coinbase const t1 = new MTX(); t1.addInput(dummyInput()); t1.addOutput(addr, 50000); await alice.add(t1.toTX()); await bob.add(t1.toTX()); // Bob misses this tx! const t2a = new MTX(); t2a.addTX(t1, 0); t2a.addOutput(addr, 24000); t2a.addOutput(addr, 24000); await alice.sign(t2a); await alice.add(t2a.toTX()); assert.notStrictEqual( (await alice.getBalance()).unconfirmed, (await bob.getBalance()).unconfirmed); // Bob doublespends. const t2b = new MTX(); t2b.addTX(t1, 0); t2b.addOutput(addr, 10000); t2b.addOutput(addr, 10000); await bob.sign(t2b); await bob.add(t2b.toTX()); // Bob sees this one. const t3 = new MTX(); t3.addTX(t2a, 0); t3.addTX(t2a, 1); t3.addOutput(addr, 30000); await alice.sign(t3); assert.strictEqual((await bob.getBalance()).unconfirmed, 20000); await alice.add(t3.toTX()); await bob.add(t3.toTX()); assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); // Bob sees t2a on the chain. await bob.add(t2a.toTX()); // Bob sees t3 on the chain. await bob.add(t3.toTX()); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000); }); it('should cleanup', () => { consensus.COINBASE_MATURITY = 100; }); });