Yemel Jardi
10 years ago
16 changed files with 515 additions and 74 deletions
@ -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 |
||||
|
} |
||||
|
}); |
||||
|
``` |
@ -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 |
||||
|
}); |
||||
|
``` |
@ -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; |
@ -0,0 +1,3 @@ |
|||||
|
module.exports = { |
||||
|
Insight: require('./insight') |
||||
|
}; |
@ -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; |
@ -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); |
||||
|
}); |
||||
|
}); |
@ -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(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
Loading…
Reference in new issue