Merge pull request #872 from eordano/feature/blockchainProvider

Add Insight and UTXO class
This commit is contained in:
Yemel Jardi 2015-01-02 17:54:21 -03:00
commit b1557a25ab
16 changed files with 515 additions and 74 deletions

View File

@ -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());

View File

@ -29,6 +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)
* [Connecting to a Insight instance to retrieve informetion](insight.md)
## Extra
* [Crypto](crypto.md)

36
docs/guide/insight.md Normal file
View File

@ -0,0 +1,36 @@
title: Insight Explorer
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 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)
```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
}
});
```

View File

@ -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 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`
## 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
});
```

View File

@ -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,5 +50,8 @@ bitcore.deps.bs58 = require('bs58');
bitcore.deps.Buffer = Buffer;
bitcore.deps.elliptic = require('elliptic');
// transport
bitcore.transport = require('./lib/transport');
// Internal usage, exposed for testing/advanced tweaking
bitcore._HDKeyCache = require('./lib/hdkeycache');

View File

@ -2,3 +2,4 @@ module.exports = require('./transaction');
module.exports.Input = require('./input');
module.exports.Output = require('./output');
module.exports.UnspentOutput = require('./unspentoutput');

View File

@ -15,7 +15,7 @@ var Signature = require('../crypto/signature');
var Sighash = require('./sighash');
var Address = require('../address');
var Unit = require('../unit');
var UnspentOutput = require('./unspentoutput');
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 UnspentOutput(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 UnspentOutput(utxo);
this.addInput(new MultiSigScriptHashInput({
output: new Output({
script: utxo.script,

View File

@ -0,0 +1,109 @@
'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');
/**
* 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 */
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;
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
});
}
/**
* Provide an informative output when displaying this object in the console
* @returns string
*/
UnspentOutput.prototype.inspect = function() {
return '<UnspentOutput: ' + this.txId + ':' + this.outputIndex +
', satoshis: ' + this.satoshis + ', address: ' + this.address + '>';
};
/**
* 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);
}
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(),
txid: this.txId,
vout: this.outputIndex,
scriptPubKey: this.script.toBuffer().toString('hex'),
amount: Unit.fromSatoshis(this.satoshis).toBTC()
};
};
module.exports = UnspentOutput;

View File

@ -0,0 +1,3 @@
module.exports = {
Insight: require('./insight')
};

View File

@ -0,0 +1,115 @@
'use strict';
var $ = require('../../util/preconditions');
var _ = require('lodash');
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');
/**
* 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);
}
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;
}
/**
* @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)) {
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, UnspentOutput);
return callback(null, unspent);
});
};
/**
* @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));
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);
});
};
/**
* 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));
request({
method: 'POST',
url: this.url + path,
json: data
}, callback);
};
module.exports = Insight;

View File

@ -2,6 +2,7 @@
* @namespace Transport
*/
module.exports = {
explorers: require('./explorers'),
Messages: require('./messages'),
Peer: require('./peer'),
Pool: require('./pool'),

View File

@ -55,6 +55,7 @@ module.exports = {
Object.keys(values).forEach(function(key){
Object.defineProperty(target, key, {
configurable: false,
enumerable: true,
value: values[key]
});
});

12
npm-shrinkwrap.json generated
View File

@ -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",

View File

@ -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"
},

View File

@ -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 UnspentOutput = bitcore.Transaction.UnspentOutput;
describe('UnspentOutput', 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(UnspentOutput(sampleData2).toObject()).to.deep.equal(sampleData2);
});
it('can be created without "new" operand', function() {
expect(UnspentOutput(sampleData1) instanceof UnspentOutput).to.equal(true);
});
it('fails if no tx id is provided', function() {
expect(function() {
return new UnspentOutput({});
}).to.throw();
});
it('fails if vout is not a number', function() {
var sample = _.cloneDeep(sampleData2);
sample.vout = '1';
expect(function() {
return new UnspentOutput(sample);
}).to.throw();
});
it('displays nicely on the console', function() {
var expected = '<UnspentOutput: a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458:0' +
', satoshis: 1020000, address: mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1>';
expect(new UnspentOutput(sampleData1).inspect()).to.equal(expected);
});
it('toString returns txid:vout', function() {
var expected = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458:0';
expect(new UnspentOutput(sampleData1).toString()).to.equal(expected);
});
it('to/from JSON roundtrip', function() {
var utxo = new UnspentOutput(sampleData2);
expect(
JSON.parse(
UnspentOutput.fromJSON(
UnspentOutput.fromObject(
UnspentOutput.fromJSON(
utxo.toJSON()
).toObject()
).toJSON()
).toJSON()
)
).to.deep.equal(sampleData2);
});
});

View File

@ -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.transport.explorers.Insight;
var Address = bitcore.Address;
var Transaction = bitcore.Transaction;
var Networks = bitcore.Networks;
describe('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();
});
});
});
});