var assert = require('assert') var bufferutils = require('../src/bufferutils') var crypto = require('../src/crypto') var networks = require('../src/networks') var sinon = require('sinon') var scripts = require('../src/scripts') var Address = require('../src/address') var HDNode = require('../src/hdnode') var Transaction = require('../src/transaction') var TransactionBuilder = require('../src/transaction_builder') var Wallet = require('../src/wallet') var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx var fixtureTx2Hex = fixtureTxes.tx function fakeTxHash(i) { var hash = new Buffer(32) hash.fill(i) return hash } function fakeTxId(i) { var hash = fakeTxHash(i) Array.prototype.reverse.call(hash) return hash.toString('hex') } describe('Wallet', function() { var seed beforeEach(function() { seed = crypto.sha256("don't use a string seed like this in real life") }) describe('constructor', function() { var wallet beforeEach(function() { wallet = new Wallet(seed) }) it('defaults to Bitcoin network', function() { assert.equal(wallet.getMasterKey().network, networks.bitcoin) }) it("generates m/0' as the main account", function() { var mainAccount = wallet.getAccountZero() assert.equal(mainAccount.index, 0 + HDNode.HIGHEST_BIT) assert.equal(mainAccount.depth, 1) }) it("generates m/0'/0 as the external account", function() { var account = wallet.getExternalAccount() assert.equal(account.index, 0) assert.equal(account.depth, 2) }) it("generates m/0'/1 as the internal account", function() { var account = wallet.getInternalAccount() assert.equal(account.index, 1) assert.equal(account.depth, 2) }) describe('when seed is not specified', function() { it('generates a seed', function() { var wallet = new Wallet() assert(wallet.getMasterKey()) }) }) describe('constructor options', function() { beforeEach(function() { wallet = new Wallet(seed, networks.testnet) }) it('uses the network if specified', function() { assert.equal(wallet.getMasterKey().network, networks.testnet) }) }) }) describe('newMasterKey', function() { it('resets accounts', function() { var wallet = new Wallet() var oldAccountZero = wallet.getAccountZero() var oldExternalAccount = wallet.getExternalAccount() var oldInternalAccount = wallet.getInternalAccount() wallet.newMasterKey(seed) assertNotEqual(wallet.getAccountZero(), oldAccountZero) assertNotEqual(wallet.getExternalAccount(), oldExternalAccount) assertNotEqual(wallet.getInternalAccount(), oldInternalAccount) }) it('resets addresses', function() { var wallet = new Wallet() wallet.generateAddress() wallet.generateChangeAddress() var oldAddresses = wallet.addresses var oldChangeAddresses = wallet.changeAddresses assert.notDeepEqual(oldAddresses, []) assert.notDeepEqual(oldChangeAddresses, []) wallet.newMasterKey(seed) assert.deepEqual(wallet.addresses, []) assert.deepEqual(wallet.changeAddresses, []) }) }) describe('generateAddress', function() { it('generate receiving addresses', function() { var wallet = new Wallet(seed, networks.testnet) var expectedAddresses = [ "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa", "n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X" ] assert.equal(wallet.generateAddress(), expectedAddresses[0]) assert.equal(wallet.generateAddress(), expectedAddresses[1]) assert.deepEqual(wallet.addresses, expectedAddresses) }) }) describe('generateChangeAddress', function() { var wallet beforeEach(function() { wallet = new Wallet(seed) }) it('generates change addresses', function() { var wallet = new Wallet(seed, networks.testnet) var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"] assert.equal(wallet.generateChangeAddress(), expectedAddresses[0]) assert.deepEqual(wallet.changeAddresses, expectedAddresses) }) }) describe('getPrivateKey', function() { var wallet beforeEach(function() { wallet = new Wallet(seed) }) it('returns the private key at the given index of external account', function() { var wallet = new Wallet(seed, networks.testnet) assertEqual(wallet.getPrivateKey(0), wallet.getExternalAccount().derive(0).privKey) assertEqual(wallet.getPrivateKey(1), wallet.getExternalAccount().derive(1).privKey) }) }) describe('getInternalPrivateKey', function() { var wallet beforeEach(function() { wallet = new Wallet(seed) }) it('returns the private key at the given index of internal account', function() { var wallet = new Wallet(seed, networks.testnet) assertEqual(wallet.getInternalPrivateKey(0), wallet.getInternalAccount().derive(0).privKey) assertEqual(wallet.getInternalPrivateKey(1), wallet.getInternalAccount().derive(1).privKey) }) }) describe('getPrivateKeyForAddress', function() { var wallet beforeEach(function() { wallet = new Wallet(seed) }) it('returns the private key for the given address', function() { var wallet = new Wallet(seed, networks.testnet) wallet.generateChangeAddress() wallet.generateAddress() wallet.generateAddress() assertEqual( wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"), wallet.getExternalAccount().derive(1).privKey ) assertEqual( wallet.getPrivateKeyForAddress("mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"), wallet.getInternalAccount().derive(0).privKey ) }) it('raises an error when address is not found', function() { var wallet = new Wallet(seed, networks.testnet) assert.throws(function() { wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X") }, /Unknown address. Make sure the address is from the keychain and has been generated/) }) }) describe('Unspent Outputs', function() { var utxo, expectedOutputKey var wallet beforeEach(function() { utxo = { "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", "confirmations": 1, "index": 0, "txId": fakeTxId(6), "value": 20000, "pending": false } }) describe('on construction', function() { beforeEach(function() { wallet = new Wallet(seed, networks.bitcoin) wallet.setUnspentOutputs([utxo]) }) it('matches the expected behaviour', function() { var output = wallet.unspents[0] assert.equal(output.address, utxo.address) assert.equal(output.value, utxo.value) }) }) describe('getBalance', function() { beforeEach(function() { var utxo1 = cloneObject(utxo) utxo1.hash = fakeTxId(5) wallet = new Wallet(seed, networks.bitcoin) wallet.setUnspentOutputs([utxo, utxo1]) }) it('sums over utxo values', function() { assert.equal(wallet.getBalance(), 40000) }) }) describe('getUnspentOutputs', function() { beforeEach(function() { wallet = new Wallet(seed, networks.bitcoin) wallet.setUnspentOutputs([utxo]) }) it('parses wallet unspents to the expected format', function() { var outputs = wallet.getUnspentOutputs() var output = outputs[0] assert.equal(utxo.address, output.address) assert.equal(utxo.index, output.index) assert.equal(utxo.value, output.value) // FIXME: remove in 2.0.0 assert.equal(utxo.txId, output.hash) assert.equal(utxo.pending, output.pending) // new in 2.0.0 assert.equal(utxo.txId, output.txId) assert.equal(utxo.confirmations, output.confirmations) }) it("ignores spent unspents (outputs with 'spent' property)", function() { var unspent = wallet.unspents[0] unspent.pending = true unspent.spent = true assert.deepEqual(wallet.getUnspentOutputs(), []) }) }) }) describe('setUnspentOutputs', function() { var utxo var expectedOutputKey var wallet beforeEach(function() { utxo = { hash: fakeTxId(0), index: 0, address: '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', value: 500000 } wallet = new Wallet(seed, networks.bitcoin) }) it('matches the expected behaviour', function() { wallet.setUnspentOutputs([utxo]) var output = wallet.unspents[0] assert.equal(output.value, utxo.value) assert.equal(output.address, utxo.address) }) describe('required fields', function() { ['index', 'address', 'hash', 'value'].forEach(function(field){ it("throws an error when " + field + " is missing", function() { delete utxo[field] assert.throws(function() { wallet.setUnspentOutputs([utxo]) }) }) }) }) }) describe('Process transaction', function() { var wallet beforeEach(function() { wallet = new Wallet(seed) }) var addresses var tx beforeEach(function() { addresses = [ '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', '1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd', '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u' ] tx = Transaction.fromHex(fixtureTx1Hex) }) describe("processPendingTx", function() { it("incoming: sets the pending flag on output", function() { wallet.addresses = [addresses[0]] wallet.processPendingTx(tx) verifyOutputAdded(0, true) }) describe("when tx ins outpoint contains a known txhash:i", function() { var spendTx beforeEach(function() { wallet.addresses = [addresses[0]] wallet.processConfirmedTx(tx) spendTx = Transaction.fromHex(fixtureTx2Hex) }) it("outgoing: sets the pending flag and 'spent' on output", function() { var txIn = spendTx.ins[0] var txInId = new Buffer(txIn.hash) Array.prototype.reverse.call(txInId) txInId = txInId.toString('hex') var unspent = wallet.unspents[0] assert(!unspent.pending) wallet.processPendingTx(spendTx) assert(unspent.pending) assert(unspent.spent, true) }) }) }) describe('processConfirmedTx', function() { it('does not throw on scripts with no corresponding Address', function() { var pubKey = wallet.getPrivateKey(0).pub var script = scripts.pubKeyOutput(pubKey) var tx2 = new Transaction() tx2.addInput(fakeTxHash(1), 0) tx2.addOutput(script, 10000) wallet.processConfirmedTx(tx2) }) describe("when tx outs contains an address owned by the wallet, an 'output' gets added to wallet.unspentMap", function() { it("works for receive address", function() { var totalOuts = outputCount() wallet.addresses = [addresses[0]] wallet.processConfirmedTx(tx) assert.equal(outputCount(), totalOuts + 1) verifyOutputAdded(0, false) }) it("works for change address", function() { var totalOuts = outputCount() wallet.changeAddresses = [addresses[1]] wallet.processConfirmedTx(tx) assert.equal(outputCount(), totalOuts + 1) verifyOutputAdded(1, false) }) function outputCount() { return Object.keys(wallet.unspentMap).length } }) describe("when tx ins contains a known txhash:i", function() { var spendTx beforeEach(function() { wallet.addresses = [addresses[0]] // the address fixtureTx2 used as input wallet.processConfirmedTx(tx) spendTx = Transaction.fromHex(fixtureTx2Hex) }) it("does not add to wallet.unspentMap", function() { wallet.processConfirmedTx(spendTx) assert.deepEqual(wallet.unspentMap, {}) }) it("deletes corresponding 'unspent'", function() { var txIn = spendTx.ins[0] var txInId = bufferutils.reverse(txIn.hash).toString('hex') var expected = txInId + ':' + txIn.index assert(expected in wallet.unspentMap) wallet.processConfirmedTx(spendTx) assert(!(expected in wallet.unspentMap)) }) }) }) it("does nothing when none of the involved addresses belong to the wallet", function() { wallet.processConfirmedTx(tx) assert.deepEqual(wallet.unspentMap, {}) }) function verifyOutputAdded(index, pending) { var txOut = tx.outs[index] var key = tx.getId() + ":" + index var output = wallet.unspentMap[key] assert.deepEqual(output.txHash, tx.getHash()) assert.equal(output.value, txOut.value) assert.equal(output.pending, pending) var txOutAddress = Address.fromOutputScript(txOut.script).toString() assert.equal(output.address, txOutAddress) } }) describe('createTx', function() { var wallet var address1, address2 var to, value beforeEach(function() { to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue' value = 500000 address1 = "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa" address2 = "n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X" // set up 3 utxos var utxos = [ { "txId": fakeTxId(1), "index": 0, "address": address1, "value": 400000 // not enough for value }, { "txId": fakeTxId(2), "index": 1, "address": address1, "value": 500000 // enough for only value }, { "txId": fakeTxId(3), "index": 0, "address" : address2, "value": 510000 // enough for value and fee } ] wallet = new Wallet(seed, networks.testnet) wallet.setUnspentOutputs(utxos) wallet.generateAddress() wallet.generateAddress() }) describe('transaction fee', function() { it('allows fee to be specified', function() { var fee = 30000 var tx = wallet.createTx(to, value, { fixedFee: fee }) assert.equal(getFee(wallet, tx), fee) }) it('allows fee to be set to zero', function() { value = 510000 var fee = 0 var tx = wallet.createTx(to, value, { fixedFee: fee }) assert.equal(getFee(wallet, tx), fee) }) it('does not overestimate fees when network has dustSoftThreshold', function() { var utxo = { txId: fakeTxId(0), index: 0, address: "LeyySKbQrRRwodKEj1W4a8y3YQupPLw5os", value: 500000 } var wallet = new Wallet(seed, networks.litecoin) wallet.setUnspentOutputs([utxo]) wallet.generateAddress() value = 200000 var tx = wallet.createTx(utxo.address, value) assert.equal(getFee(wallet, tx), 100000) }) function getFee(wallet, tx) { var inputValue = tx.ins.reduce(function(accum, input) { var txId = bufferutils.reverse(input.hash).toString('hex') return accum + wallet.unspentMap[txId + ':' + input.index].value }, 0) return tx.outs.reduce(function(accum, output) { return accum - output.value }, inputValue) } }) describe('choosing utxo', function() { it('takes fees into account', function() { var tx = wallet.createTx(to, value) assert.equal(tx.ins.length, 1) assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) assert.equal(tx.ins[0].index, 0) }) it('uses confirmed outputs', function() { var tx2 = new Transaction() tx2.addInput(fakeTxId(4), 0) tx2.addOutput(address2, 530000) wallet.processConfirmedTx(tx2) var tx = wallet.createTx(to, value) assert.equal(tx.ins.length, 1) assert.deepEqual(tx.ins[0].hash, tx2.getHash()) assert.equal(tx.ins[0].index, 0) }) it('ignores pending outputs', function() { var tx2 = new Transaction() tx2.addInput(fakeTxId(4), 0) tx2.addOutput(address2, 530000) wallet.processPendingTx(tx2) var tx = wallet.createTx(to, value) assert.equal(tx.ins.length, 1) assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) assert.equal(tx.ins[0].index, 0) }) }) describe('changeAddress', function() { it('should allow custom changeAddress', function() { var changeAddress = 'mfrFjnKZUvTcvdAK2fUX5D8v1Epu5H8JCk' var fromValue = 510000 var toValue = fromValue / 2 var fee = 1e3 var tx = wallet.createTx(to, toValue, { fixedFee: fee, changeAddress: changeAddress }) assert.equal(tx.outs.length, 2) var outAddress0 = Address.fromOutputScript(tx.outs[0].script, networks.testnet) var outAddress1 = Address.fromOutputScript(tx.outs[1].script, networks.testnet) assert.equal(outAddress0.toString(), to) assert.equal(tx.outs[0].value, toValue) assert.equal(outAddress1.toString(), changeAddress) assert.equal(tx.outs[1].value, fromValue - (toValue + fee)) }) }) describe('transaction outputs', function() { it('includes the specified address and amount', function() { var tx = wallet.createTx(to, value) assert.equal(tx.outs.length, 1) var out = tx.outs[0] var outAddress = Address.fromOutputScript(out.script, networks.testnet) assert.equal(outAddress.toString(), to) assert.equal(out.value, value) }) describe('change', function() { it('uses the last change address if there is any', function() { var fee = 0 wallet.generateChangeAddress() wallet.generateChangeAddress() var tx = wallet.createTx(to, value, { fixedFee: fee }) assert.equal(tx.outs.length, 2) var out = tx.outs[1] var outAddress = Address.fromOutputScript(out.script, networks.testnet) assert.equal(outAddress.toString(), wallet.changeAddresses[1]) assert.equal(out.value, 10000) }) it('generates a change address if there is not any', function() { var fee = 0 assert.equal(wallet.changeAddresses.length, 0) var tx = wallet.createTx(to, value, { fixedFee: fee }) assert.equal(wallet.changeAddresses.length, 1) var out = tx.outs[1] var outAddress = Address.fromOutputScript(out.script, networks.testnet) assert.equal(outAddress.toString(), wallet.changeAddresses[0]) assert.equal(out.value, 10000) }) it('skips change if it is not above dust threshold', function() { var tx1 = wallet.createTx(to, value - 546) assert.equal(tx1.outs.length, 1) var tx2 = wallet.createTx(to, value - 547) assert.equal(tx2.outs.length, 2) }) }) }) describe('signing', function() { afterEach(function() { TransactionBuilder.prototype.sign.restore() }) it('signs the inputs with respective keys', function() { var fee = 30000 sinon.spy(TransactionBuilder.prototype, "sign") wallet.createTx(to, value, { fixedFee: fee }) var priv1 = wallet.getPrivateKeyForAddress(address1) var priv2 = wallet.getPrivateKeyForAddress(address2) // FIXME: boo (required) side effects priv1.pub.Q.affineX, priv2.pub.Q.affineX assert(TransactionBuilder.prototype.sign.calledWith(0, priv2)) assert(TransactionBuilder.prototype.sign.calledWith(1, priv1)) }) }) describe('when value is below dust threshold', function() { it('throws an error', function() { var value = 546 assert.throws(function() { wallet.createTx(to, value) }, /546 must be above dust threshold \(546 Satoshis\)/) }) }) describe('when there is not enough money', function() { it('throws an error', function() { var value = 1400001 assert.throws(function() { wallet.createTx(to, value) }, /Not enough funds \(incl. fee\): 1410000 < 1410001/) }) }) }) function assertEqual(obj1, obj2){ assert.equal(obj1.toString(), obj2.toString()) } function assertNotEqual(obj1, obj2){ assert.notEqual(obj1.toString(), obj2.toString()) } // quick and dirty: does not deal with functions on object function cloneObject(obj){ return JSON.parse(JSON.stringify(obj)) } })