From 16dc68cbaa18877890a9b0f14c58e687c5d0c3ba Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 14:37:09 +0800 Subject: [PATCH 01/31] wallet exposes unspent outputs via a getter also add reverseEndian method to convert --- src/convert.js | 7 ++++++- src/wallet.js | 20 ++++++++++++++++++++ test/convert.js | 9 +++++++++ test/wallet.js | 39 ++++++++++++++++++++++++++++++++------- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/convert.js b/src/convert.js index ddf4282..e54a466 100644 --- a/src/convert.js +++ b/src/convert.js @@ -158,6 +158,10 @@ function wordArrayToBytes(wordArray) { return wordsToBytes(wordArray.words) } +function reverseEndian (hex) { + return bytesToHex(hexToBytes(hex).reverse()) +} + module.exports = { lpad: lpad, bytesToHex: bytesToHex, @@ -175,5 +179,6 @@ module.exports = { bytesToWords: bytesToWords, wordsToBytes: wordsToBytes, bytesToWordArray: bytesToWordArray, - wordArrayToBytes: wordArrayToBytes + wordArrayToBytes: wordArrayToBytes, + reverseEndian: reverseEndian } diff --git a/src/wallet.js b/src/wallet.js index da3aa79..63486cd 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -60,6 +60,26 @@ var Wallet = function (seed, options) { return this.changeAddresses[this.changeAddresses.length - 1] } + this.getUnspentOutputs = function() { + var utxo = [] + + for(var key in this.outputs){ + var hashAndIndex = key.split(":") + var output = this.outputs[key] + + utxo.push({ + hash: hashAndIndex[0], + hashLittleEndian: convert.reverseEndian(hashAndIndex[0]), + outputIndex: parseInt(hashAndIndex[1]), + scriptPubKey: output.scriptPubKey, + address: output.address, + value: output.value + }) + } + + return utxo + } + // Processes a transaction object // If "verified" is true, then we trust the transaction as "final" this.processTx = function(tx, verified) { diff --git a/test/convert.js b/test/convert.js index efa4a82..4727692 100644 --- a/test/convert.js +++ b/test/convert.js @@ -93,4 +93,13 @@ describe('convert', function() { } }) }) + + describe('reverseEndian', function() { + it('works', function() { + var bigEndian = "6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7" + var littleEdian = "c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a" + assert.deepEqual(convert.reverseEndian(bigEndian), littleEdian) + assert.deepEqual(convert.reverseEndian(littleEdian), bigEndian) + }) + }) }) diff --git a/test/wallet.js b/test/wallet.js index b72e2d7..e02b10d 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -6,17 +6,13 @@ var SHA256 = require('crypto-js/sha256') var Crypto = require('crypto-js') describe('Wallet', function() { - var seed; + var seed, wallet; beforeEach(function(){ seed = convert.wordArrayToBytes(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 mainnet', function() { assert.equal(wallet.getMasterKey().network, 'mainnet') }) @@ -47,7 +43,6 @@ describe('Wallet', function() { }) describe('constructor options', function() { - var wallet; beforeEach(function() { wallet = new Wallet(seed, {network: 'testnet'}) }) @@ -149,6 +144,36 @@ describe('Wallet', function() { }) }) + describe('Unspent Outputs', function(){ + var expectedUtxo, expectedOutputKey; + beforeEach(function(){ + expectedUtxo = [ + { + "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7", + "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a", + "outputIndex": 0, + "scriptPubKey":"76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac", + "address" : "1azpkpcfczkduetfbqul4mokqai3m3hmxv", + "value": 20000 + } + ] + expectedOutputKey = expectedUtxo[0].hash + ":" + expectedUtxo[0].outputIndex + }) + + describe('getUnspentOutputs', function(){ + it('parses wallet outputs to the expect format', function(){ + wallet.outputs[expectedOutputKey] = { + output: expectedOutputKey, + scriptPubKey: expectedUtxo[0].scriptPubKey, + address: expectedUtxo[0].address, + value: expectedUtxo[0].value + } + + assert.deepEqual(wallet.getUnspentOutputs(), expectedUtxo) + }) + }) + }) + function assertEqual(obj1, obj2){ assert.equal(obj1.toString(), obj2.toString()) } From 26afbccc98ea44b113c9efef29a72d14c7e32cd2 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 15:09:00 +0800 Subject: [PATCH 02/31] wallet allows setting unspent outputs --- src/wallet.js | 17 +++++++++++++++++ test/wallet.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/wallet.js b/src/wallet.js index 63486cd..44c592e 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -80,6 +80,23 @@ var Wallet = function (seed, options) { return utxo } + this.setUnspentOutputs = function(utxo) { + var outputs = {} + + utxo.forEach(function(o){ + var hash = o.hash || convert.reverseEndian(o.hashLittleEndian) + var key = hash + ":" + o.outputIndex + outputs[key] = { + output: key, + scriptPubKey: o.scriptPubKey, + address: o.address, + value: o.value + } + }) + + this.outputs = outputs + } + // Processes a transaction object // If "verified" is true, then we trust the transaction as "final" this.processTx = function(tx, verified) { diff --git a/test/wallet.js b/test/wallet.js index e02b10d..549cc70 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -172,6 +172,40 @@ describe('Wallet', function() { assert.deepEqual(wallet.getUnspentOutputs(), expectedUtxo) }) }) + + describe('setUnspentOutputs', function(){ + var utxo; + beforeEach(function(){ + utxo = cloneObject(expectedUtxo) + }) + + it('uses hashLittleEndian when hash is not present', function(){ + delete utxo[0]['hash'] + + wallet.setUnspentOutputs(utxo) + verifyOutputs() + }) + + it('uses hash when hashLittleEndian is not present', function(){ + delete utxo[0]['hashLittleEndian'] + + wallet.setUnspentOutputs(utxo) + verifyOutputs() + }) + + it('uses hash when both hash and hashLittleEndian are present', function(){ + wallet.setUnspentOutputs(utxo) + verifyOutputs() + }) + + function verifyOutputs() { + var output = wallet.outputs[expectedOutputKey] + assert(output) + assert.equal(output.value, utxo[0].value) + assert.equal(output.address, utxo[0].address) + assert.equal(output.scriptPubKey, utxo[0].scriptPubKey) + } + }) }) function assertEqual(obj1, obj2){ @@ -181,4 +215,9 @@ describe('Wallet', function() { 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)) + } }) From 01dc34d720d0bc207eeca83e92c3f2182a7f778d Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 15:37:03 +0800 Subject: [PATCH 03/31] throw error when unspent output does not have required keys --- src/wallet.js | 53 +++++++++++++++++++++++++++++++++++++++++--------- test/wallet.js | 26 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 44c592e..dcc8801 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -83,20 +83,55 @@ var Wallet = function (seed, options) { this.setUnspentOutputs = function(utxo) { var outputs = {} - utxo.forEach(function(o){ - var hash = o.hash || convert.reverseEndian(o.hashLittleEndian) - var key = hash + ":" + o.outputIndex - outputs[key] = { - output: key, - scriptPubKey: o.scriptPubKey, - address: o.address, - value: o.value - } + utxo.forEach(function(uo){ + validateUnspentOutput(uo) + var o = unspentOutputToOutput(uo) + outputs[o.output] = o }) this.outputs = outputs } + function unspentOutputToOutput(o) { + var hash = o.hash || convert.reverseEndian(o.hashLittleEndian) + var key = hash + ":" + o.outputIndex + return { + output: key, + scriptPubKey: o.scriptPubKey, + address: o.address, + value: o.value + } + } + + function validateUnspentOutput(uo) { + var missingField; + + if(isNullOrUndefined(uo.hash) && isNullOrUndefined(uo.hashLittleEndian)){ + missingField = "hash(or hashLittleEndian)" + } + + var requiredKeys = ['outputIndex', 'scriptPubKey', 'address', 'value'] + requiredKeys.forEach(function(key){ + if(isNullOrUndefined(uo[key])){ + missingField = key + } + }) + + if(missingField) { + var message = [ + 'Invalid unspent output: key', field, 'is missing.', + 'A valid unspent output must contain' + ] + message.push(requiredKeys.join(', ')) + message.push("and hash(or hashLittleEndian)") + throw new Error(message.join(' ')) + } + } + + function isNullOrUndefined(value){ + return value == undefined + } + // Processes a transaction object // If "verified" is true, then we trust the transaction as "final" this.processTx = function(tx, verified) { diff --git a/test/wallet.js b/test/wallet.js index 549cc70..37aa4f7 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -198,6 +198,32 @@ describe('Wallet', function() { verifyOutputs() }) + describe('required fields', function(){ + it("throws an error when hash and hashLittleEndian are both missing", function(){ + delete utxo[0]['hash'] + delete utxo[0]['hashLittleEndian'] + + var errorMessage = 'Invalid unspent output: key hash(or hashLittleEndian) is missing. ' + + 'A valid unspent output must contain outputIndex, scriptPubKey, address, value and hash(or hashLittleEndian)' + + assert.throws(function() { + wallet.setUnspentOutputs(utxo) + }, Error, errorMessage) + }); + + ['outputIndex', 'scriptPubKey', 'address', 'value'].forEach(function(field){ + it("throws an error when " + field + " is missing", function(){ + delete utxo[0][field] + var errorMessage = 'Invalid unspent output: key ' + field + + ' is missing. A valid unspent output must contain outputIndex, scriptPubKey, address, value and hash(or hashLittleEndian)' + + assert.throws(function() { + wallet.setUnspentOutputs(utxo) + }, Error, errorMessage) + }) + }) + }) + function verifyOutputs() { var output = wallet.outputs[expectedOutputKey] assert(output) From c39aa6cb0793f704a153a3a7c71e60a38e2abaf1 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 15:43:55 +0800 Subject: [PATCH 04/31] minor refactoring --- src/wallet.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index dcc8801..8c7ab56 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -64,17 +64,7 @@ var Wallet = function (seed, options) { var utxo = [] for(var key in this.outputs){ - var hashAndIndex = key.split(":") - var output = this.outputs[key] - - utxo.push({ - hash: hashAndIndex[0], - hashLittleEndian: convert.reverseEndian(hashAndIndex[0]), - outputIndex: parseInt(hashAndIndex[1]), - scriptPubKey: output.scriptPubKey, - address: output.address, - value: output.value - }) + utxo.push(outputToUnspentOutput(this.outputs[key])) } return utxo @@ -92,6 +82,19 @@ var Wallet = function (seed, options) { this.outputs = outputs } + function outputToUnspentOutput(output){ + var hashAndIndex = output.output.split(":") + + return { + hash: hashAndIndex[0], + hashLittleEndian: convert.reverseEndian(hashAndIndex[0]), + outputIndex: parseInt(hashAndIndex[1]), + scriptPubKey: output.scriptPubKey, + address: output.address, + value: output.value + } + } + function unspentOutputToOutput(o) { var hash = o.hash || convert.reverseEndian(o.hashLittleEndian) var key = hash + ":" + o.outputIndex From 423124966facadf4e67a5e30b7c287b1220c7d9a Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 20:28:20 +0800 Subject: [PATCH 05/31] add toAddress and getOutType tests to script --- test/script.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/script.js b/test/script.js index f8e378e..73cf257 100644 --- a/test/script.js +++ b/test/script.js @@ -7,7 +7,7 @@ var sha256ripe160 = Util.sha256ripe160; var Convert = require('../src/convert.js') var bytesToHex = Convert.bytesToHex; -var hexToBytes = Convert.hexToBytes; +var hexToBytes = Convert.hexToBytes; describe('Script', function() { describe('constructor', function() { @@ -23,7 +23,7 @@ describe('Script', function() { assert.throws(function(){ new Script({}) }) }) }) - + describe('2-of-3 Multi-Signature', function() { var compressedPubKeys = [] var numSigs @@ -32,7 +32,7 @@ describe('Script', function() { compressedPubKeys = ['02ea1297665dd733d444f31ec2581020004892cdaaf3dd6c0107c615afb839785f', '02fab2dea1458990793f56f42e4a47dbf35a12a351f26fa5d7e0cc7447eaafa21f', '036c6802ce7e8113723dd92cdb852e492ebb157a871ca532c3cb9ed08248ff0e19'] - + numSigs = 2; }) @@ -42,13 +42,36 @@ describe('Script', function() { var multisig = sha256ripe160(script.buffer) var multiSigAddress = Address(multisig,network).toString() var redeemScript = bytesToHex(script.buffer) - + assert.ok(Address.validate(multiSigAddress)) assert.equal(Address.getVersion(multiSigAddress),'0x05') assert.equal(multiSigAddress,'32vYjxBb7pHJJyXgNk8UoK3BdRDxBzny2v') assert.equal(Address(sha256ripe160(hexToBytes(redeemScript)),network).toString(), - multiSigAddress) + multiSigAddress) + }) + }) + + describe('getOutType', function() { + it('works for p2sh', function() { + var script = Script.fromHex("a914e8c300c87986efa84c37c0519929019ef86eb5b487") + assert.equal(script.getOutType(), 'P2SH') }) + it('works for pubkey', function() { + var script = Script.fromHex("76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac") + assert.equal(script.getOutType(), 'Pubkey') + }) + }) + + describe('toAddress', function() { + it('works for p2sh type output', function() { + var script = Script.fromHex("a914e8c300c87986efa84c37c0519929019ef86eb5b487") + assert.equal(script.toAddress(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') + }) + + it('works for pubkey type output', function() { + var script = Script.fromHex("76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac") + assert.equal(script.toAddress(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + }) }) }) From c3880c0cdf5324b28b554f8bc3b052def77fafc0 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 20:30:49 +0800 Subject: [PATCH 06/31] script.toAddress -> script.getToAddress --- src/script.js | 2 +- src/transaction.js | 2 +- test/script.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/script.js b/src/script.js index 7958d12..c47945c 100644 --- a/src/script.js +++ b/src/script.js @@ -150,7 +150,7 @@ Script.prototype.toScriptHash = function() { return util.sha256ripe160(this.buffer) } -Script.prototype.toAddress = function() { +Script.prototype.getToAddress = function() { var outType = this.getOutType(); if (outType == 'Pubkey') { diff --git a/src/transaction.js b/src/transaction.js index 20d03dd..3cbf5b0 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -415,7 +415,7 @@ var TransactionOut = function (data) { : data.address ? Script.createOutputScript(data.address) : new Script(); - if (this.script.buffer.length > 0) this.address = this.script.toAddress(); + if (this.script.buffer.length > 0) this.address = this.script.getToAddress(); this.value = Array.isArray(data.value) ? convert.bytesToNum(data.value) diff --git a/test/script.js b/test/script.js index 73cf257..76f3a6f 100644 --- a/test/script.js +++ b/test/script.js @@ -63,15 +63,15 @@ describe('Script', function() { }) }) - describe('toAddress', function() { + describe('getToAddress', function() { it('works for p2sh type output', function() { var script = Script.fromHex("a914e8c300c87986efa84c37c0519929019ef86eb5b487") - assert.equal(script.toAddress(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') + assert.equal(script.getToAddress(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') }) it('works for pubkey type output', function() { var script = Script.fromHex("76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac") - assert.equal(script.toAddress(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + assert.equal(script.getToAddress(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') }) }) }) From 83381186d136064a48bc4136b983d346a99b07d4 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 21:01:40 +0800 Subject: [PATCH 07/31] add (some) fromAddress and getInType tests to script --- src/script.js | 6 ++++++ test/script.js | 34 ++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/script.js b/src/script.js index c47945c..85c4454 100644 --- a/src/script.js +++ b/src/script.js @@ -150,6 +150,7 @@ Script.prototype.toScriptHash = function() { return util.sha256ripe160(this.buffer) } +//TODO: support testnet Script.prototype.getToAddress = function() { var outType = this.getOutType(); @@ -164,6 +165,11 @@ Script.prototype.getToAddress = function() { return new Address(this.chunks[1], 5) } +//TODO: support testnet +Script.prototype.getFromAddress = function(){ + return new Address(this.simpleInHash()); +} + /** * Compare the script to known templates of scriptSig. * diff --git a/test/script.js b/test/script.js index 76f3a6f..d5b7ef5 100644 --- a/test/script.js +++ b/test/script.js @@ -10,6 +10,14 @@ var bytesToHex = Convert.bytesToHex; var hexToBytes = Convert.hexToBytes; describe('Script', function() { + var p2shScriptPubKey, pubkeyScriptPubkey, addressScriptSig + + beforeEach(function(){ + p2shScriptPubKey = "a914e8c300c87986efa84c37c0519929019ef86eb5b487" + pubkeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac" + addressScriptSig = "48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8" + }) + describe('constructor', function() { it('works for a byte array', function() { assert.ok(new Script([])) @@ -53,25 +61,39 @@ describe('Script', function() { describe('getOutType', function() { it('works for p2sh', function() { - var script = Script.fromHex("a914e8c300c87986efa84c37c0519929019ef86eb5b487") + var script = Script.fromHex(p2shScriptPubKey) assert.equal(script.getOutType(), 'P2SH') }) it('works for pubkey', function() { - var script = Script.fromHex("76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac") + var script = Script.fromHex(pubkeyScriptPubKey) assert.equal(script.getOutType(), 'Pubkey') }) }) + describe('getInType', function() { + it('works for address', function() { + var script = Script.fromHex(addressScriptSig) + assert.equal(script.getInType(), 'Address') + }) + }) + describe('getToAddress', function() { it('works for p2sh type output', function() { - var script = Script.fromHex("a914e8c300c87986efa84c37c0519929019ef86eb5b487") - assert.equal(script.getToAddress(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') + var script = Script.fromHex(p2shScriptPubKey) + assert.equal(script.getToAddress().toString(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') }) it('works for pubkey type output', function() { - var script = Script.fromHex("76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac") - assert.equal(script.getToAddress(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + var script = Script.fromHex(pubkeyScriptPubKey) + assert.equal(script.getToAddress().toString(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + }) + }) + + describe('getFromAddress', function() { + it('works for address type input', function() { + var script = Script.fromHex(addressScriptSig) + assert.equal(script.getFromAddress().toString(), '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u') }) }) }) From dbb56813660990a123765265f24050c4e0518d6a Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 21:10:56 +0800 Subject: [PATCH 08/31] add tests and refactor wallet.processTx --- src/wallet.js | 50 ++++++++++++++-------- test/fixtures/mainnet_tx.json | 4 ++ test/wallet.js | 80 +++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/mainnet_tx.json diff --git a/src/wallet.js b/src/wallet.js index 8c7ab56..ec19147 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -135,31 +135,35 @@ var Wallet = function (seed, options) { return value == undefined } - // Processes a transaction object - // If "verified" is true, then we trust the transaction as "final" - this.processTx = function(tx, verified) { + this.processTx = function(tx) { var txhash = convert.bytesToHex(tx.getHash()) - for (var i = 0; i < tx.outs.length; i++) { - if (this.addresses.indexOf(tx.outs[i].address.toString()) >= 0) { - me.outputs[txhash+':'+i] = { - output: txhash+':'+i, - value: tx.outs[i].value, - address: tx.outs[i].address.toString(), - timestamp: new Date().getTime() / 1000, - pending: true + + tx.outs.forEach(function(txOut, i){ + var address = txOut.address.toString() + if (isMyAddress(address)) { + var output = txhash+':'+i + me.outputs[output] = { + output: output, + value: txOut.value, + address: address, + scriptPubKey: convert.bytesToHex(txOut.script.buffer) //TODO: txOut.scriptPubKey() + // timestamp: new Date().getTime() / 1000, + // pending: true } } - } - for (var i = 0; i < tx.ins.length; i++) { - var op = tx.ins[i].outpoint + }) + + tx.ins.forEach(function(txIn, i){ + var op = txIn.outpoint var o = me.outputs[op.hash+':'+op.index] if (o) { o.spend = txhash+':'+i - o.spendpending = true - o.timestamp = new Date().getTime() / 1000 + // o.spendpending = true + // o.timestamp = new Date().getTime() / 1000 } - } + }) } + // Processes an output from an external source of the form // { output: txhash:index, value: integer, address: address } // Excellent compatibility with SX and pybitcointools @@ -276,6 +280,18 @@ var Wallet = function (seed, options) { throw new Error('Unknown address. Make sure the address is from the keychain and has been generated.') } } + + 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) + } }; module.exports = Wallet; diff --git a/test/fixtures/mainnet_tx.json b/test/fixtures/mainnet_tx.json new file mode 100644 index 0000000..f42bd7b --- /dev/null +++ b/test/fixtures/mainnet_tx.json @@ -0,0 +1,4 @@ +{ + "prevTx": "0100000001e0214ebebb0fd3414d3fdc0dbf3b0f4b247a296cafc984558622c3041b0fcc9b010000008b48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8ffffffff03a0860100000000001976a91400ea3576c8fcb0bc8392f10e23a3425ae24efea888ac40420f00000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188acf07cd210000000001976a9146fb93c557ee62b109370fd9003e456917401cbfa88ac00000000", + "tx": "0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000" +} diff --git a/test/wallet.js b/test/wallet.js index 37aa4f7..a7161e2 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -1,10 +1,15 @@ var Wallet = require('../src/wallet.js') var HDNode = require('../src/hdwallet.js') +var Transaction = require('../src/transaction.js').Transaction var convert = require('../src/convert.js') var assert = require('assert') var SHA256 = require('crypto-js/sha256') var Crypto = require('crypto-js') +var fixtureTxes = require('./fixtures/mainnet_tx') +var fixtureTx1Hex = fixtureTxes.prevTx +var fixtureTx2Hex = fixtureTxes.tx + describe('Wallet', function() { var seed, wallet; beforeEach(function(){ @@ -234,6 +239,81 @@ describe('Wallet', function() { }) }) + describe('processTx', function(){ + var tx; + + beforeEach(function(){ + tx = Transaction.deserialize(fixtureTx1Hex) + }) + + describe("when tx outs contains an address owned by the wallet, an 'output' gets added to wallet.outputs", function(){ + it("works for receive address", function(){ + var totalOuts = outputCount() + wallet.addresses = [tx.outs[0].address.toString()] + + wallet.processTx(tx) + + assert.equal(outputCount(), totalOuts + 1) + verifyOutputAdded(0) + }) + + it("works for change address", function(){ + var totalOuts = outputCount() + wallet.changeAddresses = [tx.outs[1].address.toString()] + + wallet.processTx(tx) + + assert.equal(outputCount(), totalOuts + 1) + verifyOutputAdded(1) + }) + + function outputCount(){ + return Object.keys(wallet.outputs).length + } + + function verifyOutputAdded(index) { + var txOut = tx.outs[index] + var key = convert.bytesToHex(tx.getHash()) + ":" + index + var output = wallet.outputs[key] + assert.equal(output.output, key) + assert.equal(output.value, txOut.value) + assert.equal(output.address, txOut.address) + assert.equal(output.scriptPubKey, convert.bytesToHex(txOut.script.buffer)) + } + }) + + describe("when tx ins outpoint contains a known txhash:i, the corresponding 'output' gets updated", function(){ + beforeEach(function(){ + wallet.addresses = [tx.outs[0].address.toString()] // the address fixtureTx2 is spending from + wallet.processTx(tx) + + tx = Transaction.deserialize(fixtureTx2Hex) + }) + + it("does not add to wallet.outputs", function(){ + var outputs = wallet.outputs + wallet.processTx(tx) + assert.deepEqual(wallet.outputs, outputs) + }) + + it("sets spend with the transaction hash and input index", function(){ + wallet.processTx(tx) + + var txIn = tx.ins[0] + var key = txIn.outpoint.hash + ":" + txIn.outpoint.index + var output = wallet.outputs[key] + + assert.equal(output.spend, convert.bytesToHex(tx.getHash()) + ':' + 0) + }) + }) + + it("does nothing when none of the involved addresses belong to the wallet", function(){ + var outputs = wallet.outputs + wallet.processTx(tx) + assert.deepEqual(wallet.outputs, outputs) + }) + }) + function assertEqual(obj1, obj2){ assert.equal(obj1.toString(), obj2.toString()) } From e574693594e78204506dd7ca4c88d633cf0405d5 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 21:19:50 +0800 Subject: [PATCH 09/31] wallet.outputs[0].output -> wallet.outputs[0].receive output is overloaded. Considering we have output.spend, output.receive makes sense to me --- src/wallet.js | 12 ++++++------ test/wallet.js | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index ec19147..484700f 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -76,14 +76,14 @@ var Wallet = function (seed, options) { utxo.forEach(function(uo){ validateUnspentOutput(uo) var o = unspentOutputToOutput(uo) - outputs[o.output] = o + outputs[o.receive] = o }) this.outputs = outputs } function outputToUnspentOutput(output){ - var hashAndIndex = output.output.split(":") + var hashAndIndex = output.receive.split(":") return { hash: hashAndIndex[0], @@ -99,7 +99,7 @@ var Wallet = function (seed, options) { var hash = o.hash || convert.reverseEndian(o.hashLittleEndian) var key = hash + ":" + o.outputIndex return { - output: key, + receive: key, scriptPubKey: o.scriptPubKey, address: o.address, value: o.value @@ -143,7 +143,7 @@ var Wallet = function (seed, options) { if (isMyAddress(address)) { var output = txhash+':'+i me.outputs[output] = { - output: output, + receive: output, value: txOut.value, address: address, scriptPubKey: convert.bytesToHex(txOut.script.buffer) //TODO: txOut.scriptPubKey() @@ -226,7 +226,7 @@ var Wallet = function (seed, options) { : [toOut, halfChangeOut, halfChangeOut] var tx = new Bitcoin.Transaction({ - ins: utxo.map(function(x) { return x.output }), + ins: utxo.map(function(x) { return x.receive }), outs: outs }) this.sign(tx) @@ -239,7 +239,7 @@ var Wallet = function (seed, options) { sum = utxo.reduce(function(t,p) { return t + o.value },0); utxo[changeIndex].value += sum - value - fee; var tx = new Bitcoin.Transaction({ - ins: utxo.map(function(x) { return x.output }), + ins: utxo.map(function(x) { return x.receive }), outs: outputs }) this.sign(tx) diff --git a/test/wallet.js b/test/wallet.js index a7161e2..6bd7a7a 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -168,7 +168,7 @@ describe('Wallet', function() { describe('getUnspentOutputs', function(){ it('parses wallet outputs to the expect format', function(){ wallet.outputs[expectedOutputKey] = { - output: expectedOutputKey, + receive: expectedOutputKey, scriptPubKey: expectedUtxo[0].scriptPubKey, address: expectedUtxo[0].address, value: expectedUtxo[0].value @@ -275,7 +275,7 @@ describe('Wallet', function() { var txOut = tx.outs[index] var key = convert.bytesToHex(tx.getHash()) + ":" + index var output = wallet.outputs[key] - assert.equal(output.output, key) + assert.equal(output.receive, key) assert.equal(output.value, txOut.value) assert.equal(output.address, txOut.address) assert.equal(output.scriptPubKey, convert.bytesToHex(txOut.script.buffer)) From b7d65fb75777a8b5a02d0223d1b0cf234d927256 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 23:48:21 +0800 Subject: [PATCH 10/31] Transaction is able to estimate fees --- src/transaction.js | 9 +++++++++ test/fixtures/mainnet_tx.json | 3 ++- test/transaction.js | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/transaction.js b/src/transaction.js index 3cbf5b0..1f14fa6 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -377,6 +377,15 @@ Transaction.prototype.validateSig = function(index, script, sig, pub) { convert.coerceToBytes(pub)); } +Transaction.feePerKb = 20000 +Transaction.prototype.estimateFee = function(feePerKb){ + var feePerKb = feePerKb || Transaction.feePerKb + var size = this.ins.length * 180 + this.outs.length * 34 + 10 + var sizeInKb = BigInteger.valueOf(Math.ceil(size / 1000)) + + return BigInteger.valueOf(feePerKb).multiply(sizeInKb).intValue() +} + var TransactionIn = function (data) { if (typeof data == "string") this.outpoint = { hash: data.split(':')[0], index: data.split(':')[1] } diff --git a/test/fixtures/mainnet_tx.json b/test/fixtures/mainnet_tx.json index f42bd7b..f78f876 100644 --- a/test/fixtures/mainnet_tx.json +++ b/test/fixtures/mainnet_tx.json @@ -1,4 +1,5 @@ { "prevTx": "0100000001e0214ebebb0fd3414d3fdc0dbf3b0f4b247a296cafc984558622c3041b0fcc9b010000008b48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8ffffffff03a0860100000000001976a91400ea3576c8fcb0bc8392f10e23a3425ae24efea888ac40420f00000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188acf07cd210000000001976a9146fb93c557ee62b109370fd9003e456917401cbfa88ac00000000", - "tx": "0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000" + "tx": "0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000", + "bigTx": "010000000ee7b73e229790c1e79a02f0c871813b3cf26a4156c5b8d942e88b38fe8d3f43a0000000008c493046022100fd3d8fef44fb0962ba3f07bee1d4cafb84e60e38e6c7d9274504b3638a8d2f520221009fce009044e615b6883d4bf62e04c48f9fe236e19d644b082b2f0ae5c98e045c014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff7bfc005f3880a606027c7cd7dd02a0f6a6572eeb84a91aa158311be13695a7ea010000008b483045022100e2e61c40f26e2510b76dc72ea2f568ec514fce185c719e18bca9caaef2b20e9e02207f1100fc79eb0584e970c7f18fb226f178951d481767b4092d50d13c50ccba8b014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff0e0f8e6bf951fbb84d7d8ef833a1cbf5bb046ea7251973ac6e7661c755386ee3010000008a473044022048f1611e403710f248f7caf479965a6a5f63cdfbd9a714fef4ec1b68331ade1d022074919e79376c363d4575b2fc21513d5949471703efebd4c5ca2885e810eb1fa4014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffe6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b000000008b483045022100886c07cad489dfcf4b364af561835d5cf985f07adf8bd1d5bd6ddea82b0ce6b2022045bdcbcc2b5fc55191bb997039cf59ff70e8515c56b62f293a9add770ba26738014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffe6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b010000008a4730440220535d49b819fdf294d27d82aff2865ed4e18580f0ca9796d793f611cb43a44f47022019584d5e300c415f642e37ba2a814a1e1106b4a9b91dc2a30fb57ceafe041181014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffd3051677216ea53baa2e6d7f6a75434ac338438c59f314801c8496d1e6d1bf6d010000008b483045022100bf612b0fa46f49e70ab318ca3458d1ed5f59727aa782f7fac5503f54d9b43a590220358d7ed0e3cee63a5a7e972d9fad41f825d95de2fd0c5560382468610848d489014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff1e751ccc4e7d973201e9174ec78ece050ef2fadd6a108f40f76a9fa314979c31010000008b483045022006e263d5f73e05c48a603e3bd236e8314e5420721d5e9020114b93e8c9220e1102210099d3dead22f4a792123347a238c87e67b55b28a94a0bb7793144cc7ad94a0168014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff25c4cf2c61743b3f4252d921d937cca942cf32e4f3fa4a544d0b26f014337084010000008a47304402207d6e87588be47bf2d97eaf427bdd992e9d6b306255711328aee38533366a88b50220623099595ae442cb77eaddb3f91753a4fc9df56fde69cfec584c7f97e05533c8014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffecd93c87eb43c48481e6694904305349bdea94b01104579fa9f02bff66c89663010000008a473044022020f59498aee0cf82cb113768ef3cb721000346d381ff439adb4d405f791252510220448de723aa59412266fabbc689ec25dc94b1688c27a614982047513a80173514014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffa1fdc0a79ff98d5b6154176e321c22f4f8450dbd950bd013ad31135f5604411e010000008b48304502210088167867f87327f9c0db0444267ff0b6a026eedd629d8f16fe44a34c18e706bf0220675c8baebf89930e2d6e4463adefc50922653af99375242e38f5ee677418738a014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffb89e8249c3573b58bf1ec7433185452dd57ab8e1daab01c3cc6ddc8b66ad3de8000000008b4830450220073d50ac5ec8388d5b3906921f9368c31ad078c8e1fb72f26d36b533f35ee327022100c398b23e6692e11dca8a1b64aae2ff70c6a781ed5ee99181b56a2f583a967cd4014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff45ee07e182084454dacfad1e61b04ffdf9c7b01003060a6c841a01f4fff8a5a0010000008b483045022100991d1bf60c41358f08b20e53718a24e05ac0608915df4f6305a5b47cb61e5da7022003f14fc1cc5b737e2c3279a4f9be1852b49dbb3d9d6cc4c8af6a666f600dced8014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff4cba12549f1d70f8e60aea8b546c8357f7c099e7c7d9d8691d6ee16e7dfa3170010000008c493046022100f14e2b0ef8a8e206db350413d204bc0a5cd779e556b1191c2d30b5ec023cde6f022100b90b2d2bf256c98a88f7c3a653b93cec7d25bb6a517db9087d11dbd189e8851c014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffa4b3aed39eb2a1dc6eae4609d9909724e211c153927c230d02bd33add3026959010000008b483045022100a8cebb4f1c58f5ba1af91cb8bd4a2ed4e684e9605f5a9dc8b432ed00922d289d0220251145d2d56f06d936fd0c51fa884b4a6a5fafd0c3318f72fb05a5c9aa372195014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff0240d52303000000001976a914167c3e1f10cc3b691c73afbdb211e156e3e3f25c88ac15462e00000000001976a914290f7d617b75993e770e5606335fa0999a28d71388ac00000000" } diff --git a/test/transaction.js b/test/transaction.js index 8a67b78..636f124 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -2,6 +2,10 @@ var Transaction = require('../src/transaction').Transaction var convert = require('../src/convert') var ECKey = require('../src/eckey').ECKey var assert = require('assert') +var fixtureTxes = require('./fixtures/mainnet_tx') +var fixtureTx1Hex = fixtureTxes.prevTx +var fixtureTx2Hex = fixtureTxes.tx +var fixtureTxBigHex = fixtureTxes.bigTx describe('Transaction', function() { describe('deserialize', function() { @@ -170,6 +174,22 @@ describe('Transaction', function() { }) }) + describe('estimateFee', function(){ + it('works for fixture tx 1', function(){ + var tx = Transaction.deserialize(fixtureTx1Hex) + assert.equal(tx.estimateFee(), 20000) + }) + + it('works for fixture big tx', function(){ + var tx = Transaction.deserialize(fixtureTxBigHex) + assert.equal(tx.estimateFee(), 60000) + }) + + it('allow feePerKb to be passed in as an argument', function(){ + var tx = Transaction.deserialize(fixtureTx2Hex) + assert.equal(tx.estimateFee(10000), 10000) + }) + }) }) }) From eb9e98aa7ba647a2087a43f493d8387bdda0d6c8 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 22 Mar 2014 23:49:17 +0800 Subject: [PATCH 11/31] minor refactoring: Transaction tests use fixtures --- test/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/transaction.js b/test/transaction.js index 636f124..d09af8b 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -69,7 +69,7 @@ describe('Transaction', function() { describe('creating a transaction', function() { var tx, prevTx beforeEach(function() { - prevTx = Transaction.deserialize('0100000001e0214ebebb0fd3414d3fdc0dbf3b0f4b247a296cafc984558622c3041b0fcc9b010000008b48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8ffffffff03a0860100000000001976a91400ea3576c8fcb0bc8392f10e23a3425ae24efea888ac40420f00000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188acf07cd210000000001976a9146fb93c557ee62b109370fd9003e456917401cbfa88ac00000000') + prevTx = Transaction.deserialize(fixtureTx1Hex) tx = new Transaction() }) @@ -161,7 +161,7 @@ describe('Transaction', function() { var validTx beforeEach(function() { - validTx = Transaction.deserialize('0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000') + validTx = Transaction.deserialize(fixtureTx2Hex) }) it('returns true for valid signature', function(){ From 5d79b094d47d52ec8873124b108d9050f8fb81b9 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sun, 23 Mar 2014 20:16:27 +0800 Subject: [PATCH 12/31] remove processOutput and processExistingOutput instead of processOutput, use processTx instead processExistingOutput is time based which is not reliable --- src/wallet.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 484700f..1885e00 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -164,28 +164,6 @@ var Wallet = function (seed, options) { }) } - // Processes an output from an external source of the form - // { output: txhash:index, value: integer, address: address } - // Excellent compatibility with SX and pybitcointools - this.processOutput = function(o) { - if (!this.outputs[o.output] || this.outputs[o.output].pending) - this.outputs[o.output] = o; - } - - this.processExistingOutputs = function() { - var t = new Date().getTime() / 1000 - for (var o in this.outputs) { - if (o.pending && t > o.timestamp + 1200) - delete this.outputs[o] - if (o.spendpending && t > o.timestamp + 1200) { - o.spendpending = false - o.spend = false - delete o.timestamp - } - } - } - var peoInterval = setInterval(this.processExistingOutputs, 10000) - this.getUtxoToPay = function(value) { var h = [] for (var out in this.outputs) h.push(this.outputs[out]) From 3d12d3b03889a3d359f127d8af210a28cc9aaf8b Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sun, 23 Mar 2014 21:34:52 +0800 Subject: [PATCH 13/31] implement and use txOut.scriptPubKey --- src/transaction.js | 7 +++++-- src/wallet.js | 2 +- test/transaction.js | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index 1f14fa6..7401c54 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -433,8 +433,7 @@ var TransactionOut = function (data) { : data.value; }; -TransactionOut.prototype.clone = function () -{ +TransactionOut.prototype.clone = function() { var newTxout = new TransactionOut({ script: this.script.clone(), value: this.value @@ -442,6 +441,10 @@ TransactionOut.prototype.clone = function () return newTxout; }; +TransactionOut.prototype.scriptPubKey = function() { + return convert.bytesToHex(this.script.buffer) +} + module.exports = { Transaction: Transaction, TransactionIn: TransactionIn, diff --git a/src/wallet.js b/src/wallet.js index 1885e00..5475957 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -146,7 +146,7 @@ var Wallet = function (seed, options) { receive: output, value: txOut.value, address: address, - scriptPubKey: convert.bytesToHex(txOut.script.buffer) //TODO: txOut.scriptPubKey() + scriptPubKey: txOut.scriptPubKey() // timestamp: new Date().getTime() / 1000, // pending: true } diff --git a/test/transaction.js b/test/transaction.js index d09af8b..41bcea2 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -1,7 +1,12 @@ -var Transaction = require('../src/transaction').Transaction +var T = require('../src/transaction') +var Transaction = T.Transaction +var TransactionOut = T.TransactionOut + var convert = require('../src/convert') var ECKey = require('../src/eckey').ECKey +var Script = require('../src/script') var assert = require('assert') + var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx var fixtureTx2Hex = fixtureTxes.tx @@ -192,5 +197,17 @@ describe('Transaction', function() { }) }) + describe('TransactionOut', function() { + describe('scriptPubKey', function() { + it('returns hex string', function() { + var txOut = new TransactionOut({ + value: 50000, + script: Script.createOutputScript("1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv") + }) + + assert.equal(txOut.scriptPubKey(), "76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac") + }) + }) + }) }) From 2dc0f69d000b77d049e3ebb57362062d67174577 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 00:19:46 +0800 Subject: [PATCH 14/31] createTX returns tx with expected inputs and outputs --- src/wallet.js | 48 ++++++++++++++++++++ test/wallet.js | 116 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 3 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 5475957..5c153cc 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -164,6 +164,54 @@ var Wallet = function (seed, options) { }) } + function getCandidateOutputs(value){ + var unspent = [] + for (var key in me.outputs){ + var output = me.outputs[key] + if(!output.value.spend) unspent.push(output) + } + + var sortByValueDesc = unspent.sort(function(o1, o2){ + return o2.value - o1.value + }) + + return sortByValueDesc; + } + + this.createTx = function(to, value, fixedFee) { + var tx = new Transaction() + tx.addOutput(to, value) + + var utxo = getCandidateOutputs(value) + var totalInValue = 0 + for(var i=0; i 0) { + tx.addOutput(changeAddress, change) + // TODO: recalculate fee + } + break; + } + + // TODO: sign tx + return tx + } + + function getChangeAddress() { + if(me.changeAddresses.length === 0) me.generateChangeAddress() + return me.changeAddresses[me.changeAddresses.length - 1] + } + this.getUtxoToPay = function(value) { var h = [] for (var out in this.outputs) h.push(this.outputs[out]) diff --git a/test/wallet.js b/test/wallet.js index 6bd7a7a..841d1cf 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -1,6 +1,9 @@ var Wallet = require('../src/wallet.js') var HDNode = require('../src/hdwallet.js') -var Transaction = require('../src/transaction.js').Transaction +var T = require('../src/transaction.js') +var Transaction = T.Transaction +var TransactionOut = T.TransactionOut +var Script = require('../src/script.js') var convert = require('../src/convert.js') var assert = require('assert') var SHA256 = require('crypto-js/sha256') @@ -158,7 +161,7 @@ describe('Wallet', function() { "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a", "outputIndex": 0, "scriptPubKey":"76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac", - "address" : "1azpkpcfczkduetfbqul4mokqai3m3hmxv", + "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", "value": 20000 } ] @@ -284,7 +287,7 @@ describe('Wallet', function() { describe("when tx ins outpoint contains a known txhash:i, the corresponding 'output' gets updated", function(){ beforeEach(function(){ - wallet.addresses = [tx.outs[0].address.toString()] // the address fixtureTx2 is spending from + wallet.addresses = [tx.outs[0].address.toString()] // the address fixtureTx2 used as input wallet.processTx(tx) tx = Transaction.deserialize(fixtureTx2Hex) @@ -314,6 +317,113 @@ describe('Wallet', function() { }) }) + describe('createTx', function(){ + var to, value, prevTx; + + beforeEach(function(){ + to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' + value = 500000 + + // generate 2 addresses + wallet.generateAddress() + wallet.generateAddress() + + // set up 3 utxo + utxo = [ + { + "hash": fakeTxHash(1), + "outputIndex": 0, + "scriptPubKey": scriptPubKeyFor(wallet.addresses[0], 300000), + "address" : wallet.addresses[0], + "value": 400000 // not enough for value + }, + { + "hash": fakeTxHash(2), + "outputIndex": 1, + "scriptPubKey": scriptPubKeyFor(wallet.addresses[0], 500000), + "address" : wallet.addresses[0], + "value": 500000 // enough for only value + }, + { + "hash": fakeTxHash(3), + "outputIndex": 0, + "scriptPubKey": scriptPubKeyFor(wallet.addresses[1], 520000), + "address" : wallet.addresses[1], + "value": 520000 // enough for value and fee + } + ] + wallet.setUnspentOutputs(utxo) + + function scriptPubKeyFor(address, value){ + var txOut = new TransactionOut({ + value: value, + script: Script.createOutputScript(address) + }) + + return txOut.scriptPubKey() + } + }) + + function fakeTxHash(i) { + return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i + } + + describe('choosing utxo', function(){ + it('calculates fees', function(){ + var tx = wallet.createTx(to, value) + + assert.equal(tx.ins.length, 1) + assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) + }) + + it('allows fee to be specified', function(){ + var fee = 30000 + var tx = wallet.createTx(to, value, fee) + + assert.equal(tx.ins.length, 2) + assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) + assert.deepEqual(tx.ins[1].outpoint, { hash: fakeTxHash(2), index: 1 }) + }) + }) + + 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] + assert.equal(out.address, to) + assert.equal(out.value, value) + }) + + describe('change', function(){ + it('uses the last change address if there is any', function(){ + var fee = 15000 + wallet.generateChangeAddress() + wallet.generateChangeAddress() + var tx = wallet.createTx(to, value, fee) + + assert.equal(tx.outs.length, 2) + var out = tx.outs[1] + assert.equal(out.address, wallet.changeAddresses[1]) + assert.equal(out.value, 5000) + }) + + it('generates a change address if there is not any', function(){ + var fee = 15000 + assert.equal(wallet.changeAddresses.length, 0) + + var tx = wallet.createTx(to, value, fee) + + assert.equal(wallet.changeAddresses.length, 1) + var out = tx.outs[1] + assert.equal(out.address, wallet.changeAddresses[0]) + assert.equal(out.value, 5000) + }) + }) + }) + }) + function assertEqual(obj1, obj2){ assert.equal(obj1.toString(), obj2.toString()) } From 8f0413da987dfb12cc3eb45133828fab9dde585a Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 01:01:50 +0800 Subject: [PATCH 15/31] always assume change output exists when estimating fee --- src/wallet.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 5c153cc..e3180cc 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -191,14 +191,12 @@ var Wallet = function (seed, options) { totalInValue += output.value if(totalInValue < value) continue; - var fee = fixedFee || tx.estimateFee() + var fee = fixedFee || estimateFeePadChangeOutput(tx) if(totalInValue < value + fee) continue; var change = totalInValue - value - fee - var changeAddress = getChangeAddress() if(change > 0) { - tx.addOutput(changeAddress, change) - // TODO: recalculate fee + tx.addOutput(getChangeAddress(), change) } break; } @@ -207,6 +205,12 @@ var Wallet = function (seed, options) { return tx } + function estimateFeePadChangeOutput(tx){ + var tmpTx = tx.clone() + tmpTx.addOutput(getChangeAddress(), 0) + return tmpTx.estimateFee() + } + function getChangeAddress() { if(me.changeAddresses.length === 0) me.generateChangeAddress() return me.changeAddresses[me.changeAddresses.length - 1] From 913b48e87fede1f41948cfd295952f5aa05daf4b Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 01:29:10 +0800 Subject: [PATCH 16/31] sign tx --- package.json | 3 ++- src/wallet.js | 3 ++- test/wallet.js | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 36d1f36..159e326 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "mocha": "1.18.2", "istanbul": "0.1.30", "uglify-js": "2.4.13", - "node-browserify": "https://github.com/substack/node-browserify/tarball/master" + "node-browserify": "https://github.com/substack/node-browserify/tarball/master", + "sinon": "^1.9.0" }, "testling": { "browsers": [ diff --git a/src/wallet.js b/src/wallet.js index e3180cc..87df650 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -201,7 +201,8 @@ var Wallet = function (seed, options) { break; } - // TODO: sign tx + this.sign(tx) + return tx } diff --git a/test/wallet.js b/test/wallet.js index 841d1cf..dbee547 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -13,6 +13,8 @@ var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx var fixtureTx2Hex = fixtureTxes.tx +var sinon = require('sinon') + describe('Wallet', function() { var seed, wallet; beforeEach(function(){ @@ -318,37 +320,38 @@ describe('Wallet', function() { }) describe('createTx', function(){ - var to, value, prevTx; + var to, value; + var address1, address2; beforeEach(function(){ to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' value = 500000 // generate 2 addresses - wallet.generateAddress() - wallet.generateAddress() + address1 = wallet.generateAddress() + address2 = wallet.generateAddress() // set up 3 utxo utxo = [ { "hash": fakeTxHash(1), "outputIndex": 0, - "scriptPubKey": scriptPubKeyFor(wallet.addresses[0], 300000), - "address" : wallet.addresses[0], + "scriptPubKey": scriptPubKeyFor(address1, 300000), + "address" : address1, "value": 400000 // not enough for value }, { "hash": fakeTxHash(2), "outputIndex": 1, - "scriptPubKey": scriptPubKeyFor(wallet.addresses[0], 500000), - "address" : wallet.addresses[0], + "scriptPubKey": scriptPubKeyFor(address1, 500000), + "address" : address1, "value": 500000 // enough for only value }, { "hash": fakeTxHash(3), "outputIndex": 0, - "scriptPubKey": scriptPubKeyFor(wallet.addresses[1], 520000), - "address" : wallet.addresses[1], + "scriptPubKey": scriptPubKeyFor(address2, 520000), + "address" : address2, "value": 520000 // enough for value and fee } ] @@ -422,6 +425,22 @@ describe('Wallet', function() { }) }) }) + + describe('signing', function(){ + afterEach(function(){ + Transaction.prototype.sign.restore() + }) + + it('signes the inputs with respective keys', function(){ + var fee = 30000 + sinon.stub(Transaction.prototype, "sign") + + var tx = wallet.createTx(to, value, fee) + + assert(Transaction.prototype.sign.calledWith(0, wallet.getPrivateKeyForAddress(address2))) + assert(Transaction.prototype.sign.calledWith(1, wallet.getPrivateKeyForAddress(address1))) + }) + }) }) function assertEqual(obj1, obj2){ From 7c81bfef724aba6383eb9aa807fdfc8f07b93b7e Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 01:42:36 +0800 Subject: [PATCH 17/31] check for dust before creating transaction --- src/wallet.js | 8 ++++++++ test/wallet.js | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 87df650..5607c54 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -179,6 +179,8 @@ var Wallet = function (seed, options) { } this.createTx = function(to, value, fixedFee) { + checkDust(value) + var tx = new Transaction() tx.addOutput(to, value) @@ -206,6 +208,12 @@ var Wallet = function (seed, options) { return tx } + function checkDust(value){ + if (isNullOrUndefined(value) || value < 5430) { + throw new Error("Value below dust threshold") + } + } + function estimateFeePadChangeOutput(tx){ var tmpTx = tx.clone() tmpTx.addOutput(getChangeAddress(), 0) diff --git a/test/wallet.js b/test/wallet.js index dbee547..97b6e66 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -367,10 +367,6 @@ describe('Wallet', function() { } }) - function fakeTxHash(i) { - return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i - } - describe('choosing utxo', function(){ it('calculates fees', function(){ var tx = wallet.createTx(to, value) @@ -441,6 +437,20 @@ describe('Wallet', function() { assert(Transaction.prototype.sign.calledWith(1, wallet.getPrivateKeyForAddress(address1))) }) }) + + describe('when value is below dust threshold', function(){ + it('throws an error', function(){ + var value = 5429 + + assert.throws(function() { + wallet.createTx(to, value) + }, Error, 'Value below dust threshold') + }) + }) + + function fakeTxHash(i) { + return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i + } }) function assertEqual(obj1, obj2){ From 90921798cff549bfb1b06893f635a85948b7b36c Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 02:19:17 +0800 Subject: [PATCH 18/31] throws error on insufficient fund --- src/wallet.js | 9 +++++++++ test/wallet.js | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/wallet.js b/src/wallet.js index 5607c54..4748099 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -203,6 +203,8 @@ var Wallet = function (seed, options) { break; } + checkInsufficientFund(totalInValue, value, fee) + this.sign(tx) return tx @@ -214,6 +216,13 @@ var Wallet = function (seed, options) { } } + function checkInsufficientFund(totalInValue, value, fee) { + if(totalInValue < value + fee) { + throw new Error('Not enough money to send funds including transaction fee. Have: ' + + totalInValue + ', needed: ' + (value + fee)) + } + } + function estimateFeePadChangeOutput(tx){ var tmpTx = tx.clone() tmpTx.addOutput(getChangeAddress(), 0) diff --git a/test/wallet.js b/test/wallet.js index 97b6e66..4885931 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -448,6 +448,16 @@ describe('Wallet', function() { }) }) + describe('when there is not enough money', function(){ + it('throws an error', function(){ + var value = 1400001 + + assert.throws(function() { + wallet.createTx(to, value) + }, Error, 'Not enough money to send funds including transaction fee. Have: 1420000, needed: 1420001') + }) + }) + function fakeTxHash(i) { return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i } From 471bc7ed97158bb92e7ad56e1f3302326efd90a0 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 02:42:41 +0800 Subject: [PATCH 19/31] skip change if it is not above dust threshold --- src/wallet.js | 11 ++++++++--- test/wallet.js | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 4748099..b558c2a 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -197,7 +197,7 @@ var Wallet = function (seed, options) { if(totalInValue < value + fee) continue; var change = totalInValue - value - fee - if(change > 0) { + if(change > 0 && !isDust(change)) { tx.addOutput(getChangeAddress(), change) } break; @@ -210,9 +210,14 @@ var Wallet = function (seed, options) { return tx } + this.dustThreshold = 5430 + function isDust(amount) { + return amount <= me.dustThreshold + } + function checkDust(value){ - if (isNullOrUndefined(value) || value < 5430) { - throw new Error("Value below dust threshold") + if (isNullOrUndefined(value) || isDust(value)) { + throw new Error("Value must be above dust threshold") } } diff --git a/test/wallet.js b/test/wallet.js index 4885931..4922cdb 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -397,7 +397,7 @@ describe('Wallet', function() { describe('change', function(){ it('uses the last change address if there is any', function(){ - var fee = 15000 + var fee = 5000 wallet.generateChangeAddress() wallet.generateChangeAddress() var tx = wallet.createTx(to, value, fee) @@ -405,11 +405,11 @@ describe('Wallet', function() { assert.equal(tx.outs.length, 2) var out = tx.outs[1] assert.equal(out.address, wallet.changeAddresses[1]) - assert.equal(out.value, 5000) + assert.equal(out.value, 15000) }) it('generates a change address if there is not any', function(){ - var fee = 15000 + var fee = 5000 assert.equal(wallet.changeAddresses.length, 0) var tx = wallet.createTx(to, value, fee) @@ -417,7 +417,13 @@ describe('Wallet', function() { assert.equal(wallet.changeAddresses.length, 1) var out = tx.outs[1] assert.equal(out.address, wallet.changeAddresses[0]) - assert.equal(out.value, 5000) + assert.equal(out.value, 15000) + }) + + it('skips change if it is not above dust threshold', function(){ + var fee = 14570 + var tx = wallet.createTx(to, value) + assert.equal(tx.outs.length, 1) }) }) }) @@ -440,11 +446,11 @@ describe('Wallet', function() { describe('when value is below dust threshold', function(){ it('throws an error', function(){ - var value = 5429 + var value = 5430 assert.throws(function() { wallet.createTx(to, value) - }, Error, 'Value below dust threshold') + }, /Value must be above dust threshold/) }) }) From 2501868f5283d5d7c971383301c114a31b454681 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 02:49:40 +0800 Subject: [PATCH 20/31] remove old methods --- src/wallet.js | 96 +++++++++------------------------------------------ 1 file changed, 16 insertions(+), 80 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index b558c2a..93c4471 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -147,8 +147,6 @@ var Wallet = function (seed, options) { value: txOut.value, address: address, scriptPubKey: txOut.scriptPubKey() - // timestamp: new Date().getTime() / 1000, - // pending: true } } }) @@ -158,26 +156,10 @@ var Wallet = function (seed, options) { var o = me.outputs[op.hash+':'+op.index] if (o) { o.spend = txhash+':'+i - // o.spendpending = true - // o.timestamp = new Date().getTime() / 1000 } }) } - function getCandidateOutputs(value){ - var unspent = [] - for (var key in me.outputs){ - var output = me.outputs[key] - if(!output.value.spend) unspent.push(output) - } - - var sortByValueDesc = unspent.sort(function(o1, o2){ - return o2.value - o1.value - }) - - return sortByValueDesc; - } - this.createTx = function(to, value, fixedFee) { checkDust(value) @@ -221,11 +203,18 @@ var Wallet = function (seed, options) { } } - function checkInsufficientFund(totalInValue, value, fee) { - if(totalInValue < value + fee) { - throw new Error('Not enough money to send funds including transaction fee. Have: ' + - totalInValue + ', needed: ' + (value + fee)) + function getCandidateOutputs(value){ + var unspent = [] + for (var key in me.outputs){ + var output = me.outputs[key] + if(!output.value.spend) unspent.push(output) } + + var sortByValueDesc = unspent.sort(function(o1, o2){ + return o2.value - o1.value + }) + + return sortByValueDesc; } function estimateFeePadChangeOutput(tx){ @@ -239,64 +228,11 @@ var Wallet = function (seed, options) { return me.changeAddresses[me.changeAddresses.length - 1] } - this.getUtxoToPay = function(value) { - var h = [] - for (var out in this.outputs) h.push(this.outputs[out]) - var utxo = h.filter(function(x) { return !x.spend }); - var valuecompare = function(a,b) { return a.value > b.value; } - var high = utxo.filter(function(o) { return o.value >= value; }) - .sort(valuecompare); - if (high.length > 0) return [high[0]]; - utxo.sort(valuecompare); - var totalval = 0; - for (var i = 0; i < utxo.length; i++) { - totalval += utxo[i].value; - if (totalval >= value) return utxo.slice(0,i+1); - } - throw ("Not enough money to send funds including transaction fee. Have: " - + (totalval / 100000000) + ", needed: " + (value / 100000000)); - } - - this.mkSend = function(to, value, fee) { - var utxo = this.getUtxoToPay(value + fee) - var sum = utxo.reduce(function(t,o) { return t + o.value },0), - remainder = sum - value - fee - if (value < 5430) throw new Error("Amount below dust threshold!") - var unspentOuts = 0; - for (var o in this.outputs) { - if (!this.outputs[o].spend) unspentOuts += 1 - if (unspentOuts >= 5) return - } - var change = this.addresses[this.addresses.length - 1] - var toOut = { address: to, value: value }, - changeOut = { address: change, value: remainder } - halfChangeOut = { address: change, value: Math.floor(remainder/2) }; - - var outs = - remainder < 5430 ? [toOut] - : remainder < 10860 ? [toOut, changeOut] - : unspentOuts == 5 ? [toOut, changeOut] - : [toOut, halfChangeOut, halfChangeOut] - - var tx = new Bitcoin.Transaction({ - ins: utxo.map(function(x) { return x.receive }), - outs: outs - }) - this.sign(tx) - return tx - } - - this.mkSendToOutputs = function(outputs, changeIndex, fee) { - var value = outputs.reduce(function(t,o) { return t + o.value },0), - utxo = this.getUtxoToPay(value + fee), - sum = utxo.reduce(function(t,p) { return t + o.value },0); - utxo[changeIndex].value += sum - value - fee; - var tx = new Bitcoin.Transaction({ - ins: utxo.map(function(x) { return x.receive }), - outs: outputs - }) - this.sign(tx) - return tx + function checkInsufficientFund(totalInValue, value, fee) { + if(totalInValue < value + fee) { + throw new Error('Not enough money to send funds including transaction fee. Have: ' + + totalInValue + ', needed: ' + (value + fee)) + } } this.sign = function(tx) { From 756e8771818c9bbbd838090bccb3b6aab52eb500 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Mon, 24 Mar 2014 02:55:13 +0800 Subject: [PATCH 21/31] lock down sinon version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 159e326..be6a161 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "istanbul": "0.1.30", "uglify-js": "2.4.13", "node-browserify": "https://github.com/substack/node-browserify/tarball/master", - "sinon": "^1.9.0" + "sinon": "1.9.0" }, "testling": { "browsers": [ From 77b5d1ee214d50cffb30421ff9eb8c85e8c91332 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 10:35:33 +0800 Subject: [PATCH 22/31] getCandidateOuputs ignores spent outputs --- src/wallet.js | 2 +- test/wallet.js | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 93c4471..43ad691 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -207,7 +207,7 @@ var Wallet = function (seed, options) { var unspent = [] for (var key in me.outputs){ var output = me.outputs[key] - if(!output.value.spend) unspent.push(output) + if(!output.spend) unspent.push(output) } var sortByValueDesc = unspent.sort(function(o1, o2){ diff --git a/test/wallet.js b/test/wallet.js index 4922cdb..80074e1 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -356,15 +356,6 @@ describe('Wallet', function() { } ] wallet.setUnspentOutputs(utxo) - - function scriptPubKeyFor(address, value){ - var txOut = new TransactionOut({ - value: value, - script: Script.createOutputScript(address) - }) - - return txOut.scriptPubKey() - } }) describe('choosing utxo', function(){ @@ -383,6 +374,25 @@ describe('Wallet', function() { assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) assert.deepEqual(tx.ins[1].outpoint, { hash: fakeTxHash(2), index: 1 }) }) + + it('ignores spent outputs', function(){ + utxo.push( + { + "hash": fakeTxHash(4), + "outputIndex": 0, + "scriptPubKey": scriptPubKeyFor(address2, 520000), + "address" : address2, + "value": 530000 // enough but spent before createTx + } + ) + wallet.setUnspentOutputs(utxo) + wallet.outputs[fakeTxHash(4) + ":" + 0].spend = fakeTxHash(5) + ":" + 0 + + var tx = wallet.createTx(to, value) + + assert.equal(tx.ins.length, 1) + assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) + }) }) describe('transaction outputs', function(){ @@ -467,6 +477,15 @@ describe('Wallet', function() { function fakeTxHash(i) { return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i } + + function scriptPubKeyFor(address, value){ + var txOut = new TransactionOut({ + value: value, + script: Script.createOutputScript(address) + }) + + return txOut.scriptPubKey() + } }) function assertEqual(obj1, obj2){ From 9fc5505730233ebaf71fafe28b0e8c620363521c Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 10:41:03 +0800 Subject: [PATCH 23/31] getUnspentOutputs excludes spent outputs --- src/wallet.js | 3 ++- test/wallet.js | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 43ad691..37e7187 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -64,7 +64,8 @@ var Wallet = function (seed, options) { var utxo = [] for(var key in this.outputs){ - utxo.push(outputToUnspentOutput(this.outputs[key])) + var output = this.outputs[key] + if(!output.spend) utxo.push(outputToUnspentOutput(output)) } return utxo diff --git a/test/wallet.js b/test/wallet.js index 80074e1..5f3996e 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -171,16 +171,23 @@ describe('Wallet', function() { }) describe('getUnspentOutputs', function(){ - it('parses wallet outputs to the expect format', function(){ + beforeEach(function(){ wallet.outputs[expectedOutputKey] = { receive: expectedOutputKey, scriptPubKey: expectedUtxo[0].scriptPubKey, address: expectedUtxo[0].address, value: expectedUtxo[0].value } + }) + it('parses wallet outputs to the expect format', function(){ assert.deepEqual(wallet.getUnspentOutputs(), expectedUtxo) }) + + it('excludes spent outputs', function(){ + wallet.outputs[expectedOutputKey].spend = "sometxn:m" + assert.deepEqual(wallet.getUnspentOutputs(), []) + }) }) describe('setUnspentOutputs', function(){ From 58ab0b631efbe03c7418820381b0371af82a7ecd Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 11:04:27 +0800 Subject: [PATCH 24/31] getBalance returns total unspent --- src/wallet.js | 6 +++++ test/wallet.js | 65 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 37e7187..51609ca 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -60,6 +60,12 @@ var Wallet = function (seed, options) { 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 = [] diff --git a/test/wallet.js b/test/wallet.js index 5f3996e..a495f62 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -157,31 +157,58 @@ describe('Wallet', function() { describe('Unspent Outputs', function(){ var expectedUtxo, expectedOutputKey; beforeEach(function(){ - expectedUtxo = [ - { - "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7", - "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a", - "outputIndex": 0, - "scriptPubKey":"76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac", - "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", - "value": 20000 - } - ] - expectedOutputKey = expectedUtxo[0].hash + ":" + expectedUtxo[0].outputIndex + expectedUtxo = { + "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7", + "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a", + "outputIndex": 0, + "scriptPubKey":"76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac", + "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", + "value": 20000 + } + expectedOutputKey = expectedUtxo.hash + ":" + expectedUtxo.outputIndex + }) + + function addUtxoToOutput(utxo){ + var key = utxo.hash + ":" + utxo.outputIndex + wallet.outputs[key] = { + receive: key, + scriptPubKey: utxo.scriptPubKey, + address: utxo.address, + value: utxo.value + } + } + + describe('getBalance', function(){ + var utxo1 + + beforeEach(function(){ + utxo1 = cloneObject(expectedUtxo) + utxo1.hash = utxo1.hash.replace('7', 'l') + }) + + it('sums over utxo values', function(){ + addUtxoToOutput(expectedUtxo) + addUtxoToOutput(utxo1) + + assert.deepEqual(wallet.getBalance(), 40000) + }) + + it('excludes spent outputs', function(){ + addUtxoToOutput(expectedUtxo) + addUtxoToOutput(utxo1) + wallet.outputs[utxo1.hash + ':' + utxo1.outputIndex].spend = "sometxn:m" + + assert.deepEqual(wallet.getBalance(), 20000) + }) }) describe('getUnspentOutputs', function(){ beforeEach(function(){ - wallet.outputs[expectedOutputKey] = { - receive: expectedOutputKey, - scriptPubKey: expectedUtxo[0].scriptPubKey, - address: expectedUtxo[0].address, - value: expectedUtxo[0].value - } + addUtxoToOutput(expectedUtxo) }) it('parses wallet outputs to the expect format', function(){ - assert.deepEqual(wallet.getUnspentOutputs(), expectedUtxo) + assert.deepEqual(wallet.getUnspentOutputs(), [expectedUtxo]) }) it('excludes spent outputs', function(){ @@ -193,7 +220,7 @@ describe('Wallet', function() { describe('setUnspentOutputs', function(){ var utxo; beforeEach(function(){ - utxo = cloneObject(expectedUtxo) + utxo = cloneObject([expectedUtxo]) }) it('uses hashLittleEndian when hash is not present', function(){ From 7bd312de71a010a7d4474591cefbfbd41e850554 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 11:21:23 +0800 Subject: [PATCH 25/31] get rid of magic numbers in fee estimation --- src/transaction.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/transaction.js b/src/transaction.js index 7401c54..6059fc5 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -379,8 +379,12 @@ Transaction.prototype.validateSig = function(index, script, sig, pub) { Transaction.feePerKb = 20000 Transaction.prototype.estimateFee = function(feePerKb){ + var uncompressedInSize = 180 + var outSize = 34 + var fixedPadding = 34 var feePerKb = feePerKb || Transaction.feePerKb - var size = this.ins.length * 180 + this.outs.length * 34 + 10 + + var size = this.ins.length * uncompressedInSize + this.outs.length * outSize + fixedPadding var sizeInKb = BigInteger.valueOf(Math.ceil(size / 1000)) return BigInteger.valueOf(feePerKb).multiply(sizeInKb).intValue() From 7bfa6ab9d256c962af4afb5ae0abf9cc98519167 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 12:25:25 +0800 Subject: [PATCH 26/31] remove scriptPubKey from wallet.outputs, set/get unspent outputs as it is unused --- src/wallet.js | 5 +---- test/wallet.js | 23 +++-------------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index 51609ca..bb1718f 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -96,7 +96,6 @@ var Wallet = function (seed, options) { hash: hashAndIndex[0], hashLittleEndian: convert.reverseEndian(hashAndIndex[0]), outputIndex: parseInt(hashAndIndex[1]), - scriptPubKey: output.scriptPubKey, address: output.address, value: output.value } @@ -107,7 +106,6 @@ var Wallet = function (seed, options) { var key = hash + ":" + o.outputIndex return { receive: key, - scriptPubKey: o.scriptPubKey, address: o.address, value: o.value } @@ -120,7 +118,7 @@ var Wallet = function (seed, options) { missingField = "hash(or hashLittleEndian)" } - var requiredKeys = ['outputIndex', 'scriptPubKey', 'address', 'value'] + var requiredKeys = ['outputIndex', 'address', 'value'] requiredKeys.forEach(function(key){ if(isNullOrUndefined(uo[key])){ missingField = key @@ -153,7 +151,6 @@ var Wallet = function (seed, options) { receive: output, value: txOut.value, address: address, - scriptPubKey: txOut.scriptPubKey() } } }) diff --git a/test/wallet.js b/test/wallet.js index a495f62..6f8d558 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -161,7 +161,6 @@ describe('Wallet', function() { "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7", "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a", "outputIndex": 0, - "scriptPubKey":"76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac", "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", "value": 20000 } @@ -172,7 +171,6 @@ describe('Wallet', function() { var key = utxo.hash + ":" + utxo.outputIndex wallet.outputs[key] = { receive: key, - scriptPubKey: utxo.scriptPubKey, address: utxo.address, value: utxo.value } @@ -248,18 +246,18 @@ describe('Wallet', function() { delete utxo[0]['hashLittleEndian'] var errorMessage = 'Invalid unspent output: key hash(or hashLittleEndian) is missing. ' + - 'A valid unspent output must contain outputIndex, scriptPubKey, address, value and hash(or hashLittleEndian)' + 'A valid unspent output must contain outputIndex, address, value and hash(or hashLittleEndian)' assert.throws(function() { wallet.setUnspentOutputs(utxo) }, Error, errorMessage) }); - ['outputIndex', 'scriptPubKey', 'address', 'value'].forEach(function(field){ + ['outputIndex', 'address', 'value'].forEach(function(field){ it("throws an error when " + field + " is missing", function(){ delete utxo[0][field] var errorMessage = 'Invalid unspent output: key ' + field + - ' is missing. A valid unspent output must contain outputIndex, scriptPubKey, address, value and hash(or hashLittleEndian)' + ' is missing. A valid unspent output must contain outputIndex, address, value and hash(or hashLittleEndian)' assert.throws(function() { wallet.setUnspentOutputs(utxo) @@ -273,7 +271,6 @@ describe('Wallet', function() { assert(output) assert.equal(output.value, utxo[0].value) assert.equal(output.address, utxo[0].address) - assert.equal(output.scriptPubKey, utxo[0].scriptPubKey) } }) }) @@ -317,7 +314,6 @@ describe('Wallet', function() { assert.equal(output.receive, key) assert.equal(output.value, txOut.value) assert.equal(output.address, txOut.address) - assert.equal(output.scriptPubKey, convert.bytesToHex(txOut.script.buffer)) } }) @@ -370,21 +366,18 @@ describe('Wallet', function() { { "hash": fakeTxHash(1), "outputIndex": 0, - "scriptPubKey": scriptPubKeyFor(address1, 300000), "address" : address1, "value": 400000 // not enough for value }, { "hash": fakeTxHash(2), "outputIndex": 1, - "scriptPubKey": scriptPubKeyFor(address1, 500000), "address" : address1, "value": 500000 // enough for only value }, { "hash": fakeTxHash(3), "outputIndex": 0, - "scriptPubKey": scriptPubKeyFor(address2, 520000), "address" : address2, "value": 520000 // enough for value and fee } @@ -414,7 +407,6 @@ describe('Wallet', function() { { "hash": fakeTxHash(4), "outputIndex": 0, - "scriptPubKey": scriptPubKeyFor(address2, 520000), "address" : address2, "value": 530000 // enough but spent before createTx } @@ -511,15 +503,6 @@ describe('Wallet', function() { function fakeTxHash(i) { return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i } - - function scriptPubKeyFor(address, value){ - var txOut = new TransactionOut({ - value: value, - script: Script.createOutputScript(address) - }) - - return txOut.scriptPubKey() - } }) function assertEqual(obj1, obj2){ From 75218e784fce97858ec3cb8f0ad536728714e8db Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 12:28:26 +0800 Subject: [PATCH 27/31] Transaction fee calculation does not need BigInteger --- src/transaction.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index 6059fc5..8686a48 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -383,11 +383,9 @@ Transaction.prototype.estimateFee = function(feePerKb){ var outSize = 34 var fixedPadding = 34 var feePerKb = feePerKb || Transaction.feePerKb - var size = this.ins.length * uncompressedInSize + this.outs.length * outSize + fixedPadding - var sizeInKb = BigInteger.valueOf(Math.ceil(size / 1000)) - return BigInteger.valueOf(feePerKb).multiply(sizeInKb).intValue() + return feePerKb * Math.ceil(size / 1000) } var TransactionIn = function (data) { From 83db6483fa7a164615a61b9e0d3bf8dda8482dee Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 12:29:55 +0800 Subject: [PATCH 28/31] remove unnecessary imports from wallet --- src/wallet.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/wallet.js b/src/wallet.js index bb1718f..0021727 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1,11 +1,5 @@ -var Script = require('./script'); -var ECKey = require('./eckey').ECKey; var convert = require('./convert'); -var assert = require('assert'); -var BigInteger = require('./jsbn/jsbn'); var Transaction = require('./transaction').Transaction; -var TransactionIn = require('./transaction').TransactionIn; -var TransactionOut = require('./transaction').TransactionOut; var HDNode = require('./hdwallet.js') var rng = require('secure-random'); From f7d4895b7463c0e4867a2ba59931b4ff24ee9d92 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 13:22:54 +0800 Subject: [PATCH 29/31] add async version of setUnspentOutputs --- src/wallet.js | 10 ++++++++++ test/wallet.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/wallet.js b/src/wallet.js index 0021727..9e02c6a 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -83,6 +83,16 @@ var Wallet = function (seed, options) { this.outputs = outputs } + this.setUnspentOutputsAsync = function(utxo, callback) { + try { + this.setUnspentOutputs(utxo) + } catch(err) { + return callback(err) + } + + return callback() + } + function outputToUnspentOutput(output){ var hashAndIndex = output.receive.split(":") diff --git a/test/wallet.js b/test/wallet.js index 6f8d558..8966dd5 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -273,6 +273,37 @@ describe('Wallet', function() { assert.equal(output.address, utxo[0].address) } }) + + describe('setUnspentOutputsAsync', function(){ + var utxo; + beforeEach(function(){ + utxo = cloneObject([expectedUtxo]) + }) + + afterEach(function(){ + wallet.setUnspentOutputs.restore() + }) + + it('calls setUnspentOutputs', function(){ + sinon.stub(wallet, "setUnspentOutputs") + + var callback = sinon.spy() + var tx = wallet.setUnspentOutputsAsync(utxo, callback) + + assert(wallet.setUnspentOutputs.calledWith(utxo)) + assert(callback.called) + }) + + it('when setUnspentOutputs throws an error, it invokes callback with error', function(){ + sinon.stub(wallet, "setUnspentOutputs").throws() + + var callback = sinon.spy() + var tx = wallet.setUnspentOutputsAsync(utxo, callback) + + assert(callback.called) + assert(callback.args[0][0] instanceof Error) + }) + }) }) describe('processTx', function(){ From 68b08b638a9eccd7eaae66f1a51f637be0dada31 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 25 Mar 2014 13:39:27 +0800 Subject: [PATCH 30/31] add async version of createTx --- src/wallet.js | 16 ++++++++++++++++ test/wallet.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/wallet.js b/src/wallet.js index 9e02c6a..ef1405a 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -200,6 +200,22 @@ var Wallet = function (seed, options) { return tx } + this.createTxAsync = function(to, value, fixedFee, callback){ + if(fixedFee instanceof Function) { + callback = fixedFee + fixedFee = undefined + } + var tx = null + + try { + tx = this.createTx(to, value, fixedFee) + } catch(err) { + return callback(err) + } + + callback(null, tx) + } + this.dustThreshold = 5430 function isDust(amount) { return amount <= me.dustThreshold diff --git a/test/wallet.js b/test/wallet.js index 8966dd5..f19bc3b 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -536,6 +536,50 @@ describe('Wallet', function() { } }) + describe('createTxAsync', function(){ + var to, value, fee; + + beforeEach(function(){ + to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' + value = 500000 + fee = 10000 + }) + + afterEach(function(){ + wallet.createTx.restore() + }) + + it('calls createTx', function(){ + sinon.stub(wallet, "createTx").returns("fakeTx") + + var callback = sinon.spy() + var tx = wallet.createTxAsync(to, value, callback) + + assert(wallet.createTx.calledWith(to, value)) + assert(callback.calledWith(null, "fakeTx")) + }) + + it('calls createTx correctly when fee is specified', function(){ + sinon.stub(wallet, "createTx").returns("fakeTx") + + var callback = sinon.spy() + var tx = wallet.createTxAsync(to, value, fee, callback) + + assert(wallet.createTx.calledWith(to, value, fee)) + assert(callback.calledWith(null, "fakeTx")) + }) + + it('when createTx throws an error, it invokes callback with error', function(){ + sinon.stub(wallet, "createTx").throws() + + var callback = sinon.spy() + var tx = wallet.createTxAsync(to, value, callback) + + assert(callback.called) + assert(callback.args[0][0] instanceof Error) + }) + }) + function assertEqual(obj1, obj2){ assert.equal(obj1.toString(), obj2.toString()) } From 8d2525dba17fed10d81a1e75ad3f5b7f5963a53d Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Wed, 26 Mar 2014 08:37:07 +0800 Subject: [PATCH 31/31] allow feePerKb to be set to zero --- src/transaction.js | 3 ++- test/transaction.js | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/transaction.js b/src/transaction.js index 8686a48..fa9566d 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -382,7 +382,8 @@ Transaction.prototype.estimateFee = function(feePerKb){ var uncompressedInSize = 180 var outSize = 34 var fixedPadding = 34 - var feePerKb = feePerKb || Transaction.feePerKb + + if(feePerKb == undefined) feePerKb = Transaction.feePerKb var size = this.ins.length * uncompressedInSize + this.outs.length * outSize + fixedPadding return feePerKb * Math.ceil(size / 1000) diff --git a/test/transaction.js b/test/transaction.js index 41bcea2..ab0226f 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -194,6 +194,11 @@ describe('Transaction', function() { var tx = Transaction.deserialize(fixtureTx2Hex) assert.equal(tx.estimateFee(10000), 10000) }) + + it('allow feePerKb to be set to 0', function(){ + var tx = Transaction.deserialize(fixtureTx2Hex) + assert.equal(tx.estimateFee(0), 0) + }) }) })