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.
This commit is contained in:
Daniel Cousens 2014-08-14 11:03:54 +10:00
parent 7c22067f69
commit 2f00c9ab35
2 changed files with 177 additions and 171 deletions

View file

@ -7,7 +7,7 @@ var HDNode = require('./hdnode')
var Transaction = require('./transaction') var Transaction = require('./transaction')
var Script = require('./script') var Script = require('./script')
function Wallet(seed, network) { function Wallet(seed, network, unspents) {
seed = seed || crypto.randomBytes(32) seed = seed || crypto.randomBytes(32)
network = network || networks.bitcoin network = network || networks.bitcoin
@ -26,7 +26,7 @@ function Wallet(seed, network) {
this.changeAddresses = [] this.changeAddresses = []
// Transaction output data // Transaction output data
this.outputs = {} this.outputs = unspents ? processUnspentOutputs(unspents) : {}
this.generateAddress = function() { this.generateAddress = function() {
var key = externalAccount.derive(this.addresses.length) var key = externalAccount.derive(this.addresses.length)
@ -77,16 +77,11 @@ function Wallet(seed, network) {
} }
this.setUnspentOutputs = function(utxo) { this.setUnspentOutputs = function(utxo) {
var outputs = {} console.warn('setUnspentOutputs is deprecated, please use the constructor option instead')
utxo.forEach(function(uo){ this.outputs = processUnspentOutputs(utxo)
validateUnspentOutput(uo)
var o = unspentOutputToOutput(uo)
outputs[o.from] = o
})
this.outputs = outputs
} }
this.processPendingTx = function(tx){ this.processPendingTx = function(tx){
processTx(tx, true) processTx(tx, true)
} }
@ -95,6 +90,8 @@ function Wallet(seed, network) {
processTx(tx, false) processTx(tx, false)
} }
var me = this
function processTx(tx, isPending) { function processTx(tx, isPending) {
var txid = tx.getId() var txid = tx.getId()
@ -241,49 +238,13 @@ function outputToUnspentOutput(output){
return { return {
hash: hashAndIndex[0], hash: hashAndIndex[0],
outputIndex: parseInt(hashAndIndex[1]), index: parseInt(hashAndIndex[1]),
address: output.address, address: output.address,
value: output.value, value: output.value,
pending: output.pending 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) { function estimatePaddedFee(tx, network) {
var tmpTx = tx.clone() var tmpTx = tx.clone()
tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0) tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0)
@ -291,8 +252,34 @@ function estimatePaddedFee(tx, network) {
return network.estimateFee(tmpTx) return network.estimateFee(tmpTx)
} }
function isNullOrUndefined(value) { function processUnspentOutputs(utxos) {
return value == undefined 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*/) { function getCandidateOutputs(outputs/*, value*/) {

View file

@ -26,13 +26,17 @@ function fakeTxId(i) {
} }
describe('Wallet', function() { describe('Wallet', function() {
var seed, wallet var seed
beforeEach(function(){ beforeEach(function(){
seed = crypto.sha256("don't use a string seed like this in real life") seed = crypto.sha256("don't use a string seed like this in real life")
wallet = new Wallet(seed)
}) })
describe('constructor', function() { describe('constructor', function() {
var wallet
beforeEach(function(){
wallet = new Wallet(seed)
})
it('defaults to Bitcoin network', function() { it('defaults to Bitcoin network', function() {
assert.equal(wallet.getMasterKey().network, networks.bitcoin) assert.equal(wallet.getMasterKey().network, networks.bitcoin)
}) })
@ -116,6 +120,11 @@ describe('Wallet', function() {
}) })
describe('generateChangeAddress', function(){ describe('generateChangeAddress', function(){
var wallet
beforeEach(function(){
wallet = new Wallet(seed)
})
it('generates change addresses', function(){ it('generates change addresses', function(){
var wallet = new Wallet(seed, networks.testnet) var wallet = new Wallet(seed, networks.testnet)
var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"] var expectedAddresses = ["mnXiDR4MKsFxcKJEZjx4353oXvo55iuptn"]
@ -126,6 +135,11 @@ describe('Wallet', function() {
}) })
describe('getPrivateKey', 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(){ it('returns the private key at the given index of external account', function(){
var wallet = new Wallet(seed, networks.testnet) var wallet = new Wallet(seed, networks.testnet)
@ -135,6 +149,11 @@ describe('Wallet', function() {
}) })
describe('getInternalPrivateKey', 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(){ it('returns the private key at the given index of internal account', function(){
var wallet = new Wallet(seed, networks.testnet) var wallet = new Wallet(seed, networks.testnet)
@ -144,6 +163,11 @@ describe('Wallet', function() {
}) })
describe('getPrivateKeyForAddress', function(){ describe('getPrivateKeyForAddress', function(){
var wallet
beforeEach(function(){
wallet = new Wallet(seed)
})
it('returns the private key for the given address', function(){ it('returns the private key for the given address', function(){
var wallet = new Wallet(seed, networks.testnet) var wallet = new Wallet(seed, networks.testnet)
wallet.generateChangeAddress() wallet.generateChangeAddress()
@ -162,6 +186,7 @@ describe('Wallet', function() {
it('raises an error when address is not found', function(){ it('raises an error when address is not found', function(){
var wallet = new Wallet(seed, networks.testnet) var wallet = new Wallet(seed, networks.testnet)
assert.throws(function() { assert.throws(function() {
wallet.getPrivateKeyForAddress("n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X") 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/)
@ -169,51 +194,55 @@ describe('Wallet', function() {
}) })
describe('Unspent Outputs', function(){ describe('Unspent Outputs', function(){
var expectedUtxo, expectedOutputKey var utxo, expectedOutputKey
var wallet
beforeEach(function(){ beforeEach(function(){
expectedUtxo = { utxo = {
"hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7",
"outputIndex": 0,
"address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv",
"value": 20000, "hash": fakeTxId(6),
"pending": true "index": 0,
"pending": true,
"value": 20000
} }
expectedOutputKey = expectedUtxo.hash + ":" + expectedUtxo.outputIndex
expectedOutputKey = utxo.hash + ":" + utxo.index
}) })
function addUtxoToOutput(utxo){ describe('on construction', function(){
var key = utxo.hash + ":" + utxo.outputIndex beforeEach(function(){
wallet.outputs[key] = { wallet = new Wallet(seed, networks.bitcoin, [utxo])
from: key, })
address: utxo.address,
value: utxo.value, it('matches the expected behaviour', function(){
pending: utxo.pending var output = wallet.outputs[expectedOutputKey]
}
} assert(output)
assert.equal(output.value, utxo.value)
assert.equal(output.address, utxo.address)
})
})
describe('getBalance', function(){ describe('getBalance', function(){
var utxo1
beforeEach(function(){ beforeEach(function(){
utxo1 = cloneObject(expectedUtxo) var utxo1 = cloneObject(utxo)
utxo1.hash = utxo1.hash.replace('7', 'l') utxo1.hash = fakeTxId(5)
wallet = new Wallet(seed, networks.bitcoin, [utxo, utxo1])
}) })
it('sums over utxo values', function(){ it('sums over utxo values', function(){
addUtxoToOutput(expectedUtxo)
addUtxoToOutput(utxo1)
assert.equal(wallet.getBalance(), 40000) assert.equal(wallet.getBalance(), 40000)
}) })
}) })
describe('getUnspentOutputs', function(){ describe('getUnspentOutputs', function(){
beforeEach(function(){ beforeEach(function(){
addUtxoToOutput(expectedUtxo) wallet = new Wallet(seed, networks.bitcoin, [utxo])
}) })
it('parses wallet outputs to the expect format', function(){ 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(){ it("ignores pending spending outputs (outputs with 'to' property)", function(){
@ -223,40 +252,54 @@ describe('Wallet', function() {
assert.deepEqual(wallet.getUnspentOutputs(), []) assert.deepEqual(wallet.getUnspentOutputs(), [])
}) })
}) })
})
// FIXME: remove in 2.x.y
describe('setUnspentOutputs', function(){ describe('setUnspentOutputs', function(){
var utxo var utxo
var expectedOutputKey
beforeEach(function(){ beforeEach(function(){
utxo = cloneObject([expectedUtxo]) utxo = {
hash: fakeTxId(0),
index: 0,
address: '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi',
value: 500000
}
expectedOutputKey = utxo.hash + ":" + utxo.index
wallet = new Wallet(seed, networks.bitcoin)
}) })
it('matches the expected behaviour', function(){ it('matches the expected behaviour', function(){
wallet.setUnspentOutputs(utxo) wallet.setUnspentOutputs([utxo])
verifyOutputs()
var output = wallet.outputs[expectedOutputKey]
assert(output)
assert.equal(output.value, utxo.value)
assert.equal(output.address, utxo.address)
}) })
describe('required fields', function(){ describe('required fields', function(){
['outputIndex', 'address', 'hash', 'value'].forEach(function(field){ ['index', 'address', 'hash', 'value'].forEach(function(field){
it("throws an error when " + field + " is missing", function(){ it("throws an error when " + field + " is missing", function(){
delete utxo[0][field] delete utxo[field]
assert.throws(function() { assert.throws(function() {
wallet.setUnspentOutputs(utxo) wallet.setUnspentOutputs([utxo])
}, new RegExp('Invalid unspent output: key ' + field + ' is missing'))
}) })
}) })
}) })
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(){ describe('Process transaction', function(){
var wallet
beforeEach(function(){
wallet = new Wallet(seed)
})
var addresses var addresses
var tx var tx
@ -389,39 +432,42 @@ describe('Wallet', function() {
}) })
describe('createTx', function(){ describe('createTx', function(){
var to, value var wallet
var address1, address2 var address1, address2
var to, value
beforeEach(function(){ beforeEach(function(){
to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue'
value = 500000 value = 500000
// generate 2 addresses address1 = "n1GyUANZand9Kw6hGSV9837cCC9FFUQzQa"
address1 = wallet.generateAddress() address2 = "n2fiWrHqD6GM5GiEqkbWAc6aaZQp3ba93X"
address2 = wallet.generateAddress()
// set up 3 utxo // set up 3 utxos
utxo = [ var utxos = [
{ {
"hash": fakeTxId(1), "hash": fakeTxId(1),
"outputIndex": 0, "index": 0,
"address" : address1, "address": address1,
"value": 400000 // not enough for value "value": 400000 // not enough for value
}, },
{ {
"hash": fakeTxId(2), "hash": fakeTxId(2),
"outputIndex": 1, "index": 1,
"address" : address1, "address": address1,
"value": 500000 // enough for only value "value": 500000 // enough for only value
}, },
{ {
"hash": fakeTxId(3), "hash": fakeTxId(3),
"outputIndex": 0, "index": 0,
"address" : address2, "address" : address2,
"value": 510000 // enough for value and fee "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(){ describe('transaction fee', function(){
@ -441,17 +487,18 @@ describe('Wallet', function() {
}) })
it('does not overestimate fees when network has dustSoftThreshold', function(){ it('does not overestimate fees when network has dustSoftThreshold', function(){
var wallet = new Wallet(seed, networks.litecoin) var utxo = {
var address = wallet.generateAddress()
wallet.setUnspentOutputs([{
hash: fakeTxId(0), hash: fakeTxId(0),
outputIndex: 0, index: 0,
address: address, address: "LeyySKbQrRRwodKEj1W4a8y3YQupPLw5os",
value: 500000 value: 500000
}]) }
var wallet = new Wallet(seed, networks.litecoin, [utxo])
wallet.generateAddress()
value = 200000 value = 200000
var tx = wallet.createTx(address, value) var tx = wallet.createTx(utxo.address, value)
assert.equal(getFee(wallet, tx), 100000) assert.equal(getFee(wallet, tx), 100000)
}) })
@ -477,18 +524,25 @@ describe('Wallet', function() {
assert.equal(tx.ins[0].index, 0) assert.equal(tx.ins[0].index, 0)
}) })
it('ignores pending outputs', function(){ it('uses confirmed outputs', function(){
utxo.push( var tx2 = new Transaction()
{ tx2.addInput(fakeTxId(4), 0)
"hash": fakeTxId(4), tx2.addOutput(address2, 530000)
"outputIndex": 0,
"address" : address2,
"value": 530000,
"pending": true
}
)
wallet.setUnspentOutputs(utxo)
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) var tx = wallet.createTx(to, value)
assert.equal(tx.ins.length, 1) 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(){ describe('changeAddress', function(){
it('should allow custom 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 changeAddress = 'mfrFjnKZUvTcvdAK2fUX5D8v1Epu5H8JCk'
var to = 'mt7MyTVVEWnbwpF5hBn6fgnJcv95Syk2ue' var fromValue = 510000
var toValue = value / 2 var toValue = fromValue / 2
var fee = 1e3 var fee = 1e3
var tx = wallet.createTx(to, toValue, fee, changeAddress) var tx = wallet.createTx(to, toValue, fee, changeAddress)
@ -549,7 +568,7 @@ describe('Wallet', function() {
assert.equal(tx.outs[0].value, toValue) assert.equal(tx.outs[0].value, toValue)
assert.equal(outAddress1.toString(), changeAddress) 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) assert.equal(tx.outs.length, 1)
var out = tx.outs[0] 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(outAddress.toString(), to)
assert.equal(out.value, value) assert.equal(out.value, value)
@ -574,7 +593,7 @@ describe('Wallet', function() {
assert.equal(tx.outs.length, 2) assert.equal(tx.outs.length, 2)
var out = tx.outs[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[1]) assert.equal(outAddress.toString(), wallet.changeAddresses[1])
assert.equal(out.value, 10000) assert.equal(out.value, 10000)
@ -588,7 +607,7 @@ describe('Wallet', function() {
assert.equal(wallet.changeAddresses.length, 1) assert.equal(wallet.changeAddresses.length, 1)
var out = tx.outs[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(outAddress.toString(), wallet.changeAddresses[0])
assert.equal(out.value, 10000) assert.equal(out.value, 10000)