diff --git a/src/address.js b/src/address.js index d1d7b82..ded547e 100644 --- a/src/address.js +++ b/src/address.js @@ -1,5 +1,21 @@ var assert = require('assert') var base58check = require('./base58check') +var networks = require('./networks') +var Script = require('./script') + +function findScriptTypeByVersion(queryVersion) { + for (var networkName in networks) { + var network = networks[networkName] + + for (var versionName in network) { + var version = network[versionName] + + if (version === queryVersion) { + return versionName + } + } + } +} function Address(hash, version) { assert(Buffer.isBuffer(hash), 'First argument must be a Buffer') @@ -10,18 +26,55 @@ function Address(hash, version) { this.version = version } +Address.Error = function(message) { + this.name = 'AddressError' + this.message = message +} +Address.Error.prototype = new Error() +Address.Error.prototype.constructor = Address.Error + // Import functions Address.fromBase58Check = function(string) { var decode = base58check.decode(string) return new Address(decode.payload, decode.version) } -Address.prototype.fromString = Address.prototype.fromBase58Check + +Address.fromScriptPubKey = function(script, network) { + network = network || networks.bitcoin + + var type = script.getOutType() + + if (type === 'pubkeyhash') { + return new Address(new Buffer(script.chunks[2]), network.pubKeyHash) + } + + else if (type === 'scripthash') { + return new Address(new Buffer(script.chunks[1]), network.scriptHash) + } + + throw new Address.Error(type + ' has no matching Address') +} // Export functions Address.prototype.toBase58Check = function () { return base58check.encode(this.hash, this.version) } + +Address.prototype.toScriptPubKey = function() { + var scriptType = findScriptTypeByVersion(this.version) + + if (scriptType === 'pubKeyHash') { + return Script.createPubKeyHashScriptPubKey(this.hash) + } + + else if (scriptType === 'scriptHash') { + return Script.createP2SHScriptPubKey(this.hash) + } + + throw new Address.Error(this + ' has no matching script') +} + Address.prototype.toString = Address.prototype.toBase58Check module.exports = Address diff --git a/src/eckey.js b/src/eckey.js index 87d2934..fa238a9 100644 --- a/src/eckey.js +++ b/src/eckey.js @@ -1,7 +1,7 @@ var assert = require('assert') var base58check = require('./base58check') var ecdsa = require('./ecdsa') -var network = require('./network') +var networks = require('./networks') var secureRandom = require('secure-random') var Address = require('./address') @@ -72,7 +72,7 @@ ECKey.prototype.toHex = function() { } ECKey.prototype.toWIF = function(version) { - version = version || network.bitcoin.wif + version = version || networks.bitcoin.wif var buffer = this.toBuffer() if (this.pub.compressed) { @@ -115,7 +115,7 @@ ECPubKey.prototype.verify = function(hash, sig) { } ECPubKey.prototype.getAddress = function(version) { - version = version || network.bitcoin.pubKeyHash + version = version || networks.bitcoin.pubKeyHash return new Address(crypto.hash160(this.toBuffer()), version) } diff --git a/src/hdwallet.js b/src/hdwallet.js index 146b82d..966185d 100644 --- a/src/hdwallet.js +++ b/src/hdwallet.js @@ -8,7 +8,7 @@ var CJS = require('crypto-js') var crypto = require('./crypto') var ECKey = require('./eckey').ECKey var ECPubKey = require('./eckey').ECPubKey -var Network = require('./network') +var networks = require('./networks') var sec = require('./sec') var ecparams = sec("secp256k1") @@ -27,7 +27,7 @@ function HDWallet(seed, networkString) { this.chaincode = I.slice(32) this.network = networkString || 'bitcoin' - if(!Network.hasOwnProperty(this.network)) { + if(!networks.hasOwnProperty(this.network)) { throw new Error("Unknown network: " + this.network) } @@ -70,8 +70,8 @@ HDWallet.fromBuffer = function(input) { var version = input.readUInt32BE(0) var type - for(var name in Network) { - var network = Network[name] + for(var name in networks) { + var network = networks[name] for(var t in network.bip32) { if (version != network.bip32[t]) continue @@ -128,7 +128,7 @@ HDWallet.prototype.getAddress = function() { HDWallet.prototype.toBuffer = function(priv) { // Version - var version = Network[this.network].bip32[priv ? 'priv' : 'pub'] + var version = networks[this.network].bip32[priv ? 'priv' : 'pub'] var buffer = new Buffer(HDWallet.LENGTH) // 4 bytes: version bytes @@ -245,7 +245,7 @@ HDWallet.prototype.derivePrivate = function(index) { } HDWallet.prototype.getKeyVersion = function() { - return Network[this.network].pubKeyHash + return networks[this.network].pubKeyHash } HDWallet.prototype.toString = HDWallet.prototype.toBase58 diff --git a/src/index.js b/src/index.js index c236a02..484342d 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,6 @@ module.exports = { Transaction: T.Transaction, TransactionIn: T.TransactionIn, TransactionOut: T.TransactionOut, - network: require('./network'), + networks: require('./networks'), Wallet: require('./wallet') } diff --git a/src/network.js b/src/networks.js similarity index 100% rename from src/network.js rename to src/networks.js diff --git a/src/script.js b/src/script.js index 8f3cd2c..6add3f1 100644 --- a/src/script.js +++ b/src/script.js @@ -1,18 +1,17 @@ -var assert = require('assert') var Address = require('./address') +var assert = require('assert') var crypto = require('./crypto') -var convert = require('./convert') -var Network = require('./network') var Opcode = require('./opcode') function Script(data) { - this.buffer = data || [] - if(!Array.isArray(this.buffer)) { - throw new Error('expect Script to be initialized with Array, but got ' + data) - } + data = data || [] + assert(Array.isArray(data), 'Expected Array, got ' + data) + + this.buffer = data this.parse() } +// Import operations Script.fromBuffer = function(buffer) { assert(Buffer.isBuffer(buffer)) // FIXME: transitionary @@ -23,30 +22,13 @@ Script.fromHex = function(hex) { return Script.fromBuffer(new Buffer(hex, 'hex')) } -Script.fromPubKey = function(str) { - var script = new Script() - var s = str.split(' ') - for (var i in s) { - if (Opcode.map.hasOwnProperty(s[i])) { - script.writeOp(Opcode.map[s[i]]) - } else { - script.writeBytes(convert.hexToBytes(s[i])) - } - } - return script +// Export operations +Script.prototype.toBuffer = function() { + return new Buffer(this.buffer) } -Script.fromScriptSig = function(str) { - var script = new Script() - var s = str.split(' ') - for (var i in s) { - if (Opcode.map.hasOwnProperty(s[i])) { - script.writeOp(Opcode.map[s[i]]) - } else { - script.writeBytes(convert.hexToBytes(s[i])) - } - } - return script +Script.prototype.toHex = function() { + return this.toBuffer().toString('hex') } /** @@ -202,40 +184,10 @@ function isSmallIntOp(opcode) { ((opcode >= Opcode.map.OP_1) && (opcode <= Opcode.map.OP_16))) } -/** - * Returns the address corresponding to this output in hash160 form. - * Assumes strange scripts are P2SH - */ -Script.prototype.toScriptHash = function() { - if(isPubkeyhash.call(this)) { - return this.chunks[2] - } - - if(isScripthash.call(this)) { - return crypto.hash160(this.buffer) - } - +Script.prototype.getHash = function() { return crypto.hash160(this.buffer) } -Script.prototype.getToAddress = function(network) { - network = network || Network.bitcoin - - if(isPubkeyhash.call(this)) { - return new Address(new Buffer(this.chunks[2]), network.pubKeyHash) - } - - assert(isScripthash.call(this)) - - return new Address(new Buffer(this.chunks[1]), network.scriptHash) -} - -Script.prototype.getFromAddress = function(version) { - version = version || Network.bitcoin.pubKeyHash - - return new Address(this.simpleInHash(), version) -} - /** * Compare the script to known templates of scriptSig. * @@ -281,58 +233,6 @@ Script.prototype.getInType = function() { } } -/** - * Returns the affected public key for this input. - * - * This currently only works with payToPubKeyHash transactions. It will also - * work in the future for standard payToScriptHash transactions that use a - * single public key. - * - * However for multi-key and other complex transactions, this will only return - * one of the keys or raise an error. Therefore, it is recommended for indexing - * purposes to use Script#simpleInHash or Script#simpleOutHash instead. - * - * @deprecated - */ -Script.prototype.simpleInPubKey = function() { - switch (this.getInType()) { - case 'pubkeyhash': - return this.chunks[1] - case 'pubkey': - // TODO: Theoretically, we could recover the pubkey from the sig here. - // See https://bitcointalk.org/?topic=6430.0 - throw new Error('Script does not contain pubkey') - default: - throw new Error('Encountered non-standard scriptSig') - } -} - -/** - * Returns the affected address hash for this input. - * - * For standard transactions, this will return the hash of the pubKey that - * can spend this output. - * - * In the future, for standard payToScriptHash inputs, this will return the - * scriptHash. - * - * Note: This function provided for convenience. If you have the corresponding - * scriptPubKey available, you are urged to use Script#simpleOutHash instead - * as it is more reliable for non-standard payToScriptHash transactions. - * - * This method is useful for indexing transactions. - */ -Script.prototype.simpleInHash = function() { - return crypto.hash160(this.simpleInPubKey()) -} - -/** - * Old name for Script#simpleInHash. - * - * @deprecated - */ -Script.prototype.simpleInPubKeyHash = Script.prototype.simpleInHash - /** * Add an op code to the script. */ @@ -347,7 +247,7 @@ Script.prototype.writeOp = function(opcode) { Script.prototype.writeBytes = function(data) { // FIXME: Script module doesn't support buffers yet if (Buffer.isBuffer(data)) data = Array.prototype.slice.call(data); - assert(Array.isArray(data), "Expect a byte array. Got" + data) + assert(Array.isArray(data), "Expected a byte array. Got " + data) if (data.length < Opcode.map.OP_PUSHDATA1) { this.buffer.push(data.length) @@ -369,48 +269,32 @@ Script.prototype.writeBytes = function(data) { this.chunks.push(data) } -/** - * Create an output for an address - */ -Script.createOutputScript = function(address, network) { - assert(address instanceof Address) - network = network || Network.bitcoin - +// OP_DUP OP_HASH160 {pubKeyHash} OP_EQUALVERIFY OP_CHECKSIG +Script.createPubKeyHashScriptPubKey = function(hash) { var script = new Script() - // Standard pay-to-script-hash - if (address.version === network.scriptHash) { - script.writeOp(Opcode.map.OP_HASH160) - script.writeBytes(address.hash) - script.writeOp(Opcode.map.OP_EQUAL) - - return script - } - - assert.strictEqual(address.version, network.pubKeyHash, 'Unknown address type') - - // Standard pay-to-pubkey-hash script.writeOp(Opcode.map.OP_DUP) script.writeOp(Opcode.map.OP_HASH160) - script.writeBytes(address.hash) + script.writeBytes(hash) script.writeOp(Opcode.map.OP_EQUALVERIFY) script.writeOp(Opcode.map.OP_CHECKSIG) return script } -/** - * Extract pubkeys from a multisig script - */ +// OP_HASH160 {scriptHash} OP_EQUAL +Script.createP2SHScriptPubKey = function(hash) { + var script = new Script() -Script.prototype.extractPubkeys = function() { - return this.chunks.filter(function(chunk) { - return(chunk[0] == 4 && chunk.length == 65 || chunk[0] < 4 && chunk.length == 33) - }) + script.writeOp(Opcode.map.OP_HASH160) + script.writeBytes(hash) + script.writeOp(Opcode.map.OP_EQUAL) + + return script } // m [pubKeys ...] n OP_CHECKMULTISIG -Script.createMultisigOutputScript = function(m, pubKeys) { +Script.createMultisigScriptPubKey = function(m, pubKeys) { var script = new Script() pubKeys = pubKeys.sort() @@ -433,7 +317,15 @@ Script.createPubKeyHashScriptSig = function(signature, pubKey) { } // OP_0 [signatures ...] -Script.createMultisigScriptSig = function(signatures) { +Script.createMultisigScriptSig = function(signatures, scriptPubKey) { + if (scriptPubKey) { + assert(isMultisig.call(scriptPubKey)) + + var m = scriptPubKey.chunks[0] + var k = m - (Opcode.map.OP_1 - 1) + assert(k <= signatures.length, 'Not enough signatures provided') + } + var inScript = new Script() inScript.writeOp(Opcode.map.OP_0) @@ -451,18 +343,6 @@ Script.createP2SHScriptSig = function(scriptSig, scriptPubKey) { return inScript } -// [signatures ...] {m [pubKeys ...] n OP_CHECKSIG} -Script.createP2SHMultisigScriptSig = function(signatures, scriptPubKey) { - assert(isMultisig.call(scriptPubKey)) - - var m = scriptPubKey.chunks[0] - var k = m - (Opcode.map.OP_1 - 1) - assert(k <= signatures.length, 'Not enough signatures provided') - - var scriptSig = Script.createMultisigScriptSig(signatures) - return Script.createP2SHScriptSig(scriptSig, scriptPubKey) -} - Script.prototype.clone = function() { return new Script(this.buffer) } diff --git a/src/transaction.js b/src/transaction.js index 72f09fc..a0d77d0 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -9,7 +9,6 @@ var convert = require('./convert') var crypto = require('./crypto') var ECKey = require('./eckey').ECKey var ecdsa = require('./ecdsa') -var Network = require('./network') var Transaction = function (doc) { if (!(this instanceof Transaction)) { return new Transaction(doc) } @@ -90,11 +89,10 @@ Transaction.prototype.addInput = function (tx, outIndex) { * i) An existing TransactionOut object * ii) An address object or a string address, and a value * iii) An address:value string - * iv) Either ii), iii) with an optional network argument * * FIXME: This is a bit convoluted */ -Transaction.prototype.addOutput = function (address, value, network) { +Transaction.prototype.addOutput = function (address, value) { if (arguments[0] instanceof TransactionOut) { this.outs.push(arguments[0]) return @@ -105,19 +103,15 @@ Transaction.prototype.addOutput = function (address, value, network) { var args = arguments[0].split(':') address = args[0] value = parseInt(args[1]) - - network = arguments[1] } address = Address.fromBase58Check(address) } - network = network || Network.bitcoin - this.outs.push(new TransactionOut({ value: value, - script: Script.createOutputScript(address, network), - network: network + script: address.toScriptPubKey(), + address: address // TODO: Remove me })) } @@ -364,56 +358,18 @@ Transaction.deserialize = function(buffer) { /** * Signs a standard output at some index with the given key - * FIXME: network support is ugly */ -Transaction.prototype.sign = function(index, key, type, network) { +Transaction.prototype.sign = function(index, key, type) { assert(key instanceof ECKey) - network = network || Network.bitcoin - var address = key.pub.getAddress(network.pubKeyHash) - - // FIXME: Assumed prior TX was pay-to-pubkey-hash - var script = Script.createOutputScript(address, network) + var script = key.pub.getAddress().toScriptPubKey() var signature = this.signScriptSig(index, script, key, type) + // FIXME: Assumed prior TX was pay-to-pubkey-hash var scriptSig = Script.createPubKeyHashScriptSig(signature, key.pub) this.setScriptSig(index, scriptSig) } -// Takes outputs of the form [{ output: 'txhash:index', address: 'address' },...] -Transaction.prototype.signWithKeys = function(keys, outputs, type) { - type = type || SIGHASH_ALL - - var addrdata = keys.map(function(key) { - assert(key instanceof ECKey) - - return { - key: key, - address: key.getAddress().toString() - } - }) - - var hmap = {} - outputs.forEach(function(o) { - hmap[o.output] = o - }) - - for (var i = 0; i < this.ins.length; i++) { - var outpoint = this.ins[i].outpoint.hash + ':' + this.ins[i].outpoint.index - var histItem = hmap[outpoint] - - if (!histItem) continue; - - var thisInputAddrdata = addrdata.filter(function(a) { - return a.address == histItem.address - }) - - if (thisInputAddrdata.length === 0) continue; - - this.sign(i,thisInputAddrdata[0].key) - } -} - Transaction.prototype.signScriptSig = function(index, script, key, type) { type = type || SIGHASH_ALL @@ -458,14 +414,8 @@ var TransactionIn = function (data) { this.outpoint = { hash: data.hash, index: data.index } } - if (data.scriptSig) { - this.script = Script.fromScriptSig(data.scriptSig) - } else if (data.script) { - this.script = data.script - } else { - this.script = new Script(data.script) - } - + assert(data.script, 'Invalid TxIn parameters') + this.script = data.script this.sequence = data.sequence || this.defaultSequence } @@ -480,38 +430,20 @@ TransactionIn.prototype.clone = function () { }) } -// FIXME: Support for alternate networks -var TransactionOut = function (data) { - this.script = - data.script instanceof Script ? data.script.clone() - : Array.isArray(data.script) ? new Script(data.script) - : typeof data.script == "string" ? new Script(convert.hexToBytes(data.script)) - : data.scriptPubKey ? Script.fromScriptSig(data.scriptPubKey) - : data.address ? Script.createOutputScript(data.address) - : new Script() +function TransactionOut(data) { + this.script = data.script + this.value = data.value + this.address = data.address - var network = data.network || Network.bitcoin - if (this.script.buffer.length > 0) { - this.address = this.script.getToAddress(network) - } - - this.value = - Array.isArray(data.value) ? convert.bytesToNum(data.value) - : "string" == typeof data.value ? parseInt(data.value) - : data.value instanceof BigInteger ? parseInt(data.value.toString()) - : data.value + if (data.address) this.address = data.address } TransactionOut.prototype.clone = function() { - var newTxout = new TransactionOut({ + return new TransactionOut({ script: this.script.clone(), - value: this.value + value: this.value, + address: this.address }) - return newTxout -} - -TransactionOut.prototype.scriptPubKey = function() { - return convert.bytesToHex(this.script.buffer) } module.exports = { diff --git a/src/wallet.js b/src/wallet.js index e843dba..08333c3 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1,8 +1,9 @@ +var Address = require('./address') var convert = require('./convert') -var Transaction = require('./transaction').Transaction var HDNode = require('./hdwallet.js') +var networks = require('./networks') var rng = require('secure-random') -var Network = require('./network') +var Transaction = require('./transaction').Transaction function Wallet(seed, options) { if (!(this instanceof Wallet)) { return new Wallet(seed, options); } @@ -42,7 +43,6 @@ function Wallet(seed, options) { } this.newMasterKey(seed, network) - this.generateAddress = function() { var key = externalAccount.derive(this.addresses.length) this.addresses.push(key.getAddress().toString()) @@ -150,9 +150,17 @@ function Wallet(seed, options) { var txhash = tx.getHash() tx.outs.forEach(function(txOut, i){ - var address = txOut.address.toString() + var address + + try { + address = Address.fromScriptPubKey(txOut.script, networks[network]).toString() + } catch(e) { + if (!(e instanceof Address.Error)) throw e + } + if (isMyAddress(address)) { - var output = txhash+':'+i + var output = txhash + ':' + i + me.outputs[output] = { receive: output, value: txOut.value, @@ -165,7 +173,7 @@ function Wallet(seed, options) { var op = txIn.outpoint var o = me.outputs[op.hash+':'+op.index] if (o) { - o.spend = txhash+':'+i + o.spend = txhash + ':' +i } }) } @@ -174,7 +182,7 @@ function Wallet(seed, options) { checkDust(value) var tx = new Transaction() - tx.addOutput(to, value, Network[network]) + tx.addOutput(to, value) var utxo = getCandidateOutputs(value) var totalInValue = 0 @@ -190,7 +198,7 @@ function Wallet(seed, options) { var change = totalInValue - value - fee if(change > 0 && !isDust(change)) { - tx.addOutput(changeAddress || getChangeAddress(), change, Network[network]) + tx.addOutput(changeAddress || getChangeAddress(), change) } break } @@ -246,7 +254,7 @@ function Wallet(seed, options) { function estimateFeePadChangeOutput(tx){ var tmpTx = tx.clone() - tmpTx.addOutput(getChangeAddress(), 0, Network[network]) + tmpTx.addOutput(getChangeAddress(), 0) return tmpTx.estimateFee() } @@ -266,7 +274,7 @@ function Wallet(seed, options) { 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), false, Network[network]) + tx.sign(i, me.getPrivateKeyForAddress(output.address), false) } }) return tx diff --git a/test/address.js b/test/address.js index 25a541f..c276e87 100644 --- a/test/address.js +++ b/test/address.js @@ -1,15 +1,17 @@ var assert = require('assert') var Address = require('..').Address +var networks = require('..').networks +var Script = require('..').Script var b58fixtures = require('./fixtures/base58') var fixtures = require('./fixtures/address') -describe('Address', function() { - var bothVectors = fixtures.pubKeyHash.concat(fixtures.scriptHash) +function h2b(h) { return new Buffer(h, 'hex') } +describe('Address', function() { describe('Constructor', function() { it('does not mutate the input', function() { - bothVectors.forEach(function(f) { + fixtures.valid.forEach(function(f) { var hash = new Buffer(f.hex, 'hex') var addr = new Address(hash, f.version) @@ -28,8 +30,8 @@ describe('Address', function() { }) }) - bothVectors.forEach(function(f) { - it('imports ' + f.description + ' correctly', function() { + fixtures.valid.forEach(function(f) { + it('imports ' + f.description + '(' + f.network + ') correctly', function() { var addr = Address.fromBase58Check(f.base58check) assert.equal(addr.version, f.version) @@ -38,9 +40,21 @@ describe('Address', function() { }) }) + describe('fromScriptPubKey', function() { + fixtures.valid.forEach(function(f) { + it('imports ' + f.description + '(' + f.network + ') correctly', function() { + var script = Script.fromHex(f.script) + var addr = Address.fromScriptPubKey(script, networks[f.network]) + + assert.equal(addr.version, f.version) + assert.equal(addr.hash.toString('hex'), f.hex) + }) + }) + }) + describe('toBase58Check', function() { - bothVectors.forEach(function(f) { - it('exports ' + f.description + ' correctly', function() { + fixtures.valid.forEach(function(f) { + it('exports ' + f.description + '(' + f.network + ') correctly', function() { var addr = Address.fromBase58Check(f.base58check) var result = addr.toBase58Check() @@ -48,4 +62,25 @@ describe('Address', function() { }) }) }) + + describe('toScriptPubKey', function() { + fixtures.valid.forEach(function(f) { + it('imports ' + f.description + '(' + f.network + ') correctly', function() { + var addr = Address.fromBase58Check(f.base58check) + var script = addr.toScriptPubKey() + + assert.equal(script.toHex(), f.script) + }) + }) + + fixtures.invalid.toScriptPubKey.forEach(function(f) { + it('throws on ' + f.description, function() { + var addr = new Address(h2b(f.hex), f.version) + + assert.throws(function() { + addr.toScriptPubKey() + }) + }) + }) + }) }) diff --git a/test/fixtures/address.js b/test/fixtures/address.js index dd4fa28..7a6becc 100644 --- a/test/fixtures/address.js +++ b/test/fixtures/address.js @@ -1,30 +1,45 @@ module.exports = { - pubKeyHash: [ + valid: [ { - description: 'pubKeyHash (bitcoin)', + description: 'pubKeyHash', + network: 'bitcoin', version: 0, hex: '751e76e8199196d454941c45d1b3a323f1433bd6', - base58check: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH' + base58check: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', + script: '76a914751e76e8199196d454941c45d1b3a323f1433bd688ac' }, { - description: 'pubKeyHash (testnet)', + description: 'pubKeyHash', + network: 'testnet', version: 111, hex: '751e76e8199196d454941c45d1b3a323f1433bd6', - base58check: 'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r' - } - ], - scriptHash: [ - { - description: 'scriptHash (bitcoin)', - version: 5, - hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6', - base58check: '3LRW7jeCvQCRdPF8S3yUCfRAx4eqXFmdcr' + base58check: 'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', + script: '76a914751e76e8199196d454941c45d1b3a323f1433bd688ac' }, { - description: 'scriptHash (testnet)', + description: 'scriptHash', + network: 'bitcoin', + version: 5, + hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6', + base58check: '3LRW7jeCvQCRdPF8S3yUCfRAx4eqXFmdcr', + script: 'a914cd7b44d0b03f2d026d1e586d7ae18903b0d385f687' + }, + { + description: 'scriptHash', + network: 'testnet', version: 196, hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6', - base58check: '2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w' + base58check: '2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w', + script: 'a914cd7b44d0b03f2d026d1e586d7ae18903b0d385f687' } - ] + ], + invalid: { + toScriptPubKey: [ + { + description: 'Unknown Address version', + version: 0x99, + hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6' + } + ] + } } diff --git a/test/fixtures/script.js b/test/fixtures/script.js new file mode 100644 index 0000000..8fb5d21 --- /dev/null +++ b/test/fixtures/script.js @@ -0,0 +1,54 @@ +module.exports = { + valid: [ + { + description: 'P2SH ScriptPubKey', + hex: 'a914e8c300c87986efa84c37c0519929019ef86eb5b487', + type: 'scripthash', + hash: '0ba47b56a573bab4b430ad6ed3ec79270e04b066', + scriptPubKey: true + }, + { + description: 'PubKeyHash ScriptPubKey', + hex: '76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac', + type: 'pubkeyhash', + hash: 'a5313f33d5c7b81674b35f7f3febc3522ef234db', + scriptPubKey: true + }, + { + description: 'pubKeyHash scriptSig', + hex: '48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8', + type: 'pubkeyhash', + hash: 'b9bac2a5c5c29bb27c382d41fa3d179c646c78fd', + scriptPubKey: false + }, + { + description: 'Valid multisig script', + hex: '5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae', + type: 'multisig', + hash: 'f1c98f0b74ecabcf78ae20dfa224bb6666051fbe', + scriptPubKey: true + }, + { + description: 'OP_RETURN script', + hex:'6a2606deadbeef03f895a2ad89fb6d696497af486cb7c644a27aa568c7a18dd06113401115185474', + type: 'nulldata', + hash: 'ec88f016655477663455fe6a8e83508c348ea145', + scriptPubKey: true + }, + { + description: 'Non standard script', + hex: 'aa206fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000087', + type: 'nonstandard', + hash: '3823382e70d1930989813d3459988e0d7c2861d8', + scriptPubKey: true + }, + { + description: 'Invalid multisig script', + asm: '0 0 0 OP_CHECKmulTISIG', + hex: '000000ae', + type: 'nonstandard', + hash: '62ede8963f9387544935f168745262f703dab1fb', + scriptPubKey: true + } + ] +} diff --git a/test/integration/p2sh.js b/test/integration/p2sh.js index b52f9a6..55978bb 100644 --- a/test/integration/p2sh.js +++ b/test/integration/p2sh.js @@ -5,7 +5,7 @@ var ECKey = require('../../src/eckey').ECKey; var T = require('../../src/transaction'); var Transaction = T.Transaction; var Script = require('../../src/script'); -var network = require('../../src/network'); +var networks = require('../../src/networks'); var crypto = require('../../src/crypto'); var helloblock = require('helloblock-js')({ @@ -31,7 +31,7 @@ describe('p2sh', function() { }) var redeemScript = Script.createMultisigOutputScript(2, pubKeyBuffers) var hash160 = crypto.hash160(redeemScript.buffer) - var multisigAddress = new Address(hash160, network.testnet.scriptHash) + var multisigAddress = new Address(hash160, networks.testnet.scriptHash) // Check what our target address's starting value is var targetAddress = 'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r'; @@ -50,7 +50,7 @@ describe('p2sh', function() { var tx = new Transaction() var unspent = resource[0]; tx.addInput(unspent.txHash, unspent.index) - tx.addOutput(targetAddress, 100000, network.testnet) + tx.addOutput(targetAddress, 100000, networks.testnet) var signatures = privKeys.map(function(privKey) { return tx.signScriptSig(0, redeemScript, privKey) diff --git a/test/message.js b/test/message.js index 6589250..12831da 100644 --- a/test/message.js +++ b/test/message.js @@ -1,7 +1,7 @@ var assert = require('assert') var ECKey = require('../src/eckey').ECKey var Message = require('../').Message -var network = require('../').network +var networks = require('../').networks var fixtures = require('./fixtures/message') @@ -86,7 +86,7 @@ describe('Message', function() { var key = ECKey.makeRandom() var sig = Message.sign(key, msg) - var addr = key.pub.getAddress(network.testnet.pubKeyHash) + var addr = key.pub.getAddress(networks.testnet.pubKeyHash) assert(Message.verify(addr, sig, msg)) }) }) diff --git a/test/script.js b/test/script.js index 3d15610..218e971 100644 --- a/test/script.js +++ b/test/script.js @@ -1,34 +1,16 @@ var assert = require('assert') var crypto = require('..').crypto -var network = require('..').network +var networks = require('..').networks var Address = require('../src/address.js') var Script = require('../src/script.js') +var fixtures = require('./fixtures/script') + function b2h(b) { return new Buffer(b).toString('hex') } function h2b(h) { return new Buffer(h, 'hex') } describe('Script', function() { - var p2shScriptPubKey, pubkeyScriptPubkey, addressScriptSig - - beforeEach(function(){ - p2shScriptPubKey = "a914e8c300c87986efa84c37c0519929019ef86eb5b487" - pubkeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac" - addressScriptSig = "48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8" - - // txid: 09dd94f2c85262173da87a745a459007bb1eed6eeb6bfa238a0cd91a16cf7790 - validMultisigScript = '5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae' - - // txid: 5e9be7fb36ee49ce84bee4c8ef38ad0efc0608b78dae1c2c99075297ef527890 - opreturnScript = '6a2606deadbeef03f895a2ad89fb6d696497af486cb7c644a27aa568c7a18dd06113401115185474' - - // asm: "0 0 0 OP_CHECKMULTISIG" - invalidMultisigScript = '000000ae' - - // txid: a4bfa8ab6435ae5f25dae9d89e4eb67dfa94283ca751f393c1ddc5a837bbc31b - nonStandardScript = 'aa206fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000087' - }) - describe('constructor', function() { it('works for a byte array', function() { assert.ok(new Script([])) @@ -43,65 +25,69 @@ describe('Script', function() { }) }) - describe('getOutType', function() { - it('supports p2sh', function() { - var script = Script.fromHex(p2shScriptPubKey) - assert.equal(script.getOutType(), 'scripthash') + describe('fromHex/toHex', function() { + fixtures.valid.forEach(function(f) { + it('decodes/encodes ' + f.description, function() { + assert.equal(Script.fromHex(f.hex).toHex(), f.hex) + }) }) + }) - it('supports pubkeyhash', function() { - var script = Script.fromHex(pubkeyScriptPubKey) - assert.equal(script.getOutType(), 'pubkeyhash') - }) + describe('getHash', function() { + it('matches the test vectors', function() { + fixtures.valid.forEach(function(f) { + var script = Script.fromHex(f.hex) - it('supports multisig', function() { - var script = Script.fromHex(validMultisigScript) - assert.equal(script.getOutType(), 'multisig') - }) - - it('supports null_data', function() { - var script = Script.fromHex(opreturnScript) - assert.equal(script.getOutType(), 'nulldata') - }) - - it('supports nonstandard script', function() { - var script = Script.fromHex(nonStandardScript) - assert.equal(script.getOutType(), 'nonstandard') - }) - - it('identifies invalid multisig script as nonstandard', function() { - var script = Script.fromHex(invalidMultisigScript) - assert.equal(script.getOutType(), 'nonstandard') + assert.equal(script.getHash().toString('hex'), f.hash) + }) }) }) describe('getInType', function() { - it('works for address', function() { - var script = Script.fromHex(addressScriptSig) - assert.equal(script.getInType(), 'pubkeyhash') + fixtures.valid.forEach(function(f) { + if (!f.scriptPubKey) { + it('supports ' + f.description, function() { + var script = Script.fromHex(f.hex) + + assert.equal(script.getInType(), f.type) + }) + } }) }) - describe('getToAddress', function() { - it('works for p2sh type output', function() { - var script = Script.fromHex(p2shScriptPubKey) - assert.equal(script.getToAddress().toString(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') - }) + describe('getOutType', function() { + fixtures.valid.forEach(function(f) { + if (f.scriptPubKey) { + it('supports ' + f.description, function() { + var script = Script.fromHex(f.hex) - it('works for pubkey type output', function() { - var script = Script.fromHex(pubkeyScriptPubKey) - assert.equal(script.getToAddress().toString(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + assert.equal(script.getOutType(), f.type) + }) + } }) }) - describe('getFromAddress', function() { - it('works for address type input', function() { - var script = Script.fromHex(addressScriptSig) - assert.equal(script.getFromAddress().toString(), '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u') + describe('pay-to-pubKeyHash', function() { + it('matches the test data', function() { + var address = Address.fromBase58Check('19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + var script = Script.createPubKeyHashScriptPubKey(address.hash) + + // FIXME: not good TDD + assert.equal(script.toHex(), fixtures.valid[1].hex) }) }) - describe('2-of-3 Multi-Signature', function() { + describe('pay-to-scriptHash', function() { + it('matches the test data', function() { + var hash = new Buffer('e8c300c87986efa84c37c0519929019ef86eb5b4', 'hex') + var script = Script.createP2SHScriptPubKey(hash) + + // FIXME: not good TDD + assert.equal(script.toHex(), fixtures.valid[0].hex) + }) + }) + + describe('2-of-3 Multi-Signature scriptPubKey', function() { var pubKeys beforeEach(function() { @@ -113,10 +99,10 @@ describe('Script', function() { }) it('should create valid redeemScript', function() { - var redeemScript = Script.createMultisigOutputScript(2, pubKeys) + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) var hash160 = crypto.hash160(redeemScript.buffer) - var multisigAddress = new Address(hash160, network.bitcoin.scriptHash) + var multisigAddress = new Address(hash160, networks.bitcoin.scriptHash) assert.equal(multisigAddress.toString(), '32vYjxBb7pHJJyXgNk8UoK3BdRDxBzny2v') }) @@ -134,10 +120,20 @@ describe('Script', function() { var expected = '0047304402207515cf147d201f411092e6be5a64a6006f9308fad7b2a8fdaab22cd86ce764c202200974b8aca7bf51dbf54150d3884e1ae04f675637b926ec33bf75939446f6ca2801483045022100ef253c1faa39e65115872519e5f0a33bbecf430c0f35cf562beabbad4da24d8d02201742be8ee49812a73adea3007c9641ce6725c32cd44ddb8e3a3af460015d14050147522102359c6e3f04cefbf089cf1d6670dc47c3fb4df68e2bad1fa5a369f9ce4b42bbd1210395a9d84d47d524548f79f435758c01faec5da2b7e551d3b8c995b7e06326ae4a52ae' it('should create a valid P2SH multisig scriptSig', function() { - var redeemScript = Script.createMultisigOutputScript(2, pubKeys) - var actual = Script.createP2SHMultisigScriptSig(signatures, redeemScript) + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) + var redeemScriptSig = Script.createMultisigScriptSig(signatures) - assert.equal(b2h(actual.buffer), expected) + var scriptSig = Script.createP2SHScriptSig(redeemScriptSig, redeemScript) + + assert.equal(b2h(scriptSig.buffer), expected) + }) + + it('should throw on not enough signatures', function() { + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) + + assert.throws(function() { + Script.createMultisigScriptSig(signatures.slice(1), redeemScript) + }) }) }) }) diff --git a/test/transaction.js b/test/transaction.js index e672411..d4e1e13 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -2,11 +2,11 @@ var assert = require('assert') var Address = require('../src/address') var ECKey = require('../src/eckey').ECKey +var networks = require('..').networks var T = require('../src/transaction') var Transaction = T.Transaction var TransactionOut = T.TransactionOut var Script = require('../src/script') -var network = require('..').network var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx @@ -72,9 +72,7 @@ describe('Transaction', function() { var output = tx.outs[0] assert.equal(output.value, 5000000000) - assert.equal(b2h(output.script.toScriptHash()), "dd40dedd8f7e37466624c4dacc6362d8e7be23dd") - // assert.equal(output.address.toString(), "n1gqLjZbRH1biT5o4qiVMiNig8wcCPQeB9") - // TODO: address is wrong because it's a testnet transaction. Transaction needs to support testnet + assert.deepEqual(output.script, Address.fromBase58Check('n1gqLjZbRH1biT5o4qiVMiNig8wcCPQeB9').toScriptPubKey()) }) it('assigns hash to deserialized object', function(){ @@ -176,7 +174,7 @@ describe('Transaction', function() { it('supports alternative networks', function(){ var addr = 'mkHJaNR7uuwRG1JrmTZsV4MszaTKjCBvCR' - tx.addOutput(addr, 40000, network.testnet) + tx.addOutput(addr, 40000) verifyTransactionOut() assert.equal(tx.outs[0].address.toString(), addr) @@ -250,7 +248,7 @@ describe('Transaction', function() { it('works for multi-sig redeem script', function() { var tx = new Transaction() tx.addInput('d6f72aab8ff86ff6289842a0424319bf2ddba85dc7c52757912297f948286389', 0) - tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1, network.testnet) + tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1) var privKeys = [ '5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf', @@ -260,13 +258,14 @@ describe('Transaction', function() { }) var pubKeys = privKeys.map(function(eck) { return eck.pub }) var pubKeyBuffers = pubKeys.map(function(q) { return q.toBuffer() }) - var redeemScript = Script.createMultisigOutputScript(2, pubKeyBuffers) + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeyBuffers) var signatures = privKeys.map(function(privKey) { return tx.signScriptSig(0, redeemScript, privKey) }) - var scriptSig = Script.createP2SHMultisigScriptSig(signatures, redeemScript) + var redeemScriptSig = Script.createMultisigScriptSig(signatures) + var scriptSig = Script.createP2SHScriptSig(redeemScriptSig, redeemScript) tx.setScriptSig(0, scriptSig) signatures.forEach(function(sig, i){ @@ -279,18 +278,6 @@ describe('Transaction', function() { }) describe('TransactionOut', function() { - describe('scriptPubKey', function() { - it('returns hex string', function() { - var address = Address.fromBase58Check("1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv") - - var txOut = new TransactionOut({ - value: 50000, - script: Script.createOutputScript(address) - }) - - assert.equal(txOut.scriptPubKey(), "76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac") - }) - }) }) }) diff --git a/test/wallet.js b/test/wallet.js index dd4d2ed..a7de4d1 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -1,12 +1,14 @@ -var Wallet = require('../src/wallet.js') +var assert = require('assert') +var crypto = require('../').crypto +var sinon = require('sinon') + +var Address = require('..').Address 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 assert = require('assert') -var sinon = require('sinon') -var crypto = require('../').crypto +var Wallet = require('../src/wallet.js') var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx @@ -308,17 +310,24 @@ describe('Wallet', function() { }) describe('processTx', function(){ + var addresses var tx beforeEach(function(){ + addresses = [ + '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', + '1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd', + '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u' + ] + 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.addresses = [addresses[0]] wallet.processTx(tx) assert.equal(outputCount(), totalOuts + 1) @@ -327,7 +336,7 @@ describe('Wallet', function() { it("works for change address", function(){ var totalOuts = outputCount() - wallet.changeAddresses = [tx.outs[1].address.toString()] + wallet.changeAddresses = [addresses[1]] wallet.processTx(tx) @@ -345,13 +354,15 @@ describe('Wallet', function() { var output = wallet.outputs[key] assert.equal(output.receive, key) assert.equal(output.value, txOut.value) - assert.equal(output.address, txOut.address) + + var txOutAddress = Address.fromScriptPubKey(txOut.script).toString() + assert.equal(output.address, txOutAddress) } }) 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.addresses = [addresses[0]] // the address fixtureTx2 used as input wallet.processTx(tx) tx = Transaction.deserialize(fixtureTx2Hex)