walletdb. txdb. webhooks.

This commit is contained in:
Christopher Jeffrey 2016-03-02 19:37:55 -08:00
parent ad8090dc7c
commit e6048e85c8
7 changed files with 411 additions and 155 deletions

View File

@ -844,7 +844,7 @@ HDPrivateKey.prototype.toJSON = function toJSON(passphrase) {
return json;
}
json.xpubkey = this.hd.xpubkey;
json.xpubkey = this.xpubkey;
return json;
};

View File

@ -182,72 +182,94 @@ HTTPServer.prototype._init = function _init() {
// Create/get wallet
this.post('/wallet/:id', function(req, res, next, send) {
req.body.id = req.params.id;
self.node.walletdb.createJSON(req.body.id, req.body, function(err, json) {
self.node.walletdb.create(req.body, function(err, wallet) {
var wallet;
if (err)
return next(err);
if (!json)
if (!wallet)
return send(404);
json = wallet.toJSON();
wallet.destroy();
send(200, json);
});
});
// Update wallet / sync address depth
this.put('/wallet/:id', function(req, res, next, send) {
req.body.id = req.params.id;
self.node.walletdb.saveJSON(req.body.id, req.body, function(err, json) {
var id = req.params.id;
var receive = req.body.receiveDepth;
var change = req.body.changeDepth;
self.node.walletdb.syncDepth(id, receive, change, function(err) {
if (err)
return next(err);
if (!json)
return send(404);
send(200, json);
send(200, { success: true });
});
});
// Wallet Balance
this.get('/wallet/:id/balance', function(req, res, next, send) {
self.node.walletdb.getBalance(req.params.id, function(err, balance) {
if (err)
return next(err);
if (!coins.length)
return send(404);
send(200, { balance: utils.btc(balance) });
});
});
// Wallet UTXOs
this.get('/wallet/:id/utxo', function(req, res, next, send) {
self.node.walletdb.getJSON(req.params.id, function(err, json) {
self.node.walletdb.getUnspent(req.params.id, function(err, coins) {
if (err)
return next(err);
if (!json)
if (!coins.length)
return send(404);
self.node.getCoinByAddress(Object.keys(json.addressMap), function(err, coins) {
if (err)
return next(err);
if (!coins.length)
return send(404);
send(200, coins.map(function(coin) { return coin.toJSON(); }));
});
send(200, coins.map(function(coin) { return coin.toJSON(); }));
});
});
// Wallet TXs
this.get('/wallet/:id/tx', function(req, res, next, send) {
self.node.walletdb.getJSON(req.params.id, function(err, json) {
self.node.walletdb.getAll(req.params.id, function(err, txs) {
if (err)
return next(err);
if (!json)
if (!txs.length)
return send(404);
self.node.getTXByAddress(Object.keys(json.addressMap), function(err, txs) {
if (err)
return next(err);
if (!txs.length)
return send(404);
send(200, coins.map(function(tx) { return tx.toJSON(); }));
});
send(200, txs.map(function(tx) { return tx.toJSON(); }));
});
});
// Wallet Pending TXs
this.get('/wallet/:id/pending', function(req, res, next, send) {
self.node.walletdb.getPending(req.params.id, function(err, txs) {
if (err)
return next(err);
if (!txs.length)
return send(404);
send(200, txs.map(function(tx) { return tx.toJSON(); }));
});
});
// Emit events for any wallet txs that come in.
this.node.on('wallet tx', function(tx, ids) {
self.sendWebhook({ tx: tx, ids: ids });
});
};
HTTPServer.prototype.listen = function listen(port, host, callback) {
@ -370,6 +392,42 @@ HTTPServer.prototype.del = function del(path, callback) {
this.routes.del.push({ path: path, callback: callback });
};
// Send webhooks to notify other servers
// of incoming txs and wallet updates.
HTTPServer.prototype.sendWebhook = function sendWebhook(msg, callback) {
var request, body, secret, hmac;
callback = utils.ensure(callback);
if (!this.options.webhook)
return callback();
try {
request = require('request');
} catch (e) {
return callback(e);
}
body = new Buffer(JSON.stringify(msg) + '\n', 'utf8');
secret = new Buffer(this.options.webhook.secret || '', 'utf8');
hmac = utils.sha512hmac(body, secret);
request({
method: 'POST',
uri: this.options.webhook.endpoint,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': body.length + '',
'X-Bcoin-Hmac': hmac.toString('hex')
},
body: body
}, function(err, res, body) {
if (err)
return callback(err);
return callback();
});
};
/**
* Helpers
*/

View File

@ -81,7 +81,8 @@ Fullnode.prototype._init = function _init() {
// and blockdb.
this.http = new bcoin.http(this, {
key: this.options.httpKey,
cert: this.options.httpCert
cert: this.options.httpCert,
webhook: this.options.webhook
});
// Bind to errors
@ -97,12 +98,20 @@ Fullnode.prototype._init = function _init() {
self.emit('error', err);
});
this.walletdb.on('error', function(err) {
self.emit('error', err);
});
// Emit events for any TX we see that's
// is relevant to one of our wallets.
this.walletdb.on('wallet tx', function(tx, ids) {
self.emit('wallet tx', tx, ids);
});
this.on('tx', function(tx) {
self.walletdb.tx.addTX(tx, function(err, updated) {
if (updated)
self.emit('wallet tx', tx);
self.walletdb.tx.addTX(tx, function(err) {
if (err)
self.emit('error', err);
});
});

View File

@ -43,7 +43,7 @@ function Node(options) {
this.pool = null;
this.chain = null;
this.miner = null;
this.profiler = null;
this.walletdb = null;
Node.global = this;
}

View File

@ -14,18 +14,27 @@ var EventEmitter = require('events').EventEmitter;
* TXPool
*/
function TXPool(prefix, db) {
function TXPool(prefix, db, options) {
var self = this;
if (!(this instanceof TXPool))
return new TXPool(wallet, txs);
return new TXPool(prefix, db, options);
EventEmitter.call(this);
if (!options)
options = {};
this.db = db;
this.prefix = prefix || 'pool';
this.options = options;
this.busy = false;
this.jobs = [];
if (options.addressFilter)
this._hasAddress = options.addressFilter;
this.setMaxListeners(Number.MAX_SAFE_INTEGER);
}
utils.inherits(TXPool, EventEmitter);
@ -93,6 +102,7 @@ TXPool.prototype._add = function add(tx, callback, force) {
var p = this.prefix + '/';
var hash = tx.hash('hex');
var updated = false;
var own = false;
var batch;
var unlock = this._lock(add, [tx, callback], force);
@ -112,6 +122,7 @@ TXPool.prototype._add = function add(tx, callback, force) {
batch = self.db.batch();
if (existing) {
own = true;
// Tricky - update the tx and coin in storage,
// and remove pending flag to mark as confirmed.
if (existing.ts === 0 && tx.ts !== 0) {
@ -217,6 +228,7 @@ TXPool.prototype._add = function add(tx, callback, force) {
return done(null, false);
updated = true;
own = true;
type = input.getType();
address = input.getAddress();
@ -250,6 +262,8 @@ TXPool.prototype._add = function add(tx, callback, force) {
if (!result)
return next();
own = true;
self.getTX(input.prevout.hash, function(err, result) {
if (err)
return done(err);
@ -305,6 +319,7 @@ TXPool.prototype._add = function add(tx, callback, force) {
key = hash + '/' + i;
coin = bcoin.coin(tx, i);
own = true;
self.db.get(p + 'o/' + key, function(err, orphans) {
var some;
@ -400,6 +415,9 @@ TXPool.prototype._add = function add(tx, callback, force) {
if (err)
return done(err);
if (!own)
return done(null, false);
batch.put(p + 't/t/' + hash, tx.toExtended());
if (tx.ts === 0)
batch.put(p + 'p/t/' + hash, new Buffer([]));
@ -418,11 +436,12 @@ TXPool.prototype._add = function add(tx, callback, force) {
self.emit('tx', tx);
if (tx.ts !== 0)
self.emit('confirmed', tx);
if (updated) {
if (tx.ts !== 0)
self.emit('confirmed', tx);
if (updated)
self.emit('updated', tx);
}
return done(null, true);
});
@ -856,6 +875,8 @@ TXPool.prototype.getHeightHashes = function getHeightHashes(height, callback) {
if (err)
return callback(err);
txs = utils.uniqs(txs);
return callback(null, txs);
});
}

View File

@ -44,6 +44,8 @@ function Wallet(options) {
options.master = bcoin.hd.fromSeed();
this.options = options;
this.db = options.db || null;
this.tx = options.tx || null;
this.provider = options.provider || null;
this.addresses = [];
this.master = options.master || null;
@ -87,6 +89,10 @@ function Wallet(options) {
this.id = this.getID();
// Non-alphanumeric IDs will break leveldb sorting.
if (!/^[a-zA-Z0-9]$/.test(this.id))
throw new Error('Wallet IDs must be alphanumeric.');
this.addKey(this.accountKey);
(options.keys || []).forEach(function(key) {
@ -124,6 +130,63 @@ Wallet.prototype._init = function _init() {
assert(this.receiveAddress);
assert(!this.receiveAddress.change);
assert(this.changeAddress.change);
if (!this.tx)
return;
this.tx.on('tx', this._onTX = function(tx) {
if (!self.ownInput(tx) && !self.ownOutput(tx))
return;
self.emit('tx', tx);
});
this.tx.on('updated', this._onUpdated = function(tx) {
if (!self.ownInput(tx) && !self.ownOutput(tx))
return;
self.emit('updated', tx);
if (!self.provider)
return;
self.getBalance(function(err, balance) {
if (err)
return self.emit('error', err);
self.emit('balance', balance);
});
});
this.tx.on('confirmed', this._onConfirmed = function(tx) {
if (!self.ownInput(tx) && !self.ownOutput(tx))
return;
self.syncOutputDepth(tx);
self.emit('confirmed', tx);
});
};
Wallet.prototype.destroy = function destroy() {
if (this.tx) {
if (this._onTX) {
this.tx.removeListener('tx', this._onTX);
delete this._onTX;
}
if (this._onUpdated) {
this.tx.removeListener('updated', this._onUpdated);
delete this._onUpdated;
}
if (this._onConfirmed) {
this.tx.removeListener('confirmed', this._onConfirmed);
delete this._onConfirmed;
}
}
this.db = null;
this.tx = null;
this.provider = null;
};
Wallet.prototype.addKey = function addKey(key) {
@ -341,6 +404,10 @@ Wallet.prototype.deriveAddress = function deriveAddress(change, index) {
if (this.witness)
this.addressMap[address.getProgramAddress()] = data.path;
// Update the DB with the new address.
if (this.db)
this.db.update(this, address);
this.emit('add address', address);
return address;
@ -473,27 +540,9 @@ Wallet.prototype.fillPrevout = function fillPrevout(tx, callback) {
return this.provider.fillCoin(tx, callback);
};
Wallet.prototype.sync = function sync(callback) {
Wallet.prototype.createTX = function createTX(options, outputs, callback) {
var self = this;
callback = utils.ensure(callback);
this.getUnspent(function(err, unspent) {
if (err)
return callback(err);
unspent.forEach(function(coin) {
self.syncOutputDepth(coin);
});
return callback();
});
};
Wallet.prototype.tx = function _tx(options, outputs, callback) {
var self = this;
var tx = bcoin.mtx();
var target;
var tx;
if (typeof outputs === 'function') {
callback = outputs;
@ -508,6 +557,9 @@ Wallet.prototype.tx = function _tx(options, outputs, callback) {
if (!Array.isArray(outputs))
outputs = [outputs];
// Create mutable tx
tx = bcoin.mtx();
// Add the outputs
outputs.forEach(function(output) {
tx.addOutput(output);
@ -521,20 +573,9 @@ Wallet.prototype.tx = function _tx(options, outputs, callback) {
// Sort members a la BIP69
tx.sortMembers();
// Find the necessary locktime if there is
// a checklocktimeverify script in the unspents.
target = tx.getTargetLocktime();
// No target value. The unspents have an
// incompatible locktime type.
if (!target)
return callback(new Error('Incompatible locktime.'));
// Set the locktime to target value or
// `height - whatever` to avoid fee sniping.
if (target.value > 0)
tx.setLocktime(target.value);
else if (options.locktime != null)
if (options.locktime != null)
tx.setLocktime(options.locktime);
else
tx.avoidFeeSniping();
@ -758,12 +799,10 @@ Wallet.prototype.sign = function sign(tx, type, index) {
};
Wallet.prototype.addTX = function addTX(tx, callback) {
this.syncOutputDepth(tx);
if (!this.tx)
return callback(new Error('No transaction pool available.'));
if (!this.provider)
return callback(new Error('No wallet provider available.'));
return this.provider.addTX(tx, callback);
return this.tx.add(tx, callback);
};
Wallet.prototype.getAll = function getAll(callback) {
@ -929,6 +968,10 @@ Wallet.prototype.toJSON = function toJSON() {
receiveDepth: this.receiveDepth,
changeDepth: this.changeDepth,
master: this.master.toJSON(this.options.passphrase),
// Store account key to:
// a. save ourselves derivations.
// b. allow deriving of addresses without decrypting the master key.
accountKey: this.accountKey.xpubkey,
addressMap: this.addressMap,
keys: this.keys.map(function(key) {
return key.xpubkey;
@ -961,6 +1004,39 @@ Wallet._fromJSON = function _fromJSON(json, passphrase) {
};
};
// For updating the address table quickly
// without decrypting the master key.
Wallet.sync = function sync(json, options) {
var master, wallet;
assert.equal(json.v, 3);
assert.equal(json.name, 'wallet');
if (json.network)
assert.equal(json.network, network.type);
master = json.master;
json.master = json.accountKey;
wallet = new Wallet(json);
if (options.txs != null) {
options.txs.forEach(function(tx) {
wallet.syncOutputDepth(tx);
});
}
if (options.receiveDepth != null)
wallet.setReceiveDepth(options.receiveDepth);
if (options.changeDepth != null)
wallet.setChangeDepth(options.changeDepth);
wallet = wallet.toJSON();
wallet.master = master;
return wallet;
};
Wallet.fromJSON = function fromJSON(json, passphrase) {
return new Wallet(Wallet._fromJSON(json, passphrase));
};

View File

@ -83,7 +83,7 @@ WalletDB.prototype.dump = function dump(callback) {
});
}
records[key] = value.slice(0, 50).toString('hex');
records[key] = value;
next();
});
@ -116,8 +116,75 @@ WalletDB.prototype._init = function _init() {
this.db = WalletDB._db[this.file];
this.tx = new bcoin.txdb('w', this.db);
this.tx._hasAddress = this.hasAddress.bind(this);
this.tx = new bcoin.txdb('w', this.db, {
addressFilter: this.hasAddress.bind(this)
});
this.tx.on('error', function(err) {
self.emit('error', err);
});
this.tx.on('updated', function(tx) {
self.getIDs(tx.getOutputAddresses(), function(err, ids) {
if (err)
return self.emit('error', err);
self.emit('wallet tx', tx, ids);
// Only sync for confirmed txs.
if (tx.ts === 0)
return;
utils.forEachSerial(ids, function(id, next) {
self.getJSON(id, function(err, json) {
if (err) {
self.emit('error', err);
return next();
}
// Allocate new addresses if necessary.
json = bcoin.wallet.sync(json, { txs: [tx] });
self.saveJSON(id, json, function(err) {
if (err)
return next(err);
next();
});
});
}, function(err) {
if (err)
self.emit('error', err);
});
});
});
};
WalletDB.prototype.syncDepth = function syncDepth(id, changeDepth, receiveDepth, callback) {
callback = utils.ensure(callback);
if (!receiveDepth)
receiveDepth = 0;
if (!changeDepth)
changeDepth = 0;
self.getJSON(id, function(err, json) {
if (err)
return callback(err);
// Allocate new addresses if necessary.
json = bcoin.wallet.sync(json, {
receiveDepth: receiveDepth,
changeDepth: changeDepth
});
self.saveJSON(id, json, function(err) {
if (err)
return callback(err);
self.emit('sync depth', id, receiveDepth, changeDepth);
callback();
});
});
};
WalletDB.prototype.getJSON = function getJSON(id, callback) {
@ -251,6 +318,8 @@ WalletDB.prototype._removeDB = function _removeDB(id, callback) {
};
WalletDB.prototype.get = function get(id, passphrase, callback) {
var self = this;
if (typeof passphrase === 'function') {
callback = passphrase;
passphrase = null;
@ -268,14 +337,15 @@ WalletDB.prototype.get = function get(id, passphrase, callback) {
return callback();
try {
wallet = bcoin.wallet.fromJSON(options, passphrase);
wallet.provider = self;
options = bcoin.wallet._fromJSON(options, passphrase);
options.db = self;
options.tx = self.tx;
options.provider = self;
wallet = new bcoin.wallet(options);
} catch (e) {
return callback(e);
}
wallet.on('add address', self._onAddress(wallet, wallet.id));
return callback(null, wallet);
});
};
@ -286,14 +356,8 @@ WalletDB.prototype.save = function save(options, callback) {
callback = utils.ensure(callback);
if (options instanceof bcoin.wallet) {
if (!options.provider) {
options.on('add address', self._onAddress(options, options.id));
options.provider = self;
}
if (options instanceof bcoin.wallet)
options = options.toJSON();
}
if (options instanceof bcoin.wallet)
assert(options.db === this);
this.saveJSON(options.id, options, callback);
};
@ -304,7 +368,7 @@ WalletDB.prototype.remove = function remove(id, callback) {
callback = utils.ensure(callback);
if (id instanceof bcoin.wallet) {
id.provider = null;
id.destroy();
id = id.id;
}
@ -337,13 +401,18 @@ WalletDB.prototype.create = function create(options, callback) {
if (json) {
try {
wallet = bcoin.wallet.fromJSON(json, options.passphrase);
wallet.provider = self;
options = bcoin.wallet._fromJSON(json, options.passphrase);
options.db = self;
options.tx = self.tx;
options.provider = self;
wallet = new bcoin.wallet(options);
} catch (e) {
return callback(e);
}
done();
} else {
options.db = self;
options.tx = self.tx;
options.provider = self;
wallet = new bcoin.wallet(options);
self.saveJSON(wallet.id, wallet.toJSON(), done);
@ -353,44 +422,46 @@ WalletDB.prototype.create = function create(options, callback) {
if (err)
return callback(err);
wallet.on('add address', self._onAddress(wallet, wallet.id));
return callback(null, wallet);
}
});
};
WalletDB.prototype._onAddress = function _onAddress(wallet, id) {
WalletDB.prototype.update = function update(wallet, address) {
var self = this;
return function(address) {
var batch = self.db.batch();
var batch;
// Ugly hack to avoid extra writes.
if (!wallet.changeAddress && wallet.changeDepth > 1)
return;
batch = this.db.batch();
batch.put(
'w/a/' + address.getKeyAddress() + '/' + wallet.id,
new Buffer([]));
if (address.type === 'multisig') {
batch.put(
'w/a/' + address.getKeyAddress() + '/' + id,
'w/a/' + address.getScriptAddress() + '/' + wallet.id,
new Buffer([]));
}
if (address.type === 'multisig') {
batch.put(
'w/a/' + address.getScriptAddress() + '/' + id,
new Buffer([]));
}
if (address.witness) {
batch.put(
'w/a/' + address.getProgramAddress() + '/' + wallet.id,
new Buffer([]));
}
if (address.witness) {
batch.put(
'w/a/' + address.getProgramAddress() + '/' + id,
new Buffer([]));
}
batch.write(function(err) {
if (err)
self.emit('error', err);
batch.write(function(err) {
self._saveDB(wallet.id, wallet.toJSON(), function(err) {
if (err)
self.emit('error', err);
self._saveDB(wallet.id, wallet.toJSON(), function(err) {
if (err)
self.emit('error', err);
});
});
};
});
};
WalletDB.prototype.addTX = function addTX(tx, callback) {
@ -448,7 +519,8 @@ WalletDB.prototype.getLast = function getLast(id, callback) {
};
WalletDB.prototype.getAddresses = function getAddresses(id, callback) {
if (typeof id === 'string')
// Try to avoid a database lookup if we can...
if (typeof id === 'string' && bcoin.address.validate(id))
return callback(null, [id]);
if (Array.isArray(id))
@ -476,44 +548,6 @@ WalletDB.prototype.getAddresses = function getAddresses(id, callback) {
});
};
WalletDB.prototype.getIDs = function _getIDs(address, callback) {
var self = this;
var ids = [];
var iter = this.db.db.iterator({
gte: 'w/a/' + address,
lte: 'w/a/' + address + '~',
keys: true,
values: false,
fillCache: false,
keyAsBuffer: false
});
callback = utils.ensure(callback);
(function next() {
iter.next(function(err, key, value) {
if (err) {
return iter.end(function() {
callback(err);
});
}
if (key === undefined) {
return iter.end(function(err) {
if (err)
return callback(err);
return callback(null, ids);
});
}
ids.push(key.split('/')[2]);
next();
});
})();
};
WalletDB.prototype.hasAddress = function hasAddress(address, callback) {
var self = this;
@ -556,6 +590,64 @@ WalletDB.prototype.hasAddress = function hasAddress(address, callback) {
})();
};
WalletDB.prototype.getIDs = function getIDs(address, callback) {
var self = this;
var ids = [];
if (Array.isArray(address)) {
return utils.forEachSerial(address, function(address, next) {
self.getIDs(address, function(err, id) {
if (err)
return next(err);
ids = ids.concat(id);
next();
});
}, function(err) {
if (err)
return callback(err);
ids = utils.uniqs(ids);
return callback(null, ids);
});
}
var iter = this.db.db.iterator({
gte: 'w/a/' + address,
lte: 'w/a/' + address + '~',
keys: true,
values: false,
fillCache: false,
keyAsBuffer: false
});
callback = utils.ensure(callback);
(function next() {
iter.next(function(err, key, value) {
if (err) {
return iter.end(function() {
callback(err);
});
}
if (key === undefined) {
return iter.end(function(err) {
if (err)
return callback(err);
return callback(null, ids);
});
}
ids.push(key.split('/')[3]);
next();
});
})();
};
/**
* Expose
*/