From d618aa9822fbdb1bb887e6f62d44e383518fd5be Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 14 Aug 2014 11:00:18 +1000 Subject: [PATCH 01/11] Wallet: deprecates newMasterKey --- src/wallet.js | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index c056c95..dd492ba 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -7,14 +7,18 @@ var HDNode = require('./hdnode') var Transaction = require('./transaction') function Wallet(seed, network) { + seed = seed || crypto.randomBytes(32) network = network || networks.bitcoin // Stored in a closure to make accidental serialization less likely - var masterkey = null + var masterkey = HDNode.fromSeedBuffer(seed, network) var me = this - var accountZero = null - var internalAccount = null - var externalAccount = null + + // HD first-level child derivation method should be hardened + // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 + var accountZero = masterkey.deriveHardened(0) + var externalAccount = accountZero.derive(0) + var internalAccount = accountZero.derive(1) // Addresses this.addresses = [] @@ -23,25 +27,6 @@ function Wallet(seed, network) { // Transaction output data this.outputs = {} - // Make a new master key - this.newMasterKey = function(seed) { - seed = seed || crypto.randomBytes(32) - masterkey = HDNode.fromSeedBuffer(seed, network) - - // HD first-level child derivation method should be hardened - // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 - accountZero = masterkey.deriveHardened(0) - externalAccount = accountZero.derive(0) - internalAccount = accountZero.derive(1) - - me.addresses = [] - me.changeAddresses = [] - - me.outputs = {} - } - - this.newMasterKey(seed) - this.generateAddress = function() { var key = externalAccount.derive(this.addresses.length) this.addresses.push(key.getAddress().toString()) @@ -71,6 +56,25 @@ function Wallet(seed, network) { return utxo } + // FIXME: remove in 2.x.y + this.newMasterKey = function(seed) { + console.warn('newMasterKey is deprecated, please make a new Wallet instance instead') + + seed = seed || crypto.randomBytes(32) + masterkey = HDNode.fromSeedBuffer(seed, network) + + // HD first-level child derivation method should be hardened + // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 + accountZero = masterkey.deriveHardened(0) + externalAccount = accountZero.derive(0) + internalAccount = accountZero.derive(1) + + me.addresses = [] + me.changeAddresses = [] + + me.outputs = {} + } + this.setUnspentOutputs = function(utxo) { var outputs = {} From 8b3470e8cabe982f6fe40faac35e88271d33b9fc Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 13 Aug 2014 12:03:53 +1000 Subject: [PATCH 02/11] Wallet: move all free functions out of Wallet scope --- src/wallet.js | 137 +++++++++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 69 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index dd492ba..b188808 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -86,59 +86,6 @@ function Wallet(seed, network) { this.outputs = outputs } - - function outputToUnspentOutput(output){ - var hashAndIndex = output.from.split(":") - - return { - hash: hashAndIndex[0], - outputIndex: parseInt(hashAndIndex[1]), - address: output.address, - value: output.value, - pending: output.pending - } - } - - function unspentOutputToOutput(o) { - var hash = o.hash - var key = hash + ":" + o.outputIndex - return { - from: key, - address: o.address, - value: o.value, - pending: o.pending - } - } - - function validateUnspentOutput(uo) { - var missingField - - if (isNullOrUndefined(uo.hash)) { - missingField = "hash" - } - - var requiredKeys = ['outputIndex', 'address', 'value'] - requiredKeys.forEach(function (key) { - if (isNullOrUndefined(uo[key])){ - missingField = key - } - }) - - if (missingField) { - var message = [ - 'Invalid unspent output: key', missingField, 'is missing.', - 'A valid unspent output must contain' - ] - message.push(requiredKeys.join(', ')) - message.push("and hash") - throw new Error(message.join(' ')) - } - } - - function isNullOrUndefined(value) { - return value == undefined - } - this.processPendingTx = function(tx){ processTx(tx, true) } @@ -193,7 +140,7 @@ function Wallet(seed, network) { this.createTx = function(to, value, fixedFee, changeAddress) { assert(value > network.dustThreshold, value + ' must be above dust threshold (' + network.dustThreshold + ' Satoshis)') - var utxos = getCandidateOutputs(value) + var utxos = getCandidateOutputs(this.outputs, value) var accum = 0 var subTotal = value var addresses = [] @@ -229,21 +176,6 @@ function Wallet(seed, network) { return tx } - function getCandidateOutputs() { - var unspent = [] - - for (var key in me.outputs) { - var output = me.outputs[key] - if (!output.pending) unspent.push(output) - } - - var sortByValueDesc = unspent.sort(function(o1, o2){ - return o2.value - o1.value - }) - - return sortByValueDesc - } - function estimateFeePadChangeOutput(tx) { var tmpTx = tx.clone() tmpTx.addOutput(getChangeAddress(), network.dustSoftThreshold || 0) @@ -305,4 +237,71 @@ function Wallet(seed, network) { } } +function outputToUnspentOutput(output){ + var hashAndIndex = output.from.split(":") + + return { + hash: hashAndIndex[0], + outputIndex: parseInt(hashAndIndex[1]), + address: output.address, + value: output.value, + pending: output.pending + } +} + +function unspentOutputToOutput(o) { + var hash = o.hash + var key = hash + ":" + o.outputIndex + return { + from: key, + address: o.address, + value: o.value, + pending: o.pending + } +} + +function validateUnspentOutput(uo) { + var missingField + + if (isNullOrUndefined(uo.hash)) { + missingField = "hash" + } + + var requiredKeys = ['outputIndex', 'address', 'value'] + requiredKeys.forEach(function (key) { + if (isNullOrUndefined(uo[key])){ + missingField = key + } + }) + + if (missingField) { + var message = [ + 'Invalid unspent output: key', missingField, 'is missing.', + 'A valid unspent output must contain' + ] + message.push(requiredKeys.join(', ')) + message.push("and hash") + throw new Error(message.join(' ')) + } +} + +function isNullOrUndefined(value) { + return value == undefined +} + +function getCandidateOutputs(outputs/*, value*/) { + var unspent = [] + + for (var key in outputs) { + var output = outputs[key] + if (!output.pending) unspent.push(output) + } + + var sortByValueDesc = unspent.sort(function(o1, o2){ + return o2.value - o1.value + }) + + return sortByValueDesc +} + module.exports = Wallet From abc3e6c71584c7b6c5311f7eab91e18bf5c0d083 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 13 Aug 2014 12:17:12 +1000 Subject: [PATCH 03/11] Wallet: move estimatePaddedTxFee out of Wallet scope --- src/wallet.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index b188808..b2d9a05 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -5,6 +5,7 @@ var networks = require('./networks') var Address = require('./address') var HDNode = require('./hdnode') var Transaction = require('./transaction') +var Script = require('./script') function Wallet(seed, network) { seed = seed || crypto.randomBytes(32) @@ -155,7 +156,7 @@ function Wallet(seed, network) { var outpoint = utxo.from.split(':') tx.addInput(outpoint[0], parseInt(outpoint[1])) - var fee = fixedFee == undefined ? estimateFeePadChangeOutput(tx) : fixedFee + var fee = fixedFee == undefined ? estimatePaddedFee(tx, network) : fixedFee accum += utxo.value subTotal = value + fee @@ -176,13 +177,6 @@ function Wallet(seed, network) { return tx } - function estimateFeePadChangeOutput(tx) { - var tmpTx = tx.clone() - tmpTx.addOutput(getChangeAddress(), network.dustSoftThreshold || 0) - - return network.estimateFee(tmpTx) - } - function getChangeAddress() { if(me.changeAddresses.length === 0) me.generateChangeAddress(); return me.changeAddresses[me.changeAddresses.length - 1] @@ -285,6 +279,13 @@ function validateUnspentOutput(uo) { } } +function estimatePaddedFee(tx, network) { + var tmpTx = tx.clone() + tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0) + + return network.estimateFee(tmpTx) +} + function isNullOrUndefined(value) { return value == undefined } From 7c22067f6934abe254fba9b51d9cccf00b657173 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 13 Aug 2014 13:56:17 +1000 Subject: [PATCH 04/11] Wallet: clarify getPrivateKeyForAddress method structure This does repeat the O(n) lookup several times, but that can be fixed by using an O(1) lookup instead (and will be later). Clarity first. --- src/wallet.js | 15 ++++++++++----- test/wallet.js | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index b2d9a05..f377377 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -208,13 +208,18 @@ function Wallet(seed, network) { } this.getPrivateKeyForAddress = function(address) { - var index - if((index = this.addresses.indexOf(address)) > -1) { + assert(isMyAddress(address), 'Unknown address. Make sure the address is from the keychain and has been generated') + + if (isReceiveAddress(address)) { + var index = this.addresses.indexOf(address) + return this.getPrivateKey(index) - } else if((index = this.changeAddresses.indexOf(address)) > -1) { + } + + if (isChangeAddress(address)) { + var index = this.changeAddresses.indexOf(address) + return this.getInternalPrivateKey(index) - } else { - throw new Error('Unknown address. Make sure the address is from the keychain and has been generated.') } } diff --git a/test/wallet.js b/test/wallet.js index 80fd7a8..456775d 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -164,7 +164,7 @@ describe('Wallet', 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./) + }, /Unknown address. Make sure the address is from the keychain and has been generated/) }) }) From 2f00c9ab353a5fac49c882d65433a6a737d3534b Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 14 Aug 2014 11:03:54 +1000 Subject: [PATCH 05/11] Wallet: rework unspents to primarily work on initialization The RegExp for the UTXO validation was removed as the errors are now more verbose and specific to each case. --- src/wallet.js | 85 +++++++--------- test/wallet.js | 263 ++++++++++++++++++++++++++----------------------- 2 files changed, 177 insertions(+), 171 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index f377377..8dd2ca2 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -7,7 +7,7 @@ var HDNode = require('./hdnode') var Transaction = require('./transaction') var Script = require('./script') -function Wallet(seed, network) { +function Wallet(seed, network, unspents) { seed = seed || crypto.randomBytes(32) network = network || networks.bitcoin @@ -26,7 +26,7 @@ function Wallet(seed, network) { this.changeAddresses = [] // Transaction output data - this.outputs = {} + this.outputs = unspents ? processUnspentOutputs(unspents) : {} this.generateAddress = function() { var key = externalAccount.derive(this.addresses.length) @@ -77,16 +77,11 @@ function Wallet(seed, network) { } this.setUnspentOutputs = function(utxo) { - var outputs = {} + console.warn('setUnspentOutputs is deprecated, please use the constructor option instead') - utxo.forEach(function(uo){ - validateUnspentOutput(uo) - var o = unspentOutputToOutput(uo) - outputs[o.from] = o - }) - - this.outputs = outputs + this.outputs = processUnspentOutputs(utxo) } + this.processPendingTx = function(tx){ processTx(tx, true) } @@ -95,6 +90,8 @@ function Wallet(seed, network) { processTx(tx, false) } + var me = this + function processTx(tx, isPending) { var txid = tx.getId() @@ -241,49 +238,13 @@ function outputToUnspentOutput(output){ return { hash: hashAndIndex[0], - outputIndex: parseInt(hashAndIndex[1]), + index: parseInt(hashAndIndex[1]), address: output.address, value: output.value, pending: output.pending } } -function unspentOutputToOutput(o) { - var hash = o.hash - var key = hash + ":" + o.outputIndex - return { - from: key, - address: o.address, - value: o.value, - pending: o.pending - } -} - -function validateUnspentOutput(uo) { - var missingField - - if (isNullOrUndefined(uo.hash)) { - missingField = "hash" - } - - var requiredKeys = ['outputIndex', 'address', 'value'] - requiredKeys.forEach(function (key) { - if (isNullOrUndefined(uo[key])){ - missingField = key - } - }) - - if (missingField) { - var message = [ - 'Invalid unspent output: key', missingField, 'is missing.', - 'A valid unspent output must contain' - ] - message.push(requiredKeys.join(', ')) - message.push("and hash") - throw new Error(message.join(' ')) - } -} - function estimatePaddedFee(tx, network) { var tmpTx = tx.clone() tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0) @@ -291,8 +252,34 @@ function estimatePaddedFee(tx, network) { return network.estimateFee(tmpTx) } -function isNullOrUndefined(value) { - return value == undefined +function processUnspentOutputs(utxos) { + var outputs = {} + + utxos.forEach(function(utxo){ + var hash = new Buffer(utxo.hash, 'hex') + var index = utxo.index + var address = utxo.address + var value = utxo.value + + // FIXME: remove alternative in 2.x.y + if (index === undefined) index = utxo.outputIndex + + assert.equal(hash.length, 32, 'Expected hash length of 32, got ' + hash.length) + assert.equal(typeof index, 'number', 'Expected number index, got ' + index) + assert.doesNotThrow(function() { Address.fromBase58Check(address) }, 'Expected Base58 Address, got ' + address) + assert.equal(typeof value, 'number', 'Expected number value, got ' + value) + + var key = utxo.hash + ':' + utxo.index + + outputs[key] = { + from: key, + address: address, + value: value, + pending: utxo.pending + } + }) + + return outputs } function getCandidateOutputs(outputs/*, value*/) { diff --git a/test/wallet.js b/test/wallet.js index 456775d..c08ae2f 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -26,13 +26,17 @@ function fakeTxId(i) { } describe('Wallet', function() { - var seed, wallet + var seed beforeEach(function(){ seed = crypto.sha256("don't use a string seed like this in real life") - wallet = new Wallet(seed) }) describe('constructor', function() { + var wallet + beforeEach(function(){ + wallet = new Wallet(seed) + }) + it('defaults to Bitcoin network', function() { assert.equal(wallet.getMasterKey().network, networks.bitcoin) }) @@ -116,6 +120,11 @@ describe('Wallet', function() { }) 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"] @@ -126,6 +135,11 @@ describe('Wallet', function() { }) 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) @@ -135,6 +149,11 @@ describe('Wallet', function() { }) 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) @@ -144,6 +163,11 @@ describe('Wallet', function() { }) 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() @@ -162,6 +186,7 @@ describe('Wallet', function() { 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/) @@ -169,51 +194,55 @@ describe('Wallet', function() { }) describe('Unspent Outputs', function(){ - var expectedUtxo, expectedOutputKey + var utxo, expectedOutputKey + var wallet + beforeEach(function(){ - expectedUtxo = { - "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7", - "outputIndex": 0, + utxo = { "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", - "value": 20000, - "pending": true + "hash": fakeTxId(6), + "index": 0, + "pending": true, + "value": 20000 } - expectedOutputKey = expectedUtxo.hash + ":" + expectedUtxo.outputIndex + + expectedOutputKey = utxo.hash + ":" + utxo.index }) - function addUtxoToOutput(utxo){ - var key = utxo.hash + ":" + utxo.outputIndex - wallet.outputs[key] = { - from: key, - address: utxo.address, - value: utxo.value, - pending: utxo.pending - } - } + describe('on construction', function(){ + beforeEach(function(){ + wallet = new Wallet(seed, networks.bitcoin, [utxo]) + }) + + it('matches the expected behaviour', function(){ + var output = wallet.outputs[expectedOutputKey] + + assert(output) + assert.equal(output.value, utxo.value) + assert.equal(output.address, utxo.address) + }) + }) describe('getBalance', function(){ - var utxo1 - beforeEach(function(){ - utxo1 = cloneObject(expectedUtxo) - utxo1.hash = utxo1.hash.replace('7', 'l') + var utxo1 = cloneObject(utxo) + utxo1.hash = fakeTxId(5) + + wallet = new Wallet(seed, networks.bitcoin, [utxo, utxo1]) }) it('sums over utxo values', function(){ - addUtxoToOutput(expectedUtxo) - addUtxoToOutput(utxo1) - assert.equal(wallet.getBalance(), 40000) }) }) describe('getUnspentOutputs', function(){ beforeEach(function(){ - addUtxoToOutput(expectedUtxo) + wallet = new Wallet(seed, networks.bitcoin, [utxo]) }) it('parses wallet outputs to the expect format', function(){ - assert.deepEqual(wallet.getUnspentOutputs(), [expectedUtxo]) + assert.deepEqual(wallet.getUnspentOutputs(), [utxo]) }) it("ignores pending spending outputs (outputs with 'to' property)", function(){ @@ -223,40 +252,54 @@ describe('Wallet', function() { assert.deepEqual(wallet.getUnspentOutputs(), []) }) }) + }) - describe('setUnspentOutputs', function(){ - var utxo - beforeEach(function(){ - utxo = cloneObject([expectedUtxo]) - }) + // FIXME: remove in 2.x.y + describe('setUnspentOutputs', function(){ + var utxo + var expectedOutputKey - it('matches the expected behaviour', function(){ - wallet.setUnspentOutputs(utxo) - verifyOutputs() - }) + beforeEach(function(){ + utxo = { + hash: fakeTxId(0), + index: 0, + address: '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', + value: 500000 + } - describe('required fields', function(){ - ['outputIndex', 'address', 'hash', 'value'].forEach(function(field){ - it("throws an error when " + field + " is missing", function(){ - delete utxo[0][field] + expectedOutputKey = utxo.hash + ":" + utxo.index - assert.throws(function() { - wallet.setUnspentOutputs(utxo) - }, new RegExp('Invalid unspent output: key ' + field + ' is missing')) + wallet = new Wallet(seed, networks.bitcoin) + }) + + it('matches the expected behaviour', function(){ + wallet.setUnspentOutputs([utxo]) + + var output = wallet.outputs[expectedOutputKey] + assert(output) + 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]) }) }) }) - - function verifyOutputs() { - var output = wallet.outputs[expectedOutputKey] - assert(output) - assert.equal(output.value, utxo[0].value) - assert.equal(output.address, utxo[0].address) - } }) }) describe('Process transaction', function(){ + var wallet + beforeEach(function(){ + wallet = new Wallet(seed) + }) + var addresses var tx @@ -389,39 +432,42 @@ describe('Wallet', function() { }) describe('createTx', function(){ - var to, value + var wallet var address1, address2 + var to, value beforeEach(function(){ - to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' + to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue' value = 500000 - // generate 2 addresses - address1 = wallet.generateAddress() - address2 = wallet.generateAddress() + address1 = "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa" + address2 = "n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X" - // set up 3 utxo - utxo = [ + // set up 3 utxos + var utxos = [ { "hash": fakeTxId(1), - "outputIndex": 0, - "address" : address1, + "index": 0, + "address": address1, "value": 400000 // not enough for value }, { "hash": fakeTxId(2), - "outputIndex": 1, - "address" : address1, + "index": 1, + "address": address1, "value": 500000 // enough for only value }, { "hash": fakeTxId(3), - "outputIndex": 0, + "index": 0, "address" : address2, "value": 510000 // enough for value and fee } ] - wallet.setUnspentOutputs(utxo) + + wallet = new Wallet(seed, networks.testnet, utxos) + wallet.generateAddress() + wallet.generateAddress() }) describe('transaction fee', function(){ @@ -441,17 +487,18 @@ describe('Wallet', function() { }) it('does not overestimate fees when network has dustSoftThreshold', function(){ - var wallet = new Wallet(seed, networks.litecoin) - var address = wallet.generateAddress() - wallet.setUnspentOutputs([{ + var utxo = { hash: fakeTxId(0), - outputIndex: 0, - address: address, + index: 0, + address: "LeyySKbQrRRwodKEj1W4a8y3YQupPLw5os", value: 500000 - }]) + } + + var wallet = new Wallet(seed, networks.litecoin, [utxo]) + wallet.generateAddress() value = 200000 - var tx = wallet.createTx(address, value) + var tx = wallet.createTx(utxo.address, value) assert.equal(getFee(wallet, tx), 100000) }) @@ -477,18 +524,25 @@ describe('Wallet', function() { assert.equal(tx.ins[0].index, 0) }) - it('ignores pending outputs', function(){ - utxo.push( - { - "hash": fakeTxId(4), - "outputIndex": 0, - "address" : address2, - "value": 530000, - "pending": true - } - ) - wallet.setUnspentOutputs(utxo) + 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) @@ -497,46 +551,11 @@ describe('Wallet', function() { }) }) - describe('works for testnet', function(){ - it('should create transaction', function(){ - var wallet = new Wallet(seed, networks.testnet) - var address = wallet.generateAddress() - - wallet.setUnspentOutputs([{ - hash: fakeTxId(0), - outputIndex: 0, - address: address, - value: value - }]) - - var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue' - var toValue = value - 10000 - - var tx = wallet.createTx(to, toValue) - assert.equal(tx.outs.length, 1) - - var outAddress = Address.fromOutputScript(tx.outs[0].script, networks.testnet) - assert.equal(outAddress.toString(), to) - assert.equal(tx.outs[0].value, toValue) - }) - }) - describe('changeAddress', function(){ it('should allow custom changeAddress', function(){ - var wallet = new Wallet(seed, networks.testnet) - var address = wallet.generateAddress() - - wallet.setUnspentOutputs([{ - hash: fakeTxId(0), - outputIndex: 0, - address: address, - value: value - }]) - assert.equal(wallet.getBalance(), value) - var changeAddress = 'mfrFjnKZUvTcvdAK2fUX5D8v1Epu5H8JCk' - var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue' - var toValue = value / 2 + var fromValue = 510000 + var toValue = fromValue / 2 var fee = 1e3 var tx = wallet.createTx(to, toValue, fee, changeAddress) @@ -549,7 +568,7 @@ describe('Wallet', function() { assert.equal(tx.outs[0].value, toValue) assert.equal(outAddress1.toString(), changeAddress) - assert.equal(tx.outs[1].value, value - (toValue + fee)) + assert.equal(tx.outs[1].value, fromValue - (toValue + fee)) }) }) @@ -559,7 +578,7 @@ describe('Wallet', function() { assert.equal(tx.outs.length, 1) var out = tx.outs[0] - var outAddress = Address.fromOutputScript(out.script) + var outAddress = Address.fromOutputScript(out.script, networks.testnet) assert.equal(outAddress.toString(), to) assert.equal(out.value, value) @@ -574,7 +593,7 @@ describe('Wallet', function() { assert.equal(tx.outs.length, 2) var out = tx.outs[1] - var outAddress = Address.fromOutputScript(out.script) + var outAddress = Address.fromOutputScript(out.script, networks.testnet) assert.equal(outAddress.toString(), wallet.changeAddresses[1]) assert.equal(out.value, 10000) @@ -588,7 +607,7 @@ describe('Wallet', function() { assert.equal(wallet.changeAddresses.length, 1) var out = tx.outs[1] - var outAddress = Address.fromOutputScript(out.script) + var outAddress = Address.fromOutputScript(out.script, networks.testnet) assert.equal(outAddress.toString(), wallet.changeAddresses[0]) assert.equal(out.value, 10000) From 7fd41fae4ae8022de3fbf9fb9ff4335d28ee1c31 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 14 Aug 2014 10:27:14 +1000 Subject: [PATCH 06/11] Wallet: move most instance methods to prototype --- src/wallet.js | 123 ++++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 53 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 8dd2ca2..d904360 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -28,35 +28,6 @@ function Wallet(seed, network, unspents) { // Transaction output data this.outputs = unspents ? processUnspentOutputs(unspents) : {} - this.generateAddress = function() { - var key = externalAccount.derive(this.addresses.length) - this.addresses.push(key.getAddress().toString()) - return this.addresses[this.addresses.length - 1] - } - - this.generateChangeAddress = function() { - var key = internalAccount.derive(this.changeAddresses.length) - this.changeAddresses.push(key.getAddress().toString()) - return this.changeAddresses[this.changeAddresses.length - 1] - } - - this.getBalance = function() { - return this.getUnspentOutputs().reduce(function(memo, output){ - return memo + output.value - }, 0) - } - - this.getUnspentOutputs = function() { - var utxo = [] - - for(var key in this.outputs){ - var output = this.outputs[key] - if(!output.to) utxo.push(outputToUnspentOutput(output)) - } - - return utxo - } - // FIXME: remove in 2.x.y this.newMasterKey = function(seed) { console.warn('newMasterKey is deprecated, please make a new Wallet instance instead') @@ -76,12 +47,6 @@ function Wallet(seed, network, unspents) { me.outputs = {} } - this.setUnspentOutputs = function(utxo) { - console.warn('setUnspentOutputs is deprecated, please use the constructor option instead') - - this.outputs = processUnspentOutputs(utxo) - } - this.processPendingTx = function(tx){ processTx(tx, true) } @@ -161,7 +126,7 @@ function Wallet(seed, network, unspents) { var change = accum - subTotal if (change > network.dustThreshold) { - tx.addOutput(changeAddress || getChangeAddress(), change) + tx.addOutput(changeAddress || this.getChangeAddress(), change) } break @@ -174,23 +139,6 @@ function Wallet(seed, network, unspents) { return tx } - function getChangeAddress() { - if(me.changeAddresses.length === 0) me.generateChangeAddress(); - return me.changeAddresses[me.changeAddresses.length - 1] - } - - this.signWith = function(tx, addresses) { - assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs') - - addresses.forEach(function(address, i) { - var key = me.getPrivateKeyForAddress(address) - - tx.sign(i, key) - }) - - return tx - } - this.getMasterKey = function() { return masterkey } this.getAccountZero = function() { return accountZero } this.getInternalAccount = function() { return internalAccount } @@ -233,6 +181,75 @@ function Wallet(seed, network, unspents) { } } +Wallet.prototype.generateAddress = function() { + var k = this.addresses.length + var address = this.getExternalAccount().derive(k).getAddress() + + this.addresses.push(address.toString()) + + return this.getReceiveAddress() +} + +Wallet.prototype.generateChangeAddress = function() { + var k = this.changeAddresses.length + var address = this.getInternalAccount().derive(k).getAddress() + + this.changeAddresses.push(address.toString()) + + return this.getChangeAddress() +} + +Wallet.prototype.getBalance = function() { + return this.getUnspentOutputs().reduce(function(accum, output) { + return accum + output.value + }, 0) +} + +Wallet.prototype.getChangeAddress = function() { + if (this.changeAddresses.length === 0) { + this.generateChangeAddress() + } + + return this.changeAddresses[this.changeAddresses.length - 1] +} + +Wallet.prototype.getReceiveAddress = function() { + if (this.addresses.length === 0) { + this.generateAddress() + } + + return this.addresses[this.addresses.length - 1] +} + +Wallet.prototype.getUnspentOutputs = function() { + var utxo = [] + + for(var key in this.outputs){ + var output = this.outputs[key] + if(!output.to) utxo.push(outputToUnspentOutput(output)) + } + + return utxo +} + +Wallet.prototype.setUnspentOutputs = function(utxo) { + console.warn('setUnspentOutputs is deprecated, please use the constructor option instead') + + this.outputs = processUnspentOutputs(utxo) +} + +Wallet.prototype.signWith = function(tx, addresses) { + assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs') + + addresses.forEach(function(address, i) { + var key = this.getPrivateKeyForAddress(address) + + tx.sign(i, key) + }, this) + + return tx +} + function outputToUnspentOutput(output){ var hashAndIndex = output.from.split(":") From 6df785bb6591bf15e2b0fcc2d784c81ecd7fa008 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 14 Aug 2014 10:39:14 +1000 Subject: [PATCH 07/11] Wallet: move createTx to prototype --- src/wallet.js | 96 ++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index d904360..ced3ac5 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -25,6 +25,8 @@ function Wallet(seed, network, unspents) { this.addresses = [] this.changeAddresses = [] + this.network = network + // Transaction output data this.outputs = unspents ? processUnspentOutputs(unspents) : {} @@ -100,58 +102,11 @@ function Wallet(seed, network, unspents) { }) } - this.createTx = function(to, value, fixedFee, changeAddress) { - assert(value > network.dustThreshold, value + ' must be above dust threshold (' + network.dustThreshold + ' Satoshis)') - - var utxos = getCandidateOutputs(this.outputs, value) - var accum = 0 - var subTotal = value - var addresses = [] - - var tx = new Transaction() - tx.addOutput(to, value) - - for (var i = 0; i < utxos.length; ++i) { - var utxo = utxos[i] - addresses.push(utxo.address) - - var outpoint = utxo.from.split(':') - tx.addInput(outpoint[0], parseInt(outpoint[1])) - - var fee = fixedFee == undefined ? estimatePaddedFee(tx, network) : fixedFee - - accum += utxo.value - subTotal = value + fee - if (accum >= subTotal) { - var change = accum - subTotal - - if (change > network.dustThreshold) { - tx.addOutput(changeAddress || this.getChangeAddress(), change) - } - - break - } - } - - assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal) - - this.signWith(tx, addresses) - return tx - } - this.getMasterKey = function() { return masterkey } this.getAccountZero = function() { return accountZero } this.getInternalAccount = function() { return internalAccount } this.getExternalAccount = function() { return externalAccount } - this.getPrivateKey = function(index) { - return externalAccount.derive(index).privKey - } - - this.getInternalPrivateKey = function(index) { - return internalAccount.derive(index).privKey - } - this.getPrivateKeyForAddress = function(address) { assert(isMyAddress(address), 'Unknown address. Make sure the address is from the keychain and has been generated') @@ -181,6 +136,45 @@ function Wallet(seed, network, unspents) { } } +Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) { + assert(value > this.network.dustThreshold, value + ' must be above dust threshold (' + this.network.dustThreshold + ' Satoshis)') + + var utxos = getCandidateOutputs(this.outputs, value) + var accum = 0 + var subTotal = value + var addresses = [] + + var tx = new Transaction() + tx.addOutput(to, value) + + for (var i = 0; i < utxos.length; ++i) { + var utxo = utxos[i] + addresses.push(utxo.address) + + var outpoint = utxo.from.split(':') + tx.addInput(outpoint[0], parseInt(outpoint[1])) + + var fee = fixedFee == undefined ? estimatePaddedFee(tx, this.network) : fixedFee + + accum += utxo.value + subTotal = value + fee + if (accum >= subTotal) { + var change = accum - subTotal + + if (change > this.network.dustThreshold) { + tx.addOutput(changeAddress || this.getChangeAddress(), change) + } + + break + } + } + + assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal) + + this.signWith(tx, addresses) + return tx +} + Wallet.prototype.generateAddress = function() { var k = this.addresses.length var address = this.getExternalAccount().derive(k).getAddress() @@ -213,6 +207,14 @@ Wallet.prototype.getChangeAddress = function() { return this.changeAddresses[this.changeAddresses.length - 1] } +Wallet.prototype.getInternalPrivateKey = function(index) { + return this.getInternalAccount().derive(index).privKey +} + +Wallet.prototype.getPrivateKey = function(index) { + return this.getExternalAccount().derive(index).privKey +} + Wallet.prototype.getReceiveAddress = function() { if (this.addresses.length === 0) { this.generateAddress() From c13177bb29acc7d3ab85d726190802722d05a4a6 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 14 Aug 2014 17:29:14 +1000 Subject: [PATCH 08/11] Wallet: rename masterkey to masterKey --- src/wallet.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index ced3ac5..ad7ccbd 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -12,12 +12,12 @@ function Wallet(seed, network, unspents) { network = network || networks.bitcoin // Stored in a closure to make accidental serialization less likely - var masterkey = HDNode.fromSeedBuffer(seed, network) + var masterKey = HDNode.fromSeedBuffer(seed, network) var me = this // HD first-level child derivation method should be hardened // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 - var accountZero = masterkey.deriveHardened(0) + var accountZero = masterKey.deriveHardened(0) var externalAccount = accountZero.derive(0) var internalAccount = accountZero.derive(1) @@ -35,11 +35,11 @@ function Wallet(seed, network, unspents) { console.warn('newMasterKey is deprecated, please make a new Wallet instance instead') seed = seed || crypto.randomBytes(32) - masterkey = HDNode.fromSeedBuffer(seed, network) + masterKey = HDNode.fromSeedBuffer(seed, network) // HD first-level child derivation method should be hardened // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 - accountZero = masterkey.deriveHardened(0) + accountZero = masterKey.deriveHardened(0) externalAccount = accountZero.derive(0) internalAccount = accountZero.derive(1) @@ -102,10 +102,10 @@ function Wallet(seed, network, unspents) { }) } - this.getMasterKey = function() { return masterkey } + this.getMasterKey = function() { return masterKey } this.getAccountZero = function() { return accountZero } - this.getInternalAccount = function() { return internalAccount } this.getExternalAccount = function() { return externalAccount } + this.getInternalAccount = function() { return internalAccount } this.getPrivateKeyForAddress = function(address) { assert(isMyAddress(address), 'Unknown address. Make sure the address is from the keychain and has been generated') From 2b4d94cd92495992ce27425acf40440c6e025dc5 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 16 Aug 2014 14:19:19 +0800 Subject: [PATCH 09/11] Wallet: move processPendingTx & processConfirmedTx to prototype also, move processTx out of Wallet scope --- src/wallet.js | 129 ++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index ad7ccbd..23142d5 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -49,91 +49,28 @@ function Wallet(seed, network, unspents) { me.outputs = {} } - this.processPendingTx = function(tx){ - processTx(tx, true) - } - - this.processConfirmedTx = function(tx){ - processTx(tx, false) - } - - var me = this - - function processTx(tx, isPending) { - var txid = tx.getId() - - tx.outs.forEach(function(txOut, i) { - var address - - try { - address = Address.fromOutputScript(txOut.script, network).toString() - } catch(e) { - if (!(e.message.match(/has no matching Address/))) throw e - } - - if (isMyAddress(address)) { - var output = txid + ':' + i - - me.outputs[output] = { - from: output, - value: txOut.value, - address: address, - pending: isPending - } - } - }) - - tx.ins.forEach(function(txIn, i) { - // copy and convert to big-endian hex - var txinId = new Buffer(txIn.hash) - Array.prototype.reverse.call(txinId) - txinId = txinId.toString('hex') - - var output = txinId + ':' + txIn.index - - if (!(output in me.outputs)) return - - if (isPending) { - me.outputs[output].to = txid + ':' + i - me.outputs[output].pending = true - } else { - delete me.outputs[output] - } - }) - } - this.getMasterKey = function() { return masterKey } this.getAccountZero = function() { return accountZero } this.getExternalAccount = function() { return externalAccount } this.getInternalAccount = function() { return internalAccount } this.getPrivateKeyForAddress = function(address) { - assert(isMyAddress(address), 'Unknown address. Make sure the address is from the keychain and has been generated') + var myAddresses = this.addresses.concat(this.changeAddresses) + assert(includeAddress(myAddresses, address), + 'Unknown address. Make sure the address is from the keychain and has been generated') - if (isReceiveAddress(address)) { + if (includeAddress(this.addresses, address)) { var index = this.addresses.indexOf(address) return this.getPrivateKey(index) } - if (isChangeAddress(address)) { + if (includeAddress(this.changeAddresses, address)) { var index = this.changeAddresses.indexOf(address) return this.getInternalPrivateKey(index) } } - - function isReceiveAddress(address){ - return me.addresses.indexOf(address) > -1 - } - - function isChangeAddress(address){ - return me.changeAddresses.indexOf(address) > -1 - } - - function isMyAddress(address) { - return isReceiveAddress(address) || isChangeAddress(address) - } } Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) { @@ -175,6 +112,14 @@ Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) { return tx } +Wallet.prototype.processPendingTx = function(tx){ + processTx.bind(this)(tx, true) +} + +Wallet.prototype.processConfirmedTx = function(tx){ + processTx.bind(this)(tx, false) +} + Wallet.prototype.generateAddress = function() { var k = this.addresses.length var address = this.getExternalAccount().derive(k).getAddress() @@ -316,4 +261,52 @@ function getCandidateOutputs(outputs/*, value*/) { return sortByValueDesc } +function processTx(tx, isPending) { + var txid = tx.getId() + + tx.outs.forEach(function(txOut, i) { + var address + + try { + address = Address.fromOutputScript(txOut.script, this.network).toString() + } catch(e) { + if (!(e.message.match(/has no matching Address/))) throw e + } + + var myAddresses = this.addresses.concat(this.changeAddresses) + if (includeAddress(myAddresses, address)) { + var output = txid + ':' + i + + this.outputs[output] = { + from: output, + value: txOut.value, + address: address, + pending: isPending + } + } + }, this) + + tx.ins.forEach(function(txIn, i) { + // copy and convert to big-endian hex + var txinId = new Buffer(txIn.hash) + Array.prototype.reverse.call(txinId) + txinId = txinId.toString('hex') + + var output = txinId + ':' + txIn.index + + if (!(output in this.outputs)) return + + if (isPending) { + this.outputs[output].to = txid + ':' + i + this.outputs[output].pending = true + } else { + delete this.outputs[output] + } + }, this) +} + +function includeAddress(addresses, address) { + return addresses.indexOf(address) > -1 +} + module.exports = Wallet From 00d58604a47ecbe169c991ddc4f7e5fb247f7d57 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 16 Aug 2014 14:24:24 +0800 Subject: [PATCH 10/11] Wallet: move getPrivateKeyForAddress to prototype also, move the assert to the end to simplify the logic --- src/wallet.js | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 23142d5..137ec86 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -53,24 +53,6 @@ function Wallet(seed, network, unspents) { this.getAccountZero = function() { return accountZero } this.getExternalAccount = function() { return externalAccount } this.getInternalAccount = function() { return internalAccount } - - this.getPrivateKeyForAddress = function(address) { - var myAddresses = this.addresses.concat(this.changeAddresses) - assert(includeAddress(myAddresses, address), - 'Unknown address. Make sure the address is from the keychain and has been generated') - - if (includeAddress(this.addresses, address)) { - var index = this.addresses.indexOf(address) - - return this.getPrivateKey(index) - } - - if (includeAddress(this.changeAddresses, address)) { - var index = this.changeAddresses.indexOf(address) - - return this.getInternalPrivateKey(index) - } - } } Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) { @@ -160,6 +142,22 @@ Wallet.prototype.getPrivateKey = function(index) { return this.getExternalAccount().derive(index).privKey } +Wallet.prototype.getPrivateKeyForAddress = function(address) { + if (includeAddress(this.addresses, address)) { + var index = this.addresses.indexOf(address) + + return this.getPrivateKey(index) + } + + if (includeAddress(this.changeAddresses, address)) { + var index = this.changeAddresses.indexOf(address) + + return this.getInternalPrivateKey(index) + } + + assert(false, 'Unknown address. Make sure the address is from the keychain and has been generated') +} + Wallet.prototype.getReceiveAddress = function() { if (this.addresses.length === 0) { this.generateAddress() From e55676cf8e6b441dad5d747ea5da062338971c9d Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 16 Aug 2014 14:27:12 +0800 Subject: [PATCH 11/11] Wallet: remove unncessary comments --- src/wallet.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 137ec86..6e26612 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -13,7 +13,6 @@ function Wallet(seed, network, unspents) { // Stored in a closure to make accidental serialization less likely var masterKey = HDNode.fromSeedBuffer(seed, network) - var me = this // HD first-level child derivation method should be hardened // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 @@ -21,24 +20,19 @@ function Wallet(seed, network, unspents) { var externalAccount = accountZero.derive(0) var internalAccount = accountZero.derive(1) - // Addresses this.addresses = [] this.changeAddresses = [] - this.network = network - - // Transaction output data this.outputs = unspents ? processUnspentOutputs(unspents) : {} // FIXME: remove in 2.x.y + var me = this this.newMasterKey = function(seed) { console.warn('newMasterKey is deprecated, please make a new Wallet instance instead') seed = seed || crypto.randomBytes(32) masterKey = HDNode.fromSeedBuffer(seed, network) - // HD first-level child derivation method should be hardened - // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254 accountZero = masterKey.deriveHardened(0) externalAccount = accountZero.derive(0) internalAccount = accountZero.derive(1)