From 476f009b4d5fb983b020368ca74e97d38b969b6e Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 30 Dec 2014 22:12:24 -0300 Subject: [PATCH 1/9] Add Insight and UTXO class --- index.js | 4 ++ lib/explorers/insight.js | 80 ++++++++++++++++++++++++++++++++++ lib/transaction/transaction.js | 80 +++++----------------------------- lib/util/js.js | 1 + lib/utxo.js | 72 ++++++++++++++++++++++++++++++ npm-shrinkwrap.json | 12 ++++- package.json | 5 +++ 7 files changed, 185 insertions(+), 69 deletions(-) create mode 100644 lib/explorers/insight.js create mode 100644 lib/utxo.js diff --git a/index.js b/index.js index b3b14caef..aeb1df013 100644 --- a/index.js +++ b/index.js @@ -53,5 +53,9 @@ bitcore.deps.bs58 = require('bs58'); bitcore.deps.Buffer = Buffer; bitcore.deps.elliptic = require('elliptic'); +// blockchain explorers +bitcore.explorers = {}; +bitcore.explorers.Insight = require('./lib/explorers/insight'); + // Internal usage, exposed for testing/advanced tweaking bitcore._HDKeyCache = require('./lib/hdkeycache'); diff --git a/lib/explorers/insight.js b/lib/explorers/insight.js new file mode 100644 index 000000000..be8313a74 --- /dev/null +++ b/lib/explorers/insight.js @@ -0,0 +1,80 @@ +'use strict'; + +var Networks = require('../networks'); +var JSUtil = require('../util/js'); +var $ = require('../util/preconditions'); +var _ = require('lodash'); +var Address = require('../address'); +var Transaction = require('../transaction'); +var UTXO = require('../utxo'); + +var request = require('request'); + +// var insight = new Insight(Networks.livenet); + +function Insight(url, network) { + if (!url && !network) { + return new Insight(Networks.defaultNetwork); + } + if (Networks.get(url)) { + network = Networks.get(url); + if (network === Networks.livenet) { + url = 'https://insight.bitpay.com'; + } else { + url = 'https://test-insight.bitpay.com'; + } + } + JSUtil.defineImmutable(this, { + url: url, + network: Networks.get(network) || Networks.defaultNetwork + }); + return this; +} + +Insight.prototype.getUnspentUtxos = function(addresses, callback) { + $.checkArgument(_.isFunction(callback)); + if (!_.isArray(addresses)) { + addresses = [addresses]; + } + addresses = _.map(addresses, function(address) { return new Address(address); }); + + this.requestPost('/api/addrs/utxo', { + addrs: _.map(addresses, function(address) { return address.toString(); }).join(',') + }, function(err, res, unspent) { + if (err || res.statusCode !== 200) { + return callback(err || res); + } + unspent = _.map(unspent, UTXO); + + return callback(null, unspent); + }); +}; + +Insight.prototype.broadcast = function(transaction, callback) { + $.checkArgument(JSUtil.isHexa(transaction) || transaction instanceof Transaction); + $.checkArgument(_.isFunction(callback)); + if (transaction instanceof Transaction) { + transaction = transaction.serialize(); + } + + this.requestPost('/api/tx/send', { + rawtx: transaction + }, function(err, res, body) { + if (err || res.statusCode !== 200) { + return callback(err || body); + } + return callback(null, body ? body.txid : null); + }); +}; + +Insight.prototype.requestPost = function(path, data, callback) { + $.checkArgument(_.isString(path)); + $.checkArgument(_.isFunction(callback)); + request({ + method: 'POST', + url: this.url + path, + json: data + }, callback); +}; + +module.exports = Insight; diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 6b4ca35b0..1af6b7f56 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -15,7 +15,7 @@ var Signature = require('../crypto/signature'); var Sighash = require('./sighash'); var Address = require('../address'); -var Unit = require('../unit'); +var UTXO = require('../utxo'); var Input = require('./input'); var PublicKeyHashInput = Input.PublicKeyHash; var MultiSigScriptHashInput = Input.MultiSigScriptHash; @@ -293,68 +293,23 @@ Transaction.prototype._newTransaction = function() { * @param {number=} threshold */ Transaction.prototype.from = function(utxo, pubkeys, threshold) { + if (_.isArray(utxo)) { + var self = this; + _.each(utxo, function(utxo) { + self.from(utxo, pubkeys, threshold); + }); + return this; + } if (pubkeys && threshold) { - this._fromMultiSigP2SH(utxo, pubkeys, threshold); + this._fromMultisigUtxo(utxo, pubkeys, threshold); } else { this._fromNonP2SH(utxo); } return this; }; -Transaction.prototype._fromMultiSigP2SH = function(utxo, pubkeys, threshold) { - if (Transaction._isNewUtxo(utxo)) { - this._fromMultisigNewUtxo(utxo, pubkeys, threshold); - } else if (Transaction._isOldUtxo(utxo)) { - this._fromMultisigOldUtxo(utxo, pubkeys, threshold); - } else { - throw new Transaction.Errors.UnrecognizedUtxoFormat(utxo); - } -}; - Transaction.prototype._fromNonP2SH = function(utxo) { - var self = this; - if (_.isArray(utxo)) { - _.each(utxo, function(single) { - self._fromNonP2SH(single); - }); - return; - } - if (Transaction._isNewUtxo(utxo)) { - this._fromNewUtxo(utxo); - } else if (Transaction._isOldUtxo(utxo)) { - this._fromOldUtxo(utxo); - } else { - throw new Transaction.Errors.UnrecognizedUtxoFormat(utxo); - } -}; - -Transaction._isNewUtxo = function(utxo) { - var isDefined = function(param) { - return !_.isUndefined(param); - }; - return _.all(_.map([utxo.txId, utxo.outputIndex, utxo.satoshis, utxo.script], isDefined)); -}; - -Transaction._isOldUtxo = function(utxo) { - var isDefined = function(param) { - return !_.isUndefined(param); - }; - return _.all(_.map([utxo.txid, utxo.vout, utxo.scriptPubKey, utxo.amount], isDefined)); -}; - -Transaction.prototype._fromOldUtxo = function(utxo) { - return this._fromNewUtxo({ - address: utxo.address && new Address(utxo.address), - txId: utxo.txid, - outputIndex: utxo.vout, - script: util.isHexa(utxo.script) ? new buffer.Buffer(utxo.scriptPubKey, 'hex') : utxo.scriptPubKey, - satoshis: Unit.fromBTC(utxo.amount).satoshis - }); -}; - -Transaction.prototype._fromNewUtxo = function(utxo) { - utxo.address = utxo.address && new Address(utxo.address); - utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script); + utxo = new UTXO(utxo); this.inputs.push(new PublicKeyHashInput({ output: new Output({ script: utxo.script, @@ -368,19 +323,8 @@ Transaction.prototype._fromNewUtxo = function(utxo) { this._inputAmount += utxo.satoshis; }; -Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold) { - return this._fromMultisigNewUtxo({ - address: utxo.address && new Address(utxo.address), - txId: utxo.txid, - outputIndex: utxo.vout, - script: new buffer.Buffer(utxo.scriptPubKey, 'hex'), - satoshis: Unit.fromBTC(utxo.amount).satoshis - }, pubkeys, threshold); -}; - -Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) { - utxo.address = utxo.address && new Address(utxo.address); - utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script); +Transaction.prototype._fromMultisigUtxo = function(utxo, pubkeys, threshold) { + utxo = new UTXO(utxo); this.addInput(new MultiSigScriptHashInput({ output: new Output({ script: utxo.script, diff --git a/lib/util/js.js b/lib/util/js.js index 85c128428..05c9562cf 100644 --- a/lib/util/js.js +++ b/lib/util/js.js @@ -55,6 +55,7 @@ module.exports = { Object.keys(values).forEach(function(key){ Object.defineProperty(target, key, { configurable: false, + enumerable: true, value: values[key] }); }); diff --git a/lib/utxo.js b/lib/utxo.js new file mode 100644 index 000000000..1456db7a5 --- /dev/null +++ b/lib/utxo.js @@ -0,0 +1,72 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('./util/preconditions'); +var JSUtil = require('./util/js'); + +var Script = require('./script'); +var Address = require('./address'); +var Unit = require('./unit'); + +function UTXO(data) { + /* jshint maxcomplexity: 20 */ + /* jshint maxstatements: 20 */ + if (!(this instanceof UTXO)) { + return new UTXO(data); + } + $.checkArgument(_.isObject(data), 'Must provide an object from where to extract data'); + var address = data.address ? new Address(data.address) : undefined; + var txId = data.txid ? data.txid : data.txId; + if (!txId || !JSUtil.isHexaString(txId) || txId.length > 64) { + // TODO: Use the errors library + throw new Error('Invalid TXID in object', data); + } + var outputIndex = _.isUndefined(data.vout) ? data.outputIndex : data.vout; + if (!_.isNumber(outputIndex)) { + throw new Error('Invalid outputIndex, received ' + outputIndex); + } + $.checkArgument(data.scriptPubKey || data.script, 'Must provide the scriptPubKey for that output!'); + var script = new Script(data.scriptPubKey || data.script); + $.checkArgument(data.amount || data.satoshis, 'Must provide the scriptPubKey for that output!'); + var amount = data.amount ? new Unit.fromBTC(data.amount).toSatoshis() : data.satoshis; + $.checkArgument(_.isNumber(amount), 'Amount must be a number'); + JSUtil.defineImmutable(this, { + address: address, + txId: txId, + outputIndex: outputIndex, + script: script, + satoshis: amount + }); +} + +UTXO.prototype.inspect = function() { + return ''; +}; + +UTXO.prototype.toString = function() { + return this.txId + ':' + this.outputIndex; +}; + +UTXO.fromJSON = UTXO.fromObject = function(data) { + if (_.isString(data)) { + data = JSON.parse(data); + } + return new UTXO(data); +}; + +UTXO.prototype.toJSON = function() { + return JSON.stringify(this.toObject()); +}; + +UTXO.prototype.toObject = function() { + return { + address: this.address.toObject(), + txid: this.txId, + vout: this.outputIndex, + scriptPubKey: this.script.toObject(), + amount: Unit.fromSatoshis(this.satoshis).toBTC() + }; +}; + +module.exports = UTXO; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 62cd95261..02adf129e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "bitcore", - "version": "0.8.0", + "version": "0.8.5", "dependencies": { "aes": { "version": "0.1.0", @@ -17,6 +17,11 @@ "from": "bn.js@0.16.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-0.16.0.tgz" }, + "browser-request": { + "version": "0.3.3", + "from": "browser-request@*", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz" + }, "bs58": { "version": "2.0.0", "from": "bs58@2.0.0", @@ -89,6 +94,11 @@ "from": "protobufjs@3.0.0", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-3.0.0.tgz" }, + "request": { + "version": "2.51.0", + "from": "request@*", + "resolved": "https://registry.npmjs.org/request/-/request-2.51.0.tgz" + }, "sha512": { "version": "0.0.1", "from": "sha512@=0.0.1", diff --git a/package.json b/package.json index a0aa3dfd2..d94b9e805 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,13 @@ "type": "git", "url": "https://github.com/bitpay/bitcore.git" }, + "browser": { + "request": "browser-request" + }, "dependencies": { "asn1.js": "=0.4.1", "bn.js": "=0.16.0", + "browser-request": "^0.3.3", "bs58": "=2.0.0", "bufferput": "^0.1.2", "buffers": "^0.1.1", @@ -81,6 +85,7 @@ "jsrsasign": "=0.0.3", "lodash": "=2.4.1", "protobufjs": "=3.0.0", + "request": "^2.51.0", "sha512": "=0.0.1", "socks5-client": "^0.3.6" }, From 9362927b43cf598e87bcccda06bf5ab9e42a2b69 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 31 Dec 2014 00:53:21 -0300 Subject: [PATCH 2/9] Add test coverage for Insight --- test/explorers/insight.js | 103 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 test/explorers/insight.js diff --git a/test/explorers/insight.js b/test/explorers/insight.js new file mode 100644 index 000000000..5dff460dd --- /dev/null +++ b/test/explorers/insight.js @@ -0,0 +1,103 @@ +'use strict'; + +var sinon = require('sinon'); +var should = require('chai').should(); +var expect = require('chai').expect; +var bitcore = require('../..'); + +var Insight = bitcore.explorers.Insight; +var Address = bitcore.Address; +var Transaction = bitcore.Transaction; +var Networks = bitcore.Networks; + +describe.only('Insight', function() { + + describe('instantiation', function() { + it('can be created without any parameters', function() { + var insight = new Insight(); + insight.url.should.equal('https://insight.bitpay.com'); + insight.network.should.equal(Networks.livenet); + }); + it('can be created providing just a network', function() { + var insight = new Insight(Networks.testnet); + insight.url.should.equal('https://test-insight.bitpay.com'); + insight.network.should.equal(Networks.testnet); + }); + it('can be created with a custom url', function() { + var url = 'https://localhost:1234'; + var insight = new Insight(url); + insight.url.should.equal(url); + }); + it('can be created with a custom url and network', function() { + var url = 'https://localhost:1234'; + var insight = new Insight(url, Networks.testnet); + insight.url.should.equal(url); + insight.network.should.equal(Networks.testnet); + }); + it('defaults to defaultNetwork on a custom url', function() { + var insight = new Insight('https://localhost:1234'); + insight.network.should.equal(Networks.defaultNetwork); + }); + }); + + describe('getting unspent utxos', function() { + var insight = new Insight(); + var address = '371mZyMp4t6uVtcEr4DAAbTZyby9Lvia72'; + beforeEach(function() { + insight.requestPost = sinon.stub(); + insight.requestPost.onFirstCall().callsArgWith(2, null, {statusCode: 200}); + }); + it('can receive an address', function(callback) { + insight.getUnspentUtxos(new Address(address), callback); + }); + it('can receive a address as a string', function(callback) { + insight.getUnspentUtxos(address, callback); + }); + it('can receive an array of addresses', function(callback) { + insight.getUnspentUtxos([address, new Address(address)], callback); + }); + it('errors if server is not available', function(callback) { + insight.requestPost.onFirstCall().callsArgWith(2, 'Unable to connect'); + insight.getUnspentUtxos(address, function(error) { + expect(error).to.equal('Unable to connect'); + callback(); + }); + }); + it('errors if server returns errorcode', function(callback) { + insight.requestPost.onFirstCall().callsArgWith(2, null, {statusCode: 400}); + insight.getUnspentUtxos(address, function(error) { + expect(error).to.deep.equal({statusCode: 400}); + callback(); + }); + }); + }); + + describe('broadcasting a transaction', function() { + var insight = new Insight(); + var tx = require('../data/tx_creation.json')[0][7]; + beforeEach(function() { + insight.requestPost = sinon.stub(); + insight.requestPost.onFirstCall().callsArgWith(2, null, {statusCode: 200}); + }); + it('accepts a raw transaction', function(callback) { + insight.broadcast(tx, callback); + }); + it('accepts a transaction model', function(callback) { + insight.broadcast(new Transaction(tx), callback); + }); + it('errors if server is not available', function(callback) { + insight.requestPost.onFirstCall().callsArgWith(2, 'Unable to connect'); + insight.broadcast(tx, function(error) { + expect(error).to.equal('Unable to connect'); + callback(); + }); + }); + it('errors if server returns errorcode', function(callback) { + insight.requestPost.onFirstCall().callsArgWith(2, null, {statusCode: 400}, 'error'); + insight.broadcast(tx, function(error) { + expect(error).to.equal('error'); + callback(); + }); + }); + }); +}); From 1cf108ab473b104e28b400e58c17aaaea0df7ba3 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 31 Dec 2014 01:21:59 -0300 Subject: [PATCH 3/9] Add tests for UTXO --- index.js | 1 + lib/utxo.js | 6 ++-- test/explorers/insight.js | 2 +- test/utxo.js | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 test/utxo.js diff --git a/index.js b/index.js index aeb1df013..0194dee21 100644 --- a/index.js +++ b/index.js @@ -45,6 +45,7 @@ bitcore.Script = require('./lib/script'); bitcore.Transaction = require('./lib/transaction'); bitcore.URI = require('./lib/uri'); bitcore.Unit = require('./lib/unit'); +bitcore.UTXO = require('./lib/utxo'); // dependencies, subject to change bitcore.deps = {}; diff --git a/lib/utxo.js b/lib/utxo.js index 1456db7a5..f77ddf3bf 100644 --- a/lib/utxo.js +++ b/lib/utxo.js @@ -49,7 +49,7 @@ UTXO.prototype.toString = function() { }; UTXO.fromJSON = UTXO.fromObject = function(data) { - if (_.isString(data)) { + if (JSUtil.isValidJSON(data)) { data = JSON.parse(data); } return new UTXO(data); @@ -61,10 +61,10 @@ UTXO.prototype.toJSON = function() { UTXO.prototype.toObject = function() { return { - address: this.address.toObject(), + address: this.address.toString(), txid: this.txId, vout: this.outputIndex, - scriptPubKey: this.script.toObject(), + scriptPubKey: this.script.toBuffer().toString('hex'), amount: Unit.fromSatoshis(this.satoshis).toBTC() }; }; diff --git a/test/explorers/insight.js b/test/explorers/insight.js index 5dff460dd..41b6745f7 100644 --- a/test/explorers/insight.js +++ b/test/explorers/insight.js @@ -10,7 +10,7 @@ var Address = bitcore.Address; var Transaction = bitcore.Transaction; var Networks = bitcore.Networks; -describe.only('Insight', function() { +describe('Insight', function() { describe('instantiation', function() { it('can be created without any parameters', function() { diff --git a/test/utxo.js b/test/utxo.js new file mode 100644 index 000000000..a5bd94e81 --- /dev/null +++ b/test/utxo.js @@ -0,0 +1,75 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var should = chai.should(); +var expect = chai.expect; + +var bitcore = require('..'); +var UTXO = bitcore.UTXO; + +describe('UTXO', function() { + + var sampleData1 = { + 'address': 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1', + 'txId': 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + 'outputIndex': 0, + 'script': 'OP_DUP OP_HASH160 20 0x88d9931ea73d60eaf7e5671efc0552b912911f2a OP_EQUALVERIFY OP_CHECKSIG', + 'satoshis': 1020000 + }; + var sampleData2 = { + 'txid': 'e42447187db5a29d6db161661e4bc66d61c3e499690fe5ea47f87b79ca573986', + 'vout': 1, + 'address': 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up', + 'scriptPubKey': '76a914073b7eae2823efa349e3b9155b8a735526463a0f88ac', + 'amount': 0.01080000 + }; + + it('roundtrip from raw data', function() { + expect(UTXO(sampleData2).toObject()).to.deep.equal(sampleData2); + }); + + it('can be created without "new" operand', function() { + expect(UTXO(sampleData1) instanceof UTXO).to.equal(true); + }); + + it('fails if no tx id is provided', function() { + expect(function() { + return new UTXO({}); + }).to.throw(); + }); + + it('fails if vout is not a number', function() { + var sample = _.cloneDeep(sampleData2); + sample.vout = '1'; + expect(function() { + return new UTXO(sample); + }).to.throw(); + }); + + it('displays nicely on the console', function() { + var expected = ''; + expect(new UTXO(sampleData1).inspect()).to.equal(expected); + }); + + it('toString returns txid:vout', function() { + var expected = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458:0'; + expect(new UTXO(sampleData1).toString()).to.equal(expected); + }); + + it('to/from JSON roundtrip', function() { + var utxo = new UTXO(sampleData2); + expect( + JSON.parse( + UTXO.fromJSON( + UTXO.fromObject( + UTXO.fromJSON( + utxo.toJSON() + ).toObject() + ).toJSON() + ).toJSON() + ) + ).to.deep.equal(sampleData2); + }); +}); From 11975bc0df5e2670c944f2ed1bec7c5e3aff7e97 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 31 Dec 2014 01:29:36 -0300 Subject: [PATCH 4/9] Rename UTXO to Transaction.UnspentOutput --- index.js | 1 - lib/explorers/insight.js | 4 +-- lib/transaction/index.js | 1 + lib/transaction/transaction.js | 6 ++-- lib/{utxo.js => transaction/unspentoutput.js} | 32 +++++++++---------- test/utxo.js | 26 +++++++-------- 6 files changed, 35 insertions(+), 35 deletions(-) rename lib/{utxo.js => transaction/unspentoutput.js} (71%) diff --git a/index.js b/index.js index 0194dee21..aeb1df013 100644 --- a/index.js +++ b/index.js @@ -45,7 +45,6 @@ bitcore.Script = require('./lib/script'); bitcore.Transaction = require('./lib/transaction'); bitcore.URI = require('./lib/uri'); bitcore.Unit = require('./lib/unit'); -bitcore.UTXO = require('./lib/utxo'); // dependencies, subject to change bitcore.deps = {}; diff --git a/lib/explorers/insight.js b/lib/explorers/insight.js index be8313a74..87da50f5c 100644 --- a/lib/explorers/insight.js +++ b/lib/explorers/insight.js @@ -6,7 +6,7 @@ var $ = require('../util/preconditions'); var _ = require('lodash'); var Address = require('../address'); var Transaction = require('../transaction'); -var UTXO = require('../utxo'); +var UnspentOutput = Transaction.UnspentOutput; var request = require('request'); @@ -44,7 +44,7 @@ Insight.prototype.getUnspentUtxos = function(addresses, callback) { if (err || res.statusCode !== 200) { return callback(err || res); } - unspent = _.map(unspent, UTXO); + unspent = _.map(unspent, UnspentOutput); return callback(null, unspent); }); diff --git a/lib/transaction/index.js b/lib/transaction/index.js index 55db6e06b..e82663587 100644 --- a/lib/transaction/index.js +++ b/lib/transaction/index.js @@ -2,3 +2,4 @@ module.exports = require('./transaction'); module.exports.Input = require('./input'); module.exports.Output = require('./output'); +module.exports.UnspentOutput = require('./unspentoutput'); diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 1af6b7f56..5370e2547 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -15,7 +15,7 @@ var Signature = require('../crypto/signature'); var Sighash = require('./sighash'); var Address = require('../address'); -var UTXO = require('../utxo'); +var UnspentOutput = require('./unspentoutput'); var Input = require('./input'); var PublicKeyHashInput = Input.PublicKeyHash; var MultiSigScriptHashInput = Input.MultiSigScriptHash; @@ -309,7 +309,7 @@ Transaction.prototype.from = function(utxo, pubkeys, threshold) { }; Transaction.prototype._fromNonP2SH = function(utxo) { - utxo = new UTXO(utxo); + utxo = new UnspentOutput(utxo); this.inputs.push(new PublicKeyHashInput({ output: new Output({ script: utxo.script, @@ -324,7 +324,7 @@ Transaction.prototype._fromNonP2SH = function(utxo) { }; Transaction.prototype._fromMultisigUtxo = function(utxo, pubkeys, threshold) { - utxo = new UTXO(utxo); + utxo = new UnspentOutput(utxo); this.addInput(new MultiSigScriptHashInput({ output: new Output({ script: utxo.script, diff --git a/lib/utxo.js b/lib/transaction/unspentoutput.js similarity index 71% rename from lib/utxo.js rename to lib/transaction/unspentoutput.js index f77ddf3bf..3eb7af8d4 100644 --- a/lib/utxo.js +++ b/lib/transaction/unspentoutput.js @@ -1,18 +1,18 @@ 'use strict'; var _ = require('lodash'); -var $ = require('./util/preconditions'); -var JSUtil = require('./util/js'); +var $ = require('../util/preconditions'); +var JSUtil = require('../util/js'); -var Script = require('./script'); -var Address = require('./address'); -var Unit = require('./unit'); +var Script = require('../script'); +var Address = require('../address'); +var Unit = require('../unit'); -function UTXO(data) { +function UnspentOutput(data) { /* jshint maxcomplexity: 20 */ /* jshint maxstatements: 20 */ - if (!(this instanceof UTXO)) { - return new UTXO(data); + if (!(this instanceof UnspentOutput)) { + return new UnspentOutput(data); } $.checkArgument(_.isObject(data), 'Must provide an object from where to extract data'); var address = data.address ? new Address(data.address) : undefined; @@ -39,27 +39,27 @@ function UTXO(data) { }); } -UTXO.prototype.inspect = function() { - return ''; }; -UTXO.prototype.toString = function() { +UnspentOutput.prototype.toString = function() { return this.txId + ':' + this.outputIndex; }; -UTXO.fromJSON = UTXO.fromObject = function(data) { +UnspentOutput.fromJSON = UnspentOutput.fromObject = function(data) { if (JSUtil.isValidJSON(data)) { data = JSON.parse(data); } - return new UTXO(data); + return new UnspentOutput(data); }; -UTXO.prototype.toJSON = function() { +UnspentOutput.prototype.toJSON = function() { return JSON.stringify(this.toObject()); }; -UTXO.prototype.toObject = function() { +UnspentOutput.prototype.toObject = function() { return { address: this.address.toString(), txid: this.txId, @@ -69,4 +69,4 @@ UTXO.prototype.toObject = function() { }; }; -module.exports = UTXO; +module.exports = UnspentOutput; diff --git a/test/utxo.js b/test/utxo.js index a5bd94e81..7c178c333 100644 --- a/test/utxo.js +++ b/test/utxo.js @@ -6,9 +6,9 @@ var should = chai.should(); var expect = chai.expect; var bitcore = require('..'); -var UTXO = bitcore.UTXO; +var UnspentOutput = bitcore.Transaction.UnspentOutput; -describe('UTXO', function() { +describe('UnspentOutput', function() { var sampleData1 = { 'address': 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1', @@ -26,16 +26,16 @@ describe('UTXO', function() { }; it('roundtrip from raw data', function() { - expect(UTXO(sampleData2).toObject()).to.deep.equal(sampleData2); + expect(UnspentOutput(sampleData2).toObject()).to.deep.equal(sampleData2); }); it('can be created without "new" operand', function() { - expect(UTXO(sampleData1) instanceof UTXO).to.equal(true); + expect(UnspentOutput(sampleData1) instanceof UnspentOutput).to.equal(true); }); it('fails if no tx id is provided', function() { expect(function() { - return new UTXO({}); + return new UnspentOutput({}); }).to.throw(); }); @@ -43,28 +43,28 @@ describe('UTXO', function() { var sample = _.cloneDeep(sampleData2); sample.vout = '1'; expect(function() { - return new UTXO(sample); + return new UnspentOutput(sample); }).to.throw(); }); it('displays nicely on the console', function() { - var expected = ''; - expect(new UTXO(sampleData1).inspect()).to.equal(expected); + expect(new UnspentOutput(sampleData1).inspect()).to.equal(expected); }); it('toString returns txid:vout', function() { var expected = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458:0'; - expect(new UTXO(sampleData1).toString()).to.equal(expected); + expect(new UnspentOutput(sampleData1).toString()).to.equal(expected); }); it('to/from JSON roundtrip', function() { - var utxo = new UTXO(sampleData2); + var utxo = new UnspentOutput(sampleData2); expect( JSON.parse( - UTXO.fromJSON( - UTXO.fromObject( - UTXO.fromJSON( + UnspentOutput.fromJSON( + UnspentOutput.fromObject( + UnspentOutput.fromJSON( utxo.toJSON() ).toObject() ).toJSON() From 7cf826249bb8be306ab00e67a84a76f0d9bf3a77 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 2 Jan 2015 10:09:46 -0300 Subject: [PATCH 5/9] Add jsdocs and documentation to UnspenOutput and Insight --- docs/guide/block.md | 2 -- docs/guide/index.md | 3 +++ docs/guide/insight.md | 34 ++++++++++++++++++++++++++++ docs/guide/unspentoutput.md | 39 ++++++++++++++++++++++++++++++++ lib/explorers/insight.js | 38 +++++++++++++++++++++++++++++-- lib/transaction/unspentoutput.js | 37 ++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 docs/guide/insight.md create mode 100644 docs/guide/unspentoutput.md diff --git a/docs/guide/block.md b/docs/guide/block.md index 764d8f65e..2bcec982a 100644 --- a/docs/guide/block.md +++ b/docs/guide/block.md @@ -8,7 +8,6 @@ description: A simple interface to parse and validate a bitcoin blocks. A Block instance represents the information of a block in the bitcoin network. Given a hexadecimal string representation of the serialization of a block with its transactions, you can instantiate a Block instance. Methods are provided to calculate and check the merkle root hash (if enough data is provided), but transactions won't necessarily be valid spends, and this class won't validate them. A binary representation as a `Buffer` instance is also valid input for a Block's constructor. ```javascript - // instantiate a new block instance var block = new Block(hexaEncodedBlock); @@ -30,7 +29,6 @@ For detailed technical information about a block please visit [Blocks](https://e Each instance of Block has a BlockHeader *(which can be instantiated separately)*. The header has validation methods, to verify that the block. ```javascript - // will verify that the nonce demonstrates enough proof of work assert(block.header.validProofOfWork()); diff --git a/docs/guide/index.md b/docs/guide/index.md index 838f56723..ee6ae57dd 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -30,6 +30,9 @@ To get started, just `npm install bitcore` or `bower install bitcore`. * [Managing a pool of peers](pool.md) * [Connecting to a bitcoind instance through JSON-RPC](jsonrpc.md) +## Blockchain Explorers +* [Insight](insight.md) + ## Extra * [Crypto](crypto.md) * [Encoding](encoding.md) diff --git a/docs/guide/insight.md b/docs/guide/insight.md new file mode 100644 index 000000000..fdefb0371 --- /dev/null +++ b/docs/guide/insight.md @@ -0,0 +1,34 @@ +title: Insight Explorer +description: Allows users to fetch information about the state of the blockchain from a trusted Insight server. +--- +# Insight + +## Description + +`bitcore.explorers.Insight` is a simple agent to perform queries to the blockchain. There are currently two methods (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. + +### Retrieving Unspent UTXOs for an Address (or set of) + +```javascript +var insight = new Insight(); +insight.getUnspentUtxos('1Bitcoin...', function(err, utxos) { + if (err) { + // Handle errors... + } else { + // Maybe use the UTXOs to create a transaction + } +}); +``` + +### Broadcasting a Transaction + +```javascript +var insight = new Insight(); +insight.broadcast(tx, function(err, returnedTxId) { + if (err) { + // Handle errors... + } else { + // Mark the transaction as broadcasted + } +}); +``` diff --git a/docs/guide/unspentoutput.md b/docs/guide/unspentoutput.md new file mode 100644 index 000000000..dce425e25 --- /dev/null +++ b/docs/guide/unspentoutput.md @@ -0,0 +1,39 @@ +title: UnspentOutput +description: A stateless model to represent an unspent output and associated information. +--- +# UnspentOutput + +## Description + +`bitcore.Transaction.UnspentOutput` is a class with stateless instances that provides information about an unspent output: +- Transaction ID and output index +- The "scriptPubKey", the script included in the output +- Amount of satoshis associated +- Address, if available + +## Parameters + +The constructor is quite permissive with the input arguments. It can take outputs straight out of bitcoind's getunspent RPC call. Some of the names are not very informative for new users, so the UnspentOutput constructor also understands these aliases: +- `scriptPubKey`: just `script` is also accepted +- `amount`: expected value is in BTC, but also `satoshis` is accepted +- `vout`: this is the index of the output in the transaction, renamed to `outputIndex` +- `txid`: `txId` + +## Example + +```javascript +var utxo = new UnspentOutput({ + "txid" : "a0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9", + "vout" : 0, + "address" : "mgJT8iegL4f9NCgQFeFyfvnSw1Yj4M5Woi", + "scriptPubKey" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac", + "amount" : 0.00070000 +}); +var utxo = new UnspentOutput({ + "txId" : "a0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9", + "outputIndex" : 0, + "address" : "mgJT8iegL4f9NCgQFeFyfvnSw1Yj4M5Woi", + "script" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac", + "satoshis" : 70000 +}); +``` diff --git a/lib/explorers/insight.js b/lib/explorers/insight.js index 87da50f5c..1a21d1114 100644 --- a/lib/explorers/insight.js +++ b/lib/explorers/insight.js @@ -10,8 +10,13 @@ var UnspentOutput = Transaction.UnspentOutput; var request = require('request'); -// var insight = new Insight(Networks.livenet); - +/** + * Allows the retrieval of information regarding the state of the blockchain + * (and broadcasting of transactions) from/to a trusted Insight server. + * @param {string=} url the url of the Insight server + * @param {Network=} network whether to use livenet or testnet + * @constructor + */ function Insight(url, network) { if (!url && !network) { return new Insight(Networks.defaultNetwork); @@ -31,6 +36,17 @@ function Insight(url, network) { return this; } +/** + * @callback Insight.GetUnspentUtxosCallback + * @param {Error} err + * @param {Array.UnspentOutput} utxos + */ + +/** + * Retrieve a list of unspent outputs associated with an address or set of addresses + * @param {Address|string|Array.Address|Array.string} addresses + * @param {GetUnspentUtxosCallback} callback + */ Insight.prototype.getUnspentUtxos = function(addresses, callback) { $.checkArgument(_.isFunction(callback)); if (!_.isArray(addresses)) { @@ -50,6 +66,17 @@ Insight.prototype.getUnspentUtxos = function(addresses, callback) { }); }; +/** + * @callback Insight.BroadcastCallback + * @param {Error} err + * @param {string} txid + */ + +/** + * Broadcast a transaction to the bitcoin network + * @param {transaction|string} transaction + * @param {BroadcastCallback} callback + */ Insight.prototype.broadcast = function(transaction, callback) { $.checkArgument(JSUtil.isHexa(transaction) || transaction instanceof Transaction); $.checkArgument(_.isFunction(callback)); @@ -67,6 +94,13 @@ Insight.prototype.broadcast = function(transaction, callback) { }); }; +/** + * Internal function to make a post request to the server + * @param {string} path + * @param {?} data + * @param {function} callback + * @private + */ Insight.prototype.requestPost = function(path, data, callback) { $.checkArgument(_.isString(path)); $.checkArgument(_.isFunction(callback)); diff --git a/lib/transaction/unspentoutput.js b/lib/transaction/unspentoutput.js index 3eb7af8d4..f8aee6186 100644 --- a/lib/transaction/unspentoutput.js +++ b/lib/transaction/unspentoutput.js @@ -8,6 +8,22 @@ var Script = require('../script'); var Address = require('../address'); var Unit = require('../unit'); +/** + * Represents an unspent output information: its script, associated amount and address, + * transaction id and output index. + * + * @constructor + * @param {object} data + * @param {string} data.txid the previous transaction id + * @param {string=} data.txId alias for `txid` + * @param {number} data.vout the index in the transaction + * @param {number=} data.outputIndex alias for `vout` + * @param {string|Script} data.scriptPubKey the script that must be resolved to release the funds + * @param {string|Script=} data.script alias for `scriptPubKey` + * @param {number} data.amount amount of bitcoins associated + * @param {number=} data.satoshis alias for `amount`, but expressed in satoshis (1 BTC = 1e8 satoshis) + * @param {string|Address=} data.address the associated address to the script, if provided + */ function UnspentOutput(data) { /* jshint maxcomplexity: 20 */ /* jshint maxstatements: 20 */ @@ -39,15 +55,28 @@ function UnspentOutput(data) { }); } +/** + * Provide an informative output when displaying this object in the console + * @returns string + */ UnspentOutput.prototype.inspect = function() { return ''; }; +/** + * String representation: just "txid:index" + * @returns string + */ UnspentOutput.prototype.toString = function() { return this.txId + ':' + this.outputIndex; }; +/** + * Deserialize an UnspentOutput from an object or JSON string + * @param {object|string} data + * @return UnspentOutput + */ UnspentOutput.fromJSON = UnspentOutput.fromObject = function(data) { if (JSUtil.isValidJSON(data)) { data = JSON.parse(data); @@ -55,10 +84,18 @@ UnspentOutput.fromJSON = UnspentOutput.fromObject = function(data) { return new UnspentOutput(data); }; +/** + * Retrieve a string representation of this object + * @return {string} + */ UnspentOutput.prototype.toJSON = function() { return JSON.stringify(this.toObject()); }; +/** + * Returns a plain object (no prototype or methods) with the associated infor for this output + * @return {object} + */ UnspentOutput.prototype.toObject = function() { return { address: this.address.toString(), From 7fe17e1699bb849941aac6dc0ec51b531685fa01 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 2 Jan 2015 16:09:50 -0300 Subject: [PATCH 6/9] Move explorers inside transport --- docs/guide/insight.md | 2 +- index.js | 8 ++------ lib/transport/explorers/index.js | 3 +++ lib/{ => transport}/explorers/insight.js | 11 ++++++----- lib/transport/index.js | 1 + test/{ => transport}/explorers/insight.js | 6 +++--- 6 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 lib/transport/explorers/index.js rename lib/{ => transport}/explorers/insight.js (93%) rename test/{ => transport}/explorers/insight.js (96%) diff --git a/docs/guide/insight.md b/docs/guide/insight.md index fdefb0371..da12e7e0a 100644 --- a/docs/guide/insight.md +++ b/docs/guide/insight.md @@ -5,7 +5,7 @@ description: Allows users to fetch information about the state of the blockchain ## Description -`bitcore.explorers.Insight` is a simple agent to perform queries to the blockchain. There are currently two methods (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. +`bitcore.transport.explorers.Insight` is a simple agent to perform queries to the blockchain. There are currently two methods (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. ### Retrieving Unspent UTXOs for an Address (or set of) diff --git a/index.js b/index.js index aeb1df013..e72952c1a 100644 --- a/index.js +++ b/index.js @@ -24,9 +24,6 @@ bitcore.util.buffer = require('./lib/util/buffer'); bitcore.util.js = require('./lib/util/js'); bitcore.util.preconditions = require('./lib/util/preconditions'); -// transport -bitcore.transport = require('./lib/transport'); - // errors thrown by the library bitcore.errors = require('./lib/errors'); @@ -53,9 +50,8 @@ bitcore.deps.bs58 = require('bs58'); bitcore.deps.Buffer = Buffer; bitcore.deps.elliptic = require('elliptic'); -// blockchain explorers -bitcore.explorers = {}; -bitcore.explorers.Insight = require('./lib/explorers/insight'); +// transport +bitcore.transport = require('./lib/transport'); // Internal usage, exposed for testing/advanced tweaking bitcore._HDKeyCache = require('./lib/hdkeycache'); diff --git a/lib/transport/explorers/index.js b/lib/transport/explorers/index.js new file mode 100644 index 000000000..248395515 --- /dev/null +++ b/lib/transport/explorers/index.js @@ -0,0 +1,3 @@ +module.exports = { + Insight: require('./insight') +}; diff --git a/lib/explorers/insight.js b/lib/transport/explorers/insight.js similarity index 93% rename from lib/explorers/insight.js rename to lib/transport/explorers/insight.js index 1a21d1114..71a41c593 100644 --- a/lib/explorers/insight.js +++ b/lib/transport/explorers/insight.js @@ -1,11 +1,12 @@ 'use strict'; -var Networks = require('../networks'); -var JSUtil = require('../util/js'); -var $ = require('../util/preconditions'); +var $ = require('../../util/preconditions'); var _ = require('lodash'); -var Address = require('../address'); -var Transaction = require('../transaction'); + +var Address = require('../../address'); +var JSUtil = require('../../util/js'); +var Networks = require('../../networks'); +var Transaction = require('../../transaction'); var UnspentOutput = Transaction.UnspentOutput; var request = require('request'); diff --git a/lib/transport/index.js b/lib/transport/index.js index f219f81bc..185732e7e 100644 --- a/lib/transport/index.js +++ b/lib/transport/index.js @@ -2,6 +2,7 @@ * @namespace Transport */ module.exports = { + explorers: require('./explorers'), Messages: require('./messages'), Peer: require('./peer'), Pool: require('./pool'), diff --git a/test/explorers/insight.js b/test/transport/explorers/insight.js similarity index 96% rename from test/explorers/insight.js rename to test/transport/explorers/insight.js index 41b6745f7..5c047800a 100644 --- a/test/explorers/insight.js +++ b/test/transport/explorers/insight.js @@ -3,9 +3,9 @@ var sinon = require('sinon'); var should = require('chai').should(); var expect = require('chai').expect; -var bitcore = require('../..'); +var bitcore = require('../../..'); -var Insight = bitcore.explorers.Insight; +var Insight = bitcore.transport.explorers.Insight; var Address = bitcore.Address; var Transaction = bitcore.Transaction; var Networks = bitcore.Networks; @@ -74,7 +74,7 @@ describe('Insight', function() { describe('broadcasting a transaction', function() { var insight = new Insight(); - var tx = require('../data/tx_creation.json')[0][7]; + var tx = require('../../data/tx_creation.json')[0][7]; beforeEach(function() { insight.requestPost = sinon.stub(); insight.requestPost.onFirstCall().callsArgWith(2, null, {statusCode: 200}); From f56ddb2a0a840bef1c9d609703ec355f83b8436d Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 2 Jan 2015 16:50:14 -0300 Subject: [PATCH 7/9] Rename properly utxo to unspentoutput --- test/{utxo.js => transaction/unspentoutput.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{utxo.js => transaction/unspentoutput.js} (98%) diff --git a/test/utxo.js b/test/transaction/unspentoutput.js similarity index 98% rename from test/utxo.js rename to test/transaction/unspentoutput.js index 7c178c333..fde288197 100644 --- a/test/utxo.js +++ b/test/transaction/unspentoutput.js @@ -5,7 +5,7 @@ var chai = require('chai'); var should = chai.should(); var expect = chai.expect; -var bitcore = require('..'); +var bitcore = require('../..'); var UnspentOutput = bitcore.Transaction.UnspentOutput; describe('UnspentOutput', function() { From 2284eec829cfa7933abe0924806cea0160ee9a03 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 2 Jan 2015 17:03:52 -0300 Subject: [PATCH 8/9] Fix comments on the Insight and UTXO docs --- docs/guide/index.md | 4 +--- docs/guide/insight.md | 4 ++-- docs/guide/unspentoutput.md | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/guide/index.md b/docs/guide/index.md index ee6ae57dd..0f2cc5670 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -29,9 +29,7 @@ To get started, just `npm install bitcore` or `bower install bitcore`. * [Interface to the Bitcoin P2P network](peer.md) * [Managing a pool of peers](pool.md) * [Connecting to a bitcoind instance through JSON-RPC](jsonrpc.md) - -## Blockchain Explorers -* [Insight](insight.md) +* [Connecting to a Insight instance to retrieve informetion](insight.md) ## Extra * [Crypto](crypto.md) diff --git a/docs/guide/insight.md b/docs/guide/insight.md index da12e7e0a..6f11eb2d2 100644 --- a/docs/guide/insight.md +++ b/docs/guide/insight.md @@ -1,11 +1,11 @@ title: Insight Explorer -description: Allows users to fetch information about the state of the blockchain from a trusted Insight server. +description: Provides an interface to fetch information about the state of the blockchain from a trusted Insight server. --- # Insight ## Description -`bitcore.transport.explorers.Insight` is a simple agent to perform queries to the blockchain. There are currently two methods (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. +`bitcore.transport.explorers.Insight` is a simple agent to perform queries to the blockchain. There are currently two methods (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. The default servers are `https://insight.bitpay.com` and `https://test-insight.bitpay.com`, hosted by BitPay Inc. ### Retrieving Unspent UTXOs for an Address (or set of) diff --git a/docs/guide/unspentoutput.md b/docs/guide/unspentoutput.md index dce425e25..20b9b0500 100644 --- a/docs/guide/unspentoutput.md +++ b/docs/guide/unspentoutput.md @@ -15,7 +15,7 @@ description: A stateless model to represent an unspent output and associated inf The constructor is quite permissive with the input arguments. It can take outputs straight out of bitcoind's getunspent RPC call. Some of the names are not very informative for new users, so the UnspentOutput constructor also understands these aliases: - `scriptPubKey`: just `script` is also accepted -- `amount`: expected value is in BTC, but also `satoshis` is accepted +- `amount`: expected value in BTC. If the `satoshis` alias is used, make sure to use satoshis instead of BTC. - `vout`: this is the index of the output in the transaction, renamed to `outputIndex` - `txid`: `txId` From 48cea84435ae2b41d25a752dcd7986382d5daf96 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 2 Jan 2015 17:35:29 -0300 Subject: [PATCH 9/9] Fix links to insight --- docs/guide/insight.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guide/insight.md b/docs/guide/insight.md index 6f11eb2d2..9892846f6 100644 --- a/docs/guide/insight.md +++ b/docs/guide/insight.md @@ -5,7 +5,9 @@ description: Provides an interface to fetch information about the state of the b ## Description -`bitcore.transport.explorers.Insight` is a simple agent to perform queries to the blockchain. There are currently two methods (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. The default servers are `https://insight.bitpay.com` and `https://test-insight.bitpay.com`, hosted by BitPay Inc. +`bitcore.transport.explorers.Insight` is a simple agent to perform queries to an Insight blockchain explorer. The default servers are `https://insight.bitpay.com` and `https://test-insight.bitpay.com`, hosted by BitPay Inc. You can (and we strongly suggest you do) run your own insight server. For more information, head to https://github.com/bitpay/insight-api + +There are currently two methods implemented (the API will grow as features are requested): `getUnspentUtxos` and `broadcast`. ### Retrieving Unspent UTXOs for an Address (or set of)