wip
This commit is contained in:
parent
98ea052405
commit
b471857bf0
@ -7,7 +7,7 @@ function Encoding(servicePrefix) {
|
|||||||
this.servicePrefix = servicePrefix;
|
this.servicePrefix = servicePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
Encoding.prototype.encodeAddressIndexKey = function(address, height, txid) {
|
Encoding.prototype.encodeAddressIndexKey = function(address, height, txid, index, input) {
|
||||||
var prefix = new Buffer('00', 'hex');
|
var prefix = new Buffer('00', 'hex');
|
||||||
var buffers = [this.servicePrefix, prefix];
|
var buffers = [this.servicePrefix, prefix];
|
||||||
|
|
||||||
@ -25,6 +25,15 @@ Encoding.prototype.encodeAddressIndexKey = function(address, height, txid) {
|
|||||||
var txidBuffer = new Buffer(txid || Array(65).join('0'), 'hex');
|
var txidBuffer = new Buffer(txid || Array(65).join('0'), 'hex');
|
||||||
buffers.push(txidBuffer);
|
buffers.push(txidBuffer);
|
||||||
|
|
||||||
|
var indexBuffer = new Buffer(4);
|
||||||
|
indexBuffer.writeUInt32BE(index || 0);
|
||||||
|
buffers.push(indexBuffer);
|
||||||
|
|
||||||
|
// this is whether the address appears in an input (1) or output (0)
|
||||||
|
var inputBuffer = new Buffer(1);
|
||||||
|
inputBuffer.writeUInt8(input || 0);
|
||||||
|
buffers.push(inputBuffer);
|
||||||
|
|
||||||
return Buffer.concat(buffers);
|
return Buffer.concat(buffers);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,10 +45,14 @@ Encoding.prototype.decodeAddressIndexKey = function(buffer) {
|
|||||||
var address = reader.read(addressSize).toString('utf8');
|
var address = reader.read(addressSize).toString('utf8');
|
||||||
var height = reader.readUInt32BE();
|
var height = reader.readUInt32BE();
|
||||||
var txid = reader.read(32).toString('hex');
|
var txid = reader.read(32).toString('hex');
|
||||||
|
var index = reader.readUInt32BE();
|
||||||
|
var input = reader.readUInt8();
|
||||||
return {
|
return {
|
||||||
address: address,
|
address: address,
|
||||||
height: height,
|
height: height,
|
||||||
txid: txid,
|
txid: txid,
|
||||||
|
index: index,
|
||||||
|
input: input
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,33 @@ var index = require('../../');
|
|||||||
var log = index.log;
|
var log = index.log;
|
||||||
var errors = index.errors;
|
var errors = index.errors;
|
||||||
var bitcore = require('bitcore-lib');
|
var bitcore = require('bitcore-lib');
|
||||||
|
var Unit = bitcore.Unit;
|
||||||
var _ = bitcore.deps._;
|
var _ = bitcore.deps._;
|
||||||
var Address = bitcore.Address;
|
|
||||||
var Encoding = require('./encoding');
|
var Encoding = require('./encoding');
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
|
var Transform = require('stream').Transform;
|
||||||
|
/*
|
||||||
|
|
||||||
|
1. getAddressSummary
|
||||||
|
2. getAddressUnspentOutputs
|
||||||
|
3. bitcoind.height
|
||||||
|
4. getBlockHeader
|
||||||
|
5. getDetailedTransaction
|
||||||
|
6. getTransaction
|
||||||
|
7. sendTransaction
|
||||||
|
8. getInfo
|
||||||
|
9. bitcoind.tiphash
|
||||||
|
10. getBestBlockHash
|
||||||
|
11. isSynced
|
||||||
|
12. getAddressHistory
|
||||||
|
13. getBlock
|
||||||
|
14. getRawBlock
|
||||||
|
15. getBlockHashesByTimestamp
|
||||||
|
16. estimateFee
|
||||||
|
17. getBlockOverview
|
||||||
|
18. syncPercentage
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
var AddressService = function(options) {
|
var AddressService = function(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
@ -28,25 +51,6 @@ AddressService.dependencies = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// ---- public function prototypes
|
// ---- public function prototypes
|
||||||
AddressService.prototype.getBalance = function(address, queryMempool, callback) {
|
|
||||||
this.getUtxos(address, queryMempool, function(err, outputs) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
var satoshis = outputs.map(function(output) {
|
|
||||||
return output.satoshis;
|
|
||||||
});
|
|
||||||
|
|
||||||
var sum = satoshis.reduce(function(a, b) {
|
|
||||||
return a + b;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return callback(null, sum);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) {
|
AddressService.prototype.getUtxos = function(addresses, queryMempool, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@ -127,266 +131,275 @@ AddressService.prototype.stop = function(callback) {
|
|||||||
|
|
||||||
AddressService.prototype.getAPIMethods = function() {
|
AddressService.prototype.getAPIMethods = function() {
|
||||||
return [
|
return [
|
||||||
['getAddressBalance', this, this.getAddressBalance, 2],
|
|
||||||
['getAddressHistory', this, this.getAddressHistory, 2],
|
['getAddressHistory', this, this.getAddressHistory, 2],
|
||||||
['getAddressSummary', this, this.getAddressSummary, 1],
|
['getAddressSummary', this, this.getAddressSummary, 1],
|
||||||
['getAddressTxids', this, this.getAddressTxids, 2],
|
|
||||||
['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 1],
|
['getAddressUnspentOutputs', this, this.getAddressUnspentOutputs, 1],
|
||||||
['syncPercentage', this, this.syncPercentage, 0]
|
['syncPercentage', this, this.syncPercentage, 0]
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.getAddressBalance = function(addresses, options, callback) {
|
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
addresses = utils.normalizeAddressArg(addresses);
|
|
||||||
var balance = 0;
|
|
||||||
|
|
||||||
async.eachLimit(addresses, 4, function(address, next) {
|
options = options || {};
|
||||||
|
var from = options.from || 0;
|
||||||
|
var to = options.to || 0xffffffff;
|
||||||
|
|
||||||
var start = self._encoding.encodeUtxoIndexKey(address);
|
async.mapLimit(addresses, 4, function(address, next) {
|
||||||
var criteria = {
|
|
||||||
gte: start,
|
self._getAddressHistory(address, next);
|
||||||
lte: Buffer.concat([ start.slice(-36), new Buffer(new Array(73).join('f'), 'hex') ])
|
|
||||||
|
}, function(err, res) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = {
|
||||||
|
totalItems: res.length,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
items: res
|
||||||
};
|
};
|
||||||
|
|
||||||
var stream = this._db.createReadStream(criteria);
|
callback(null, results);
|
||||||
stream.on('data', function(data) {
|
|
||||||
|
|
||||||
});
|
|
||||||
stream.on('error', function(err) {
|
|
||||||
});
|
|
||||||
stream.on('end', function() {
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.getAddressHistory = function(addresses, options, callback) {
|
AddressService.prototype._getAddressHistory = function(address, options, callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var txids = [];
|
|
||||||
|
|
||||||
async.eachLimit(addresses, 4, function(address, next) {
|
var results = [];
|
||||||
self.getAddressTxids(address, options, function(err, tmpTxids) {
|
var start = self._encoding.encodeAddressIndexKey(address);
|
||||||
|
|
||||||
|
var criteria = {
|
||||||
|
gte: start,
|
||||||
|
lte: utils.getTerminalKey(start)
|
||||||
|
};
|
||||||
|
|
||||||
|
// txid stream
|
||||||
|
var txidStream = self._db.createKeyStream(criteria);
|
||||||
|
|
||||||
|
txidStream.on('close', function() {
|
||||||
|
txidStream.unpipe();
|
||||||
|
});
|
||||||
|
|
||||||
|
// tx stream
|
||||||
|
var txStream = new Transform({ objectMode: true, highWaterMark: 1000 });
|
||||||
|
|
||||||
|
var streamErr;
|
||||||
|
txStream.on('end', function() {
|
||||||
|
if (streamErr) {
|
||||||
|
return callback(streamErr);
|
||||||
|
}
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
|
|
||||||
|
// pipe txids into tx stream for processing
|
||||||
|
txidStream.pipe(txStream);
|
||||||
|
|
||||||
|
txStream._transform = function(chunk, enc, callback) {
|
||||||
|
|
||||||
|
var key = self._encoding.decodeWalletTransactionKey(chunk);
|
||||||
|
|
||||||
|
self._tx.getDetailedTransaction(key.txid, options, function(err, tx) {
|
||||||
|
|
||||||
if(err) {
|
if(err) {
|
||||||
return next(err);
|
log.error(err);
|
||||||
|
txStream.emit('error', err);
|
||||||
|
return callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
txids = _.union(txids, tmpTxids);
|
if (!tx) {
|
||||||
return next();
|
log.error('Could not find tx for txid: ' + key.txid + '. This should not be possible, check indexes.');
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(tx);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
|
||||||
});
|
});
|
||||||
}, function() {
|
|
||||||
async.mapLimit(txids, 4, function(txid, next) {
|
|
||||||
self.node.services.transaction.getTransaction(txid.toString('hex'), options, function(err, tx) {
|
|
||||||
if(err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
var txObj = tx.toObject();
|
};
|
||||||
for(var i = 0; i < txObj.inputs.length; i++) {
|
|
||||||
txObj.inputs[i].satoshis = tx.__inputValues[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
next(null, txObj);
|
txStream.on('error', function(err) {
|
||||||
});
|
log.error(err);
|
||||||
}, callback);
|
txStream.unpipe();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
txStream._flush = function(callback) {
|
||||||
|
txStream.emit('end');
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.getAddressSummary = function(addressArg, options, callback) {
|
AddressService.prototype.getAddressSummary = function(address, options, callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var startTime = new Date();
|
|
||||||
var address = new Address(addressArg);
|
|
||||||
|
|
||||||
if (_.isUndefined(options.queryMempool)) {
|
if (_.isUndefined(options.queryMempool)) {
|
||||||
options.queryMempool = true;
|
options.queryMempool = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async.waterfall([
|
var result = {
|
||||||
function(next) {
|
addrStr: address,
|
||||||
self._getAddressConfirmedSummary(address, options, next);
|
balance: 0,
|
||||||
},
|
balanceSat: 0,
|
||||||
function(result, next) {
|
totalReceived: 0,
|
||||||
self._getAddressMempoolSummary(address, options, result, next);
|
totalReceivedSat: 0,
|
||||||
},
|
totalSent: 0,
|
||||||
function(result, next) {
|
totalSentSat: 0,
|
||||||
self._setAndSortTxidsFromAppearanceIds(result, next);
|
unconfirmedBalance: 0,
|
||||||
}
|
unconfirmedBalanceSat: 0,
|
||||||
], function(err, result) {
|
unconfirmedTxApperances: 0,
|
||||||
if (err) {
|
txApperances: 0,
|
||||||
return callback(err);
|
transactions: []
|
||||||
}
|
};
|
||||||
|
|
||||||
var summary = self._transformAddressSummaryFromResult(result, options);
|
// txid criteria
|
||||||
|
var start = self._encoding.encodeAddressIndexKey(address);
|
||||||
var timeDelta = new Date() - startTime;
|
var criteria = {
|
||||||
if (timeDelta > 5000) {
|
|
||||||
var seconds = Math.round(timeDelta / 1000);
|
|
||||||
log.warn('Slow (' + seconds + 's) getAddressSummary request for address: ' + address.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, summary);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.getAddressTxids = function(address, options, callback) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var opts = options || { start: 0, end: 0xffffffff, txid: new Array(65).join('0') };
|
|
||||||
var txids = {};
|
|
||||||
|
|
||||||
var start = self._encoding.encodeAddressIndexKey(address, opts.start, opts.txid);
|
|
||||||
var end = self._encoding.encodeAddressIndexKey(address, opts.end, opts.txid);
|
|
||||||
|
|
||||||
var stream = self.db.createKeyStream({
|
|
||||||
gte: start,
|
gte: start,
|
||||||
lt: end
|
lte: utils.getTerminalKey(start)
|
||||||
|
};
|
||||||
|
|
||||||
|
// txid stream
|
||||||
|
var txidStream = self._db.createKeyStream(criteria);
|
||||||
|
|
||||||
|
txidStream.on('close', function() {
|
||||||
|
txidStream.unpipe();
|
||||||
});
|
});
|
||||||
|
|
||||||
var streamErr = null;
|
// tx stream
|
||||||
stream.on('close', function() {
|
var txStream = new Transform({ objectMode: true, highWaterMark: 1000 });
|
||||||
|
txStream.on('end', function() {
|
||||||
|
result.balance = Unit.fromSatoshis(result.balanceSat).toBTC();
|
||||||
|
result.totalReceived = Unit.fromSatoshis(result.totalReceivedSat).toBTC();
|
||||||
|
result.totalSent = Unit.fromSatoshis(result.totalSentSat).toBTC();
|
||||||
|
result.unconfirmedBalance = Unit.fromSatoshis(result.unconfirmedBalanceSat).toBTC();
|
||||||
|
callback(null, result);
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('data', function(buffer) {
|
// pipe txids into tx stream for processing
|
||||||
var key = self._encoding.decodeAddressIndexKey(buffer);
|
txidStream.pipe(txStream);
|
||||||
txids[key.txid] = true;
|
|
||||||
|
txStream._transform = function(chunk, enc, callback) {
|
||||||
|
|
||||||
|
var key = self._encoding.decodeWalletTransactionKey(chunk);
|
||||||
|
|
||||||
|
self._tx.getTransaction(key.txid, options, function(err, res) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
log.error(err);
|
||||||
|
txStream.emit('error', err);
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
log.error('Could not find tx for txid: ' + key.txid + '. This should not be possible, check indexes.');
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx = res.tx;
|
||||||
|
|
||||||
|
result.transactions.push(tx.id);
|
||||||
|
result.txApperances++;
|
||||||
|
|
||||||
|
if (key.input) {
|
||||||
|
|
||||||
|
result.balanceSat -= tx.inputValues[key.index];
|
||||||
|
result.totalSentSat += tx.inputValues[key.index];
|
||||||
|
|
||||||
|
if (res.confirmations === 0) {
|
||||||
|
|
||||||
|
result.unconfirmedBalanceSat -= tx.inputValues[key.index];
|
||||||
|
result.unconfirmedTxApperances++;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
result.balanceSat += tx.outputs[key.index].satoshis;
|
||||||
|
result.totalReceivedSat += tx.outputs[key.index].satoshis;
|
||||||
|
|
||||||
|
if (res.confirmations === 0) {
|
||||||
|
|
||||||
|
result.unconfirmedBalanceSat += tx.inputValues[key.index];
|
||||||
|
result.unconfirmedTxApperances++;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
txStream.on('error', function(err) {
|
||||||
|
log.error(err);
|
||||||
|
txStream.unpipe();
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('end', function() {
|
txStream._flush = function(callback) {
|
||||||
callback(streamErr, Object.keys(txids));
|
txStream.emit('end');
|
||||||
});
|
callback();
|
||||||
|
};
|
||||||
stream.on('error', function(err) {
|
|
||||||
streamErr = err;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressService.prototype.getAddressUnspentOutputs = function(address, options, callback) {
|
AddressService.prototype.getAddressUnspentOutputs = function(address, options, callback) {
|
||||||
|
|
||||||
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
|
|
||||||
var addresses = utils._normalizeAddressArg(address);
|
|
||||||
var cacheKey = addresses.join('');
|
|
||||||
var utxos = this.utxosCache.get(cacheKey);
|
|
||||||
|
|
||||||
function transformUnspentOutput(delta) {
|
|
||||||
var script = bitcore.Script.fromAddress(delta.address);
|
|
||||||
return {
|
|
||||||
address: delta.address,
|
|
||||||
txid: delta.txid,
|
|
||||||
outputIndex: delta.index,
|
|
||||||
script: script.toHex(),
|
|
||||||
satoshis: delta.satoshis,
|
|
||||||
timestamp: delta.timestamp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateWithMempool(confirmedUtxos, mempoolDeltas) {
|
|
||||||
if (!mempoolDeltas || !mempoolDeltas.length) {
|
|
||||||
return confirmedUtxos;
|
|
||||||
}
|
|
||||||
var isSpentOutputs = false;
|
|
||||||
var mempoolUnspentOutputs = [];
|
|
||||||
var spentOutputs = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < mempoolDeltas.length; i++) {
|
|
||||||
var delta = mempoolDeltas[i];
|
|
||||||
if (delta.prevtxid && delta.satoshis <= 0) {
|
|
||||||
if (!spentOutputs[delta.prevtxid]) {
|
|
||||||
spentOutputs[delta.prevtxid] = [delta.prevout];
|
|
||||||
} else {
|
|
||||||
spentOutputs[delta.prevtxid].push(delta.prevout);
|
|
||||||
}
|
|
||||||
isSpentOutputs = true;
|
|
||||||
} else {
|
|
||||||
mempoolUnspentOutputs.push(transformUnspentOutput(delta));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var utxos = mempoolUnspentOutputs.reverse().concat(confirmedUtxos);
|
|
||||||
|
|
||||||
if (isSpentOutputs) {
|
|
||||||
return utxos.filter(function(utxo) {
|
|
||||||
if (!spentOutputs[utxo.txid]) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return (spentOutputs[utxo.txid].indexOf(utxo.outputIndex) === -1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return utxos;
|
|
||||||
}
|
|
||||||
|
|
||||||
function finish(mempoolDeltas) {
|
|
||||||
if (utxos) {
|
|
||||||
return setImmediate(function() {
|
|
||||||
callback(null, updateWithMempool(utxos, mempoolDeltas));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.client.getAddressUtxos({addresses: addresses}, function(err, response) {
|
|
||||||
if (err) {
|
|
||||||
return callback(self._wrapRPCError(err));
|
|
||||||
}
|
|
||||||
var utxos = response.result.reverse();
|
|
||||||
self.utxosCache.set(cacheKey, utxos);
|
|
||||||
callback(null, updateWithMempool(utxos, mempoolDeltas));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryMempool) {
|
|
||||||
self.client.getAddressMempool({addresses: addresses}, function(err, response) {
|
|
||||||
if (err) {
|
|
||||||
return callback(self._wrapRPCError(err));
|
|
||||||
}
|
|
||||||
finish(response.result);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
AddressService.prototype.syncPercentage = function(callback) {
|
|
||||||
return callback(null, ((this._tip.height / this._block.getBestBlockHeight()) * 100).toFixed(2) + '%');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
AddressService.prototype.getAddressTxidsWithHeights = function(address, options, callback) {
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
if (_.isUndefined(options.queryMempool)) {
|
||||||
|
options.queryMempool = true;
|
||||||
|
}
|
||||||
|
|
||||||
var opts = options || {};
|
var results = [];
|
||||||
var txids = {};
|
|
||||||
|
|
||||||
var start = self._encoding.encodeAddressIndexKey(address, opts.start || 0);
|
var start = self._encoding.encodeUtxoIndexKey(address);
|
||||||
var end = Buffer.concat([ start.slice(0, -36), new Buffer((opts.end || 'ffffffff'), 'hex') ]);
|
var criteria = {
|
||||||
|
|
||||||
var stream = self.db.createKeyStream({
|
|
||||||
gte: start,
|
gte: start,
|
||||||
lt: end
|
lt: utils.getTerminalKey(start)
|
||||||
|
};
|
||||||
|
|
||||||
|
var utxoStream = self._db.createReadStream(criteria);
|
||||||
|
|
||||||
|
var streamErr;
|
||||||
|
utxoStream.on('end', function() {
|
||||||
|
if (streamErr) {
|
||||||
|
return callback(streamErr);
|
||||||
|
}
|
||||||
|
callback(null, results);
|
||||||
});
|
});
|
||||||
|
|
||||||
var streamErr = null;
|
utxoStream.on('error', function(err) {
|
||||||
|
|
||||||
stream.on('data', function(buffer) {
|
|
||||||
var key = self._encoding.decodeAddressIndexKey(buffer);
|
|
||||||
txids[key.txid] = key.height;
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', function() {
|
|
||||||
callback(streamErr, txids);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', function(err) {
|
|
||||||
streamErr = err;
|
streamErr = err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
utxoStream.on('data', function(data) {
|
||||||
|
var key = self._decodeUtxoIndexKey(data.key);
|
||||||
|
var value = self._encoding.decodeUtxoIndexValue(data.value);
|
||||||
|
results.push({
|
||||||
|
address: address,
|
||||||
|
txid: key.txid,
|
||||||
|
vout: key.oudputIndex,
|
||||||
|
ts: null,
|
||||||
|
scriptPubKey: value.scriptBuffer.toString('hex'),
|
||||||
|
amount: Unit.fromSatoshis(value.satoshis).toBTC(),
|
||||||
|
confirmations: self._p2p.getBestHeight() - value.height,
|
||||||
|
satoshis: value.satoshis,
|
||||||
|
confirmationsFromCache: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---- private function prototypes
|
// ---- private function prototypes
|
||||||
AddressService.prototype._setListeners = function() {
|
AddressService.prototype._setListeners = function() {
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,28 @@ var BN = require('bn.js');
|
|||||||
var consensus = require('bcoin').consensus;
|
var consensus = require('bcoin').consensus;
|
||||||
var constants = require('../../constants');
|
var constants = require('../../constants');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
1. getAddressSummary
|
||||||
|
2. getAddressUnspentOutputs
|
||||||
|
3. bitcoind.height
|
||||||
|
4. getBlockHeader
|
||||||
|
5. getDetailedTransaction
|
||||||
|
6. getTransaction
|
||||||
|
7. sendTransaction
|
||||||
|
8. getInfo
|
||||||
|
9. bitcoind.tiphash
|
||||||
|
10. getBestBlockHash
|
||||||
|
11. isSynced
|
||||||
|
12. getAddressHistory
|
||||||
|
13. getBlock
|
||||||
|
14. getRawBlock
|
||||||
|
15. getBlockHashesByTimestamp
|
||||||
|
16. estimateFee
|
||||||
|
17. getBlockOverview
|
||||||
|
18. syncPercentage
|
||||||
|
|
||||||
|
*/
|
||||||
var BlockService = function(options) {
|
var BlockService = function(options) {
|
||||||
|
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
@ -67,27 +89,23 @@ BlockService.prototype.getAPIMethods = function() {
|
|||||||
return methods;
|
return methods;
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getBestBlockHash = function() {
|
BlockService.prototype.getBestBlockHash = function(callback) {
|
||||||
return this._meta[this._meta.length - 1].hash;
|
return callback(null, this._meta[this._meta.length - 1].hash);
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getBlock = function(hash, callback) {
|
BlockService.prototype.getBlock = function(hash, callback) {
|
||||||
var self = this;
|
|
||||||
this._db.get(this._encoding.encodeBlockKey(hash), function(err, data) {
|
|
||||||
if(err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback(null, self._encoding.decodeBlockValue(data));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BlockService.prototype._getHash = function(blockArg) {
|
blockArg = this._getHash(blockArg);
|
||||||
|
|
||||||
return (_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) &&
|
if (!blockArg) {
|
||||||
this._meta[blockArg] ? this._meta[blockArg] : null;
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getBlock(blockArg, callback);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
BlockService.prototype.getBlockHeader = function(blockArg, callback) {
|
BlockService.prototype.getBlockHeader = function(blockArg, callback) {
|
||||||
|
|
||||||
blockArg = this._getHash(blockArg);
|
blockArg = this._getHash(blockArg);
|
||||||
@ -112,14 +130,6 @@ BlockService.prototype.getBlockHeader = function(blockArg, callback) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype._getBlock = function(hash, callback) {
|
|
||||||
var block = this._blockQueue(hash);
|
|
||||||
if (block) {
|
|
||||||
return callback(null, block);
|
|
||||||
}
|
|
||||||
this._db.get(this._encoding.encodeBlockKey(hash), callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
BlockService.prototype.getBlockOverview = function(hash, callback) {
|
BlockService.prototype.getBlockOverview = function(hash, callback) {
|
||||||
|
|
||||||
this._getBlock(hash, function(err, block) {
|
this._getBlock(hash, function(err, block) {
|
||||||
@ -169,14 +179,18 @@ BlockService.prototype.getPublishEvents = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype.getRawBlock = function(hash, callback) {
|
BlockService.prototype.getRawBlock = function(hash, callback) {
|
||||||
this.getBlock(hash, function(err, data) {
|
this.getBlock(hash, function(err, block) {
|
||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
data.toString();
|
callback(null, block.toString());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.isSynced = function(callback) {
|
||||||
|
callback(null, this._p2p.getBestHeight <= this._tip.height);
|
||||||
|
};
|
||||||
|
|
||||||
BlockService.prototype.start = function(callback) {
|
BlockService.prototype.start = function(callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
@ -217,6 +231,12 @@ BlockService.prototype.subscribe = function(name, emitter) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.syncPercentage = function(callback) {
|
||||||
|
var p2pHeight = this._p2p.getBestHeight();
|
||||||
|
var percentage = ((p2pHeight / (this._tip.height || p2pHeight)) * 100).toFixed(2);
|
||||||
|
callback(null, percentage);
|
||||||
|
};
|
||||||
|
|
||||||
BlockService.prototype.unsubscribe = function(name, emitter) {
|
BlockService.prototype.unsubscribe = function(name, emitter) {
|
||||||
|
|
||||||
var index = this._subscriptions[name].indexOf(emitter);
|
var index = this._subscriptions[name].indexOf(emitter);
|
||||||
@ -327,6 +347,14 @@ BlockService.prototype._findCommonAncestor = function(block) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockService.prototype._getBlock = function(hash, callback) {
|
||||||
|
var block = this._blockQueue(hash);
|
||||||
|
if (block) {
|
||||||
|
return callback(null, block);
|
||||||
|
}
|
||||||
|
this._db.get(this._encoding.encodeBlockKey(hash), callback);
|
||||||
|
};
|
||||||
|
|
||||||
BlockService.prototype._getBlockOperations = function(block) {
|
BlockService.prototype._getBlockOperations = function(block) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
@ -379,6 +407,13 @@ BlockService.prototype._getDelta = function(tip) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockService.prototype._getHash = function(blockArg) {
|
||||||
|
|
||||||
|
return (_.isNumber(blockArg) || (blockArg.length < 40 && /^[0-9]+$/.test(blockArg))) &&
|
||||||
|
this._meta[blockArg] ? this._meta[blockArg] : null;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
BlockService.prototype._getIncompleteChainIndexes = function(block) {
|
BlockService.prototype._getIncompleteChainIndexes = function(block) {
|
||||||
var ret = [];
|
var ret = [];
|
||||||
for(var i = 0; i < this._incompleteChains.length; i++) {
|
for(var i = 0; i < this._incompleteChains.length; i++) {
|
||||||
@ -392,7 +427,9 @@ BlockService.prototype._getIncompleteChainIndexes = function(block) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype._handleReorg = function(block) {
|
BlockService.prototype._handleReorg = function(block) {
|
||||||
|
|
||||||
this._reorging = true;
|
this._reorging = true;
|
||||||
|
|
||||||
log.warn('Chain reorganization detected! Our current block tip is: ' +
|
log.warn('Chain reorganization detected! Our current block tip is: ' +
|
||||||
this._tip.hash + ' the current block: ' + block.hash + '.');
|
this._tip.hash + ' the current block: ' + block.hash + '.');
|
||||||
|
|
||||||
@ -407,8 +444,11 @@ BlockService.prototype._handleReorg = function(block) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.warn('A common ancestor block was found to at hash: ' + commonAncestor + '.');
|
log.warn('A common ancestor block was found to at hash: ' + commonAncestor + '.');
|
||||||
|
|
||||||
this._broadcast(this.subscriptions.reorg, 'block/reorg', [commonAncestor, [block]]);
|
this._broadcast(this.subscriptions.reorg, 'block/reorg', [commonAncestor, [block]]);
|
||||||
|
|
||||||
this._onReorg(commonAncestor, [block]);
|
this._onReorg(commonAncestor, [block]);
|
||||||
|
|
||||||
this._reorging = false;
|
this._reorging = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -446,8 +486,11 @@ BlockService.prototype._loadMeta = function(callback) {
|
|||||||
gte: self._encoding.encodeMetaKey(0),
|
gte: self._encoding.encodeMetaKey(0),
|
||||||
lte: self._encoding.encodeMetaKey(0xffffffff)
|
lte: self._encoding.encodeMetaKey(0xffffffff)
|
||||||
};
|
};
|
||||||
|
|
||||||
var stream = this._db.createReadStream(criteria);
|
var stream = this._db.createReadStream(criteria);
|
||||||
|
|
||||||
stream.on('error', self._onDbError.bind(self));
|
stream.on('error', self._onDbError.bind(self));
|
||||||
|
|
||||||
stream.on('end', function() {
|
stream.on('end', function() {
|
||||||
if (self._meta.length < 1) {
|
if (self._meta.length < 1) {
|
||||||
self._meta.push({
|
self._meta.push({
|
||||||
@ -475,11 +518,15 @@ BlockService.prototype._onBlock = function(block) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. log the reception
|
// 2. log the reception
|
||||||
log.debug('New block received: ' + block.hash);
|
log.debug2('New block received: ' + block.hash);
|
||||||
|
|
||||||
// 3. store the block for safe keeping
|
// 3. store the block for safe keeping
|
||||||
this._cacheBlock(block);
|
this._cacheBlock(block);
|
||||||
|
|
||||||
|
// don't process any more blocks if we are currently in a reorg
|
||||||
|
if (this._reorging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 4. determine block state, reorg, outoforder, normal
|
// 4. determine block state, reorg, outoforder, normal
|
||||||
var blockState = this._determineBlockState(block);
|
var blockState = this._determineBlockState(block);
|
||||||
|
|
||||||
@ -492,6 +539,7 @@ BlockService.prototype._onBlock = function(block) {
|
|||||||
// nothing to do, but wait until ancestor blocks come in
|
// nothing to do, but wait until ancestor blocks come in
|
||||||
break;
|
break;
|
||||||
case 'reorg':
|
case 'reorg':
|
||||||
|
this._handleReorg();
|
||||||
this.emit('reorg', block);
|
this.emit('reorg', block);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -575,7 +623,6 @@ BlockService.prototype._setListeners = function() {
|
|||||||
|
|
||||||
self._p2p.once('bestHeight', self._onBestHeight.bind(self));
|
self._p2p.once('bestHeight', self._onBestHeight.bind(self));
|
||||||
self._db.on('error', self._onDbError.bind(self));
|
self._db.on('error', self._onDbError.bind(self));
|
||||||
self.on('reorg', self._handleReorg.bind(self));
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,28 @@ var BaseService = require('../../service');
|
|||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var Bcoin = require('./bcoin');
|
var Bcoin = require('./bcoin');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
1. getAddressSummary
|
||||||
|
2. getAddressUnspentOutputs
|
||||||
|
3. bitcoind.height
|
||||||
|
4. getBlockHeader
|
||||||
|
5. getDetailedTransaction
|
||||||
|
6. getTransaction
|
||||||
|
7. sendTransaction
|
||||||
|
8. getInfo
|
||||||
|
9. bitcoind.tiphash
|
||||||
|
10. getBestBlockHash
|
||||||
|
11. isSynced
|
||||||
|
12. getAddressHistory
|
||||||
|
13. getBlock
|
||||||
|
14. getRawBlock
|
||||||
|
15. getBlockHashesByTimestamp
|
||||||
|
16. estimateFee
|
||||||
|
17. getBlockOverview
|
||||||
|
18. syncPercentage
|
||||||
|
|
||||||
|
*/
|
||||||
var P2P = function(options) {
|
var P2P = function(options) {
|
||||||
|
|
||||||
if (!(this instanceof P2P)) {
|
if (!(this instanceof P2P)) {
|
||||||
@ -61,6 +83,19 @@ P2P.prototype.getHeaders = function(filter) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
P2P.prototype.getInfo = function(callback) {
|
||||||
|
callback(null, {
|
||||||
|
version: '4.0',
|
||||||
|
protocolversion: 'latest',
|
||||||
|
blocks: this._getBestHeight(),
|
||||||
|
timeoffset: 0,
|
||||||
|
connections: this._pool.numberConnected,
|
||||||
|
difficulty: 0,
|
||||||
|
testnet: false,
|
||||||
|
relayfee: 0
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
P2P.prototype.getMempool = function(filter) {
|
P2P.prototype.getMempool = function(filter) {
|
||||||
|
|
||||||
var peer = this._getPeer();
|
var peer = this._getPeer();
|
||||||
|
|||||||
@ -6,6 +6,28 @@ var inherits = require('util').inherits;
|
|||||||
var LRU = require('lru-cache');
|
var LRU = require('lru-cache');
|
||||||
var utils = require('../../../lib/utils');
|
var utils = require('../../../lib/utils');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
1. getAddressSummary
|
||||||
|
2. getAddressUnspentOutputs
|
||||||
|
3. bitcoind.height
|
||||||
|
4. getBlockHeader
|
||||||
|
5. getDetailedTransaction
|
||||||
|
6. getTransaction
|
||||||
|
7. sendTransaction
|
||||||
|
8. getInfo
|
||||||
|
9. bitcoind.tiphash
|
||||||
|
10. getBestBlockHash
|
||||||
|
11. isSynced
|
||||||
|
12. getAddressHistory
|
||||||
|
13. getBlock
|
||||||
|
14. getRawBlock
|
||||||
|
15. getBlockHashesByTimestamp
|
||||||
|
16. estimateFee
|
||||||
|
17. getBlockOverview
|
||||||
|
18. syncPercentage
|
||||||
|
|
||||||
|
*/
|
||||||
function TimestampService(options) {
|
function TimestampService(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
this._db = this.node.services.db;
|
this._db = this.node.services.db;
|
||||||
@ -24,6 +46,7 @@ TimestampService.prototype.getAPIMethods = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TimestampService.prototype.syncPercentage = function(callback) {
|
TimestampService.prototype.syncPercentage = function(callback) {
|
||||||
|
return callback(null, ((this._tip.height / this._block.getBestBlockHeight()) * 100).toFixed(2) + '%');
|
||||||
};
|
};
|
||||||
|
|
||||||
TimestampService.prototype.getBlockHashesByTimestamp = function(callback) {
|
TimestampService.prototype.getBlockHashesByTimestamp = function(callback) {
|
||||||
|
|||||||
@ -10,77 +10,97 @@ var levelup = require('levelup');
|
|||||||
function TransactionService(options) {
|
function TransactionService(options) {
|
||||||
BaseService.call(this, options);
|
BaseService.call(this, options);
|
||||||
this._db = this.node.services.db;
|
this._db = this.node.services.db;
|
||||||
this.currentTransactions = {};
|
this._mempool = this.node.services._mempool;
|
||||||
|
this._block = this.node.services.block;
|
||||||
|
this._p2p = this.node.services.p2p;
|
||||||
}
|
}
|
||||||
|
|
||||||
inherits(TransactionService, BaseService);
|
inherits(TransactionService, BaseService);
|
||||||
|
|
||||||
TransactionService.dependencies = [
|
TransactionService.dependencies = [
|
||||||
|
'p2p',
|
||||||
'db',
|
'db',
|
||||||
'block',
|
'block',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'mempool'
|
'mempool'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
1. getAddressSummary
|
||||||
|
2. getAddressUnspentOutputs
|
||||||
|
3. bitcoind.height
|
||||||
|
4. getBlockHeader
|
||||||
|
5. getDetailedTransaction
|
||||||
|
6. getTransaction
|
||||||
|
7. sendTransaction
|
||||||
|
8. getInfo
|
||||||
|
9. bitcoind.tiphash
|
||||||
|
10. getBestBlockHash
|
||||||
|
11. isSynced
|
||||||
|
12. getAddressHistory
|
||||||
|
13. getBlock
|
||||||
|
14. getRawBlock
|
||||||
|
15. getBlockHashesByTimestamp
|
||||||
|
16. estimateFee
|
||||||
|
17. getBlockOverview
|
||||||
|
18. syncPercentage
|
||||||
|
|
||||||
|
*/
|
||||||
TransactionService.prototype.getAPIMethods = function() {
|
TransactionService.prototype.getAPIMethods = function() {
|
||||||
return [
|
return [
|
||||||
['getRawTransaction', this, this.getRawTransaction, 1],
|
['getRawTransaction', this, this.getRawTransaction, 1],
|
||||||
['getTransaction', this, this.getTransaction, 1],
|
['getTransaction', this, this.getTransaction, 1],
|
||||||
['getDetailedTransaction', this, this.getDetailedTransaction, 1],
|
['getDetailedTransaction', this, this.getDetailedTransaction, 1],
|
||||||
['sendTransaction', this, this.sendTransaction, 1],
|
['sendTransaction', this, this.sendTransaction, 1],
|
||||||
['getSpentInfo', this, this.getSpentInfo, 1],
|
|
||||||
['syncPercentage', this, this.syncPercentage, 0]
|
['syncPercentage', this, this.syncPercentage, 0]
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
TransactionService.prototype.getSpentInfo = function(txid, callback) {
|
|
||||||
};
|
|
||||||
|
|
||||||
TransactionService.prototype.getRawTransaction = function(txid, callback) {
|
TransactionService.prototype.getTransaction = function(txid, options, callback) {
|
||||||
this.getTransaction(txid, function(err, tx) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
return tx.serialize();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
TransactionService.prototype.getDetailedTransaction = TransactionService.prototype.getTransaction = function(txid, options, callback) {
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var key = self.encoding.encodeTransactionKey(txid);
|
var queryMempool = _.isUndefined(options.queryMempool) ? true : options.queryMempool;
|
||||||
|
|
||||||
async.waterfall([
|
var key = self.encoding.encodeTransactionKey(txid);
|
||||||
function(next) {
|
this._db.get(key, function(err, tx) {
|
||||||
self.node.services.db.get(key, function(err, buffer) {
|
|
||||||
if (err instanceof levelup.errors.NotFoundError) {
|
if(err) {
|
||||||
return next(null, false);
|
return callback(err);
|
||||||
} else if (err) {
|
}
|
||||||
|
|
||||||
|
if (queryMempool && !tx) {
|
||||||
|
|
||||||
|
this._mempool.getTransaction(tx, function(err, memTx) {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
var tx = self.encoding.decodeTransactionValue(buffer);
|
|
||||||
next(null, tx);
|
if (memTx) {
|
||||||
|
return callback(null, { tx: memTx, confirmations: 0});
|
||||||
|
}
|
||||||
|
return callback();
|
||||||
|
|
||||||
});
|
});
|
||||||
}, function(tx, next) {
|
|
||||||
|
} else {
|
||||||
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
return next(null, tx);
|
return callback(null, { tx: tx, confirmations: this._p2p.getBestHeight - tx.__height });
|
||||||
}
|
}
|
||||||
if (!options || !options.queryMempool) {
|
return callback();
|
||||||
return next(new Error('Transaction: ' + txid + ' not found in index'));
|
|
||||||
}
|
}
|
||||||
self.node.services.mempool.getTransaction(txid, function(err, tx) {
|
|
||||||
if (err instanceof levelup.errors.NotFoundError) {
|
});
|
||||||
return callback(new Error('Transaction: ' + txid + ' not found in index or mempool'));
|
|
||||||
} else if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
self._getMissingInputValues(tx, next);
|
|
||||||
});
|
|
||||||
}], callback);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TransactionService.prototype.onBlock = function(block, connectBlock, callback) {
|
TransactionService.prototype._onBlock = function(block, connectBlock, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var action = 'put';
|
var action = 'put';
|
||||||
var reverseAction = 'del';
|
var reverseAction = 'del';
|
||||||
@ -156,6 +176,7 @@ TransactionService.prototype._onReorg = function(commonAncestor, newBlockList) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TransactionService.prototype.start = function(callback) {
|
TransactionService.prototype.start = function(callback) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
self._setListeners();
|
self._setListeners();
|
||||||
|
|
||||||
@ -173,7 +194,7 @@ TransactionService.prototype.start = function(callback) {
|
|||||||
|
|
||||||
self._tip = tip;
|
self._tip = tip;
|
||||||
self.prefix = prefix;
|
self.prefix = prefix;
|
||||||
self.encoding = new Encoding(self.prefix);
|
self._encoding = new Encoding(self.prefix);
|
||||||
self._startSubscriptions();
|
self._startSubscriptions();
|
||||||
callback();
|
callback();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user