diff --git a/package.json b/package.json index 36d1f36..be6a161 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/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/script.js b/src/script.js index 7958d12..85c4454 100644 --- a/src/script.js +++ b/src/script.js @@ -150,7 +150,8 @@ Script.prototype.toScriptHash = function() { return util.sha256ripe160(this.buffer) } -Script.prototype.toAddress = function() { +//TODO: support testnet +Script.prototype.getToAddress = function() { var outType = this.getOutType(); if (outType == 'Pubkey') { @@ -164,6 +165,11 @@ Script.prototype.toAddress = 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/src/transaction.js b/src/transaction.js index 20d03dd..fa9566d 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -377,6 +377,18 @@ Transaction.prototype.validateSig = function(index, script, sig, pub) { convert.coerceToBytes(pub)); } +Transaction.feePerKb = 20000 +Transaction.prototype.estimateFee = function(feePerKb){ + var uncompressedInSize = 180 + var outSize = 34 + var fixedPadding = 34 + + if(feePerKb == undefined) feePerKb = Transaction.feePerKb + var size = this.ins.length * uncompressedInSize + this.outs.length * outSize + fixedPadding + + return feePerKb * Math.ceil(size / 1000) +} + var TransactionIn = function (data) { if (typeof data == "string") this.outpoint = { hash: data.split(':')[0], index: data.split(':')[1] } @@ -415,7 +427,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) @@ -424,8 +436,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 @@ -433,6 +444,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 da3aa79..ef1405a 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'); @@ -60,111 +54,209 @@ var Wallet = function (seed, options) { return this.changeAddresses[this.changeAddresses.length - 1] } - // Processes a transaction object - // If "verified" is true, then we trust the transaction as "final" - this.processTx = function(tx, verified) { + this.getBalance = function() { + return this.getUnspentOutputs().reduce(function(memo, output){ + return memo + output.value + }, 0) + } + + this.getUnspentOutputs = function() { + var utxo = [] + + for(var key in this.outputs){ + var output = this.outputs[key] + if(!output.spend) utxo.push(outputToUnspentOutput(output)) + } + + return utxo + } + + this.setUnspentOutputs = function(utxo) { + var outputs = {} + + utxo.forEach(function(uo){ + validateUnspentOutput(uo) + var o = unspentOutputToOutput(uo) + outputs[o.receive] = o + }) + + 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(":") + + return { + hash: hashAndIndex[0], + hashLittleEndian: convert.reverseEndian(hashAndIndex[0]), + outputIndex: parseInt(hashAndIndex[1]), + address: output.address, + value: output.value + } + } + + function unspentOutputToOutput(o) { + var hash = o.hash || convert.reverseEndian(o.hashLittleEndian) + var key = hash + ":" + o.outputIndex + return { + receive: key, + 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', '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 + } + + 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] = { + receive: output, + value: txOut.value, + address: address, } } - } - 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 } - } - } - // 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]) - 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.output }), - outs: outs }) + } + + this.createTx = function(to, value, fixedFee) { + checkDust(value) + + var tx = new Transaction() + tx.addOutput(to, value) + + var utxo = getCandidateOutputs(value) + var totalInValue = 0 + for(var i=0; i 0 && !isDust(change)) { + tx.addOutput(getChangeAddress(), change) + } + break; + } + + checkInsufficientFund(totalInValue, value, fee) + 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.output }), - outs: outputs - }) - this.sign(tx) - 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 + } + + function checkDust(value){ + if (isNullOrUndefined(value) || isDust(value)) { + throw new Error("Value must be above dust threshold") + } + } + + function getCandidateOutputs(value){ + var unspent = [] + for (var key in me.outputs){ + var output = me.outputs[key] + if(!output.spend) unspent.push(output) + } + + var sortByValueDesc = unspent.sort(function(o1, o2){ + return o2.value - o1.value + }) + + return sortByValueDesc; + } + + function estimateFeePadChangeOutput(tx){ + var tmpTx = tx.clone() + tmpTx.addOutput(getChangeAddress(), 0) + return tmpTx.estimateFee() + } + + function getChangeAddress() { + if(me.changeAddresses.length === 0) me.generateChangeAddress() + return me.changeAddresses[me.changeAddresses.length - 1] + } + + 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) { @@ -201,6 +293,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/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/fixtures/mainnet_tx.json b/test/fixtures/mainnet_tx.json new file mode 100644 index 0000000..f78f876 --- /dev/null +++ b/test/fixtures/mainnet_tx.json @@ -0,0 +1,5 @@ +{ + "prevTx": "0100000001e0214ebebb0fd3414d3fdc0dbf3b0f4b247a296cafc984558622c3041b0fcc9b010000008b48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8ffffffff03a0860100000000001976a91400ea3576c8fcb0bc8392f10e23a3425ae24efea888ac40420f00000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188acf07cd210000000001976a9146fb93c557ee62b109370fd9003e456917401cbfa88ac00000000", + "tx": "0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000", + "bigTx": "010000000ee7b73e229790c1e79a02f0c871813b3cf26a4156c5b8d942e88b38fe8d3f43a0000000008c493046022100fd3d8fef44fb0962ba3f07bee1d4cafb84e60e38e6c7d9274504b3638a8d2f520221009fce009044e615b6883d4bf62e04c48f9fe236e19d644b082b2f0ae5c98e045c014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff7bfc005f3880a606027c7cd7dd02a0f6a6572eeb84a91aa158311be13695a7ea010000008b483045022100e2e61c40f26e2510b76dc72ea2f568ec514fce185c719e18bca9caaef2b20e9e02207f1100fc79eb0584e970c7f18fb226f178951d481767b4092d50d13c50ccba8b014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff0e0f8e6bf951fbb84d7d8ef833a1cbf5bb046ea7251973ac6e7661c755386ee3010000008a473044022048f1611e403710f248f7caf479965a6a5f63cdfbd9a714fef4ec1b68331ade1d022074919e79376c363d4575b2fc21513d5949471703efebd4c5ca2885e810eb1fa4014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffe6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b000000008b483045022100886c07cad489dfcf4b364af561835d5cf985f07adf8bd1d5bd6ddea82b0ce6b2022045bdcbcc2b5fc55191bb997039cf59ff70e8515c56b62f293a9add770ba26738014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffe6f17f35bf9f0aa7a4242ab3e29edbdb74c5274bf263e53043dddb8045cb585b010000008a4730440220535d49b819fdf294d27d82aff2865ed4e18580f0ca9796d793f611cb43a44f47022019584d5e300c415f642e37ba2a814a1e1106b4a9b91dc2a30fb57ceafe041181014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffd3051677216ea53baa2e6d7f6a75434ac338438c59f314801c8496d1e6d1bf6d010000008b483045022100bf612b0fa46f49e70ab318ca3458d1ed5f59727aa782f7fac5503f54d9b43a590220358d7ed0e3cee63a5a7e972d9fad41f825d95de2fd0c5560382468610848d489014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff1e751ccc4e7d973201e9174ec78ece050ef2fadd6a108f40f76a9fa314979c31010000008b483045022006e263d5f73e05c48a603e3bd236e8314e5420721d5e9020114b93e8c9220e1102210099d3dead22f4a792123347a238c87e67b55b28a94a0bb7793144cc7ad94a0168014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff25c4cf2c61743b3f4252d921d937cca942cf32e4f3fa4a544d0b26f014337084010000008a47304402207d6e87588be47bf2d97eaf427bdd992e9d6b306255711328aee38533366a88b50220623099595ae442cb77eaddb3f91753a4fc9df56fde69cfec584c7f97e05533c8014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffecd93c87eb43c48481e6694904305349bdea94b01104579fa9f02bff66c89663010000008a473044022020f59498aee0cf82cb113768ef3cb721000346d381ff439adb4d405f791252510220448de723aa59412266fabbc689ec25dc94b1688c27a614982047513a80173514014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffa1fdc0a79ff98d5b6154176e321c22f4f8450dbd950bd013ad31135f5604411e010000008b48304502210088167867f87327f9c0db0444267ff0b6a026eedd629d8f16fe44a34c18e706bf0220675c8baebf89930e2d6e4463adefc50922653af99375242e38f5ee677418738a014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffb89e8249c3573b58bf1ec7433185452dd57ab8e1daab01c3cc6ddc8b66ad3de8000000008b4830450220073d50ac5ec8388d5b3906921f9368c31ad078c8e1fb72f26d36b533f35ee327022100c398b23e6692e11dca8a1b64aae2ff70c6a781ed5ee99181b56a2f583a967cd4014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff45ee07e182084454dacfad1e61b04ffdf9c7b01003060a6c841a01f4fff8a5a0010000008b483045022100991d1bf60c41358f08b20e53718a24e05ac0608915df4f6305a5b47cb61e5da7022003f14fc1cc5b737e2c3279a4f9be1852b49dbb3d9d6cc4c8af6a666f600dced8014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff4cba12549f1d70f8e60aea8b546c8357f7c099e7c7d9d8691d6ee16e7dfa3170010000008c493046022100f14e2b0ef8a8e206db350413d204bc0a5cd779e556b1191c2d30b5ec023cde6f022100b90b2d2bf256c98a88f7c3a653b93cec7d25bb6a517db9087d11dbd189e8851c014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffffa4b3aed39eb2a1dc6eae4609d9909724e211c153927c230d02bd33add3026959010000008b483045022100a8cebb4f1c58f5ba1af91cb8bd4a2ed4e684e9605f5a9dc8b432ed00922d289d0220251145d2d56f06d936fd0c51fa884b4a6a5fafd0c3318f72fb05a5c9aa372195014104aa592c859fd00ed2a02609aad3a1bf72e0b42de67713e632c70a33cc488c15598a0fb419370a54d1c275b44380e8777fc01b6dc3cd43a416c6bab0e30dc1e19fffffffff0240d52303000000001976a914167c3e1f10cc3b691c73afbdb211e156e3e3f25c88ac15462e00000000001976a914290f7d617b75993e770e5606335fa0999a28d71388ac00000000" +} diff --git a/test/script.js b/test/script.js index f8e378e..d5b7ef5 100644 --- a/test/script.js +++ b/test/script.js @@ -7,9 +7,17 @@ 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() { + 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([])) @@ -23,7 +31,7 @@ describe('Script', function() { assert.throws(function(){ new Script({}) }) }) }) - + describe('2-of-3 Multi-Signature', function() { var compressedPubKeys = [] var numSigs @@ -32,7 +40,7 @@ describe('Script', function() { compressedPubKeys = ['02ea1297665dd733d444f31ec2581020004892cdaaf3dd6c0107c615afb839785f', '02fab2dea1458990793f56f42e4a47dbf35a12a351f26fa5d7e0cc7447eaafa21f', '036c6802ce7e8113723dd92cdb852e492ebb157a871ca532c3cb9ed08248ff0e19'] - + numSigs = 2; }) @@ -42,13 +50,50 @@ 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(p2shScriptPubKey) + assert.equal(script.getOutType(), 'P2SH') }) + it('works for pubkey', function() { + 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(p2shScriptPubKey) + assert.equal(script.getToAddress().toString(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') + }) + + it('works for pubkey type output', function() { + 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') + }) }) }) diff --git a/test/transaction.js b/test/transaction.js index 8a67b78..ab0226f 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -1,8 +1,17 @@ -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 +var fixtureTxBigHex = fixtureTxes.bigTx + describe('Transaction', function() { describe('deserialize', function() { var tx, serializedTx @@ -65,7 +74,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() }) @@ -157,7 +166,7 @@ describe('Transaction', function() { var validTx beforeEach(function() { - validTx = Transaction.deserialize('0100000001576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c000000006b483045022100a82a31607b837c1ae510ae3338d1d3c7cbd57c15e322ab6e5dc927d49bffa66302205f0db6c90f1fae3c8db4ebfa753d7da1b2343d653ce0331aa94ed375e6ba366c0121020497bfc87c3e97e801414fed6a0db4b8c2e01c46e2cf9dff59b406b52224a76bffffffff02409c0000000000001976a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac50c30000000000001976a91477890e8ec967c5fd4316c489d171fd80cf86997188ac00000000') + validTx = Transaction.deserialize(fixtureTx2Hex) }) it('returns true for valid signature', function(){ @@ -170,7 +179,40 @@ 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) + }) + + it('allow feePerKb to be set to 0', function(){ + var tx = Transaction.deserialize(fixtureTx2Hex) + assert.equal(tx.estimateFee(0), 0) + }) + }) }) + 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") + }) + }) + }) }) diff --git a/test/wallet.js b/test/wallet.js index b72e2d7..f19bc3b 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -1,22 +1,28 @@ var Wallet = require('../src/wallet.js') var HDNode = require('../src/hdwallet.js') +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') var Crypto = require('crypto-js') +var fixtureTxes = require('./fixtures/mainnet_tx') +var fixtureTx1Hex = fixtureTxes.prevTx +var fixtureTx2Hex = fixtureTxes.tx + +var sinon = require('sinon') + 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 +53,6 @@ describe('Wallet', function() { }) describe('constructor options', function() { - var wallet; beforeEach(function() { wallet = new Wallet(seed, {network: 'testnet'}) }) @@ -149,6 +154,432 @@ describe('Wallet', function() { }) }) + describe('Unspent Outputs', function(){ + var expectedUtxo, expectedOutputKey; + beforeEach(function(){ + expectedUtxo = { + "hash":"6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7", + "hashLittleEndian":"c7b97b432e7b3a8e31d1a4a0fa326c8fb002d19f2da5fc4feaf9c43a2762406a", + "outputIndex": 0, + "address" : "1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv", + "value": 20000 + } + expectedOutputKey = expectedUtxo.hash + ":" + expectedUtxo.outputIndex + }) + + function addUtxoToOutput(utxo){ + var key = utxo.hash + ":" + utxo.outputIndex + wallet.outputs[key] = { + receive: key, + 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(){ + addUtxoToOutput(expectedUtxo) + }) + + 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(){ + 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() + }) + + 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, address, value and hash(or hashLittleEndian)' + + assert.throws(function() { + wallet.setUnspentOutputs(utxo) + }, Error, errorMessage) + }); + + ['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, address, value and hash(or hashLittleEndian)' + + assert.throws(function() { + wallet.setUnspentOutputs(utxo) + }, Error, errorMessage) + }) + }) + }) + + function verifyOutputs() { + var output = wallet.outputs[expectedOutputKey] + assert(output) + assert.equal(output.value, utxo[0].value) + 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(){ + 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.receive, key) + assert.equal(output.value, txOut.value) + assert.equal(output.address, txOut.address) + } + }) + + 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 used as input + 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) + }) + }) + + describe('createTx', function(){ + var to, value; + var address1, address2; + + beforeEach(function(){ + to = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' + value = 500000 + + // generate 2 addresses + address1 = wallet.generateAddress() + address2 = wallet.generateAddress() + + // set up 3 utxo + utxo = [ + { + "hash": fakeTxHash(1), + "outputIndex": 0, + "address" : address1, + "value": 400000 // not enough for value + }, + { + "hash": fakeTxHash(2), + "outputIndex": 1, + "address" : address1, + "value": 500000 // enough for only value + }, + { + "hash": fakeTxHash(3), + "outputIndex": 0, + "address" : address2, + "value": 520000 // enough for value and fee + } + ] + wallet.setUnspentOutputs(utxo) + }) + + 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 }) + }) + + it('ignores spent outputs', function(){ + utxo.push( + { + "hash": fakeTxHash(4), + "outputIndex": 0, + "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(){ + 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 = 5000 + 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, 15000) + }) + + it('generates a change address if there is not any', function(){ + var fee = 5000 + 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, 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) + }) + }) + }) + + 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))) + }) + }) + + describe('when value is below dust threshold', function(){ + it('throws an error', function(){ + var value = 5430 + + assert.throws(function() { + wallet.createTx(to, value) + }, /Value must be above dust threshold/) + }) + }) + + 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 + } + }) + + 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()) } @@ -156,4 +587,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)) + } })