wip
This commit is contained in:
parent
758a98b2cd
commit
3fe2c3ea16
@ -28,23 +28,11 @@ var utils = require('../../utils');
|
||||
var AddressService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
|
||||
// this.subscriptions = {};
|
||||
// this.subscriptions['address/transaction'] = {};
|
||||
// this.subscriptions['address/balance'] = {};
|
||||
|
||||
// this._bitcoindTransactionListener = this.transactionHandler.bind(this);
|
||||
// this._bitcoindTransactionLeaveListener = this.transactionLeaveHandler.bind(this);
|
||||
// this.node.services.bitcoind.on('tx', this._bitcoindTransactionListener);
|
||||
// this.node.services.bitcoind.on('txleave', this._bitcoindTransactionLeaveListener);
|
||||
|
||||
this.maxInputsQueryLength = options.maxInputsQueryLength || constants.MAX_INPUTS_QUERY_LENGTH;
|
||||
this.maxOutputsQueryLength = options.maxOutputsQueryLength || constants.MAX_OUTPUTS_QUERY_LENGTH;
|
||||
|
||||
this.concurrency = options.concurrency || 20;
|
||||
|
||||
// this.mempoolIndex = null; // Used for larger mempool indexes
|
||||
// this.mempoolSpentIndex = {}; // Used for small quick synchronous lookups
|
||||
// this.mempoolAddressIndex = {}; // Used to check if an address is on the spend pool
|
||||
};
|
||||
|
||||
inherits(AddressService, BaseService);
|
||||
|
||||
@ -161,12 +161,15 @@ DB.prototype.start = function(callback) {
|
||||
});
|
||||
});
|
||||
|
||||
self._sync.on('synced', function() {
|
||||
self._sync.once('synced', function() {
|
||||
|
||||
self.syncing = false;
|
||||
|
||||
self.node.services.bitcoind.on('tip', function(height) {
|
||||
log.info('New tip at height: ' + height + ' hash: ' + self.node.services.bitcoind.tiphash);
|
||||
self._sync.sync();
|
||||
});
|
||||
|
||||
log.info('Initial sync complete');
|
||||
});
|
||||
|
||||
@ -185,7 +188,6 @@ DB.prototype.start = function(callback) {
|
||||
self.loadTip(self.loadConcurrentTip.bind(self, finish));
|
||||
});
|
||||
|
||||
//TODO remove!
|
||||
setImmediate(function() {
|
||||
self._checkVersion(self._setVersion.bind(self, callback));
|
||||
});
|
||||
@ -215,11 +217,12 @@ DB.prototype.loadTip = function(callback) {
|
||||
var self = this;
|
||||
|
||||
self.store.get(self.dbPrefix + 'tip', self.dbOptions, function(err, tipData) {
|
||||
|
||||
if(err && err instanceof levelup.errors.NotFoundError) {
|
||||
|
||||
self.tip = self.genesis;
|
||||
self.tip.__height = 0;
|
||||
// we need to wait for all the services to become ready,
|
||||
// then we can proceed with connecting blocks here
|
||||
|
||||
self.connectBlock(self.genesis, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
@ -228,40 +231,41 @@ DB.prototype.loadTip = function(callback) {
|
||||
self.emit('addblock', self.genesis);
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
|
||||
} else if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
} else {
|
||||
|
||||
var hash = tipData.slice(0, 32).toString('hex');
|
||||
var height = tipData.readUInt32BE(32);
|
||||
var hash = tipData.slice(0, 32).toString('hex');
|
||||
var height = tipData.readUInt32BE(32);
|
||||
|
||||
var times = 0;
|
||||
async.retry({times: 3, interval: self.retryInterval}, function(done) {
|
||||
self.node.services.bitcoind.getBlock(hash, function(err, tip) {
|
||||
if(err) {
|
||||
times++;
|
||||
log.warn('Bitcoind does not have our tip (' + hash + '). Bitcoind may have crashed and needs to catch up.');
|
||||
if(times < 3) {
|
||||
log.warn('Retrying in ' + (self.retryInterval / 1000) + ' seconds.');
|
||||
var times = 0;
|
||||
async.retry({times: 3, interval: self.retryInterval}, function(done) {
|
||||
self.node.services.bitcoind.getBlock(hash, function(err, tip) {
|
||||
if(err) {
|
||||
times++;
|
||||
log.warn('Bitcoind does not have our tip (' + hash + '). Bitcoind may have crashed and needs to catch up.');
|
||||
if(times < 3) {
|
||||
log.warn('Retrying in ' + (self.retryInterval / 1000) + ' seconds.');
|
||||
}
|
||||
return done(err);
|
||||
}
|
||||
return done(err);
|
||||
|
||||
done(null, tip);
|
||||
});
|
||||
}, function(err, tip) {
|
||||
if(err) {
|
||||
log.warn('Giving up after 3 tries. Please report this bug to https://github.com/bitpay/bitcore-node/issues');
|
||||
log.warn('Please reindex your database.');
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
done(null, tip);
|
||||
tip.__height = height;
|
||||
self.tip = tip;
|
||||
|
||||
callback();
|
||||
});
|
||||
}, function(err, tip) {
|
||||
if(err) {
|
||||
log.warn('Giving up after 3 tries. Please report this bug to https://github.com/bitpay/bitcore-node/issues');
|
||||
log.warn('Please reindex your database.');
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
tip.__height = height;
|
||||
self.tip = tip;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -433,6 +437,10 @@ DB.prototype.getSerialBlockOperations = function(block, add, callback) {
|
||||
async.eachSeries(
|
||||
this.node.services,
|
||||
function(mod, next) {
|
||||
//console.log('s***********************');
|
||||
//console.log('here');
|
||||
//console.log(mod.name, block.__height);
|
||||
//console.log('e***********************');
|
||||
if(mod.blockHandler) {
|
||||
$.checkArgument(typeof mod.blockHandler === 'function', 'blockHandler must be a function');
|
||||
|
||||
|
||||
@ -85,64 +85,61 @@ WalletService.prototype.getPublishEvents = function() {
|
||||
};
|
||||
|
||||
|
||||
WalletService.prototype.getAddressString = function(script, output) {
|
||||
var address = script.toAddress();
|
||||
WalletService.prototype.getAddressString = function(io) {
|
||||
|
||||
var address = io.script.toAddress(this.node.network);
|
||||
|
||||
if(address) {
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
var pubkey = script.getPublicKey();
|
||||
var pubkey = io.script.getPublicKey();
|
||||
if(pubkey) {
|
||||
return pubkey.toString('hex');
|
||||
}
|
||||
} catch(e) {
|
||||
//log.warn('Error getting public key from: ', script.toASM(), script.toHex());
|
||||
// if there is an error, it's because a pubkey can not be extracted from the script
|
||||
// continue on and return null
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
//TODO add back in P2PK, but for this we need to look up the utxo for this script
|
||||
if(output && output.script && output.script.isPublicKeyOut()) {
|
||||
return output.script.getPublicKey().toString('hex');
|
||||
}
|
||||
};
|
||||
|
||||
//log.warn('No utxo given for script spending a P2PK: ', script.toASM(), script.toHex());
|
||||
return null;
|
||||
WalletService.prototype._checkAddresses = function() {
|
||||
return Object.keys(this._addressMap).length > 0;
|
||||
};
|
||||
|
||||
WalletService.prototype.blockHandler = function(block, connectBlock, callback) {
|
||||
var opts = {
|
||||
block: block,
|
||||
connectBlock: connectBlock,
|
||||
fnProcessIO: this._processSerialIO,
|
||||
serial: true
|
||||
};
|
||||
console.log(block.__height);
|
||||
this._blockHandler(opts, callback);
|
||||
};
|
||||
|
||||
WalletService.prototype.concurrentBlockHandler = function(block, connectBlock, callback) {
|
||||
var opts = {
|
||||
block: block,
|
||||
connectBlock: connectBlock,
|
||||
fnProcessIO: this._processConcurrentIO
|
||||
connectBlock: connectBlock
|
||||
};
|
||||
this._blockHandler(opts, callback);
|
||||
};
|
||||
|
||||
WalletService.prototype._blockHandler = function(opts, callback) {
|
||||
|
||||
var self = this;
|
||||
if (!self._checkAddresses()) {
|
||||
return setImmediate(function() {
|
||||
callback(null, []);
|
||||
});
|
||||
}
|
||||
|
||||
var txs = opts.block.transactions;
|
||||
|
||||
async.mapSeries(txs, function(tx, next) {
|
||||
async.mapSeries(opts.block.transactions, function(tx, next) {
|
||||
self._processTransaction(opts, tx, next);
|
||||
}, function(err, operations) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, _.compact(operations));
|
||||
var ret = _.compact(_.flattenDeep(operations));
|
||||
callback(null, ret);
|
||||
});
|
||||
|
||||
};
|
||||
@ -150,10 +147,6 @@ WalletService.prototype._blockHandler = function(opts, callback) {
|
||||
WalletService.prototype._processTransaction = function(opts, tx, callback) {
|
||||
var self = this;
|
||||
|
||||
if(tx.isCoinbase()) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
tx.outputs.forEach(function(output, index) {
|
||||
output.index = index;
|
||||
});
|
||||
@ -170,23 +163,25 @@ WalletService.prototype._processTransaction = function(opts, tx, callback) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, _.compact(operations));
|
||||
callback(null, operations);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
WalletService.prototype._processConcurrentIO = function(opts, tx, io, callback) {
|
||||
|
||||
var walletIds = this._getWalletIdsFromScript(io.script);
|
||||
var self = this;
|
||||
var walletIds = self._getWalletIdsFromScript(io);
|
||||
|
||||
if (!walletIds) {
|
||||
return callback();
|
||||
}
|
||||
var actions = this._getActions(opts.connectBlock);
|
||||
var actions = self._getActions(opts.connectBlock);
|
||||
|
||||
var operations = walletIds.forEach(function(walletId) {
|
||||
var operations = walletIds.map(function(walletId) {
|
||||
return {
|
||||
type: actions[0],
|
||||
key: this._encoding.encodeWalletTransactionKey(walletId, opts.block.__height, tx.id)
|
||||
key: self._encoding.encodeWalletTransactionKey(walletId, opts.block.__height, tx.id)
|
||||
};
|
||||
});
|
||||
|
||||
@ -196,29 +191,23 @@ WalletService.prototype._processConcurrentIO = function(opts, tx, io, callback)
|
||||
|
||||
};
|
||||
|
||||
|
||||
WalletService.prototype._processSerialIO = function(opts, tx, io, callback) {
|
||||
var fn = this._processSerialOutput;
|
||||
if (io instanceof Input) {
|
||||
fn = this._processSerialInput;
|
||||
}
|
||||
fn(opts, tx. io, callback);
|
||||
fn.call(this, opts, tx, io, callback);
|
||||
};
|
||||
|
||||
WalletService.prototype._getWalletIdsFromScript= function(script) {
|
||||
WalletService.prototype._getWalletIdsFromScript = function(io) {
|
||||
|
||||
if(!script) {
|
||||
if(!io.script) {
|
||||
log.debug('Invalid script');
|
||||
return;
|
||||
}
|
||||
|
||||
var address = this.getAddressString(script);
|
||||
return this._addressMap[this.getAddressString(io)];
|
||||
|
||||
if(!address || !this._addressMap[address]) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._addressMap[address];
|
||||
};
|
||||
|
||||
WalletService.prototype._getActions = function(connect) {
|
||||
@ -234,43 +223,45 @@ WalletService.prototype._getActions = function(connect) {
|
||||
WalletService.prototype._processSerialOutput = function(opts, tx, output, callback) {
|
||||
|
||||
var self = this;
|
||||
var walletIds = self._getWalletIdsFromScript(output.script);
|
||||
var walletIds = self._getWalletIdsFromScript(output);
|
||||
|
||||
if (!walletIds) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var actions = self._getActions(opts.connectBlock);
|
||||
var walletIdsNeedingUpdate = {};
|
||||
|
||||
|
||||
async.mapSeries(walletIds, function(walletId, next) {
|
||||
|
||||
walletIdsNeedingUpdate[walletId] = true;
|
||||
self.balances[walletId] = self.balances[walletId] || 0;
|
||||
self.balances[walletId] += opts.connectBlock ? output.satoshis : (-1 * output.satoshis);
|
||||
|
||||
var operations = [{
|
||||
type: actions[0],
|
||||
key: self._encoding.encodeWalletUtxoKey(walletId, tx.id, output.index),
|
||||
value: self._encoding.encodeWalletUtxoValue(opts.block.__height, output.satoshis, output._scriptBuffer)
|
||||
},
|
||||
{
|
||||
type: actions[0],
|
||||
key: self._encoding.encodeWalletUtxoSatoshisKey(walletId, output.satoshis, tx.id, output.index),
|
||||
value: self._encoding.encodeWalletUtxoSatoshisValue(opts.block.__height, output._scriptBuffer)
|
||||
}];
|
||||
|
||||
if(opts.connectBlock) {
|
||||
self.balances[walletId] += output.satoshis;
|
||||
} else {
|
||||
self.balances[walletId] -= output.satoshis;
|
||||
}
|
||||
var operations = [
|
||||
{
|
||||
type: actions[0],
|
||||
key: self._encoding.encodeWalletUtxoKey(walletId, tx.id, output.index),
|
||||
value: self._encoding.encodeWalletUtxoValue(opts.block.__height, output.satoshis, output._scriptBuffer)
|
||||
},
|
||||
{
|
||||
type: actions[0],
|
||||
key: self._encoding.encodeWalletUtxoSatoshisKey(walletId, output.satoshis, tx.id, output.index),
|
||||
value: self._encoding.encodeWalletUtxoSatoshisValue(opts.block.__height, output._scriptBuffer)
|
||||
},
|
||||
{
|
||||
type: 'put',
|
||||
key: self._encoding.encodeWalletBalanceKey(walletId),
|
||||
value: self._encoding.encodeWalletBalanceValue(self.balances[walletId])
|
||||
}
|
||||
];
|
||||
|
||||
next(null, operations);
|
||||
|
||||
}, function(err, operations) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, operations);
|
||||
|
||||
});
|
||||
@ -281,19 +272,20 @@ WalletService.prototype._processSerialInput = function(opts, tx, input, callback
|
||||
|
||||
var self = this;
|
||||
|
||||
var actions = self._getActions(opts.connectBlock);
|
||||
//we may not have walletIds to update but this input may be spending a pay-to-pub-key utxo
|
||||
//so we must get the spending tx to check on that
|
||||
var walletIds = input.script && input.script.isPublicKeyIn() ?
|
||||
['p2pk'] :
|
||||
self._getWalletIdsFromScript(input);
|
||||
|
||||
var walletIds = self._getWalletIdsFromScript(input.script);
|
||||
|
||||
if (walletIds) {
|
||||
if (!walletIds) {
|
||||
return callback();
|
||||
}
|
||||
var walletIdsNeedingUpdate = [];
|
||||
|
||||
var actions = self._getActions(opts.connectBlock);
|
||||
|
||||
async.mapSeries(walletIds, function(walletId, next) {
|
||||
|
||||
walletIdsNeedingUpdate[walletId] = true;
|
||||
|
||||
self.node.services.transaction.getTransaction(input.prevTxId, {}, function(err, tx) {
|
||||
|
||||
if(err) {
|
||||
@ -302,7 +294,22 @@ WalletService.prototype._processSerialInput = function(opts, tx, input, callback
|
||||
|
||||
var utxo = tx.outputs[input.outputIndex];
|
||||
|
||||
var operations = [{
|
||||
if (walletId === 'p2pk') {
|
||||
|
||||
var pubKey = utxo.script.getPublicKey().toString('hex');
|
||||
walletId = self._addressMap[pubKey];
|
||||
|
||||
if (!walletId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.balances[walletId] = self.balances[walletId] || 0;
|
||||
self.balances[walletId] += opts.connectBlock ? (-1 * utxo.satoshis) : utxo.satoshis;
|
||||
|
||||
var operations = [
|
||||
{
|
||||
type: actions[1],
|
||||
key: self._encoding.encodeWalletUtxoKey(walletId, input.prevTxId, input.outputIndex),
|
||||
value: self._encoding.encodeWalletUtxoValue(tx.__height, utxo.satoshis, utxo._scriptBuffer)
|
||||
@ -311,13 +318,13 @@ WalletService.prototype._processSerialInput = function(opts, tx, input, callback
|
||||
type: actions[1],
|
||||
key: self._encoding.encodeWalletUtxoSatoshisKey(walletId, utxo.satoshis, tx.id, input.outputIndex),
|
||||
value: self._encoding.encodeWalletUtxoSatoshisValue(tx.__height, utxo._scriptBuffer)
|
||||
}];
|
||||
|
||||
if(self.connectBlock) {
|
||||
self.balances[walletId] -= utxo.satoshis;
|
||||
} else {
|
||||
self.balances[walletId] += utxo.satoshis;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'put',
|
||||
key: self._encoding.encodeWalletBalanceKey(walletId),
|
||||
value: self._encoding.encodeWalletBalanceValue(self.balances[walletId])
|
||||
}
|
||||
];
|
||||
|
||||
next(null, operations);
|
||||
|
||||
@ -327,6 +334,7 @@ WalletService.prototype._processSerialInput = function(opts, tx, input, callback
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, operations);
|
||||
|
||||
});
|
||||
@ -589,6 +597,7 @@ WalletService.prototype._endpointRegisterWallet = function() {
|
||||
WalletService.prototype._endpointPostAddresses = function() {
|
||||
var self = this;
|
||||
return function(req, res) {
|
||||
|
||||
var addresses = req.addresses;
|
||||
if (!addresses || !addresses.length) {
|
||||
return utils.sendError(new Error('addresses are required when creating a wallet.'), res);
|
||||
@ -602,7 +611,9 @@ WalletService.prototype._endpointPostAddresses = function() {
|
||||
}
|
||||
|
||||
var jobId = utils.generateJobId();
|
||||
|
||||
self._importAddresses(walletId, addresses, jobId, self._jobCompletionCallback.bind(self));
|
||||
|
||||
res.status(200).jsonp({jobId: jobId});
|
||||
};
|
||||
};
|
||||
@ -634,7 +645,9 @@ WalletService.prototype._endpointGetTransactions = function() {
|
||||
var rs = new Readable();
|
||||
|
||||
transactions.forEach(function(transaction) {
|
||||
rs.push(utils.toJSONL(self._formatTransaction(transaction)));
|
||||
if (transaction) {
|
||||
rs.push(utils.toJSONL(self._formatTransaction(transaction)));
|
||||
}
|
||||
});
|
||||
|
||||
rs.push(null);
|
||||
@ -729,101 +742,150 @@ WalletService.prototype._getUtxos = function(walletId, options, callback) {
|
||||
};
|
||||
|
||||
WalletService.prototype._getBalance = function(walletId, options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var key = self._encoding.encodeWalletBalanceKey(walletId);
|
||||
|
||||
self.store.get(key, function(err, buffer) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, self._encoding.decodeWalletBalanceValue(buffer));
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
WalletService.prototype._chunkAdresses = function(addresses) {
|
||||
|
||||
var maxLength = this.node.services.bitcoind.maxAddressesQuery;
|
||||
var groups = [];
|
||||
var groupsCount = Math.ceil(addresses.length / maxLength);
|
||||
|
||||
for(var i = 0; i < groupsCount; i++) {
|
||||
groups.push(addresses.slice(i * maxLength, Math.min(maxLength * (i + 1), addresses.length)));
|
||||
}
|
||||
|
||||
return groups;
|
||||
|
||||
};
|
||||
|
||||
WalletService.prototype._getSearchParams = function(fn, options) {
|
||||
return {
|
||||
gte: fn.call(this, options.walletId, options.start),
|
||||
lt: Buffer.concat([ fn.call(this, options.walletId, options.end).slice(0, -32), new Buffer('ff', 'hex') ])
|
||||
};
|
||||
};
|
||||
|
||||
WalletService.prototype._getTxidsFromDb = function(options, callback) {
|
||||
|
||||
var self = this;
|
||||
var txids = [];
|
||||
var encodingFn = self._encoding.encodeWalletTransactionKey.bind(self._encoding);
|
||||
|
||||
var stream = self.store.createKeyStream(self._getSearchParams(encodingFn, options));
|
||||
|
||||
var streamErr;
|
||||
stream.on('error', function(err) {
|
||||
streamErr = err;
|
||||
});
|
||||
|
||||
stream.on('data', function(data) {
|
||||
txids.push(self._encoding.decodeWalletTransactionKey(data).txid);
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
self._getTransactionsFromDb(txids, options, callback);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
WalletService.prototype._getTransactionsFromDb = function(txids, options, callback) {
|
||||
|
||||
var self = this;
|
||||
async.mapLimit(txids, 10, function(txid, next) {
|
||||
|
||||
self.node.services.transaction.getTransaction(txid, options, next);
|
||||
|
||||
}, function(err, txs) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self._cache.set(options.key, JSON.stringify(self._formatTransactions(txs)));
|
||||
|
||||
if (!options.queryMempool) {
|
||||
|
||||
options.to = options.to || txs.length;
|
||||
return callback(null, txs.slice(options.from, options.to), txs.length);
|
||||
|
||||
}
|
||||
|
||||
options.txs = txs;
|
||||
self._getTransactionsFromMempool(options, callback);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
WalletService.prototype._getTransactionsFromMempool = function(options, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
self._getAddresses(options.walletId, function(err, addresses) {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.mempool.getTransactionsByAddresses(addresses, function(err, mempoolTxs) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var txs = options.txs.concat(mempoolTxs);
|
||||
callback(null, txs.slice(options.from, options.to), txs.length);
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
WalletService.prototype._getTransactions = function(walletId, options, callback) {
|
||||
|
||||
var self = this;
|
||||
var txids = [];
|
||||
|
||||
var start = options.start || 0;
|
||||
var end = options.end || 0xffffffff;
|
||||
|
||||
var opts = {
|
||||
start: options.start || 0,
|
||||
end: options.end || Math.pow(2, 32) - 1
|
||||
start: start,
|
||||
end: end,
|
||||
from: options.from || 0,
|
||||
to: options.to || 0,
|
||||
walletId: walletId,
|
||||
key: walletId + start + end
|
||||
};
|
||||
var key = walletId + opts.start + opts.end;
|
||||
var transactions;
|
||||
|
||||
function finish(transactions) {
|
||||
var from = options.from || 0;
|
||||
var to = options.to || transactions.length;
|
||||
if (!options.queryMempool) {
|
||||
return callback(null, transactions.slice(from, to), transactions.length);
|
||||
}
|
||||
self._getAddresses(walletId, function(err, addresses) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.mempool.getTransactionsByAddresses(addresses, function(err, mempoolTxs) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
transactions = transactions.concat(mempoolTxs);
|
||||
callback(null, transactions.slice(from, to), transactions.length);
|
||||
});
|
||||
});
|
||||
if (!self._cache.peek(opts.key)) {
|
||||
|
||||
return self._getTxidsFromDb(opts, callback);
|
||||
|
||||
}
|
||||
|
||||
function mapTxids(txids) {
|
||||
async.mapLimit(txids, 10, function(txid, next) {
|
||||
self.node.services.transaction.getTransaction(txid, options, next);
|
||||
}, function(err, transactions) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self._cache.set(key, JSON.stringify(self._formatTransactions(transactions)));
|
||||
finish(transactions);
|
||||
});
|
||||
try {
|
||||
|
||||
opts.txs = JSON.parse(self._cache.get(opts.key));
|
||||
self._getTransactionsFromMempool(opts, callback);
|
||||
|
||||
} catch(e) {
|
||||
|
||||
self._cache.del(opts.key);
|
||||
return callback(e);
|
||||
|
||||
}
|
||||
|
||||
if (!self._cache.peek(key)) {
|
||||
var start = self._encoding.encodeWalletTransactionKey(walletId, opts.start);
|
||||
var end = Buffer.concat([
|
||||
self._encoding.encodeWalletTransactionKey(walletId, opts.end)
|
||||
.slice(0, -32), new Buffer('ff', 'hex') ]);
|
||||
var stream = self.store.createKeyStream({
|
||||
gte: start,
|
||||
lte: end
|
||||
});
|
||||
|
||||
var streamErr;
|
||||
stream.on('error', function(err) {
|
||||
streamErr = err;
|
||||
});
|
||||
|
||||
stream.on('data', function(data) {
|
||||
txids.push(self._encoding.decodeWalletTransactionKey(data).txid);
|
||||
});
|
||||
|
||||
stream.on('end', function() {
|
||||
mapTxids(txids);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
transactions = JSON.parse(self._cache.get(key));
|
||||
finish(transactions);
|
||||
} catch(e) {
|
||||
self._cache.del(key);
|
||||
return callback(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WalletService.prototype._removeWallet = function(walletId, callback) {
|
||||
@ -973,7 +1035,6 @@ WalletService.prototype._jobCompletionCallback = function(err, results) {
|
||||
job.reported = false;
|
||||
};
|
||||
|
||||
//TODO: if this is running as a job, then the whole process can be moved to another CPU
|
||||
WalletService.prototype._importAddresses = function(walletId, addresses, jobId, callback) {
|
||||
var self = this;
|
||||
|
||||
@ -1219,16 +1280,18 @@ WalletService.prototype._endpointJobStatus = function() {
|
||||
};
|
||||
|
||||
WalletService.prototype._endpointGetInfo = function() {
|
||||
var self = this;
|
||||
return function(req, res) {
|
||||
res.jsonp({result: 'ok'});
|
||||
res.jsonp({
|
||||
result: 'ok',
|
||||
height: self.node.services.db.tip.__height,
|
||||
hash: self.node.services.db.tip.hash
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
WalletService.prototype.setupRoutes = function(app) {
|
||||
WalletService.prototype._setupReadOnlyRoutes = function(app) {
|
||||
var s = this;
|
||||
var v = validators;
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.get('/info',
|
||||
s._endpointGetInfo()
|
||||
@ -1245,23 +1308,6 @@ WalletService.prototype.setupRoutes = function(app) {
|
||||
app.get('/wallets/:walletId',
|
||||
s._endpointGetAddresses()
|
||||
);
|
||||
app.post('/wallets/:walletId',
|
||||
s._endpointRegisterWallet()
|
||||
);
|
||||
app.delete('/wallets/:walletId',
|
||||
s._endpointRemoveWallet()
|
||||
);
|
||||
app.delete('/wallets/',
|
||||
s._endpointRemoveAllWallets()
|
||||
);
|
||||
app.put('/wallets/:walletId/addresses',
|
||||
s._endpointPutAddresses()
|
||||
);
|
||||
app.post('/wallets/:walletId/addresses',
|
||||
upload.single('addresses'),
|
||||
v.checkAddresses,
|
||||
s._endpointPostAddresses()
|
||||
);
|
||||
app.get('/wallets/:walletId/transactions',
|
||||
s._endpointGetTransactions()
|
||||
);
|
||||
@ -1279,6 +1325,38 @@ WalletService.prototype.setupRoutes = function(app) {
|
||||
);
|
||||
};
|
||||
|
||||
WalletService.prototype._setupWriteRoutes = function(app) {
|
||||
var s = this;
|
||||
var v = validators;
|
||||
|
||||
app.post('/wallets/:walletId',
|
||||
s._endpointRegisterWallet()
|
||||
);
|
||||
app.delete('/wallets/:walletId',
|
||||
s._endpointRemoveWallet()
|
||||
);
|
||||
app.delete('/wallets/',
|
||||
s._endpointRemoveAllWallets()
|
||||
);
|
||||
app.put('/wallets/:walletId/addresses',
|
||||
s._endpointPutAddresses()
|
||||
);
|
||||
app.post('/wallets/:walletId/addresses',
|
||||
upload.single('addresses'),
|
||||
v.checkAddresses,
|
||||
s._endpointPostAddresses()
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
WalletService.prototype.setupRoutes = function(app) {
|
||||
|
||||
app.use(bodyParser.json());
|
||||
this._setupReadOnlyRoutes(app);
|
||||
this._setupWriteRoutes(app);
|
||||
|
||||
};
|
||||
|
||||
WalletService.prototype.getRoutePrefix = function() {
|
||||
return 'wallet-api';
|
||||
};
|
||||
|
||||
@ -50,6 +50,7 @@ var WebService = function(options) {
|
||||
self.server.listen(self.port);
|
||||
self.createMethodsMap();
|
||||
});
|
||||
BaseService.call(this, options);
|
||||
};
|
||||
|
||||
inherits(WebService, BaseService);
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
var MAX_SAFE_INTEGER = 0x1fffffffffffff; // 2 ^ 53 - 1
|
||||
|
||||
var utils = {};
|
||||
utils.isHash = function isHash(value) {
|
||||
utils.isHash = function(value) {
|
||||
return typeof value === 'string' && value.length === 64 && /^[0-9a-fA-F]+$/.test(value);
|
||||
};
|
||||
|
||||
utils.isSafeNatural = function isSafeNatural(value) {
|
||||
utils.isSafeNatural = function(value) {
|
||||
return typeof value === 'number' &&
|
||||
isFinite(value) &&
|
||||
Math.floor(value) === value &&
|
||||
@ -15,7 +15,7 @@ utils.isSafeNatural = function isSafeNatural(value) {
|
||||
value <= MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
utils.startAtZero = function startAtZero(obj, key) {
|
||||
utils.startAtZero = function(obj, key) {
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
obj[key] = 0;
|
||||
}
|
||||
@ -26,7 +26,7 @@ if (!utils.isAbsolutePath) {
|
||||
utils.isAbsolutePath = require('path-is-absolute');
|
||||
}
|
||||
|
||||
utils.parseParamsWithJSON = function parseParamsWithJSON(paramsArg) {
|
||||
utils.parseParamsWithJSON = function(paramsArg) {
|
||||
var params = paramsArg.map(function(paramArg) {
|
||||
var param;
|
||||
try {
|
||||
|
||||
349
regtest/utils.js
Normal file
349
regtest/utils.js
Normal file
@ -0,0 +1,349 @@
|
||||
'use strict';
|
||||
|
||||
var bitcore = require('bitcore-lib');
|
||||
var _ = require('lodash');
|
||||
var mkdirp = require('mkdirp');
|
||||
var rimraf = require('rimraf');
|
||||
var fs = require('fs');
|
||||
var async = require('async');
|
||||
var spawn = require('child_process').spawn;
|
||||
var http = require('http');
|
||||
var Unit = bitcore.Unit;
|
||||
var Transaction = bitcore.Transaction;
|
||||
var PrivateKey = bitcore.PrivateKey;
|
||||
var crypto = require('crypto');
|
||||
|
||||
var utils = {};
|
||||
|
||||
utils.writeConfigFile = function(fileStr, obj) {
|
||||
fs.writeFileSync(fileStr, JSON.stringify(obj));
|
||||
};
|
||||
|
||||
utils.toArgs = function(opts) {
|
||||
return Object.keys(opts).map(function(key) {
|
||||
return '-' + key + '=' + opts[key];
|
||||
});
|
||||
};
|
||||
|
||||
utils.waitForService = function(task, callback) {
|
||||
var retryOpts = { times: 20, interval: 1000 };
|
||||
async.retry(retryOpts, task, callback);
|
||||
};
|
||||
|
||||
utils.queryBitcoreNode = function(httpOpts, callback) {
|
||||
var error;
|
||||
var request = http.request(httpOpts, function(res) {
|
||||
|
||||
if (res.statusCode !== 200 && res.statusCode !== 201) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
return callback(res.statusCode);
|
||||
}
|
||||
|
||||
var resError;
|
||||
var resData = '';
|
||||
|
||||
res.on('error', function(e) {
|
||||
resError = e;
|
||||
});
|
||||
|
||||
res.on('data', function(data) {
|
||||
resData += data;
|
||||
});
|
||||
|
||||
res.on('end', function() {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
if (httpOpts.errorFilter) {
|
||||
return callback(httpOpts.errorFilter(resError, resData));
|
||||
}
|
||||
callback(resError, resData);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
request.on('error', function(e) {
|
||||
error = e;
|
||||
callback(error);
|
||||
});
|
||||
|
||||
request.write(httpOpts.body || '');
|
||||
request.end();
|
||||
};
|
||||
|
||||
utils.waitForBitcoreNode = function(callback) {
|
||||
|
||||
bitcore.process.stdout.on('data', function(data) {
|
||||
if (debug) {
|
||||
console.log(data.toString());
|
||||
}
|
||||
});
|
||||
|
||||
bitcore.process.stderr.on('data', function(data) {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
var errorFilter = function(err, res) {
|
||||
try {
|
||||
if (JSON.parse(res).height === blockHeight) {
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
} catch(e) {
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/info', errorFilter: errorFilter });
|
||||
|
||||
waitForService(queryBitcoreNode.bind(this, httpOpts), callback);
|
||||
};
|
||||
|
||||
utils.waitForBitcoinReady = function(callback) {
|
||||
waitForService(function(callback) {
|
||||
rpc.generate(initialHeight, function(err, res) {
|
||||
if (err || (res && res.error)) {
|
||||
return callback('keep trying');
|
||||
}
|
||||
blockHeight += initialHeight;
|
||||
callback();
|
||||
});
|
||||
}, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
utils.initializeAndStartService = function(opts, callback) {
|
||||
rimraf(opts.datadir, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
mkdirp(opts.datadir, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (opts.configFile) {
|
||||
writeConfigFile(opts.configFile.file, opts.configFile.conf);
|
||||
}
|
||||
var args = _.isArray(opts.args) ? opts.args : toArgs(opts.args);
|
||||
opts.process = spawn(opts.exec, args, opts.opts);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
utils.startBitcoreNode = function(callback) {
|
||||
initializeAndStartService(bitcore, callback);
|
||||
}
|
||||
|
||||
utils.startBitcoind = function(callback) {
|
||||
initializeAndStartService(bitcoin, callback);
|
||||
}
|
||||
|
||||
utils.unlockWallet = function(callback) {
|
||||
rpc.walletPassPhrase(walletPassphrase, 3000, function(err) {
|
||||
if(err && err.code !== -15) {
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
utils.getPrivateKeysWithABalance = function(callback) {
|
||||
rpc.listUnspent(function(err, res) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var utxos = [];
|
||||
for(var i = 0; i < res.result.length; i++) {
|
||||
if (res.result[i].amount > 1) {
|
||||
utxos.push(res.result[i]);
|
||||
}
|
||||
}
|
||||
if (utxos.length <= 0) {
|
||||
return callback(new Error('no utxos available'));
|
||||
}
|
||||
async.mapLimit(utxos, 8, function(utxo, callback) {
|
||||
rpc.dumpPrivKey(utxo.address, function(err, res) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
var privKey = res.result;
|
||||
callback(null, { utxo: utxo, privKey: privKey });
|
||||
});
|
||||
}, function(err, utxos) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, utxos);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
utils.generateSpendingTxs = function(utxos) {
|
||||
return utxos.map(function(utxo) {
|
||||
txCount++;
|
||||
var toPrivKey = new PrivateKey('testnet'); //external addresses
|
||||
var changePrivKey = new PrivateKey('testnet'); //our wallet keys
|
||||
var utxoSatoshis = Unit.fromBTC(utxo.utxo.amount).satoshis;
|
||||
var satsToPrivKey = Math.round(utxoSatoshis / 2);
|
||||
var tx = new Transaction();
|
||||
|
||||
tx.from(utxo.utxo);
|
||||
tx.to(toPrivKey.toAddress().toString(), satsToPrivKey);
|
||||
tx.fee(fee);
|
||||
tx.change(changePrivKey.toAddress().toString());
|
||||
tx.sign(utxo.privKey);
|
||||
|
||||
walletPrivKeys.push(changePrivKey);
|
||||
satoshisReceived += Unit.fromBTC(utxo.utxo.amount).toSatoshis() - (satsToPrivKey + fee);
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
|
||||
utils.setupInitialTxs = function(callback) {
|
||||
getPrivateKeysWithABalance(function(err, utxos) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
initialTxs = generateSpendingTxs(utxos);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
utils.sendTxs = function(callback) {
|
||||
async.eachOfSeries(initialTxs, sendTx, callback);
|
||||
}
|
||||
|
||||
utils.sendTx = function(tx, index, callback) {
|
||||
rpc.sendRawTransaction(tx.serialize(), function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var mod = index % 2;
|
||||
if (mod === 1) {
|
||||
blockHeight++;
|
||||
rpc.generate(1, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
utils.getHttpOpts = function(opts) {
|
||||
return Object.assign({
|
||||
path: opts.path,
|
||||
method: opts.method || 'GET',
|
||||
body: opts.body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': opts.length || 0
|
||||
},
|
||||
errorFilter: opts.errorFilter
|
||||
}, bitcore.httpOpts);
|
||||
}
|
||||
|
||||
utils.registerWallet = function(callback) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId, method: 'POST' });
|
||||
queryBitcoreNode(httpOpts, callback);
|
||||
}
|
||||
|
||||
utils.uploadWallet = function(callback) {
|
||||
var addresses = JSON.stringify(walletPrivKeys.map(function(privKey) {
|
||||
if (privKey.privKey) {
|
||||
return privKey.pubKey.toString();
|
||||
}
|
||||
return privKey.toAddress().toString();
|
||||
}));
|
||||
var httpOpts = getHttpOpts({
|
||||
path: '/wallet-api/wallets/' + walletId + '/addresses',
|
||||
method: 'POST',
|
||||
body: addresses,
|
||||
length: addresses.length
|
||||
});
|
||||
async.waterfall([ queryBitcoreNode.bind(this, httpOpts) ], function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var job = JSON.parse(res);
|
||||
|
||||
Object.keys(job).should.deep.equal(['jobId']);
|
||||
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/jobs/' + job.jobId });
|
||||
|
||||
async.retry({ times: 10, interval: 1000 }, function(next) {
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var result = JSON.parse(res);
|
||||
if (result.status === 'complete') {
|
||||
return next();
|
||||
}
|
||||
next(res);
|
||||
});
|
||||
|
||||
}, function(err) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
utils.getListOfTxs = function(callback) {
|
||||
var end = Date.now() + 86400000;
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId + '/transactions?start=0&end=' + end });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
var results = [];
|
||||
res.split('\n').forEach(function(result) {
|
||||
if (result.length > 0) {
|
||||
return results.push(JSON.parse(result));
|
||||
}
|
||||
});
|
||||
|
||||
var map = initialTxs.map(function(tx) {
|
||||
return tx.serialize();
|
||||
});
|
||||
|
||||
results.forEach(function(result) {
|
||||
var tx = new Transaction(result);
|
||||
map.splice(map.indexOf(tx.uncheckedSerialize()), 1);
|
||||
});
|
||||
|
||||
map.length.should.equal(0);
|
||||
results.length.should.equal(initialTxs.length);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
utils.initGlobals = function() {
|
||||
walletPassphrase = 'test';
|
||||
txCount = 0;
|
||||
blockHeight = 0;
|
||||
walletPrivKeys = [];
|
||||
initialTxs = [];
|
||||
fee = 100000;
|
||||
feesReceived = 0;
|
||||
satoshisSent = 0;
|
||||
walletId = crypto.createHash('sha256').update('test').digest('hex');
|
||||
satoshisReceived = 0;
|
||||
}
|
||||
|
||||
utils.cleanup = function(callback) {
|
||||
bitcore.process.kill();
|
||||
bitcoin.process.kill();
|
||||
setTimeout(callback, 2000);
|
||||
}
|
||||
|
||||
module.exports = utils;
|
||||
@ -1,23 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var mkdirp = require('mkdirp');
|
||||
var rimraf = require('rimraf');
|
||||
var chai = require('chai');
|
||||
var should = chai.should();
|
||||
var spawn = require('child_process').spawn;
|
||||
var async = require('async');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Unit = bitcore.Unit;
|
||||
var Transaction = bitcore.Transaction;
|
||||
var PrivateKey = bitcore.PrivateKey;
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
var crypto = require('crypto');
|
||||
var utils = require('utils');
|
||||
|
||||
var debug = true;
|
||||
var debug = false;
|
||||
var bitcoreDataDir = '/tmp/bitcore';
|
||||
var bitcoinDataDir = '/tmp/bitcoin';
|
||||
|
||||
@ -92,414 +83,144 @@ var bitcore = {
|
||||
};
|
||||
|
||||
var rpc = new BitcoinRPC(rpcConfig);
|
||||
var walletPassphrase = 'test';
|
||||
|
||||
var numberOfStartingTxs = 49; //this should be an even number of txs
|
||||
var txCount = 0;
|
||||
var blockHeight = 0;
|
||||
|
||||
var walletPrivKeys = [];
|
||||
var initialTxs = [];
|
||||
var fee = 100000;
|
||||
var walletId = crypto.createHash('sha256').update('test').digest('hex');
|
||||
var satoshisReceived = 0;
|
||||
var walletPassphrase, txCount, blockHeight, walletPrivKeys,
|
||||
initialTxs, fee, walletId, satoshisReceived, satoshisSent, feesReceived;
|
||||
var initialHeight = 150;
|
||||
|
||||
describe('Wallet Operations', function() {
|
||||
|
||||
this.timeout(60000);
|
||||
|
||||
after(function(done) {
|
||||
bitcore.process.kill();
|
||||
bitcoin.process.kill();
|
||||
setTimeout(done, 2000);
|
||||
});
|
||||
after(cleanup);
|
||||
|
||||
before(function(done) {
|
||||
async.series([
|
||||
startBitcoind,
|
||||
waitForBitcoinReady,
|
||||
unlockWallet,
|
||||
setupInitialTxs, //generate a set of transactions to get us a predictable history
|
||||
startBitcoreNode,
|
||||
waitForBitcoreNode
|
||||
], done);
|
||||
});
|
||||
describe('Register and Upload', function() {
|
||||
|
||||
it('should register wallet', function(done) {
|
||||
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId, method: 'POST' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
res.should.deep.equal(JSON.stringify({
|
||||
walletId: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
|
||||
}));
|
||||
done();
|
||||
before(function(done) {
|
||||
initGlobals();
|
||||
async.series([
|
||||
startBitcoind,
|
||||
waitForBitcoinReady,
|
||||
unlockWallet,
|
||||
setupInitialTxs,
|
||||
startBitcoreNode,
|
||||
waitForBitcoreNode
|
||||
], done);
|
||||
});
|
||||
|
||||
it('should register wallet', function(done) {
|
||||
registerWallet(function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
res.should.deep.equal(JSON.stringify({
|
||||
walletId: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should upload a wallet', function(done) {
|
||||
uploadWallet(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should upload a wallet', function(done) {
|
||||
var addresses = JSON.stringify(walletPrivKeys.map(function(privKey) {
|
||||
return privKey.toAddress().toString();
|
||||
}));
|
||||
var httpOpts = getHttpOpts({
|
||||
path: '/wallet-api/wallets/' + walletId + '/addresses',
|
||||
method: 'POST',
|
||||
body: addresses,
|
||||
length: addresses.length
|
||||
});
|
||||
async.waterfall([ queryBitcoreNode.bind(this, httpOpts) ], function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var job = JSON.parse(res);
|
||||
describe('Load addresses at genesis block', function() {
|
||||
|
||||
Object.keys(job).should.deep.equal(['jobId']);
|
||||
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/jobs/' + job.jobId });
|
||||
|
||||
async.retry({ times: 10, interval: 1000 }, function(next) {
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var result = JSON.parse(res);
|
||||
if (result.status === 'complete') {
|
||||
return next();
|
||||
}
|
||||
next(res);
|
||||
});
|
||||
|
||||
}, function(err) {
|
||||
before(function(done) {
|
||||
sendTxs(function(err) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
waitForBitcoreNode(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a list of transactions', function(done) {
|
||||
|
||||
getListOfTxs(done);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Load addresses after syncing the blockchain', function() {
|
||||
|
||||
before(function(done) {
|
||||
initGlobals();
|
||||
async.series([
|
||||
cleanup,
|
||||
startBitcoind,
|
||||
waitForBitcoinReady,
|
||||
unlockWallet,
|
||||
setupInitialTxs,
|
||||
sendTxs,
|
||||
startBitcoreNode,
|
||||
waitForBitcoreNode,
|
||||
registerWallet,
|
||||
uploadWallet
|
||||
], done);
|
||||
});
|
||||
|
||||
it('should get list of transactions', function(done) {
|
||||
|
||||
getListOfTxs(done);
|
||||
|
||||
});
|
||||
|
||||
it('should get the balance of a wallet', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId + '/balance' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
results.satoshis.should.equal(satoshisReceived);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should get the set of utxos for the wallet', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId + '/utxos' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
var balance = 0;
|
||||
|
||||
results.utxos.forEach(function(utxo) {
|
||||
balance += utxo.satoshis;
|
||||
});
|
||||
results.height.should.equal(blockHeight);
|
||||
balance.should.equal(satoshisReceived);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the list of jobs', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/jobs' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
results.jobCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove all wallets', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets', method: 'DELETE' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
results.numberRemoved.should.equal(152);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a list of transactions', function(done) {
|
||||
var end = Date.now() + 86400000;
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId + '/transactions?start=0&end=' + end });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
//jsonl is returned, so there will be a newline at the end
|
||||
var results = res.split('\n').filter(function(result) {
|
||||
return result.length > 0;
|
||||
});
|
||||
var map = initialTxs.map(function(tx) {
|
||||
return tx.serialize();
|
||||
});
|
||||
for(var i = 0; i < results.length; i++) {
|
||||
var result = results[i];
|
||||
var tx = new Transaction(JSON.parse(result));
|
||||
map.splice(map.indexOf(tx.uncheckedSerialize()), 1);
|
||||
}
|
||||
map.length.should.equal(0);
|
||||
results.length.should.equal(numberOfStartingTxs);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the balance of a wallet', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId + '/balance' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
results.satoshis.should.equal(satoshisReceived);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should get the set of utxos for the wallet', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets/' + walletId + '/utxos' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
// all starting txs were spending to our wallet
|
||||
results.utxos.length.should.equal(numberOfStartingTxs);
|
||||
var map = initialTxs.map(function(tx) {
|
||||
return tx.txid;
|
||||
});
|
||||
var balance = 0;
|
||||
for(var i = 0; i < results.utxos.length; i++) {
|
||||
var result = results.utxos[i];
|
||||
balance += result.satoshis;
|
||||
map.splice(map.indexOf(result.txid), 1);
|
||||
}
|
||||
map.length.should.equal(0);
|
||||
results.height.should.equal(blockHeight);
|
||||
balance.should.equal(satoshisReceived);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the list of jobs', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/jobs' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
var results = JSON.parse(res);
|
||||
results.jobCount.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove all wallets', function(done) {
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/wallets', method: 'DELETE' });
|
||||
queryBitcoreNode(httpOpts, function(err, res) {
|
||||
if(err) {
|
||||
return done(err);
|
||||
}
|
||||
//walletTransactionKey = 1, walletUtxoKey = 1, walletUtxoSatoshis = 1 <-- multiples of numberOfStartingTxs
|
||||
//walletAddresses = 1, walletBalance = 1 <-- one record per index
|
||||
var results = JSON.parse(res);
|
||||
results.numberRemoved.should.equal((numberOfStartingTxs * 3) + 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function writeConfigFile(fileStr, obj) {
|
||||
fs.writeFileSync(fileStr, JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function toArgs(opts) {
|
||||
return Object.keys(opts).map(function(key) {
|
||||
return '-' + key + '=' + opts[key];
|
||||
});
|
||||
}
|
||||
|
||||
function waitForService(task, next) {
|
||||
var retryOpts = { times: 20, interval: 1000 };
|
||||
async.retry(retryOpts, task, next);
|
||||
}
|
||||
|
||||
function queryBitcoreNode(httpOpts, next) {
|
||||
var error;
|
||||
var request = http.request(httpOpts, function(res) {
|
||||
|
||||
if (res.statusCode !== 200 && res.statusCode !== 201) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
return next(res.statusCode);
|
||||
}
|
||||
|
||||
var resError;
|
||||
var resData = '';
|
||||
|
||||
res.on('error', function(e) {
|
||||
resError = e;
|
||||
});
|
||||
|
||||
res.on('data', function(data) {
|
||||
resData += data;
|
||||
});
|
||||
|
||||
res.on('end', function() {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
if (httpOpts.errorFilter) {
|
||||
return next(httpOpts.errorFilter(resError, resData));
|
||||
}
|
||||
next(resError, resData);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
request.on('error', function(e) {
|
||||
error = e;
|
||||
next(error);
|
||||
});
|
||||
|
||||
request.write(httpOpts.body || '');
|
||||
request.end();
|
||||
}
|
||||
|
||||
function waitForBitcoreNode(next) {
|
||||
bitcore.process.stdout.on('data', function(data) {
|
||||
if (debug) {
|
||||
console.log(data.toString());
|
||||
}
|
||||
});
|
||||
bitcore.process.stderr.on('data', function(data) {
|
||||
console.log(data.toString());
|
||||
});
|
||||
var errorFilter = function(err, res) {
|
||||
if (err || (res && !JSON.parse(res).result)) {
|
||||
return 'still syncing';
|
||||
}
|
||||
};
|
||||
|
||||
var httpOpts = getHttpOpts({ path: '/wallet-api/issynced', errorFilter: errorFilter });
|
||||
|
||||
waitForService(queryBitcoreNode.bind(this, httpOpts), next);
|
||||
}
|
||||
|
||||
function waitForBitcoinReady(next) {
|
||||
waitForService(function(next) {
|
||||
rpc.generate(150, function(err, res) {
|
||||
if (err || (res && res.error)) {
|
||||
return next('keep trying');
|
||||
}
|
||||
blockHeight += 150;
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
}, next);
|
||||
}
|
||||
|
||||
function initializeAndStartService(opts, next) {
|
||||
rimraf(opts.datadir, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
mkdirp(opts.datadir, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
if (opts.configFile) {
|
||||
writeConfigFile(opts.configFile.file, opts.configFile.conf);
|
||||
}
|
||||
var args = _.isArray(opts.args) ? opts.args : toArgs(opts.args);
|
||||
opts.process = spawn(opts.exec, args, opts.opts);
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startBitcoreNode(next) {
|
||||
initializeAndStartService(bitcore, next);
|
||||
}
|
||||
|
||||
function startBitcoind(next) {
|
||||
initializeAndStartService(bitcoin, next);
|
||||
}
|
||||
|
||||
function unlockWallet(next) {
|
||||
rpc.walletPassPhrase(walletPassphrase, 3000, function(err) {
|
||||
if(err && err.code !== -15) {
|
||||
return next(err);
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getPrivateKeyWithABalance(next) {
|
||||
rpc.listUnspent(function(err, res) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var utxo;
|
||||
for(var i = 0; i < res.result.length; i++) {
|
||||
if (res.result[i].amount > 1) {
|
||||
utxo = res.result[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!utxo) {
|
||||
return next(new Error('no utxos available'));
|
||||
}
|
||||
rpc.dumpPrivKey(utxo.address, function(err, res) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
var privKey = res.result;
|
||||
next(null, privKey, utxo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generateSpendingTx(privKey, utxo) {
|
||||
txCount++;
|
||||
var toPrivKey = new PrivateKey('testnet'); //external addresses
|
||||
var changePrivKey = new PrivateKey('testnet'); //our wallet keys
|
||||
var utxoSatoshis = Unit.fromBTC(utxo.amount).satoshis;
|
||||
var satsToPrivKey = Math.round(utxoSatoshis / 2);
|
||||
var tx = new Transaction();
|
||||
|
||||
tx.from(utxo);
|
||||
tx.to(toPrivKey.toAddress().toString(), satsToPrivKey);
|
||||
tx.fee(fee);
|
||||
tx.change(changePrivKey.toAddress().toString());
|
||||
tx.sign(privKey);
|
||||
|
||||
walletPrivKeys.push(changePrivKey);
|
||||
satoshisReceived += Unit.fromBTC(utxo.amount).toSatoshis() - (satsToPrivKey + fee);
|
||||
return tx;
|
||||
}
|
||||
|
||||
function setupInitialTx(index, next) {
|
||||
getPrivateKeyWithABalance(function(err, privKey, utxo) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
var tx = generateSpendingTx(privKey, utxo);
|
||||
sendTx(tx, (index % 2 === 0 ? 0 : 1), function(err, tx) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
initialTxs.push(tx);
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupInitialTxs(next) {
|
||||
async.timesSeries(numberOfStartingTxs, setupInitialTx, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
blockHeight++;
|
||||
rpc.generate(1, next);
|
||||
});
|
||||
}
|
||||
|
||||
function sendTx(tx, generateBlocks, next) {
|
||||
rpc.sendRawTransaction(tx.serialize(), function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
if (generateBlocks) {
|
||||
blockHeight += generateBlocks;
|
||||
rpc.generate(generateBlocks, function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
next(null, tx);
|
||||
});
|
||||
} else {
|
||||
next(null, tx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getHttpOpts(opts) {
|
||||
return Object.assign({
|
||||
path: opts.path,
|
||||
method: opts.method || 'GET',
|
||||
body: opts.body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': opts.length || 0
|
||||
},
|
||||
errorFilter: opts.errorFilter
|
||||
}, bitcore.httpOpts);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user