656 lines
14 KiB
JavaScript
656 lines
14 KiB
JavaScript
/**
|
|
* walletdb.js - storage for wallets
|
|
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
|
|
* https://github.com/indutny/bcoin
|
|
*/
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
|
|
|
var bcoin = require('../bcoin');
|
|
var levelup = require('levelup');
|
|
var bn = require('bn.js');
|
|
var constants = bcoin.protocol.constants;
|
|
var network = bcoin.protocol.network;
|
|
var utils = bcoin.utils;
|
|
var assert = utils.assert;
|
|
var fs = bcoin.fs;
|
|
|
|
/**
|
|
* WalletDB
|
|
*/
|
|
|
|
function WalletDB(node, options) {
|
|
var self = this;
|
|
|
|
if (!(this instanceof WalletDB))
|
|
return new WalletDB(node, options);
|
|
|
|
if (WalletDB.global)
|
|
return WalletDB.global;
|
|
|
|
if (!options)
|
|
options = {};
|
|
|
|
EventEmitter.call(this);
|
|
|
|
this.node = node;
|
|
this.options = options;
|
|
this.file = options.file;
|
|
|
|
bcoin.ensurePrefix();
|
|
|
|
if (!this.file)
|
|
this.file = bcoin.prefix + '/wallet-' + network.type + '.db';
|
|
|
|
WalletDB.global = this;
|
|
|
|
this._init();
|
|
}
|
|
|
|
utils.inherits(WalletDB, EventEmitter);
|
|
|
|
WalletDB._db = {};
|
|
|
|
WalletDB.prototype.dump = function dump(callback) {
|
|
var self = this;
|
|
var records = {};
|
|
|
|
var iter = this.db.db.iterator({
|
|
gte: 'w',
|
|
lte: 'w~',
|
|
keys: true,
|
|
values: true,
|
|
fillCache: false,
|
|
keyAsBuffer: false,
|
|
valueAsBuffer: true
|
|
});
|
|
|
|
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, records);
|
|
});
|
|
}
|
|
|
|
records[key] = value;
|
|
|
|
next();
|
|
});
|
|
})();
|
|
};
|
|
|
|
WalletDB.prototype._init = function _init() {
|
|
var self = this;
|
|
var levelup;
|
|
|
|
if (!WalletDB._db[this.file]) {
|
|
// Some lazy loading
|
|
levelup = require('levelup');
|
|
WalletDB._db[this.file] = new levelup(this.file, {
|
|
keyEncoding: 'ascii',
|
|
valueEncoding: 'binary',
|
|
createIfMissing: true,
|
|
errorIfExists: false,
|
|
compression: true,
|
|
cacheSize: 1 * 1024 * 1024,
|
|
writeBufferSize: 1 * 1024 * 1024,
|
|
// blockSize: 4 * 1024,
|
|
maxOpenFiles: 1024,
|
|
// blockRestartInterval: 16,
|
|
db: bcoin.isBrowser
|
|
? require('level-js')
|
|
: require('level' + 'down')
|
|
});
|
|
}
|
|
|
|
this.db = WalletDB._db[this.file];
|
|
|
|
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) {
|
|
if (typeof id === 'object')
|
|
id = id.id;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
return this._getDB(id, callback);
|
|
};
|
|
|
|
WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) {
|
|
var self = this;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
function cb(err, json) {
|
|
var batch;
|
|
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (json) {
|
|
batch = self.db.batch();
|
|
Object.keys(json.addressMap).forEach(function(address) {
|
|
batch.put('w/a/' + address + '/' + json.id, new Buffer([]));
|
|
});
|
|
return batch.write(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, json);
|
|
});
|
|
}
|
|
|
|
return callback(null, json);
|
|
}
|
|
|
|
return this._saveDB(id, json, cb);
|
|
};
|
|
|
|
WalletDB.prototype.removeJSON = function removeJSON(id, callback) {
|
|
var self = this;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
if (typeof id === 'object')
|
|
id = id.id;
|
|
|
|
function cb(err, json) {
|
|
var batch;
|
|
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (json) {
|
|
batch = self.db.batch();
|
|
Object.keys(json.addressMap).forEach(function(address) {
|
|
batch.del('w/a/' + address + '/' + json.id);
|
|
});
|
|
return batch.write(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
return callback(null, json);
|
|
});
|
|
}
|
|
|
|
return callback(null, json);
|
|
}
|
|
|
|
return this._removeDB(id, cb);
|
|
};
|
|
|
|
WalletDB.prototype._getDB = function _getDB(id, callback) {
|
|
var self = this;
|
|
var key;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
key = 'w/w/' + id;
|
|
|
|
this.db.get(key, function(err, json) {
|
|
if (err && err.type === 'NotFoundError')
|
|
return callback();
|
|
|
|
if (err)
|
|
return callback(err);
|
|
|
|
try {
|
|
json = JSON.parse(json.toString('utf8'));
|
|
} catch (e) {
|
|
return callback(e);
|
|
}
|
|
|
|
return callback(null, json);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype._saveDB = function _saveDB(id, json, callback) {
|
|
var key = 'w/w/' + id;
|
|
var data;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
data = new Buffer(JSON.stringify(json), 'utf8');
|
|
|
|
this.db.put(key, data, function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return callback(null, json);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype._removeDB = function _removeDB(id, callback) {
|
|
var self = this;
|
|
var key = 'w/w/' + id;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
this._getDB(id, function(err, json) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
self.db.del(key, function(err) {
|
|
if (err && err.type !== 'NotFoundError')
|
|
return callback(err);
|
|
|
|
return callback(null, json);
|
|
});
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.get = function get(id, passphrase, callback) {
|
|
var self = this;
|
|
|
|
if (typeof passphrase === 'function') {
|
|
callback = passphrase;
|
|
passphrase = null;
|
|
}
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
return this.getJSON(id, function(err, options) {
|
|
var wallet;
|
|
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (!options)
|
|
return callback();
|
|
|
|
try {
|
|
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);
|
|
}
|
|
|
|
return callback(null, wallet);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.save = function save(options, callback) {
|
|
var self = this;
|
|
var passphrase = options.passphrase;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
if (options instanceof bcoin.wallet)
|
|
assert(options.db === this);
|
|
|
|
this.saveJSON(options.id, options, callback);
|
|
};
|
|
|
|
WalletDB.prototype.remove = function remove(id, callback) {
|
|
var self = this;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
if (id instanceof bcoin.wallet) {
|
|
id.destroy();
|
|
id = id.id;
|
|
}
|
|
|
|
return this.removeJSON(id, callback);
|
|
};
|
|
|
|
WalletDB.prototype.create = function create(options, callback) {
|
|
var self = this;
|
|
var passphrase = options.passphrase;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
function getJSON(id, callback) {
|
|
if (!id)
|
|
return callback();
|
|
|
|
return self.getJSON(id, function(err, json) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return callback(null, json);
|
|
});
|
|
}
|
|
|
|
return getJSON(options.id, function(err, json) {
|
|
var wallet;
|
|
|
|
if (err)
|
|
return callback(err);
|
|
|
|
if (json) {
|
|
try {
|
|
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);
|
|
}
|
|
|
|
function done(err) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return callback(null, wallet);
|
|
}
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.update = function update(wallet, address) {
|
|
var self = this;
|
|
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.getScriptAddress() + '/' + wallet.id,
|
|
new Buffer([]));
|
|
}
|
|
|
|
if (address.witness) {
|
|
batch.put(
|
|
'w/a/' + address.getProgramAddress() + '/' + wallet.id,
|
|
new Buffer([]));
|
|
}
|
|
|
|
batch.write(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) {
|
|
return this.tx.add(tx, callback);
|
|
};
|
|
|
|
WalletDB.prototype.getAll = function getAll(id, callback) {
|
|
var self = this;
|
|
return this.getAddresses(id, function(err, addresses) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return self.tx.getAllByAddress(addresses, callback);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.getUnspent = function getUnspent(id, callback) {
|
|
var self = this;
|
|
return this.getAddresses(id, function(err, addresses) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return self.tx.getUnspentByAddress(addresses, callback);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.getPending = function getPending(id, callback) {
|
|
var self = this;
|
|
return this.getAddresses(id, function(err, addresses) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return self.tx.getPendingByAddress(addresses, callback);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.getBalance = function getBalance(id, callback) {
|
|
var self = this;
|
|
return this.getAddresses(id, function(err, addresses) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return self.tx.getBalanceByAddress(addresses, callback);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.getLast = function getLast(id, callback) {
|
|
var self = this;
|
|
return this.getAddresses(id, function(err, addresses) {
|
|
if (err)
|
|
return callback(err);
|
|
|
|
return self.tx.getLast(addresses, callback);
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.getAddresses = function getAddresses(id, callback) {
|
|
// 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))
|
|
return callback(null, id);
|
|
|
|
if (id.addressMap)
|
|
return callback(null, Object.keys(id.addressMap));
|
|
|
|
if (typeof id === 'object')
|
|
return callback(null, Object.keys(id));
|
|
|
|
return this.db.get('w/w/' + id, function(err, buf) {
|
|
var json;
|
|
|
|
if (err)
|
|
return callback(err);
|
|
|
|
try {
|
|
json = JSON.parse(buf.toString('utf8'));
|
|
} catch (e) {
|
|
return callback(e);
|
|
}
|
|
|
|
return callback(null, Object.keys(json.addressMap));
|
|
});
|
|
};
|
|
|
|
WalletDB.prototype.hasAddress = function hasAddress(address, callback) {
|
|
var self = this;
|
|
|
|
callback = utils.ensure(callback);
|
|
|
|
if (!address)
|
|
return callback(null, false);
|
|
|
|
var iter = this.db.db.iterator({
|
|
gte: 'w/a/' + address,
|
|
lte: 'w/a/' + address + '~',
|
|
keys: true,
|
|
values: false,
|
|
fillCache: false,
|
|
keyAsBuffer: false
|
|
});
|
|
|
|
(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, false);
|
|
});
|
|
}
|
|
|
|
return iter.end(function(err) {
|
|
if (err)
|
|
return callback(err);
|
|
callback(null, true);
|
|
});
|
|
});
|
|
})();
|
|
};
|
|
|
|
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
|
|
*/
|
|
|
|
module.exports = WalletDB;
|