diff --git a/src/index.js b/src/index.js index 5af8210..2b9ae4b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,3 @@ -var T = require('./transaction') - module.exports = { Address: require('./address'), base58: require('./base58'), @@ -15,9 +13,7 @@ module.exports = { HDNode: require('./hdnode'), Script: require('./script'), scripts: require('./scripts'), - Transaction: T.Transaction, - TransactionIn: T.TransactionIn, - TransactionOut: T.TransactionOut, + Transaction: require('./transaction'), networks: require('./networks'), Wallet: require('./wallet') } diff --git a/src/transaction.js b/src/transaction.js index 6ff1c23..65c0a59 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -1,5 +1,3 @@ -// FIXME: To all ye that enter here, be weary of Buffers, Arrays and Hex interchanging between the outpoints - var assert = require('assert') var bufferutils = require('./bufferutils') var crypto = require('./crypto') @@ -12,32 +10,16 @@ var ECKey = require('./eckey') var Script = require('./script') var DEFAULT_SEQUENCE = 0xffffffff +var SIGHASH_ALL = 0x01 +var SIGHASH_NONE = 0x02 +var SIGHASH_SINGLE = 0x03 +var SIGHASH_ANYONECANPAY = 0x80 -function Transaction(doc) { - if (!(this instanceof Transaction)) { return new Transaction(doc) } +function Transaction() { this.version = 1 this.locktime = 0 this.ins = [] this.outs = [] - - if (doc) { - if (doc.hash) this.hash = doc.hash; - if (doc.version) this.version = doc.version; - if (doc.locktime) this.locktime = doc.locktime; - if (doc.ins && doc.ins.length) { - doc.ins.forEach(function(input) { - this.addInput(new TransactionIn(input)) - }, this) - } - - if (doc.outs && doc.outs.length) { - doc.outs.forEach(function(output) { - this.addOutput(new TransactionOut(output)) - }, this) - } - - this.hash = this.hash || this.getHash() - } } /** @@ -45,36 +27,35 @@ function Transaction(doc) { * * Can be called with any of: * - * - An existing TransactionIn object * - A transaction and an index * - A transaction hash and an index - * - A single string argument of the form txhash:index * * Note that this method does not sign the created input. */ -Transaction.prototype.addInput = function (tx, outIndex) { - if (arguments[0] instanceof TransactionIn) { - this.ins.push(arguments[0]) - return - } - +Transaction.prototype.addInput = function(tx, index) { var hash - if (arguments[0].length > 65) { - var args = arguments[0].split(':') - hash = args[0] - outIndex = parseInt(args[1]) + + if (typeof tx === 'string') { + hash = new Buffer(tx, 'hex') + assert.equal(hash.length, 32, 'Expected Transaction or string, got ' + tx) + + // TxHash hex is big-endian, we need little-endian + Array.prototype.reverse.call(hash) } else { - hash = typeof tx === "string" ? tx : tx.hash + assert(tx instanceof Transaction, 'Expected Transaction or string, got ' + tx) + hash = crypto.hash256(tx.toBuffer()) + } - this.ins.push(new TransactionIn({ - outpoint: { - hash: hash, - index: outIndex - }, - script: Script.EMPTY - })) + assert.equal(typeof index, 'number', 'Expected number index, got ' + index) + + return (this.ins.push({ + hash: hash, + index: index, + script: Script.EMPTY, + sequence: DEFAULT_SEQUENCE + }) - 1) } /** @@ -82,33 +63,27 @@ Transaction.prototype.addInput = function (tx, outIndex) { * * Can be called with: * - * i) An existing TransactionOut object - * ii) An address object or a string address, and a value - * iii) An address:value string - * - * FIXME: This is a bit convoluted + * - A base58 address string and a value + * - An Address object and a value + * - A scriptPubKey Script and a value */ -Transaction.prototype.addOutput = function (address, value) { - if (arguments[0] instanceof TransactionOut) { - this.outs.push(arguments[0]) - return +Transaction.prototype.addOutput = function(scriptPubKey, value) { + // Attempt to get a valid address if it's a base58 address string + if (typeof scriptPubKey === 'string') { + scriptPubKey = Address.fromBase58Check(scriptPubKey) } - if (typeof address === 'string') { - if (arguments[0].indexOf(':') >= 0) { - var args = arguments[0].split(':') - address = args[0] - value = parseInt(args[1]) - } + // Attempt to get a valid script if it's an Address object + if (scriptPubKey instanceof Address) { + var address = scriptPubKey - address = Address.fromBase58Check(address) + scriptPubKey = address.toOutputScript() } - this.outs.push(new TransactionOut({ + return (this.outs.push({ + script: scriptPubKey, value: value, - script: address.toOutputScript(), - address: address // TODO: Remove me - })) + }) - 1) } Transaction.prototype.toBuffer = function () { @@ -150,13 +125,8 @@ Transaction.prototype.toBuffer = function () { writeVarInt(this.ins.length) this.ins.forEach(function(txin) { - var hash = new Buffer(txin.outpoint.hash, 'hex') // FIXME: Performance: convert on tx.addInput instead - - // TxHash hex is big-endian, we need little-endian - Array.prototype.reverse.call(hash) - - writeSlice(hash) - writeUInt32(txin.outpoint.index) + writeSlice(txin.hash) + writeUInt32(txin.index) writeVarInt(txin.script.buffer.length) writeSlice(txin.script.buffer) writeUInt32(txin.sequence) @@ -179,11 +149,6 @@ Transaction.prototype.toHex = function() { return this.toBuffer().toString('hex') } -var SIGHASH_ALL = 0x01 -var SIGHASH_NONE = 0x02 -var SIGHASH_SINGLE = 0x03 -var SIGHASH_ANYONECANPAY = 0x80 - /** * Hash transaction for signing a specific input. * @@ -226,7 +191,7 @@ Transaction.prototype.hashForSignature = function(prevOutScript, inIndex, hashTy return crypto.hash256(buffer) } -Transaction.prototype.getHash = function () { +Transaction.prototype.getId = function () { var buffer = crypto.hash256(this.toBuffer()) // Big-endian is used for TxHash @@ -240,21 +205,26 @@ Transaction.prototype.clone = function () { newTx.version = this.version newTx.locktime = this.locktime - this.ins.forEach(function(txin) { - newTx.addInput(txin.clone()) + newTx.ins = this.ins.map(function(txin) { + return { + hash: txin.hash, + index: txin.index, + script: txin.script, + sequence: txin.sequence + } }) - this.outs.forEach(function(txout) { - newTx.addOutput(txout.clone()) + newTx.outs = this.outs.map(function(txout) { + return { + script: txout.script, + value: txout.value + } }) return newTx } Transaction.fromBuffer = function(buffer) { - // Copy because we mutate (reverse TxOutHashs) - buffer = new Buffer(buffer) - var offset = 0 function readSlice(n) { offset += n @@ -276,55 +246,41 @@ Transaction.fromBuffer = function(buffer) { return vi.number } - var ins = [] - var outs = [] + var tx = new Transaction() + tx.version = readUInt32() - var version = readUInt32() var vinLen = readVarInt() - for (var i = 0; i < vinLen; ++i) { var hash = readSlice(32) - - // TxHash is little-endian, we want big-endian hex - Array.prototype.reverse.call(hash) - var vout = readUInt32() var scriptLen = readVarInt() var script = readSlice(scriptLen) var sequence = readUInt32() - ins.push({ - outpoint: { - hash: hash.toString('hex'), - index: vout, - }, + tx.ins.push({ + hash: hash, + index: vout, script: Script.fromBuffer(script), sequence: sequence }) } var voutLen = readVarInt() - for (i = 0; i < voutLen; ++i) { var value = readUInt64() var scriptLen = readVarInt() var script = readSlice(scriptLen) - outs.push({ + tx.outs.push({ value: value, script: Script.fromBuffer(script) }) } - var locktime = readUInt32() + tx.locktime = readUInt32() assert.equal(offset, buffer.length, 'Invalid transaction') - return new Transaction({ - version: version, - ins: ins, - outs: outs, - locktime: locktime - }) + return tx } Transaction.fromHex = function(hex) { @@ -334,26 +290,26 @@ Transaction.fromHex = function(hex) { /** * Signs a pubKeyHash output at some index with the given key */ -Transaction.prototype.sign = function(index, key, type) { - var prevOutScript = key.pub.getAddress().toOutputScript() - var signature = this.signInput(index, prevOutScript, key, type) +Transaction.prototype.sign = function(index, privKey, hashType) { + var prevOutScript = privKey.pub.getAddress().toOutputScript() + var signature = this.signInput(index, prevOutScript, privKey, hashType) // FIXME: Assumed prior TX was pay-to-pubkey-hash - var scriptSig = scripts.pubKeyHashInput(signature, key.pub) + var scriptSig = scripts.pubKeyHashInput(signature, privKey.pub) this.setInputScript(index, scriptSig) } -Transaction.prototype.signInput = function(index, prevOutScript, key, type) { - type = type || SIGHASH_ALL - assert(key instanceof ECKey, 'Invalid private key') +Transaction.prototype.signInput = function(index, prevOutScript, privKey, hashType) { + hashType = hashType || SIGHASH_ALL + assert(privKey instanceof ECKey, 'Expected ECKey, got ' + privKey) - var hash = this.hashForSignature(prevOutScript, index, type) - var signature = key.sign(hash) + var hash = this.hashForSignature(prevOutScript, index, hashType) + var signature = privKey.sign(hash) var DERencoded = ecdsa.serializeSig(signature) return Buffer.concat([ new Buffer(DERencoded), - new Buffer([type]) + new Buffer([hashType]) ]) } @@ -361,63 +317,15 @@ Transaction.prototype.setInputScript = function(index, script) { this.ins[index].script = script } -// FIXME: should probably be validateInput(index, pub) -Transaction.prototype.validateInput = function(index, script, pub, DERsig) { +// FIXME: could be validateInput(index, prevTxOut, pub) +Transaction.prototype.validateInput = function(index, prevOutScript, pubKey, DERsig) { var type = DERsig.readUInt8(DERsig.length - 1) DERsig = DERsig.slice(0, -1) - var hash = this.hashForSignature(script, index, type) - var sig = ecdsa.parseSig(DERsig) - - return pub.verify(hash, sig) -} - -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) -} - -function TransactionIn(data) { - assert(data.outpoint && data.script, 'Invalid TxIn parameters') - this.outpoint = data.outpoint - this.script = data.script - this.sequence = data.sequence == undefined ? DEFAULT_SEQUENCE : data.sequence -} - -TransactionIn.prototype.clone = function () { - return new TransactionIn({ - outpoint: { - hash: this.outpoint.hash, - index: this.outpoint.index - }, - script: this.script, - sequence: this.sequence - }) -} - -function TransactionOut(data) { - this.script = data.script - this.value = data.value - this.address = data.address -} + var hash = this.hashForSignature(prevOutScript, index, type) + var signature = ecdsa.parseSig(DERsig) -TransactionOut.prototype.clone = function() { - return new TransactionOut({ - script: this.script, - value: this.value, - address: this.address - }) + return pubKey.verify(hash, signature) } -module.exports = { - Transaction: Transaction, - TransactionIn: TransactionIn, - TransactionOut: TransactionOut -} +module.exports = Transaction diff --git a/src/wallet.js b/src/wallet.js index aa9355c..1501456 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -4,7 +4,7 @@ var rng = require('secure-random') var Address = require('./address') var HDNode = require('./hdnode') -var Transaction = require('./transaction').Transaction +var Transaction = require('./transaction') function Wallet(seed, network) { network = network || networks.bitcoin @@ -146,9 +146,9 @@ function Wallet(seed, network) { } function processTx(tx, isPending) { - var txhash = tx.getHash() + var txid = tx.getId() - tx.outs.forEach(function(txOut, i){ + tx.outs.forEach(function(txOut, i) { var address try { @@ -158,7 +158,7 @@ function Wallet(seed, network) { } if (isMyAddress(address)) { - var output = txhash + ':' + i + var output = txid + ':' + i me.outputs[output] = { receive: output, @@ -169,9 +169,13 @@ function Wallet(seed, network) { } }) - tx.ins.forEach(function(txIn, i){ - var op = txIn.outpoint - var output = op.hash + ':' + op.index + tx.ins.forEach(function(txIn) { + // copy and convert to big-endian hex + var txinId = new Buffer(txIn.hash) + Array.prototype.reverse.call(txinId) + txinId = txinId.toString('hex') + + var output = txinId + ':' + txIn.index if(me.outputs[output]) delete me.outputs[output] }) @@ -183,18 +187,21 @@ function Wallet(seed, network) { var utxos = getCandidateOutputs(value) var accum = 0 var subTotal = value + var addresses = [] var tx = new Transaction() tx.addOutput(to, value) for (var i = 0; i < utxos.length; ++i) { var utxo = utxos[i] + addresses.push(utxo.address) - tx.addInput(utxo.receive) - accum += utxo.value + var outpoint = utxo.receive.split(':') + tx.addInput(outpoint[0], parseInt(outpoint[1])) var fee = fixedFee == undefined ? estimateFeePadChangeOutput(tx) : fixedFee + accum += utxo.value subTotal = value + fee if (accum >= subTotal) { var change = accum - subTotal @@ -209,7 +216,7 @@ function Wallet(seed, network) { assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal) - this.sign(tx) + this.signWith(tx, addresses) return tx } @@ -228,10 +235,13 @@ function Wallet(seed, network) { return sortByValueDesc } - function estimateFeePadChangeOutput(tx){ + var feePerKb = 20000 + function estimateFeePadChangeOutput(tx) { var tmpTx = tx.clone() tmpTx.addOutput(getChangeAddress(), 0) - return tmpTx.estimateFee() + + var byteSize = tmpTx.toBuffer().length + return feePerKb * Math.ceil(byteSize / 1000) } function getChangeAddress() { @@ -239,13 +249,15 @@ function Wallet(seed, network) { return me.changeAddresses[me.changeAddresses.length - 1] } - this.sign = function(tx) { - tx.ins.forEach(function(inp,i) { - var output = me.outputs[inp.outpoint.hash + ':' + inp.outpoint.index] - if (output) { - tx.sign(i, me.getPrivateKeyForAddress(output.address)) - } + this.signWith = function(tx, addresses) { + assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs') + + addresses.forEach(function(address, i) { + var key = me.getPrivateKeyForAddress(address) + + tx.sign(i, key) }) + return tx } diff --git a/test/bitcoin.core.js b/test/bitcoin.core.js index 4cd8238..af52885 100644 --- a/test/bitcoin.core.js +++ b/test/bitcoin.core.js @@ -6,7 +6,7 @@ var networks = require('../src/networks') var Address = require('../src/address') var BigInteger = require('bigi') var ECKey = require('../src/eckey') -var Transaction = require('../src/transaction').Transaction +var Transaction = require('../src/transaction') var Script = require('../src/script') var base58_encode_decode = require("./fixtures/core/base58_encode_decode.json") @@ -147,10 +147,15 @@ describe('Bitcoin-core', function() { var prevOutIndex = input[1] // var prevOutScriptPubKey = input[2] // TODO: we don't have a ASM parser - assert.equal(txin.outpoint.hash, prevOutHash) + var actualHash = txin.hash + + // Test data is big-endian + Array.prototype.reverse.call(actualHash) + + assert.equal(actualHash.toString('hex'), prevOutHash) // we read UInt32, not Int32 - assert.equal(txin.outpoint.index & 0xffffffff, prevOutIndex) + assert.equal(txin.index & 0xffffffff, prevOutIndex) }) }) }) @@ -184,7 +189,7 @@ describe('Bitcoin-core', function() { } if (actualHash != undefined) { - // BigEndian test data + // Test data is big-endian Array.prototype.reverse.call(actualHash) assert.equal(actualHash.toString('hex'), expectedHash) diff --git a/test/transaction.js b/test/transaction.js index e9bbec6..38f031f 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -4,7 +4,7 @@ var scripts = require('../src/scripts') var Address = require('../src/address') var ECKey = require('../src/eckey') -var Transaction = require('../src/transaction').Transaction +var Transaction = require('../src/transaction') var Script = require('../src/script') var fixtureTxes = require('./fixtures/mainnet_tx') @@ -12,31 +12,24 @@ var fixtureTx1Hex = fixtureTxes.prevTx var fixtureTx2Hex = fixtureTxes.tx var fixtureTxBigHex = fixtureTxes.bigTx -function b2h(b) { return new Buffer(b).toString('hex') } -function h2b(h) { return new Buffer(h, 'hex') } - describe('Transaction', function() { - describe('deserialize', function() { + describe('toBuffer', function() { + it('matches the expected output', function() { + var expected = '010000000189632848f99722915727c5c75da8db2dbf194342a0429828f66ff88fab2af7d600000000fd1b0100483045022100e5be20d440b2bbbc886161f9095fa6d0bca749a4e41d30064f30eb97adc7a1f5022061af132890d8e4e90fedff5e9365aeeb77021afd8ef1d5c114d575512e9a130a0147304402205054e38e9d7b5c10481b6b4991fde5704cd94d49e344406e3c2ce4d18a43bf8e022051d7ba8479865b53a48bee0cce86e89a25633af5b2918aa276859489e232f51c014c8752410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b84104c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a52aeffffffff0101000000000000001976a914751e76e8199196d454941c45d1b3a323f1433bd688ac00000000' + + var actual = Transaction.fromHex(expected).toHex() + + assert.equal(actual, expected) + }) + }) + + describe('fromBuffer', function() { var tx, serializedTx beforeEach(function() { - serializedTx = [ - '0100000001344630cbff61fbc362f7e1ff2f11a344c29326e4ee96e78', - '7dc0d4e5cc02fd069000000004a493046022100ef89701f460e8660c8', - '0808a162bbf2d676f40a331a243592c36d6bd1f81d6bdf022100d29c0', - '72f1b18e59caba6e1f0b8cadeb373fd33a25feded746832ec179880c2', - '3901ffffffff0100f2052a010000001976a914dd40dedd8f7e3746662', - '4c4dacc6362d8e7be23dd88ac00000000' - ].join('') + serializedTx = '0100000001344630cbff61fbc362f7e1ff2f11a344c29326e4ee96e787dc0d4e5cc02fd069000000004a493046022100ef89701f460e8660c80808a162bbf2d676f40a331a243592c36d6bd1f81d6bdf022100d29c072f1b18e59caba6e1f0b8cadeb373fd33a25feded746832ec179880c23901ffffffff0100f2052a010000001976a914dd40dedd8f7e37466624c4dacc6362d8e7be23dd88ac00000000' tx = Transaction.fromHex(serializedTx) }) - it('returns the original after serialized again', function() { - var actual = tx.toBuffer() - var expected = serializedTx - - assert.equal(b2h(actual), expected) - }) - it('does not mutate the input buffer', function() { var buffer = new Buffer(serializedTx, 'hex') Transaction.fromBuffer(buffer) @@ -44,28 +37,27 @@ describe('Transaction', function() { assert.equal(buffer.toString('hex'), serializedTx) }) - it('decodes version correctly', function(){ + it('decodes version correctly', function() { assert.equal(tx.version, 1) }) - it('decodes locktime correctly', function(){ + it('decodes locktime correctly', function() { assert.equal(tx.locktime, 0) }) - it('decodes inputs correctly', function(){ + it('decodes inputs correctly', function() { assert.equal(tx.ins.length, 1) var input = tx.ins[0] assert.equal(input.sequence, 4294967295) - assert.equal(input.outpoint.index, 0) - assert.equal(input.outpoint.hash, "69d02fc05c4e0ddc87e796eee42693c244a3112fffe1f762c3fb61ffcb304634") + assert.equal(input.index, 0) + assert.equal(input.hash.toString('hex'), "344630cbff61fbc362f7e1ff2f11a344c29326e4ee96e787dc0d4e5cc02fd069") - assert.equal(b2h(input.script.buffer), - "493046022100ef89701f460e8660c80808a162bbf2d676f40a331a243592c36d6bd1f81d6bdf022100d29c072f1b18e59caba6e1f0b8cadeb373fd33a25feded746832ec179880c23901") + assert.equal(input.script.toHex(), "493046022100ef89701f460e8660c80808a162bbf2d676f40a331a243592c36d6bd1f81d6bdf022100d29c072f1b18e59caba6e1f0b8cadeb373fd33a25feded746832ec179880c23901") }) - it('decodes outputs correctly', function(){ + it('decodes outputs correctly', function() { assert.equal(tx.outs.length, 1) var output = tx.outs[0] @@ -74,11 +66,6 @@ describe('Transaction', function() { assert.deepEqual(output.script, Address.fromBase58Check('n1gqLjZbRH1biT5o4qiVMiNig8wcCPQeB9').toOutputScript()) }) - it('assigns hash to deserialized object', function(){ - var hashHex = "a9d4599e15b53f3eb531608ddb31f48c695c3d0b3538a6bda871e8b34f2f430c" - assert.equal(tx.hash, hashHex) - }) - it('decodes large inputs correctly', function() { // transaction has only 1 input var tx = new Transaction() @@ -107,29 +94,22 @@ describe('Transaction', function() { tx = new Transaction() }) - describe('addInput', function(){ - it('allows a Transaction object to be passed in', function(){ - tx.addInput(prevTx, 0) - verifyTransactionIn() - }) + describe('addInput', function() { + it('accepts a transaction hash', function() { + var prevTxHash = prevTx.getId() - it('allows a Transaction hash to be passed in', function(){ - tx.addInput("0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57", 0) + tx.addInput(prevTxHash, 0) verifyTransactionIn() }) - it('allows a TransactionIn object to be passed in', function(){ - var txCopy = tx.clone() - txCopy.addInput(prevTx, 0) - var transactionIn = txCopy.ins[0] - - tx.addInput(transactionIn) + it('accepts a Transaction object', function() { + tx.addInput(prevTx, 0) verifyTransactionIn() }) - it('allows a string in the form of txhash:index to be passed in', function(){ - tx.addInput("0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57:0") - verifyTransactionIn() + it('returns an index', function() { + assert.equal(tx.addInput(prevTx, 0), 0) + assert.equal(tx.addInput(prevTx, 0), 1) }) function verifyTransactionIn() { @@ -138,61 +118,56 @@ describe('Transaction', function() { var input = tx.ins[0] assert.equal(input.sequence, 4294967295) - assert.equal(input.outpoint.index, 0) - assert.equal(input.outpoint.hash, "0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57") + assert.equal(input.index, 0) + assert.equal(input.hash.toString('hex'), "576bc3c3285dbdccd8c3cbd8c03e10d7f77a5c839c744f34c3eb00511059b80c") assert.equal(input.script, Script.EMPTY) } }) - describe('addOutput', function(){ - it('allows an address and a value to be passed in', function(){ - tx.addOutput("15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3", 40000) - verifyTransactionOut() - }) + describe('addOutput', function() { + it('accepts an address string', function() { + var dest = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' - it('allows an Address object and value to be passed in', function(){ - tx.addOutput(Address.fromBase58Check('15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3'), 40000) + tx.addOutput(dest, 40000) verifyTransactionOut() }) - it('allows a string in the form of address:index to be passed in', function(){ - tx.addOutput("15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3:40000") + it('accepts an Address', function() { + var dest = Address.fromBase58Check('15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3') + + tx.addOutput(dest, 40000) verifyTransactionOut() }) - it('allows a TransactionOut object to be passed in', function(){ - var txCopy = tx.clone() - txCopy.addOutput("15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3:40000") - var transactionOut = txCopy.outs[0] + it('accepts a scriptPubKey', function() { + var dest = Address.fromBase58Check('15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3').toOutputScript() - tx.addOutput(transactionOut) + tx.addOutput(dest, 40000) verifyTransactionOut() }) - it('supports alternative networks', function(){ - var addr = 'mkHJaNR7uuwRG1JrmTZsV4MszaTKjCBvCR' - - tx.addOutput(addr, 40000) - verifyTransactionOut() + it('returns an index', function() { + var dest = '15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3' - assert.equal(tx.outs[0].address.toString(), addr) + assert.equal(tx.addOutput(dest, 40000), 0) + assert.equal(tx.addOutput(dest, 40000), 1) }) - function verifyTransactionOut(){ + function verifyTransactionOut() { assert.equal(tx.outs.length, 1) var output = tx.outs[0] assert.equal(output.value, 40000) - assert.equal(b2h(output.script.buffer), "76a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac") + assert.equal(output.script.toHex(), "76a9143443bc45c560866cfeabf1d52f50a6ed358c69f288ac") } }) - describe('sign', function(){ - it('works', function(){ - tx.addInput("0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57:0") - tx.addOutput("15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3:40000") - tx.addOutput("1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd:50000") + describe('sign', function() { + it('works', function() { + tx.addInput("0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57", 0) + tx.addOutput("15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3", 40000) + tx.addOutput("1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd", 50000) var key = ECKey.fromWIF('L44f7zxJ5Zw4EK9HZtyAnzCYz2vcZ5wiJf9AuwhJakiV4xVkxBeb') tx.sign(0, key) @@ -204,14 +179,14 @@ describe('Transaction', function() { }) }) - describe('validateInput', function(){ + describe('validateInput', function() { var validTx beforeEach(function() { validTx = Transaction.fromHex(fixtureTx2Hex) }) - it('returns true for valid signature', function(){ + it('returns true for valid signature', function() { var key = ECKey.fromWIF('L44f7zxJ5Zw4EK9HZtyAnzCYz2vcZ5wiJf9AuwhJakiV4xVkxBeb') var script = prevTx.outs[0].script var sig = new Buffer(validTx.ins[0].script.chunks[0]) @@ -219,28 +194,6 @@ describe('Transaction', function() { assert.equal(validTx.validateInput(0, script, key.pub, sig), true) }) }) - - describe('estimateFee', function(){ - it('works for fixture tx 1', function(){ - var tx = Transaction.fromHex(fixtureTx1Hex) - assert.equal(tx.estimateFee(), 20000) - }) - - it('works for fixture big tx', function(){ - var tx = Transaction.fromHex(fixtureTxBigHex) - assert.equal(tx.estimateFee(), 60000) - }) - - it('allow feePerKb to be passed in as an argument', function(){ - var tx = Transaction.fromHex(fixtureTx2Hex) - assert.equal(tx.estimateFee(10000), 10000) - }) - - it('allow feePerKb to be set to 0', function(){ - var tx = Transaction.fromHex(fixtureTx2Hex) - assert.equal(tx.estimateFee(0), 0) - }) - }) }) describe('signInput', function() { @@ -274,5 +227,32 @@ describe('Transaction', function() { assert.equal(tx.toHex(), expected) }) }) + + describe('getId', function() { + it('returns the expected txid', function() { + var tx = new Transaction() + tx.addInput('d6f72aab8ff86ff6289842a0424319bf2ddba85dc7c52757912297f948286389', 0) + tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1) + + assert.equal(tx.getId(), '7c3275f1212fd1a2add614f47a1f1f7b6d9570a97cb88e0e2664ab1752976e9f') + }) + }) + + describe('clone', function() { + it('creates a new object', function() { + var txA = new Transaction() + txA.addInput('d6f72aab8ff86ff6289842a0424319bf2ddba85dc7c52757912297f948286389', 0) + txA.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1000) + + var txB = txA.clone() + + // Enforce value equality + assert.deepEqual(txA, txB) + + // Enforce reference inequality + assert.notEqual(txA.ins[0], txB.ins[0]) + assert.notEqual(txA.outs[0], txB.outs[0]) + }) + }) }) diff --git a/test/wallet.js b/test/wallet.js index 0d857ad..ad94d03 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -6,7 +6,7 @@ var scripts = require('../src/scripts') var Address = require('../src/address') var HDNode = require('../src/hdnode') -var Transaction = require('../src/transaction').Transaction +var Transaction = require('../src/transaction') var Wallet = require('../src/wallet') var fixtureTxes = require('./fixtures/mainnet_tx') @@ -14,7 +14,15 @@ var fixtureTx1Hex = fixtureTxes.prevTx var fixtureTx2Hex = fixtureTxes.tx function fakeTxHash(i) { - return "efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + i + var hash = new Buffer(32) + hash.fill(i) + return hash +} + +function fakeTxId(i) { + var hash = fakeTxHash(i) + Array.prototype.reverse.call(hash) + return hash.toString('hex') } describe('Wallet', function() { @@ -263,12 +271,11 @@ describe('Wallet', function() { }) describe('processConfirmedTx', function(){ - it('does not fail on scripts with no corresponding Address', function() { var pubKey = wallet.getPrivateKey(0).pub var script = scripts.pubKeyOutput(pubKey) var tx2 = new Transaction() - tx2.addInput(fakeTxHash(1), 0) + tx2.addInput(fakeTxId(1), 0) // FIXME: Transaction doesn't support custom ScriptPubKeys... yet // So for now, we hijack the script with our own, and undefine the cached address @@ -303,44 +310,45 @@ describe('Wallet', function() { function outputCount(){ return Object.keys(wallet.outputs).length } - }) describe("when tx ins outpoint contains a known txhash:i", function(){ + var spendTx beforeEach(function(){ wallet.addresses = [addresses[0]] // the address fixtureTx2 used as input wallet.processConfirmedTx(tx) - tx = Transaction.fromHex(fixtureTx2Hex) + spendTx = Transaction.fromHex(fixtureTx2Hex) }) it("does not add to wallet.outputs", function(){ - var outputs = wallet.outputs - wallet.processConfirmedTx(tx) - assert.deepEqual(wallet.outputs, outputs) + wallet.processConfirmedTx(spendTx) + assert.deepEqual(wallet.outputs, {}) }) it("deletes corresponding 'output'", function(){ - wallet.processConfirmedTx(tx) + var txIn = spendTx.ins[0] + var txInId = new Buffer(txIn.hash) + Array.prototype.reverse.call(txInId) + txInId = txInId.toString('hex') - var txIn = tx.ins[0] - var key = txIn.outpoint.hash + ":" + txIn.outpoint.index - var output = wallet.outputs[key] + var expected = txInId + ':' + txIn.index + assert(expected in wallet.outputs) - assert.equal(output, undefined) + wallet.processConfirmedTx(spendTx) + assert(!(expected in wallet.outputs)) }) }) it("does nothing when none of the involved addresses belong to the wallet", function(){ - var outputs = wallet.outputs wallet.processConfirmedTx(tx) - assert.deepEqual(wallet.outputs, outputs) + assert.deepEqual(wallet.outputs, {}) }) }) function verifyOutputAdded(index, pending) { var txOut = tx.outs[index] - var key = tx.getHash() + ":" + index + var key = tx.getId() + ":" + index var output = wallet.outputs[key] assert.equal(output.receive, key) assert.equal(output.value, txOut.value) @@ -366,19 +374,19 @@ describe('Wallet', function() { // set up 3 utxo utxo = [ { - "hash": fakeTxHash(1), + "hash": fakeTxId(1), "outputIndex": 0, "address" : address1, "value": 400000 // not enough for value }, { - "hash": fakeTxHash(2), + "hash": fakeTxId(2), "outputIndex": 1, "address" : address1, "value": 500000 // enough for only value }, { - "hash": fakeTxHash(3), + "hash": fakeTxId(3), "outputIndex": 0, "address" : address2, "value": 520000 // enough for value and fee @@ -392,7 +400,8 @@ describe('Wallet', function() { var tx = wallet.createTx(to, value) assert.equal(tx.ins.length, 1) - assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) + assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) + assert.equal(tx.ins[0].index, 0) }) it('allows fee to be specified', function(){ @@ -400,8 +409,11 @@ describe('Wallet', function() { 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 }) + + assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) + assert.equal(tx.ins[0].index, 0) + assert.deepEqual(tx.ins[1].hash, fakeTxHash(2)) + assert.equal(tx.ins[1].index, 1) }) it('allows fee to be set to zero', function(){ @@ -410,13 +422,14 @@ describe('Wallet', function() { var tx = wallet.createTx(to, value, fee) assert.equal(tx.ins.length, 1) - assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) + assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) + assert.equal(tx.ins[0].index, 0) }) it('ignores pending outputs', function(){ utxo.push( { - "hash": fakeTxHash(4), + "hash": fakeTxId(4), "outputIndex": 0, "address" : address2, "value": 530000, @@ -428,7 +441,8 @@ describe('Wallet', function() { var tx = wallet.createTx(to, value) assert.equal(tx.ins.length, 1) - assert.deepEqual(tx.ins[0].outpoint, { hash: fakeTxHash(3), index: 0 }) + assert.deepEqual(tx.ins[0].hash, fakeTxHash(3)) + assert.equal(tx.ins[0].index, 0) }) }) @@ -438,7 +452,7 @@ describe('Wallet', function() { var address = wallet.generateAddress() wallet.setUnspentOutputs([{ - hash: fakeTxHash(0), + hash: fakeTxId(0), outputIndex: 0, address: address, value: value @@ -449,7 +463,9 @@ describe('Wallet', function() { var tx = wallet.createTx(to, toValue) assert.equal(tx.outs.length, 1) - assert.equal(tx.outs[0].address.toString(), to) + + var outAddress = Address.fromOutputScript(tx.outs[0].script, networks.testnet) + assert.equal(outAddress.toString(), to) assert.equal(tx.outs[0].value, toValue) }) }) @@ -460,7 +476,7 @@ describe('Wallet', function() { var address = wallet.generateAddress() wallet.setUnspentOutputs([{ - hash: fakeTxHash(0), + hash: fakeTxId(0), outputIndex: 0, address: address, value: value @@ -474,10 +490,14 @@ describe('Wallet', function() { var tx = wallet.createTx(to, toValue, fee, changeAddress) assert.equal(tx.outs.length, 2) - assert.equal(tx.outs[0].address.toString(), to) + + var outAddress0 = Address.fromOutputScript(tx.outs[0].script, networks.testnet) + var outAddress1 = Address.fromOutputScript(tx.outs[1].script, networks.testnet) + + assert.equal(outAddress0.toString(), to) assert.equal(tx.outs[0].value, toValue) - assert.equal(tx.outs[1].address.toString(), changeAddress) + assert.equal(outAddress1.toString(), changeAddress) assert.equal(tx.outs[1].value, value - (toValue + fee)) }) }) @@ -488,7 +508,9 @@ describe('Wallet', function() { assert.equal(tx.outs.length, 1) var out = tx.outs[0] - assert.equal(out.address, to) + var outAddress = Address.fromOutputScript(out.script) + + assert.equal(outAddress.toString(), to) assert.equal(out.value, value) }) @@ -501,7 +523,9 @@ describe('Wallet', function() { assert.equal(tx.outs.length, 2) var out = tx.outs[1] - assert.equal(out.address, wallet.changeAddresses[1]) + var outAddress = Address.fromOutputScript(out.script) + + assert.equal(outAddress.toString(), wallet.changeAddresses[1]) assert.equal(out.value, 15000) }) @@ -513,7 +537,9 @@ describe('Wallet', function() { assert.equal(wallet.changeAddresses.length, 1) var out = tx.outs[1] - assert.equal(out.address, wallet.changeAddresses[0]) + var outAddress = Address.fromOutputScript(out.script) + + assert.equal(outAddress.toString(), wallet.changeAddresses[0]) assert.equal(out.value, 15000) })