Implemented basic wallet db and calls to bitcoind.
This commit is contained in:
parent
86186e6147
commit
e6d569620b
@ -138,21 +138,15 @@ function loadModule(req, service) {
|
||||
try {
|
||||
// first try in the built-in bitcore-node services directory
|
||||
var serviceFile = path.resolve(__dirname, '../services/' + service.name);
|
||||
if (fs.existsSync(serviceFile + '.js')) {
|
||||
// if the file exists, we can require it, then if there is a problem, catch and display the error
|
||||
service.module = req(serviceFile);
|
||||
} else {
|
||||
// check if the package.json specifies a specific file to use
|
||||
var servicePackage = req(service.name + '/package.json');
|
||||
var serviceModule = service.name;
|
||||
if (servicePackage.bitcoreNode) {
|
||||
serviceModule = service.name + '/' + servicePackage.bitcoreNode;
|
||||
}
|
||||
service.module = req(serviceModule);
|
||||
}
|
||||
service.module = req(serviceFile);
|
||||
} catch(e) {
|
||||
log.error(e.stack);
|
||||
process.exit(-1);
|
||||
var servicePackage = req(service.name + '/package.json');
|
||||
var serviceModule = service.name;
|
||||
if (servicePackage.bitcoreNode) {
|
||||
serviceModule = service.name + '/' + servicePackage.bitcoreNode;
|
||||
}
|
||||
service.module = req(serviceModule);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
242
lib/services/wallet/index.js
Normal file
242
lib/services/wallet/index.js
Normal file
@ -0,0 +1,242 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('../../service');
|
||||
var inherits = require('util').inherits;
|
||||
var index = require('../../');
|
||||
var log = index.log;
|
||||
var errors = index.errors;
|
||||
var bitcore = require('bitcore-lib');
|
||||
var Networks = bitcore.Networks;
|
||||
var levelup = require('levelup');
|
||||
var leveldown = require('leveldown');
|
||||
var multer = require('multer');
|
||||
var storage = multer.memoryStorage();
|
||||
var upload = multer({ storage: storage });
|
||||
var validators = require('./validators');
|
||||
var utils = require('./utils');
|
||||
/**
|
||||
* The Address Service builds upon the Database Service and the Bitcoin Service to add additional
|
||||
* functionality for getting information by base58check encoded addresses. This includes getting the
|
||||
* balance for an address, the history for a collection of addresses, and unspent outputs for
|
||||
* constructing transactions. This is typically the core functionality for building a wallet.
|
||||
* @param {Object} options
|
||||
* @param {Node} options.node - An instance of the node
|
||||
* @param {String} options.name - An optional name of the service
|
||||
*/
|
||||
var WalletService = function(options) {
|
||||
BaseService.call(this, options);
|
||||
this._dbOptions = {
|
||||
keyEncoding: 'string',
|
||||
valueEncoding: 'json'
|
||||
};
|
||||
this._db = levelup(options.dbPath, this._dbOptions);
|
||||
};
|
||||
|
||||
inherits(WalletService, BaseService);
|
||||
|
||||
WalletService.dependencies = [
|
||||
'bitcoind',
|
||||
'web'
|
||||
];
|
||||
|
||||
/**
|
||||
* Called by the Node to get the available API methods for this service,
|
||||
* that can be exposed over the JSON-RPC interface.
|
||||
*/
|
||||
WalletService.prototype.getAPIMethods = function() {
|
||||
return [
|
||||
//['getWalletInfo', this, this.getInfo, 0]
|
||||
//['getWalletBalance', this, this.getWalletBalance, 2],
|
||||
//['getBlockTimestampInfo', this, this.getBlockTimestampInfo, 2],
|
||||
//['addWalletAddresses', this, this., 2],
|
||||
//['addWalletAddress', this, this.addWalletAddress, 2],
|
||||
//['addWallet', this, this.addWallet, 2],
|
||||
//['getWalletUtxos', this, this.getWalletUtxos, 2],
|
||||
//['getWalletRawTransactions', this, this.getWalletRawTransactions, 1]
|
||||
//['getWalletTxids', this, this.getWalletTxids, 1]
|
||||
//['getWalletTransactions', this, this.getWalletTransactions, 1]
|
||||
];
|
||||
};
|
||||
WalletService.prototype.start = function(callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
WalletService.prototype.stop = function(callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by the Bus to get the available events for this service.
|
||||
*/
|
||||
WalletService.prototype.getPublishEvents = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
WalletService.prototype._endpointUTXOs = function() {
|
||||
var self = this;
|
||||
return function(req, res) {
|
||||
var walletId = req.params.walletId;
|
||||
//var tip = self.node.bitcoind.tip;
|
||||
// TODO: get the height of the tip
|
||||
//var height = tip;
|
||||
var height = null;
|
||||
self._getUtxos(walletId, height, function(err, utxos) {
|
||||
if(err) {
|
||||
return utils.sendError(err);
|
||||
}
|
||||
res.status(200).jsonp({
|
||||
utxos: utxos,
|
||||
height: height
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
WalletService.prototype._endpointGetBalance= function() {
|
||||
var self = this;
|
||||
return function(req, res) {
|
||||
var walletId = req.params.walletId;
|
||||
//var tip = self.node.bitcoind.tip;
|
||||
// TODO: get the height of the tip
|
||||
//var height = tip;
|
||||
var height = null;
|
||||
self._getBalance(walletId, height, function(err, balance) {
|
||||
if(err) {
|
||||
return utils.sendError(err);
|
||||
}
|
||||
res.status(200).jsonp({
|
||||
balance: balance,
|
||||
height: height
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
WalletService.prototype._endpointGetAddresses = function() {
|
||||
var self = this;
|
||||
return function(req, res) {
|
||||
var walletId = req.params.walletId;
|
||||
|
||||
self._getAddresses(walletId, function(err, addresses) {
|
||||
if(err) {
|
||||
return utils.sendError(err);
|
||||
}
|
||||
res.status(200).jsonp({
|
||||
addresses: addresses
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
WalletService.prototype._endpointPostAddresses = function() {
|
||||
var self = this;
|
||||
return function(req, res) {
|
||||
var addresses = req.addresses;
|
||||
var walletId = utils.getWalletId();
|
||||
self._storeAddresses(walletId, addresses, function(err, hash) {
|
||||
if(err) {
|
||||
return utils.sendError(err, res);
|
||||
}
|
||||
res.status(201).jsonp({
|
||||
walletId: walletId,
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
WalletService.prototype._getUtxos = function(walletId, height, callback) {
|
||||
// TODO get the balance only to this height
|
||||
var self = this;
|
||||
self._getAddresses(walletId, function(err, addresses) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.node.services.bitcoind.getAddressUnspentOutputs(addresses, {queryMempool: false}, callback);
|
||||
});
|
||||
};
|
||||
WalletService.prototype._getBalance = function(walletId, height, callback) {
|
||||
// TODO get the balance only to this height
|
||||
var self = this;
|
||||
self._getAddresses(walletId, function(err, addresses) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
self.node.services.bitcoind.getAddressUnspentOutputs(addresses, {
|
||||
queryMempool: false
|
||||
}, function(err, utxos) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
var balance = 0;
|
||||
utxos.forEach(function(utxo) {
|
||||
balance += utxo.satoshis;
|
||||
});
|
||||
callback(null, balance);
|
||||
});
|
||||
});
|
||||
};
|
||||
WalletService.prototype._getAddresses = function(walletId, callback) {
|
||||
this._db.get(walletId, callback);
|
||||
};
|
||||
WalletService.prototype._storeAddresses = function(walletId, addresses, callback) {
|
||||
this._db.put(walletId, addresses, callback);
|
||||
};
|
||||
|
||||
WalletService.prototype._endpointGetInfo = function() {
|
||||
return function(req, res) {
|
||||
res.jsonp({result: 'ok'});
|
||||
};
|
||||
};
|
||||
WalletService.prototype.setupRoutes = function(app, express) {
|
||||
var s = this;
|
||||
var v = validators;
|
||||
app.get('/info',
|
||||
s._endpointGetInfo()
|
||||
);
|
||||
//app.get('/wallets/:walletId/txids',
|
||||
// v.checkWalletId,
|
||||
// v.checkRangeParams,
|
||||
// s._endpointTxids()
|
||||
//);
|
||||
//app.get('/wallets/:walletId/transactions',
|
||||
// v.checkWalletId,
|
||||
// v.checkRangeParams,
|
||||
// s._endpointTransactions()
|
||||
//);
|
||||
//app.get('/wallets/:walletId/rawtransactions',
|
||||
// v.checkWalletId,
|
||||
// v.checkRangeParams,
|
||||
// s._endpointRawTransactions()
|
||||
//);
|
||||
app.get('/wallets/:walletId/utxos',
|
||||
s._endpointUTXOs()
|
||||
);
|
||||
app.get('/wallets/:walletId/balance',
|
||||
s._endpointGetBalance()
|
||||
);
|
||||
app.get('/wallets/:walletId',
|
||||
s._endpointGetAddresses()
|
||||
);
|
||||
app.post('/wallets/addresses',
|
||||
upload.single('addresses'),
|
||||
v.checkAddresses,
|
||||
s._endpointPostAddresses()
|
||||
);
|
||||
//app.put('/wallets/:walletId',
|
||||
// v.checkWalletId,
|
||||
// s._endpointPutWallet()
|
||||
//);
|
||||
//app.get('/info/timestamps',
|
||||
// s._endpointGetHeightsFromTimestamps()
|
||||
//);
|
||||
//app.get('/jobs/:jobId',
|
||||
// s._endpointJobStatus()
|
||||
//);
|
||||
//app.use(s._endpointNotFound());
|
||||
};
|
||||
|
||||
WalletService.prototype.getRoutePrefix = function() {
|
||||
return 'wallet';
|
||||
};
|
||||
module.exports = WalletService;
|
||||
|
||||
691
lib/services/wallet/utils.js
Normal file
691
lib/services/wallet/utils.js
Normal file
@ -0,0 +1,691 @@
|
||||
'use strict';
|
||||
|
||||
var Writable = require('stream').Writable;
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
var fs = require('fs');
|
||||
var inherits = require('util').inherits;
|
||||
var path = require('path');
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
var BitcoinRPC = require('bitcoind-rpc');
|
||||
var _ = require('lodash');
|
||||
var async = require('async');
|
||||
var bitcore = require('bitcore-lib');
|
||||
var mkdirp = require('mkdirp');
|
||||
var ttyread = require('ttyread');
|
||||
|
||||
var exports = {};
|
||||
|
||||
exports.isInteger = function(value) {
|
||||
return typeof value === 'number' &&
|
||||
isFinite(value) &&
|
||||
Math.floor(value) === value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will create a directory if it does not already exist.
|
||||
*
|
||||
* @param {String} directory - An absolute path to the directory
|
||||
* @param {Function} callback
|
||||
*/
|
||||
exports.setupDirectory = function(directory, callback) {
|
||||
fs.access(directory, function(err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
return mkdirp(directory, callback);
|
||||
} else if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This will split a range of numbers "a" to "b" by sections
|
||||
* of the length "max".
|
||||
*
|
||||
* Example:
|
||||
* > var range = utils.splitRange(1, 10, 3);
|
||||
* > [[1, 3], [4, 6], [7, 9], [10, 10]]
|
||||
*
|
||||
* @param {Number} a - The start index (lesser)
|
||||
* @param {Number} b - The end index (greater)
|
||||
* @param {Number} max - The maximum section length
|
||||
*/
|
||||
exports.splitRange = function(a, b, max) {
|
||||
assert(b > a, '"b" is expected to be greater than "a"');
|
||||
var sections = [];
|
||||
var delta = b - a;
|
||||
var first = a;
|
||||
var last = a;
|
||||
|
||||
var length = Math.floor(delta / max);
|
||||
for (var i = 0; i < length; i++) {
|
||||
last = first + max - 1;
|
||||
sections.push([first, last]);
|
||||
first += max;
|
||||
}
|
||||
|
||||
if (last <= b) {
|
||||
sections.push([first, b]);
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
/**
|
||||
* getFileStream: Checks for the file's existence and returns a readable stream or stdin
|
||||
* @param {String} path - The path to the file
|
||||
* @param {Function} callback
|
||||
*/
|
||||
exports.getFileStream = function(filePath, callback) {
|
||||
callback(null, fs.createReadStream(filePath));
|
||||
};
|
||||
|
||||
exports.readWalletDatFile = function(filePath, network, callback) {
|
||||
assert(_.isString(network), 'Network expected to be a string.');
|
||||
var datadir = path.dirname(filePath).replace(/(\/testnet3|\/regtest)$/, '');
|
||||
var name = path.basename(filePath);
|
||||
var options = ['-datadir=' + datadir, '-wallet=' + name];
|
||||
if (network === 'testnet') {
|
||||
options.push('-testnet');
|
||||
} else if (network === 'regtest') {
|
||||
options.push('-regtest');
|
||||
}
|
||||
// TODO use ../node_modules/.bin/wallet-utility
|
||||
var exec = path.resolve(__dirname, '../node_modules/bitcore-node/bin/bitcoin-0.12.1/bin/wallet-utility');
|
||||
var wallet = spawn(exec, options);
|
||||
|
||||
var result = '';
|
||||
|
||||
wallet.stdout.on('data', function(data) {
|
||||
result += data.toString('utf8');
|
||||
});
|
||||
|
||||
var error;
|
||||
|
||||
wallet.stderr.on('data', function(data) {
|
||||
error = data.toString();
|
||||
});
|
||||
|
||||
wallet.on('close', function(code) {
|
||||
if (code === 0) {
|
||||
var addresses;
|
||||
try {
|
||||
addresses = JSON.parse(result);
|
||||
addresses = addresses.map(function(entry) {
|
||||
return entry.addr ? entry.addr : entry;
|
||||
});
|
||||
} catch(err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, addresses);
|
||||
} else if (error) {
|
||||
return callback(new Error(error));
|
||||
} else {
|
||||
var message = 'wallet-utility exited (' + code + '): ' + result;
|
||||
return callback(new Error(message));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.readWalletFile = function(filePath, network, callback) {
|
||||
if (/\.dat$/.test(filePath)) {
|
||||
exports.readWalletDatFile(filePath, network, callback);
|
||||
} else {
|
||||
exports.getFileStream(filePath, callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This will split an array into smaller arrays by size
|
||||
*
|
||||
* @param {Array} array
|
||||
* @param {Number} size - The length of resulting smaller arrays
|
||||
*/
|
||||
exports.splitArray = function(array, size) {
|
||||
var results = [];
|
||||
while (array.length) {
|
||||
results.push(array.splice(0, size));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to get the remote ip address from cloudflare headers.
|
||||
*
|
||||
* @param {Object} req - An express request object
|
||||
*/
|
||||
exports.getRemoteAddress = function(req) {
|
||||
if (req.headers['cf-connecting-ip']) {
|
||||
return req.headers['cf-connecting-ip'];
|
||||
}
|
||||
return req.socket.remoteAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
* A middleware to enable CORS
|
||||
*
|
||||
* @param {Object} req - An express request object
|
||||
* @param {Object} res - An express response object
|
||||
* @param {Function} next
|
||||
*/
|
||||
exports.enableCORS = function(req, res, next) {
|
||||
res.header('access-control-allow-origin', '*');
|
||||
res.header('access-control-allow-methods', 'GET, HEAD, PUT, POST, OPTIONS');
|
||||
var allowed = [
|
||||
'origin',
|
||||
'x-requested-with',
|
||||
'content-type',
|
||||
'accept',
|
||||
'content-length',
|
||||
'cache-control',
|
||||
'cf-connecting-ip'
|
||||
];
|
||||
res.header('access-control-allow-headers', allowed.join(', '));
|
||||
|
||||
var method = req.method && req.method.toUpperCase && req.method.toUpperCase();
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Will send error to express response
|
||||
*
|
||||
* @param {Error} err - error object
|
||||
* @param {Object} res - express response object
|
||||
*/
|
||||
exports.sendError = function(err, res) {
|
||||
if (err.statusCode) {
|
||||
res.status(err.statusCode).send(err.message);
|
||||
} else {
|
||||
console.error(err.stack);
|
||||
res.status(503).send(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Will create a writeable logger stream
|
||||
*
|
||||
* @param {Function} logger - Function to log information
|
||||
* @returns {Stream}
|
||||
*/
|
||||
exports.createLogStream = function(logger) {
|
||||
function Log(options) {
|
||||
Writable.call(this, options);
|
||||
}
|
||||
inherits(Log, Writable);
|
||||
|
||||
Log.prototype._write = function (chunk, enc, callback) {
|
||||
logger(chunk.slice(0, chunk.length - 1)); // remove new line and pass to logger
|
||||
callback();
|
||||
};
|
||||
var stream = new Log();
|
||||
|
||||
return stream;
|
||||
};
|
||||
|
||||
exports.getWalletId = function() {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
};
|
||||
|
||||
exports.getClients = function(clientsConfig) {
|
||||
var clients = [];
|
||||
for (var i = 0; i < clientsConfig.length; i++) {
|
||||
var config = clientsConfig[i];
|
||||
var remoteClient = new BitcoinRPC({
|
||||
protocol: config.rpcprotocol || 'http',
|
||||
host: config.rpchost || '127.0.0.1',
|
||||
port: config.rpcport,
|
||||
user: config.rpcuser,
|
||||
pass: config.rpcpassword,
|
||||
rejectUnauthorized: _.isUndefined(config.rpcstrict) ? true : config.rpcstrict
|
||||
});
|
||||
clients.push(remoteClient);
|
||||
}
|
||||
return clients;
|
||||
};
|
||||
|
||||
exports.setClients = function(obj, clients) {
|
||||
obj._clients = clients;
|
||||
obj._clientsIndex = 0;
|
||||
Object.defineProperty(obj, 'clients', {
|
||||
get: function() {
|
||||
var client = obj._clients[obj._clientsIndex];
|
||||
obj._clientsIndex = (obj._clientsIndex + 1) % obj._clients.length;
|
||||
return client;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false
|
||||
});
|
||||
};
|
||||
|
||||
exports.tryAllClients = function(obj, func, options, callback) {
|
||||
if (_.isFunction(options)) {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
var clientIndex = obj._clientsIndex;
|
||||
var retry = function(done) {
|
||||
var client = obj._clients[clientIndex];
|
||||
clientIndex = (clientIndex + 1) % obj._clients.length;
|
||||
func(client, done);
|
||||
};
|
||||
async.retry({times: obj._clients.length, interval: options.interval || 1000}, retry, callback);
|
||||
};
|
||||
|
||||
exports.wrapRPCError = function(errObj) {
|
||||
var err = new Error(errObj.message);
|
||||
err.code = errObj.code;
|
||||
return err;
|
||||
};
|
||||
|
||||
var PUBKEYHASH = new Buffer('01', 'hex');
|
||||
var SCRIPTHASH = new Buffer('02', 'hex');
|
||||
|
||||
exports.getAddressTypeString = function(bufferArg) {
|
||||
var buffer = bufferArg;
|
||||
if (!Buffer.isBuffer(bufferArg)) {
|
||||
buffer = new Buffer(bufferArg, 'hex');
|
||||
}
|
||||
var type = buffer.slice(0, 1);
|
||||
if (type.compare(PUBKEYHASH) === 0) {
|
||||
return 'pubkeyhash';
|
||||
} else if (type.compare(SCRIPTHASH) === 0) {
|
||||
return 'scripthash';
|
||||
} else {
|
||||
throw new TypeError('Unknown address type');
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAddressTypeBuffer = function(address) {
|
||||
var type;
|
||||
if (address.type === 'pubkeyhash') {
|
||||
type = PUBKEYHASH;
|
||||
} else if (address.type === 'scripthash') {
|
||||
type = SCRIPTHASH;
|
||||
} else {
|
||||
throw new TypeError('Unknown address type');
|
||||
}
|
||||
return type;
|
||||
};
|
||||
|
||||
exports.splitBuffer = function(buffer, size) {
|
||||
var pos = 0;
|
||||
var buffers = [];
|
||||
while (pos < buffer.length) {
|
||||
buffers.push(buffer.slice(pos, pos + size));
|
||||
pos += size;
|
||||
}
|
||||
return buffers;
|
||||
};
|
||||
|
||||
exports.exitWorker = function(worker, timeout, callback) {
|
||||
assert(worker, '"worker" is expected to be defined');
|
||||
var exited = false;
|
||||
worker.once('exit', function(code) {
|
||||
if (!exited) {
|
||||
exited = true;
|
||||
if (code !== 0) {
|
||||
var error = new Error('Worker did not exit cleanly: ' + code);
|
||||
error.code = code;
|
||||
return callback(error);
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
worker.kill('SIGINT');
|
||||
setTimeout(function() {
|
||||
if (!exited) {
|
||||
exited = true;
|
||||
worker.kill('SIGKILL');
|
||||
return callback(new Error('Worker exit timeout, force shutdown'));
|
||||
}
|
||||
}, timeout).unref();
|
||||
};
|
||||
|
||||
exports.timestampToISOString = function(timestamp) {
|
||||
return new Date(this.toIntIfNumberLike(timestamp) * 1000).toISOString();
|
||||
};
|
||||
|
||||
exports.satoshisToBitcoin = function(satoshis) {
|
||||
return satoshis / 100000000;
|
||||
};
|
||||
|
||||
exports.getPassphrase = function(callback) {
|
||||
ttyread('Enter passphrase: ', {silent: true}, callback);
|
||||
};
|
||||
|
||||
exports.acquirePassphrase = function(callback) {
|
||||
var first;
|
||||
var second;
|
||||
async.doWhilst(function(next) {
|
||||
ttyread('Enter passphrase: ', {silent: true}, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
first = result;
|
||||
ttyread('Re-enter passphrase: ', {silent: true}, function(err, result) {
|
||||
second = result;
|
||||
next();
|
||||
});
|
||||
});
|
||||
}, function() {
|
||||
if (first !== second) {
|
||||
console.log('Passphrases do not match, please re-enter.');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, first);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Important notes:
|
||||
|
||||
How the encryption/decryption schemes work.
|
||||
1. The user's passphrase and salt are hashed using scrypt algorithm. You must store the salt.
|
||||
On modern hardware this hashing function should take 1-2 seconds.
|
||||
2. The resulting hash is 48 bytes. The first 32 bytes of this hash is the "key" and the last
|
||||
16 bytes is the "iv" to decrypt the master key using AES256-cbc.
|
||||
3. The plaintext "master key" is always 32 bytes and should be as random as possible.
|
||||
You may pass in the plaintext master key to encryptSecret -or- /dev/random will be consulted.
|
||||
4. The cipherText of the master key must be stored just like the salt. For added security, you
|
||||
might store the cipherText of the master key separate from the cipherText.
|
||||
For example, if an attacker discovers your passphrase and salt (the most likely scenario), they would
|
||||
still require the cipherText of the master key in order to decrypt the cipherText of your private keys.
|
||||
Storing your encrypted master key on another device would be a better choice than keeping your salt,
|
||||
the cipherText of your master key and the cipherText of your private keys on the same computer system.
|
||||
5. The plaintext master key is then used to encrypt/decrypt the bitcoin private keys. The private keys'
|
||||
corresponding public key is used as the IV for the procedure.
|
||||
|
||||
|
||||
Specific notes regarding how private keys are transferred from a traditional "wallet.dat" file used with
|
||||
Bitcoin Core's Wallet:
|
||||
|
||||
1. Bitcoin Core's Wallet uses Berkeley DB version 4.8 to store secp256k1 elliptic curve private keys in WIF format.
|
||||
2. The same Berkeley DB, internally called "main", also stores compressed public keys for the above private keys,
|
||||
the master keys used to encrypt the above private keys and bitcoin transaction details relevant to those private keys
|
||||
3. The underlying data structure for the Berkeley database is the B-Tree (balanced tree). This is a key-value data
|
||||
structure, therefore the database is a key-value database.
|
||||
Berkeley DB documentation also refers to this as "key-record"
|
||||
This means that the data contained in this B-Tree is organized for high speed retrieval based on a key.
|
||||
In other words the database is optimized for lookups.
|
||||
4. The filename for this database file is called "wallet.dat" historically,
|
||||
but you can rename it to whatever suits you
|
||||
|
||||
*/
|
||||
//this function depends on the derivation method and its params that were originally used to hash the passphrase
|
||||
//this could be SHA512, scrypt, etc.
|
||||
exports.sha512KDF = function(passphrase, salt, derivationOptions, callback) {
|
||||
if (!derivationOptions || derivationOptions.method !== 0 || !derivationOptions.rounds) {
|
||||
return callback(new Error('SHA512 KDF method was called for, ' +
|
||||
'yet the derivations options for it were not supplied.'));
|
||||
}
|
||||
var rounds = derivationOptions.rounds || 1;
|
||||
//if salt was sent in as a string, we will have to assume the default encoding type
|
||||
if (!Buffer.isBuffer(salt)) {
|
||||
salt = new Buffer(salt, 'utf-8');
|
||||
}
|
||||
var derivation = Buffer.concat([new Buffer(''), new Buffer(passphrase), salt]);
|
||||
for(var i = 0; i < rounds; i++) {
|
||||
derivation = crypto.createHash('sha512').update(derivation).digest();
|
||||
}
|
||||
callback(null, derivation);
|
||||
};
|
||||
|
||||
exports.hashPassphrase = function(opts) {
|
||||
return exports.sha512KDF;
|
||||
};
|
||||
|
||||
exports.decryptPrivateKey = function(opts, callback) {
|
||||
exports.decryptSecret(opts, function(err, masterKey) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
opts.cipherText = opts.pkCipherText;
|
||||
//decrypt the private here using the plainText master key as the "key"
|
||||
//and the double sha256 compressed pub key as the "IV"
|
||||
opts.key = masterKey;
|
||||
opts.iv = bitcore.crypto.Hash.sha256sha256(new Buffer(opts.pubkey, 'hex'));
|
||||
exports.decrypt(opts, function(err, privateKey) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, privateKey);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//call decryptSecret first
|
||||
exports.encryptPrivateKeys = function(opts, callback) {
|
||||
if (!opts.masterKey || !opts.keys) {
|
||||
return callback(new Error('A decrypted master key, ' +
|
||||
'compressed public keys and private keys are required for encryption.'));
|
||||
}
|
||||
if (!Buffer.isBuffer(opts.masterKey)) { //we'll have to assume the master key is utf-8 encoded
|
||||
opts.masterKey = new Buffer(opts.masterKey);
|
||||
}
|
||||
assert(opts.masterKey.length === 32, 'Master Key must be 32 bytes in length, ' +
|
||||
'if you have a hex string, please pass master key in as a buffer');
|
||||
//if the master key is not 32 bytes, then take the sha256 hash
|
||||
var ret = [];
|
||||
async.mapLimit(opts.keys, 5, function(key, next) {
|
||||
var iv = bitcore.crypto.Hash.sha256sha256(new Buffer(key.pubKey, 'hex')).slice(0, 16);
|
||||
//do we want to encrypt WIF's or RAW private keys or does it matter?
|
||||
exports.encrypt({
|
||||
secret: key.privKey,
|
||||
iv: iv,
|
||||
key: opts.masterKey
|
||||
}, next);
|
||||
}, function(err, results) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
for(var i = 0; i < results.length; i++) {
|
||||
ret.push({
|
||||
cipherText: results[i],
|
||||
checkHash: bitcore.crypto.Hash.sha256(new Buffer(opts.keys[i].pubKey + results[i])).toString('hex'),
|
||||
type: 'encrypted private key',
|
||||
pubKey: opts.keys[i].pubKey
|
||||
});
|
||||
}
|
||||
callback(null, ret);
|
||||
});
|
||||
};
|
||||
|
||||
exports.encrypt = function(opts, callback) {
|
||||
if (!opts.key ||
|
||||
!opts.iv ||
|
||||
!opts.secret ||
|
||||
opts.key.length !== 32 ||
|
||||
opts.iv.length !== 16 ||
|
||||
opts.secret.length < 1) {
|
||||
return callback(new Error('Key, IV, and something to encrypt is required.'));
|
||||
}
|
||||
var cipher = crypto.createCipheriv('aes-256-cbc', opts.key, opts.iv);
|
||||
var cipherText;
|
||||
try {
|
||||
cipherText = Buffer.concat([cipher.update(opts.secret), cipher.final()]).toString('hex');
|
||||
} catch(e) {
|
||||
return callback(e);
|
||||
}
|
||||
return callback(null, cipherText);
|
||||
|
||||
};
|
||||
exports.encryptSecret = function(opts, callback) {
|
||||
var hashFunc = exports.hashPassphrase(opts.derivationOptions);
|
||||
hashFunc(opts.passphrase, opts.salt, opts.derivationOptions, function(err, hashedPassphrase) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var secret = opts.secret || crypto.randomBytes(32);
|
||||
assert(Buffer.isBuffer(secret), 'secret is expected to be a buffer');
|
||||
secret = bitcore.crypto.Hash.sha256sha256(secret);
|
||||
var firstHalf = hashedPassphrase.slice(0, 32); //AES256-cbc shared key
|
||||
var secondHalf = hashedPassphrase.slice(32, 48); //AES256-cbc IV, for cbc mode, the IV will be 16 bytes
|
||||
exports.encrypt({
|
||||
secret: secret,
|
||||
key: firstHalf,
|
||||
iv: secondHalf
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.decryptSecret = function(opts, callback) {
|
||||
var hashFunc = exports.hashPassphrase(opts.derivationOptions);
|
||||
hashFunc(opts.passphrase, opts.salt, opts.derivationOptions, function(err, hashedPassphrase) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
opts.key = hashedPassphrase;
|
||||
exports.decrypt(opts, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.decrypt = function(opts, callback) {
|
||||
if (!Buffer.isBuffer(opts.key)) {
|
||||
opts.key = new Buffer(opts.key, 'hex');
|
||||
}
|
||||
var secondHalf;
|
||||
if (opts.iv) {
|
||||
secondHalf = opts.iv.slice(0, 16);
|
||||
} else {
|
||||
secondHalf = opts.key.slice(32, 48); //AES256-cbc IV
|
||||
}
|
||||
var cipherText = new Buffer(opts.cipherText, 'hex');
|
||||
var firstHalf = opts.key.slice(0, 32); //AES256-cbc shared key
|
||||
var AESDecipher = crypto.createDecipheriv('aes-256-cbc', firstHalf, secondHalf);
|
||||
var plainText;
|
||||
try {
|
||||
plainText = Buffer.concat([AESDecipher.update(cipherText), AESDecipher.final()]).toString('hex');
|
||||
} catch(e) {
|
||||
return callback(e);
|
||||
}
|
||||
callback(null, plainText);
|
||||
};
|
||||
|
||||
exports.confirm = function(question, callback) {
|
||||
ttyread(question + ' (y/N): ', function(err, answer) {
|
||||
if (err) {
|
||||
return callback(err, false);
|
||||
}
|
||||
if (answer === 'y') {
|
||||
return callback(null, true);
|
||||
}
|
||||
callback(null, false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.encryptSecretWithPassphrase = function(secret, callback) {
|
||||
exports.acquirePassphrase(function(err, passphrase) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var salt = crypto.randomBytes(32).toString('hex');
|
||||
exports.encryptSecret({
|
||||
secret: secret,
|
||||
passphrase: passphrase,
|
||||
salt: salt
|
||||
}, function(err, cipherText) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, cipherText, salt);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.generateNonce = function() {
|
||||
var nonce = new Buffer(new Array(12));
|
||||
nonce.writeDoubleBE(Date.now());
|
||||
nonce.writeUInt32BE(process.hrtime()[1], 8);
|
||||
return nonce;
|
||||
};
|
||||
|
||||
exports.generateHashForRequest = function(method, url, nonce) {
|
||||
nonce = nonce || new Buffer(0);
|
||||
assert(Buffer.isBuffer(nonce), 'nonce must a buffer');
|
||||
var dataToSign = Buffer.concat([nonce, new Buffer(method), new Buffer(url)]);
|
||||
return bitcore.crypto.Hash.sha256sha256(dataToSign);
|
||||
};
|
||||
|
||||
exports.getWalletIdFromName = function(walletName) {
|
||||
if (!Buffer.isBuffer(walletName)) {
|
||||
walletName = new Buffer(walletName, 'utf8');
|
||||
}
|
||||
return bitcore.crypto.Hash.sha256sha256(walletName).toString('hex');
|
||||
};
|
||||
|
||||
exports.isRangeMoreThan = function(a, b) {
|
||||
if (a && !b) {
|
||||
return true;
|
||||
}
|
||||
if (!a && !b) {
|
||||
return false;
|
||||
}
|
||||
if (!a && b) {
|
||||
return false;
|
||||
}
|
||||
if (a.height > b.height) {
|
||||
return true;
|
||||
} else if (a.height < b.height) {
|
||||
return false;
|
||||
} else {
|
||||
return a.index > b.index;
|
||||
}
|
||||
};
|
||||
|
||||
exports.toHexBuffer = function(a) {
|
||||
if (!Buffer.isBuffer(a)) {
|
||||
a = new Buffer(a, 'hex');
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
exports.toIntIfNumberLike = function(a) {
|
||||
if (!/[^\d]+/.test(a)) {
|
||||
return parseInt(a);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
exports.delimitedStringParse = function(delim, str) {
|
||||
var ret = [];
|
||||
|
||||
if (delim === null) {
|
||||
return tryJSONparse(str);
|
||||
}
|
||||
|
||||
var list = str.split(delim);
|
||||
for(var i = 0; i < list.length; i++) {
|
||||
ret.push(tryJSONparse(list[i]));
|
||||
}
|
||||
ret = _.compact(ret);
|
||||
return ret.length === 0 ? false : ret;
|
||||
|
||||
function tryJSONparse(str) {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.diffTime = function(time) {
|
||||
var diff = process.hrtime(time);
|
||||
return (diff[0] * 1E9 + diff[1])/(1E9 * 1.0);
|
||||
}
|
||||
|
||||
module.exports = exports;
|
||||
246
lib/services/wallet/validators.js
Normal file
246
lib/services/wallet/validators.js
Normal file
@ -0,0 +1,246 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var bitcore = require('bitcore-lib');
|
||||
var _ = require('lodash');
|
||||
var utils = require('./utils');
|
||||
|
||||
var MAX_INT = 0xffffffff; // Math.pow(2, 32) - 1
|
||||
|
||||
exports.sanitizeRangeOptions = function(options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
options.height = options.height || 0;
|
||||
options.index = options.index || 0;
|
||||
|
||||
if (!options.limit) {
|
||||
options.limit = 10;
|
||||
} else if (options.limit > 500) {
|
||||
throw new Error('Limit exceeds maximum');
|
||||
}
|
||||
|
||||
assert(bitcore.util.js.isNaturalNumber(options.height), '"height" is expected to be a natural number');
|
||||
assert(bitcore.util.js.isNaturalNumber(options.index), '"index" is expected to be a natural number');
|
||||
assert(bitcore.util.js.isNaturalNumber(options.limit), '"limit" is expected to be a natural number');
|
||||
|
||||
assert(options.limit <= 500, '"limit" exceeds maximum');
|
||||
|
||||
if (options.end) {
|
||||
assert(bitcore.util.js.isNaturalNumber(options.end.height), '"end height" is expected to be a natural number');
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
exports.checkRangeParams = function(req, res, next) {
|
||||
assert(req.bitcoinHeight, '"bitcoinHeight" is expected to be set on the request');
|
||||
|
||||
var range = {
|
||||
height: parseInt(req.query.height),
|
||||
index: parseInt(req.query.index),
|
||||
limit: parseInt(req.query.limit),
|
||||
end: {
|
||||
height: req.bitcoinHeight,
|
||||
index: MAX_INT
|
||||
}
|
||||
};
|
||||
|
||||
if (req.query.end) {
|
||||
range.end.height = parseInt(req.query.end) || req.bitcoinHeight;
|
||||
}
|
||||
|
||||
try {
|
||||
range = exports.sanitizeRangeOptions(range);
|
||||
} catch(e) {
|
||||
return utils.sendError({
|
||||
message: 'Invalid params: ' + e.message,
|
||||
statusCode: 400
|
||||
}, res);
|
||||
}
|
||||
|
||||
assert(range.height <= range.end.height, '\'Height\' param required to be less than \'End\' param.');
|
||||
req.range = range;
|
||||
next();
|
||||
};
|
||||
|
||||
exports.checkAddress = function(req, res, next) {
|
||||
var address;
|
||||
var addressStr;
|
||||
|
||||
if (req.body.address) {
|
||||
addressStr = req.body.address;
|
||||
} else {
|
||||
addressStr = req.params.address;
|
||||
}
|
||||
|
||||
if(!addressStr) {
|
||||
return utils.sendError({
|
||||
message: 'Address param is expected',
|
||||
statusCode: 400
|
||||
}, res);
|
||||
}
|
||||
|
||||
assert(req.network, '"network" is expected to be set on the request');
|
||||
|
||||
try {
|
||||
address = new bitcore.Address(addressStr, req.network);
|
||||
} catch(e) {
|
||||
return utils.sendError({
|
||||
message: 'Invalid address: ' + e.message,
|
||||
statusCode: 400
|
||||
}, res);
|
||||
}
|
||||
|
||||
req.address = address;
|
||||
next();
|
||||
};
|
||||
|
||||
//exports.checkAddresses = function(req, res, next) {
|
||||
// var addresses = [];
|
||||
//
|
||||
// if (!req.body.addresses || !req.body.addresses.length || !Array.isArray(req.body.addresses)) {
|
||||
// return utils.sendError({
|
||||
// message: 'Addresses param is expected',
|
||||
// statusCode: 400
|
||||
// }, res);
|
||||
// }
|
||||
//
|
||||
// assert(req.network, '"network" is expected to be set on the request');
|
||||
//
|
||||
// for (var i = 0; i < req.body.addresses.length; i++) {
|
||||
// var address;
|
||||
// try {
|
||||
// address = new bitcore.Address(req.body.addresses[i], req.network);
|
||||
// } catch(e) {
|
||||
// return utils.sendError({
|
||||
// message: 'Invalid address: ' + e.message,
|
||||
// statusCode: 400
|
||||
// }, res);
|
||||
// }
|
||||
// addresses.push(address);
|
||||
// }
|
||||
//
|
||||
// req.addresses = addresses;
|
||||
// next();
|
||||
//};
|
||||
|
||||
exports.checkWalletId = function(req, res, next) {
|
||||
|
||||
if (!req.params.walletId) {
|
||||
return utils.sendError({
|
||||
message: 'Wallet id is expected',
|
||||
statusCode: 400
|
||||
}, res);
|
||||
}
|
||||
|
||||
if (req.params.walletId.length !== 64 || !bitcore.util.js.isHexa(req.params.walletId)) {
|
||||
return utils.sendError({
|
||||
message: 'Wallet id is expected to be a hexadecimal string with length of 64',
|
||||
statusCode: 400
|
||||
}, res);
|
||||
}
|
||||
|
||||
req.walletId = new Buffer(req.params.walletId, 'hex');
|
||||
next();
|
||||
|
||||
};
|
||||
|
||||
exports.checkAddresses = function(req, res, next) {
|
||||
|
||||
if (!req.file || !req.file.buffer) {
|
||||
generateError(406, 'Content-Type must be set to multipart/form' +
|
||||
' and addresses key and value must be given.');
|
||||
return;
|
||||
}
|
||||
var buf = req.file.buffer;
|
||||
var bufString = buf.toString();
|
||||
req.addresses = parse(bufString);
|
||||
if (!req.addresses) {
|
||||
generateError(415, 'Could not parse addresses buffer into something meaningful.');
|
||||
return;
|
||||
}
|
||||
next();
|
||||
|
||||
function generateError(status, msg) {
|
||||
res.status(status).jsonp({
|
||||
error: msg
|
||||
});
|
||||
}
|
||||
|
||||
//we are able to deal with json/jsonl, possibly others
|
||||
function parse(string) {
|
||||
var ret = false;
|
||||
var delims = [null, '\n', ' '];
|
||||
for(var i = 0; i < delims.length; i++) {
|
||||
ret = utils.delimitedStringParse(delims[i], string);
|
||||
if (_.isArray(ret)) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
exports.checkAuthHeaders = function(req, res) {
|
||||
var identity = req.header('x-identity');
|
||||
var signature = req.header('x-signature');
|
||||
var nonce = req.header('x-nonce');
|
||||
if (identity && (identity.length > 130 || !bitcore.util.js.isHexa(identity))) {
|
||||
utils.sendError({
|
||||
message: 'x-identity is expected to be a hexadecimal string with length of less than 131',
|
||||
statusCode: 400
|
||||
}, res);
|
||||
return false;
|
||||
}
|
||||
if (signature && (signature.length > 142 || !bitcore.util.js.isHexa(signature))) {
|
||||
utils.sendError({
|
||||
message: 'x-signature is expected to be a hexadecimal string with length of less than 143',
|
||||
statusCode: 400
|
||||
}, res);
|
||||
return false;
|
||||
}
|
||||
if (nonce && (nonce.length > 128 || nonce.length % 2 !== 0 || !bitcore.util.js.isHexa(nonce))) {
|
||||
utils.sendError({
|
||||
message: 'x-nonce is expected to be a hexadecimal string with length of less than 129',
|
||||
statusCode: 400
|
||||
}, res);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.checkDate = function(dateStrings) {
|
||||
var errors = [];
|
||||
if (!Array.isArray(dateStrings)) {
|
||||
dateStrings = [dateStrings];
|
||||
}
|
||||
for(var i = 0; i < dateStrings.length; i++) {
|
||||
internalDateCheck(dateStrings[i]);
|
||||
}
|
||||
|
||||
function internalDateCheck(dateString) {
|
||||
var date = new Date(utils.toIntIfNumberLike(dateString));
|
||||
if (date.toString() === 'Invalid Date') {
|
||||
errors.push('The date supplied: \'' + dateString +
|
||||
'\' is not a valid date string. A valid date could be: \'2016-09-01\'.');
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
exports.checkDateFunction = function(callback) {
|
||||
var self = this;
|
||||
return function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var errors = self.checkDate([args[1], args[2]]);
|
||||
if (errors.length > 0) {
|
||||
args.unshift(errors);
|
||||
} else {
|
||||
args.unshift(null);
|
||||
}
|
||||
callback.apply(null, args);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
@ -52,14 +52,18 @@
|
||||
"commander": "^2.8.1",
|
||||
"errno": "^0.1.4",
|
||||
"express": "^4.13.3",
|
||||
"leveldown": "",
|
||||
"levelup": "^1.3.3",
|
||||
"liftoff": "^2.2.0",
|
||||
"lmdb": "rvagg/lmdb#5ad819f77714925248dfecb0ba2174080dba949f",
|
||||
"lodash": "^4.17.4",
|
||||
"lru-cache": "^4.0.1",
|
||||
"mkdirp": "0.5.0",
|
||||
"multer": "^1.2.1",
|
||||
"path-is-absolute": "^1.0.0",
|
||||
"semver": "^5.0.1",
|
||||
"socket.io": "^1.4.5",
|
||||
"socket.io-client": "^1.4.5",
|
||||
"ttyread": "^1.0.2",
|
||||
"zmq": "^2.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user