diff --git a/index.js b/index.js index b3b14ca..aeb1df0 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 0000000..be8313a --- /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 6b4ca35..1af6b7f 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 85c1284..05c9562 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 0000000..1456db7 --- /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 62cd952..02adf12 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 a0aa3df..d94b9e8 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" },