diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 716c339b..cb44bfd2 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -860,7 +860,9 @@ HDPrivateKey.generate = function generate(options, network) { */ HDPrivateKey.parseBase58 = function parseBase58(xkey) { - return HDPrivateKey.parseRaw(utils.fromBase58(xkey)); + var data = HDPrivateKey.parseRaw(utils.fromBase58(xkey)); + data.xprivkey = xkey; + return data; }; HDPrivateKey.parseRaw = function parseRaw(raw) { @@ -887,7 +889,6 @@ HDPrivateKey.parseRaw = function parseRaw(raw) { assert(i < network.types.length, 'Network not found.'); data.network = type; - data.xprivkey = xkey; return data; }; @@ -935,6 +936,10 @@ HDPrivateKey.fromBase58 = function fromBase58(xkey) { return new HDPrivateKey(HDPrivateKey.parseBase58(xkey)); }; +HDPrivateKey.fromRaw = function fromRaw(raw) { + return new HDPrivateKey(HDPrivateKey.parseRaw(raw)); +}; + /** * Convert key to a more json-friendly object. * @param {String?} passphrase - Address passphrase @@ -979,8 +984,8 @@ HDPrivateKey.prototype.toJSON = function toJSON(passphrase) { HDPrivateKey.parseJSON = function parseJSON(json, passphrase) { var data = {}; - assert.equal(json.v, 1); - assert.equal(json.name, 'hdkey'); + // assert.equal(json.v, 1); + // assert.equal(json.name, 'hdkey'); if (json.encrypted && !passphrase) throw new Error('Cannot decrypt address'); @@ -1319,7 +1324,9 @@ HDPublicKey.isExtended = function isExtended(data) { */ HDPublicKey.parseBase58 = function parseBase58(xkey) { - return HDPublicKey.parseRaw(utils.fromBase58(xkey)); + var data = HDPublicKey.parseRaw(utils.fromBase58(xkey)); + data.xpubkey = xkey; + return data; }; HDPublicKey.parseRaw = function parseRaw(raw) { @@ -1345,7 +1352,6 @@ HDPublicKey.parseRaw = function parseRaw(raw) { assert(i < network.types.length, 'Network not found.'); data.network = type; - data.xpubkey = xkey; return data; }; @@ -1392,6 +1398,10 @@ HDPublicKey.fromBase58 = function fromBase58(xkey) { return new HDPublicKey(HDPublicKey.parseBase58(xkey)); }; +HDPublicKey.fromRaw = function fromRaw(data) { + return new HDPublicKey(HDPublicKey.parseRaw(data)); +}; + /** * Test an object to see if it is a HDPublicKey. * @param {Object} obj diff --git a/lib/bcoin/keyring.js b/lib/bcoin/keyring.js index 9f927aa6..48ebe7d9 100644 --- a/lib/bcoin/keyring.js +++ b/lib/bcoin/keyring.js @@ -8,6 +8,8 @@ var bcoin = require('./env'); var utils = bcoin.utils; var assert = utils.assert; +var BufferReader = require('./reader'); +var BufferWriter = require('./writer'); /** * Represents a key ring which amounts to an address. Used for {@link Wallet}. @@ -31,29 +33,20 @@ function KeyRing(options) { if (!(this instanceof KeyRing)) return new KeyRing(options); - if (options instanceof KeyRing) - return options; - - if (!options) - options = {}; - - this.addressMap = null; - this.network = bcoin.network.get(options.network); - this.key = options.key; - this.path = options.path; - this.change = !!options.change; - this.index = options.index; - this.type = options.type || 'pubkeyhash'; - this.keys = []; this.m = options.m || 1; this.n = options.n || 1; this.witness = options.witness || false; + this.path = options.path; + this.key = options.key; + this.keys = []; if (this.n > 1) this.type = 'multisig'; + this.addressMap = null; + assert(this.type === 'pubkeyhash' || this.type === 'multisig'); if (this.m < 1 || this.m > this.n) @@ -67,27 +60,6 @@ function KeyRing(options) { } } -/** - * Test an object to see if it is an KeyRing. - * @param {Object} obj - * @returns {Boolean} - */ - -KeyRing.isKeyRing = function isKeyRing(obj) { - return obj - && Array.isArray(obj.keys) - && typeof obj._getAddressMap === 'function'; -}; - -/** - * Return address ID (pubkeyhash address of pubkey). - * @returns {Base58Address} - */ - -KeyRing.prototype.getID = function getID() { - return this.getKeyAddress(); -}; - /** * Add a key to shared keys. * @param {Buffer} key @@ -97,9 +69,7 @@ KeyRing.prototype.addKey = function addKey(key) { if (utils.indexOf(this.keys, key) !== -1) return; - this.keys.push(key); - - this.keys = utils.sortKeys(this.keys); + utils.binaryInsert(this.keys, key, utils.cmp); }; /** @@ -108,14 +78,7 @@ KeyRing.prototype.addKey = function addKey(key) { */ KeyRing.prototype.removeKey = function removeKey(key) { - var index = utils.indexOf(this.keys, key); - - if (index === -1) - return; - - this.keys.splice(index, 1); - - this.keys = utils.sortKeys(this.keys); + utils.binaryRemove(this.keys, key, utils.cmp); }; /** @@ -369,7 +332,12 @@ KeyRing.prototype.getAddress = function getAddress() { return this.getKeyAddress(); }; -KeyRing.prototype._getAddressMap = function _getAddressMap() { +/** + * Create the address map for testing txs. + * @returns {AddressMap} + */ + +KeyRing.prototype.getAddressMap = function getAddressMap() { if (!this.addressMap) { this.addressMap = {}; @@ -393,7 +361,7 @@ KeyRing.prototype._getAddressMap = function _getAddressMap() { */ KeyRing.prototype.ownInput = function ownInput(tx, index) { - var addressMap = this._getAddressMap(); + var addressMap = this.getAddressMap(); if (tx instanceof bcoin.input) return tx.test(addressMap); @@ -409,7 +377,7 @@ KeyRing.prototype.ownInput = function ownInput(tx, index) { */ KeyRing.prototype.ownOutput = function ownOutput(tx, index) { - var addressMap = this._getAddressMap(); + var addressMap = this.getAddressMap(); if (tx instanceof bcoin.output) return tx.test(addressMap); @@ -552,17 +520,15 @@ KeyRing.prototype.toJSON = function toJSON() { return { v: 1, name: 'address', - address: this.getAddress(), network: this.network.type, - change: this.change, - index: this.index, + type: this.type, + m: this.m, + n: this.n, + witness: this.witness, path: this.path, key: utils.toBase58(this.key), - type: this.type, - witness: this.witness, keys: this.keys.map(utils.toBase58), - m: this.m, - n: this.n + address: this.getAddress() }; }; @@ -578,18 +544,87 @@ KeyRing.fromJSON = function fromJSON(json) { assert.equal(json.name, 'address'); return new KeyRing({ nework: json.network, - change: json.change, - index: json.index, + type: json.type, + m: json.m, + n: json.n, + witness: json.witness, path: json.path, key: utils.fromBase58(json.key), - type: json.type, - witness: json.witness, - keys: json.keys.map(utils.fromBase58), - m: json.m, - n: json.n + keys: json.keys.map(utils.fromBase58) }); }; +/** + * Serialize the keyring. + * @returns {Buffer} + */ + +KeyRing.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + var i; + + p.writeU32(this.network.magic); + p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); + p.writeU8(this.m); + p.writeU8(this.n); + p.writeU8(this.witness ? 1 : 0); + p.writeVarString(this.path, 'ascii'); + p.writeVarBytes(this.key); + p.writeU8(this.keys.length); + + for (i = 0; i < this.keys.length; i++) + p.writeVarBytes(this.keys[i]); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Instantiate a keyring from serialized data. + * @returns {KeyRing} + */ + +KeyRing.fromRaw = function fromRaw(data) { + var p = new BufferReader(data); + var network = bcoin.network.fromMagic(p.readU32()); + var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; + var m = p.readU8(); + var n = p.readU8(); + var witness = p.readU8() === 1; + var path = p.readVarString('ascii'); + var key = p.readVarBytes(); + var keys = new Array(p.readU8()); + var i; + + for (i = 0; i < keys.length; i++) + keys[i] = p.readVarBytes(); + + return new KeyRing({ + nework: network, + type: type, + m: m, + n: n, + witness: witness, + path: path, + key: key, + keys: keys + }); +}; + +/** + * Test an object to see if it is an KeyRing. + * @param {Object} obj + * @returns {Boolean} + */ + +KeyRing.isKeyRing = function isKeyRing(obj) { + return obj + && Array.isArray(obj.keys) + && typeof obj.getAddressMap === 'function'; +}; + /* * Expose */ diff --git a/lib/bcoin/network.js b/lib/bcoin/network.js index 40ebe0e4..f4916d52 100644 --- a/lib/bcoin/network.js +++ b/lib/bcoin/network.js @@ -143,6 +143,25 @@ Network.get = function get(options) { assert(false, 'Unknown network.'); }; +/** + * Get a network by its magic number. + * @returns {Network} + */ + +Network.fromMagic = function fromMagic(magic) { + var i, type; + + for (i = 0; i < network.types.length; i++) { + type = network.types[i]; + if (magic === network[type].magic) + break; + } + + assert(i < network.types.length, 'Network not found.'); + + return Network.get(type); +}; + /** * Convert the network to a string. * @returns {String} diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index 96c75878..a6df050a 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -62,9 +62,6 @@ function TXDB(db, options) { this.filter = this.options.useFilter ? new bcoin.bloom.rolling(800000, 0.01) : null; - - if (this.options.mapAddress) - this.options.indexAddress = true; } utils.inherits(TXDB, EventEmitter); @@ -75,26 +72,38 @@ TXDB.prototype._lock = function _lock(func, args, force) { TXDB.prototype._loadFilter = function loadFilter(callback) { var self = this; - var i; + var iter; if (!this.filter) return callback(); - this.db.iterate({ + iter = this.db.iterator({ gte: 'W', lte: 'W~', - transform: function(key) { - return key.split('/')[1]; - } - }, function(err, keys) { - if (err) - return callback(err); - - for (i = 0; i < keys.length; i++) - self.filter.add(keys[i], 'hex'); - - return callback(); + 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(callback); + + key = key.split('/')[1]; + + self.filter.add(key, 'hex'); + + next(); + }); + })(); }; TXDB.prototype._testFilter = function _testFilter(addresses) { @@ -120,9 +129,6 @@ TXDB.prototype._testFilter = function _testFilter(addresses) { TXDB.prototype.getMap = function getMap(tx, callback) { var input, output, addresses, table, map; - if (!this.options.indexAddress) - return callback(); - input = tx.getInputHashes(); output = tx.getOutputHashes(); addresses = utils.uniq(input.concat(output)); @@ -134,6 +140,9 @@ TXDB.prototype.getMap = function getMap(tx, callback) { if (err) return callback(err); + if (table.count === 0) + return callback(); + map = { table: table, input: [], @@ -158,14 +167,6 @@ TXDB.prototype.getMap = function getMap(tx, callback) { return callback(null, map); } - if (!this.options.mapAddress) { - table = addresses.reduce(function(out, address) { - out[address] = [address]; - return out; - }, {}); - return cb(null, table); - } - return this.mapAddresses(addresses, cb); }; @@ -177,7 +178,7 @@ TXDB.prototype.getMap = function getMap(tx, callback) { TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { var self = this; - var table = {}; + var table = { count: 0 }; if (Array.isArray(address)) { return utils.forEachSerial(address, function(address, next) { @@ -187,6 +188,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { assert(res[address]); table[address] = res[address]; + table.count += res.count; next(); }); @@ -198,13 +200,12 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { }); } - this.db.fetch('W/' + address, function(json) { - return JSON.parse(json.toString('utf8')); - }, function(err, data) { + this.wdb.getAddress(address, function(err, paths) { if (err) return callback(err); - table[address] = data ? data.wallets : []; + table[address] = paths ? Object.keys(paths) : []; + table.count += table[address].length; return callback(null, table); }); @@ -310,11 +311,6 @@ TXDB.prototype.add = function add(tx, callback, force) { if (!map) return callback(null, false); - if (self.options.mapAddress) { - if (map.all.length === 0) - return callback(null, false); - } - return self._add(tx, map, callback, force); }); }; @@ -352,30 +348,26 @@ TXDB.prototype._add = function add(tx, map, callback, force) { batch.put('t/' + hash, tx.toExtended()); - if (self.options.indexExtra) { - if (tx.ts === 0) { - assert(tx.ps > 0); - batch.put('p/' + hash, DUMMY); - batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); - } else { - batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); - } - - if (self.options.indexAddress) { - map.all.forEach(function(id) { - batch.put('T/' + id + '/' + hash, DUMMY); - if (tx.ts === 0) { - batch.put('P/' + id + '/' + hash, DUMMY); - batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); - } else { - batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); - } - }); - } + if (tx.ts === 0) { + assert(tx.ps > 0); + batch.put('p/' + hash, DUMMY); + batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); + } else { + batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); } + map.all.forEach(function(id) { + batch.put('T/' + id + '/' + hash, DUMMY); + if (tx.ts === 0) { + batch.put('P/' + id + '/' + hash, DUMMY); + batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); + } else { + batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); + } + }); + // Consume unspent money or add orphans utils.forEachSerial(tx.inputs, function(input, next, i) { var key, address; @@ -386,10 +378,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) { address = input.getHash(); // Only add orphans if this input is ours. - if (self.options.mapAddress) { - if (!address || !map.table[address].length) - return next(); - } + if (!address || !map.table[address].length) + return next(); self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { if (err) @@ -409,7 +399,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { updated = true; - if (self.options.indexAddress && address) { + if (address) { map.table[address].forEach(function(id) { batch.del('C/' + id + '/' + key); }); @@ -481,10 +471,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) { var key, coin; // Do not add unspents for outputs that aren't ours. - if (self.options.mapAddress) { - if (!address || !map.table[address].length) - return next(); - } + if (!address || !map.table[address].length) + return next(); key = hash + '/' + i; @@ -548,7 +536,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) { return next(err); if (!orphans) { - if (self.options.indexAddress && address) { + if (address) { map.table[address].forEach(function(id) { batch.put('C/' + id + '/' + key, DUMMY); }); @@ -719,30 +707,24 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) { batch.put('t/' + hash, tx.toExtended()); - if (self.options.indexExtra) { - batch.del('p/' + hash); - batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.del('m/' + pad32(existing.ps) + '/' + hash); - batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); + batch.del('p/' + hash); + batch.put('h/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.del('m/' + pad32(existing.ps) + '/' + hash); + batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY); - if (self.options.indexAddress) { - map.all.forEach(function(id) { - batch.del('P/' + id + '/' + hash); - batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); - batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash); - batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); - }); - } - } + map.all.forEach(function(id) { + batch.del('P/' + id + '/' + hash); + batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY); + batch.del('M/' + id + '/' + pad32(existing.ps) + '/' + hash); + batch.put('M/' + id + '/' + pad32(tx.ts) + '/' + hash, DUMMY); + }); utils.forEachSerial(tx.outputs, function(output, next, i) { var address = output.getHash(); // Only update coins if this output is ours. - if (self.options.mapAddress) { - if (!address || !map.table[address].length) - return next(); - } + if (!address || !map.table[address].length) + return next(); self.getCoin(hash, i, function(err, coin) { if (err) @@ -813,11 +795,6 @@ TXDB.prototype.remove = function remove(hash, callback, force) { if (!map) return callback(null, false); - if (self.options.mapAddress) { - if (map.all.length === 0) - return callback(null, false); - } - return self._remove(tx, map, callback, force); }); }); @@ -847,11 +824,6 @@ TXDB.prototype.lazyRemove = function lazyRemove(tx, callback, force) { if (!map) return callback(null, false); - if (self.options.mapAddress) { - if (map.all.length === 0) - return callback(null, false); - } - return self._remove(tx, map, callback, force); }); }; @@ -879,29 +851,25 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { batch.del('t/' + hash); - if (self.options.indexExtra) { - if (tx.ts === 0) { - batch.del('p/' + hash); - batch.del('m/' + pad32(tx.ps) + '/' + hash); - } else { - batch.del('h/' + pad32(tx.height) + '/' + hash); - batch.del('m/' + pad32(tx.ts) + '/' + hash); - } - - if (self.options.indexAddress) { - map.all.forEach(function(id) { - batch.del('T/' + id + '/' + hash); - if (tx.ts === 0) { - batch.del('P/' + id + '/' + hash); - batch.del('M/' + id + '/' + pad32(tx.ps) + '/' + hash); - } else { - batch.del('H/' + id + '/' + pad32(tx.height) + '/' + hash); - batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash); - } - }); - } + if (tx.ts === 0) { + batch.del('p/' + hash); + batch.del('m/' + pad32(tx.ps) + '/' + hash); + } else { + batch.del('h/' + pad32(tx.height) + '/' + hash); + batch.del('m/' + pad32(tx.ts) + '/' + hash); } + map.all.forEach(function(id) { + batch.del('T/' + id + '/' + hash); + if (tx.ts === 0) { + batch.del('P/' + id + '/' + hash); + batch.del('M/' + id + '/' + pad32(tx.ps) + '/' + hash); + } else { + batch.del('H/' + id + '/' + pad32(tx.height) + '/' + hash); + batch.del('M/' + id + '/' + pad32(tx.ts) + '/' + hash); + } + }); + this.fillHistory(tx, function(err) { if (err) return callback(err); @@ -916,12 +884,10 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { if (!input.coin) return; - if (self.options.mapAddress) { - if (!address || !map.table[address].length) - return; - } + if (!address || !map.table[address].length) + return; - if (self.options.indexAddress && address) { + if (address) { map.table[address].forEach(function(id) { batch.put('C/' + id + '/' + key, DUMMY); }); @@ -936,15 +902,13 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) { var key = hash + '/' + i; var address = output.getHash(); - if (self.options.mapAddress) { - if (!address || !map.table[address].length) - return; - } + if (!address || !map.table[address].length) + return; if (output.script.isUnspendable()) return; - if (self.options.indexAddress && address) { + if (address) { map.table[address].forEach(function(id) { batch.del('C/' + id + '/' + key); }); @@ -1000,11 +964,6 @@ TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { if (!map) return callback(null, false); - if (self.options.mapAddress) { - if (map.all.length === 0) - return callback(null, false); - } - return self._unconfirm(tx, map, callback, force); }); }); @@ -1043,21 +1002,17 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) { batch.put('t/' + hash, tx.toExtended()); - if (self.options.indexExtra) { - batch.put('p/' + hash, DUMMY); - batch.del('h/' + pad32(height) + '/' + hash); - batch.del('m/' + pad32(ts) + '/' + hash); - batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); + batch.put('p/' + hash, DUMMY); + batch.del('h/' + pad32(height) + '/' + hash); + batch.del('m/' + pad32(ts) + '/' + hash); + batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY); - if (self.options.indexAddress) { - map.all.forEach(function(id) { - batch.put('P/' + id + '/' + hash, DUMMY); - batch.del('H/' + id + '/' + pad32(height) + '/' + hash); - batch.del('M/' + id + '/' + pad32(ts) + '/' + hash); - batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); - }); - } - } + map.all.forEach(function(id) { + batch.put('P/' + id + '/' + hash, DUMMY); + batch.del('H/' + id + '/' + pad32(height) + '/' + hash); + batch.del('M/' + id + '/' + pad32(ts) + '/' + hash); + batch.put('M/' + id + '/' + pad32(tx.ps) + '/' + hash, DUMMY); + }); utils.forEachSerial(tx.outputs, function(output, next, i) { self.getCoin(hash, i, function(err, coin) { @@ -1730,56 +1685,6 @@ TXDB.prototype.getBalance = function getBalance(address, callback) { }); }; -/** - * Get hashes of all transactions in the database. - * @param {WalletID|WalletID[]} address - By address (can be null). - * @param {Function} callback - Returns [Error, {@link Hash}[]]. - */ - -TXDB.prototype.getHistoryHashesByAddress = function getHistoryHashesByAddress(address, callback) { - return this.getHistoryHashes(address, callback); -}; - -/** - * Get all transactions. - * @param {WalletID|WalletID[]} address - By address (can be null). - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - -TXDB.prototype.getHistoryByAddress = function getHistoryByAddress(address, callback) { - return this.getHistory(address, callback); -}; - -/** - * Get coins. - * @param {WalletID|WalletID[]} address - By address (can be null). - * @param {Function} callback - Returns [Error, {@link Coin}[]]. - */ - -TXDB.prototype.getCoinsByAddress = function getCoins(address, callback) { - return this.getCoins(address, callback); -}; - -/** - * Get unconfirmed transactions. - * @param {WalletID|WalletID[]} address - By address (can be null). - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - -TXDB.prototype.getUnconfirmedByAddress = function getUnconfirmedByAddress(address, callback) { - return this.getUnconfirmed(address, callback); -}; - -/** - * Calculate balance. - * @param {WalletID|WalletID[]} address - By address (can be null). - * @param {Function} callback - Returns [Error, {@link Balance}]. - */ - -TXDB.prototype.getBalanceByAddress = function getBalanceByAddress(address, callback) { - return this.getBalance(address, callback); -}; - /** * @param {WalletID|WalletID[]} address - By address (can be null). * @param {Number} now - Current time. diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 873f56db..30a4e33a 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -10,6 +10,7 @@ var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); var assert = utils.assert; var constants = bcoin.protocol.constants; +var BufferReader = require('./reader'); var BufferWriter = require('./writer'); /** @@ -27,14 +28,10 @@ var BufferWriter = require('./writer'); * address. * @param {Number?} options.changeDepth - The index of the _next_ change * address. - * @param {Boolean?} options.copayBIP45 - Use copay-style BIP45 if bip45 - * derivation is used. * @param {Number?} options.lookahead - Amount of lookahead addresses * (default=5). * @param {String?} options.type - Type of wallet (pubkeyhash, multisig) * (default=pubkeyhash). - * @param {String?} options.derivation - Derivation type (bip44, bip45) - * (default=bip44). * @param {Boolean?} options.compressed - Whether to use compressed * public keys (default=true). * @param {Number?} options.m - `m` value for multisig. @@ -81,13 +78,10 @@ function Wallet(options) { this.accountIndex = options.accountIndex || 0; this.receiveDepth = options.receiveDepth || 1; this.changeDepth = options.changeDepth || 1; - this.copayBIP45 = options.copayBIP45 || false; this.lookahead = options.lookahead != null ? options.lookahead : 5; - this.cosignerIndex = -1; this.initialized = false; this.type = options.type || 'pubkeyhash'; - this.derivation = options.derivation || 'bip44'; this.compressed = options.compressed !== false; this.keys = []; this.m = options.m || 1; @@ -105,18 +99,9 @@ function Wallet(options) { throw new Error('m ranges between 1 and n'); if (!this.accountKey) { - key = this.master.key; - - assert(key); - - if (this.derivation === 'bip45') - key = key.derivePurpose45().hdPublicKey; - else if (this.derivation === 'bip44') - key = key.deriveAccount44(this.accountIndex).hdPublicKey; - else - assert(false); - - this.accountKey = key; + assert(this.master.key); + key = this.master.key.deriveAccount44(this.accountIndex); + this.accountKey = key.hdPublicKey; } if (!this.id) @@ -125,11 +110,11 @@ function Wallet(options) { // Non-alphanumeric IDs will break leveldb sorting. assert(/^[a-zA-Z0-9]+$/.test(this.id), 'Wallet IDs must be alphanumeric.'); - this._addKey(this.accountKey); + this.pushKey(this.accountKey); if (options.keys) { for (i = 0; i < options.keys.length; i++) - this._addKey(options.keys[i]); + this.pushKey(options.keys[i]); } } @@ -188,30 +173,30 @@ Wallet.prototype.init = function init(callback) { }; /** - * Add a public account/purpose key to the wallet for multisig. - * @param {HDPublicKey|Base58String} key - Account (bip44) or - * Purpose (bip45) key (can be in base58 form). + * Add a public account key to the wallet (multisig). + * Does not update the database. + * @param {HDPublicKey} key - Account (bip44) + * key (can be in base58 form). * @throws Error on non-hdkey/non-accountkey. */ -Wallet.prototype._addKey = function addKey(key) { +Wallet.prototype.pushKey = function pushKey(key) { var result = false; - var index, i; + var index = -1; + var i; assert(key, 'Key required.'); if (Array.isArray(key)) { for (i = 0; i < key.length; i++) { - if (this._addKey(key[i])) + if (this.pushKey(key[i])) result = true; } return result; } - if (key instanceof bcoin.wallet) { - assert(key.derivation === this.derivation); + if (key instanceof bcoin.wallet) key = key.accountKey; - } if (bcoin.hd.isExtended(key)) key = bcoin.hd.fromBase58(key); @@ -222,13 +207,8 @@ Wallet.prototype._addKey = function addKey(key) { if (!bcoin.hd.isHD(key)) throw new Error('Must add HD keys to wallet.'); - if (this.derivation === 'bip44') { - if (!key.isAccount44()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - } else if (this.derivation === 'bip45') { - if (!key.isPurpose45()) - throw new Error('Must add HD purpose keys to BIP45 wallet.'); - } + if (!key.isAccount44()) + throw new Error('Must add HD account keys to BIP44 wallet.'); for (i = 0; i < this.keys.length; i++) { if (this.keys[i].equal(key)) { @@ -237,40 +217,33 @@ Wallet.prototype._addKey = function addKey(key) { } } - if (index != null) + if (index !== -1) return false; - assert(!this._keysFinalized); + if (this.keys.length === this.n) + throw new Error('Cannot add more keys.'); this.keys.push(key); - if (this.keys.length === this.n) - this._finalizeKeys(); - return true; }; -Wallet.prototype.addKey = function addKey(key, callback) { - if (this._addKey(key)) - this.init(callback); -}; - /** - * Remove a public account/purpose key to the wallet for multisig. - * @param {HDPublicKey|Base58String} key - Account (bip44) or Purpose - * (bip45) key (can be in base58 form). + * Remove a public account key to the wallet (multisig). + * Does not update the database. + * @param {HDPublicKey} key - Account (bip44) + * key (can be in base58 form). * @throws Error on non-hdkey/non-accountkey. */ -Wallet.prototype._removeKey = function removeKey(key) { +Wallet.prototype.spliceKey = function spliceKey(key) { var result = false; - var index, i; - - assert(!this._keysFinalized); + var index = -1; + var i; if (Array.isArray(key)) { for (i = 0; i < key.length; i++) { - if (this._removeKey(key[i])) + if (this.spliceKey(key[i])) result = true; } return result; @@ -278,10 +251,8 @@ Wallet.prototype._removeKey = function removeKey(key) { assert(key, 'Key required.'); - if (key instanceof bcoin.wallet) { - assert(key.derivation === this.derivation); + if (key instanceof bcoin.wallet) key = key.accountKey; - } if (bcoin.hd.isExtended(key)) key = bcoin.hd.fromBase58(key); @@ -292,13 +263,8 @@ Wallet.prototype._removeKey = function removeKey(key) { if (!bcoin.hd.isHD(key)) throw new Error('Must add HD keys to wallet.'); - if (this.derivation === 'bip44') { - if (!key.isAccount44()) - throw new Error('Must add HD account keys to BIP44 wallet.'); - } else if (this.derivation === 'bip45') { - if (!key.isPurpose45()) - throw new Error('Must add HD purpose keys to BIP45 wallet.'); - } + if (!key.isAccount44()) + throw new Error('Must add HD account keys to BIP44 wallet.'); for (i = 0; i < this.keys.length; i++) { if (this.keys[i].equal(key)) { @@ -307,40 +273,69 @@ Wallet.prototype._removeKey = function removeKey(key) { } } - if (index == null) + if (index === -1) return false; + if (this.keys.length === this.n) + throw new Error('Cannot remove key.'); + this.keys.splice(index, 1); return true; }; -Wallet.prototype.removeKey = function removeKey(key, callback) { - var self = this; +/** + * Add a public account key to the wallet (multisig). + * Saves the key in the wallet database. + * @param {HDPublicKey} key + * @param {Function} callback + */ - if (this.keys.length === this.n) - return callback(new Error('Cannot remove the fucking key now.')); +Wallet.prototype.addKey = function addKey(key, callback) { + var result = false; - if (this._removeKey(key)) - this.save(callback); -}; - -Wallet.prototype._finalizeKeys = function _finalizeKeys() { - var i; - - assert(!this._keysFinalized); - this._keysFinalized = true; - - this.keys = utils.sortHDKeys(this.keys); - - for (i = 0; i < this.keys.length; i++) { - if (this.keys[i].equal(this.accountKey)) { - this.cosignerIndex = i; - break; - } + try { + result = this.pushKey(key); + } catch (e) { + return callback(e); } - assert(this.cosignerIndex !== -1); + if (!result) + return callback(null, result); + + this.init(function(err) { + if (err) + return callback(err); + + return callback(null, result); + }); +}; + +/** + * Remove a public account key from the wallet (multisig). + * Remove the key from the wallet database. + * @param {HDPublicKey} key + * @param {Function} callback + */ + +Wallet.prototype.removeKey = function removeKey(key, callback) { + var result = false; + + try { + result = this.spliceKey(key); + } catch (e) { + return callback(e); + } + + if (!result) + return callback(null, result); + + this.save(function(err) { + if (err) + return callback(err); + + return callback(null, result); + }); }; /** @@ -473,10 +468,7 @@ Wallet.prototype.deriveAddress = function deriveAddress(change, index) { data = this.parsePath(path); } else { data = { - path: this.createPath(this.cosignerIndex, change, index), - cosignerIndex: this.copayBIP45 - ? constants.hd.HARDENED - 1 - : this.cosignerIndex, + path: this.createPath(change, index), change: change, index: index }; @@ -490,8 +482,6 @@ Wallet.prototype.deriveAddress = function deriveAddress(change, index) { options = { network: this.network, key: key.publicKey, - change: data.change, - index: data.index, path: data.path, type: this.type, witness: this.witness, @@ -502,7 +492,7 @@ Wallet.prototype.deriveAddress = function deriveAddress(change, index) { for (i = 0; i < this.keys.length; i++) { key = this.keys[i]; - path = this.createPath(i, data.change, data.index); + path = this.createPath(data.change, data.index); key = key.derive(path); options.keys.push(key.publicKey); } @@ -516,16 +506,28 @@ Wallet.prototype.deriveAddress = function deriveAddress(change, index) { return address; }; +/** + * Save the wallet to the database. Necessary + * when address depth and keys change. + * @param {Function} callback + */ + Wallet.prototype.save = function save(callback) { this.db.save(this, callback); }; +/** + * Save addresses to path map. + * @param {KeyRing[]} address + * @param {Function} callback + */ + Wallet.prototype.saveAddress = function saveAddress(address, callback) { this.db.saveAddress(this.id, address, callback); }; /** - * Test whether the wallet posesses an address. + * Test whether the wallet possesses an address. * @param {Base58Address} address * @returns {Boolean} */ @@ -536,20 +538,13 @@ Wallet.prototype.hasAddress = function hasAddress(address, callback) { /** * Create a path. - * @param {Number} cosignerIndex - The index of the target key. - * Note that this will always be `0x80000000 - 1` if `copayBIP45` - * is enabled. * @param {Boolean} change - Whether the key is on the change branch. * @param {Number} index - The index to derive to. * @returns {String} path */ -Wallet.prototype.createPath = function createPath(cosignerIndex, change, index) { - if (this.copayBIP45) - cosignerIndex = constants.hd.HARDENED - 1; - +Wallet.prototype.createPath = function createPath(change, index) { return 'm' - + (this.derivation === 'bip45' ? '/' + cosignerIndex : '') + '/' + (change ? 1 : 0) + '/' + index; }; @@ -558,31 +553,20 @@ Wallet.prototype.createPath = function createPath(cosignerIndex, change, index) * Parse a path. * @param {String} path * @returns {Object} { - * path: String, cosignerIndex: Number?, + * path: String, * change: Boolean, index: Number * } */ Wallet.prototype.parsePath = function parsePath(path) { - var parts; + var parts = path.split('/'); - if (this.derivation === 'bip45') - assert(/^m\/\d+\/\d+\/\d+$/.test(path)); - else - assert(/^m\/\d+\/\d+$/.test(path)); - - parts = path.split('/'); - - if (this.derivation === 'bip45' && this.copayBIP45) - assert(+parts[parts.length - 3] === constants.hd.HARDENED - 1); + assert(/^m\/\d+\/\d+$/.test(path)); return { path: path, - cosignerIndex: this.derivation === 'bip45' - ? +parts[parts.length - 3] - : null, - change: +parts[parts.length - 2] === 1, - index: +parts[parts.length - 1] + change: parseInt(parts[parts.length - 2], 10) === 1, + index: parseInt(parts[parts.length - 1], 10) }; }; @@ -1061,7 +1045,7 @@ Wallet.prototype.getRedeem = function getRedeem(hash, callback) { */ Wallet.prototype.zap = function zap(now, age, callback) { - return this.db.zapWallet(this.id, now, age, callback); + return this.db.zap(this.id, now, age, callback); }; /** @@ -1225,24 +1209,12 @@ Wallet.prototype.sign = function sign(tx, options, callback) { return callback(null, 0); } + master = master.deriveAccount44(self.accountIndex); + for (i = 0; i < addresses.length; i++) { address = addresses[i]; - key = master; - - if (self.derivation === 'bip44') { - key = key.deriveAccount44(self.accountIndex); - assert.equal(key.xpubkey, self.accountKey.xpubkey); - } else if (self.derivation === 'bip45') { - key = key.derivePurpose45(); - assert.equal(key.xpubkey, self.accountKey.xpubkey); - } else { - assert(false); - } - - key = key.derive(address.path); - + key = master.derive(address.path); assert(utils.equal(key.getPublicKey(), address.key)); - total += address.sign(tx, key, options.index, options.type); } @@ -1529,8 +1501,6 @@ Wallet.prototype.inspect = function inspect() { ? this.programAddress : null, witness: this.witness, - derivation: this.derivation, - copayBIP45: this.copayBIP45, accountIndex: this.accountIndex, receiveDepth: this.receiveDepth, changeDepth: this.changeDepth, @@ -1559,8 +1529,6 @@ Wallet.prototype.toJSON = function toJSON() { m: this.m, n: this.n, witness: this.witness, - derivation: this.derivation, - copayBIP45: this.copayBIP45, accountIndex: this.accountIndex, receiveDepth: this.receiveDepth, changeDepth: this.changeDepth, @@ -1572,89 +1540,6 @@ Wallet.prototype.toJSON = function toJSON() { }; }; -Wallet.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); - var i; - - p.writeU32(this.network.magic); - p.writeVarString(this.id, 'ascii'); - p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); - p.writeU8(this.m); - p.writeU8(this.n); - p.writeU8(this.witness ? 1 : 0); - p.writeU8(this.derivation === 'bip44' ? 44 : 45); - p.writeU8(this.copayBIP45 ? 1 : 0); - p.writeU32(this.accountIndex); - p.writeU32(this.receiveDepth); - p.writeU32(this.changeDepth); - p.writeVarBytes(this.master.toRaw()); - p.writeBytes(this.accountKey.toRaw()); // 82 bytes - p.writeVarint(this.keys.length); - - for (i = 0; i < this.keys.length; i++) - p.writeBytes(this.keys[i].toRaw()); - - if (!writer) - p = p.render(); - - return p; -}; - -Wallet.fromRaw = function fromRaw(data) { - return new Wallet(Wallet.parseRaw(data)); -}; - -Wallet.parseRaw = function parseRaw(data) { - var networks = bcoin.protocol.network; - var p = new BufferReader(data); - var magic = p.readU32(); - var id = this.readVarString('ascii'); - var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; - var m = p.readU8(); - var n = p.readU8(); - var witness = p.readU8() === 1; - var derivation = p.readU8() === 44 ? 'bip44' : 'bip45'; - var copayBIP45 = p.readU8() === 1; - var accountIndex = p.readU32(); - var receiveDepth = p.readU32(); - var changeDepth = p.readU32(); - var master = MasterKey.fromRaw(p.readVarBytes()); - var accountKey = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); - var count = p.readVarint(); - var keys = []; - var i, type, network; - - for (i = 0; i < count; i++) - keys.push(bcoin.hd.PublicKey.fromRaw(p.readBytes(82))); - - for (i = 0; i < networks.types.length; i++) { - type = networks.types[i]; - if (magic === networks[type].magic) { - network = type; - break; - } - } - - assert(network, 'Network not found.'); - - return { - network: network, - id: id, - type: type, - m: m, - n: n, - witness: witness, - derivation: derivation, - copayBIP45: copayBIP45, - accountIndex: accountIndex, - receiveDepth: receiveDepth, - changeDepth: changeDepth, - master: master, - accountKey: accountKey, - keys: keys - }; -}; - /** * Handle a deserialized JSON wallet object. * @returns {Object} A "naked" wallet (a @@ -1677,8 +1562,6 @@ Wallet.parseJSON = function parseJSON(json) { m: json.m, n: json.n, witness: json.witness, - derivation: json.derivation, - copayBIP45: json.copayBIP45, accountIndex: json.accountIndex, receiveDepth: json.receiveDepth, changeDepth: json.changeDepth, @@ -1690,6 +1573,71 @@ Wallet.parseJSON = function parseJSON(json) { }; }; +Wallet.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + var i; + + p.writeU32(this.network.magic); + p.writeVarString(this.id, 'utf8'); + p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); + p.writeU8(this.m); + p.writeU8(this.n); + p.writeU8(this.witness ? 1 : 0); + p.writeU32(this.accountIndex); + p.writeU32(this.receiveDepth); + p.writeU32(this.changeDepth); + p.writeVarBytes(this.master.toRaw()); + p.writeBytes(this.accountKey.toRaw()); + p.writeU8(this.keys.length); + + for (i = 0; i < this.keys.length; i++) + p.writeBytes(this.keys[i].toRaw()); + + if (!writer) + p = p.render(); + + return p; +}; + +Wallet.fromRaw = function fromRaw(data) { + return new Wallet(Wallet.parseRaw(data)); +}; + +Wallet.parseRaw = function parseRaw(data) { + var p = new BufferReader(data); + var network = bcoin.network.fromMagic(p.readU32()); + var id = p.readVarString('utf8'); + var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; + var m = p.readU8(); + var n = p.readU8(); + var witness = p.readU8() === 1; + var accountIndex = p.readU32(); + var receiveDepth = p.readU32(); + var changeDepth = p.readU32(); + var master = MasterKey.fromRaw(p.readVarBytes()); + var accountKey = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); + var keys = new Array(p.readU8()); + var i; + + for (i = 0; i < keys.length; i++) + keys[i] = bcoin.hd.PublicKey.fromRaw(p.readBytes(82)); + + return { + network: network.type, + id: id, + type: type, + m: m, + n: n, + witness: witness, + accountIndex: accountIndex, + receiveDepth: receiveDepth, + changeDepth: changeDepth, + master: master, + accountKey: accountKey, + keys: keys + }; +}; + /** * Instantiate a Wallet from a * jsonified wallet object. @@ -1713,85 +1661,163 @@ Wallet.isWallet = function isWallet(obj) { && obj.deriveAddress === 'function'; }; +/* + * Master Key + */ + function MasterKey(options) { - this.json = options.json; + if (!(this instanceof MasterKey)) + return new MasterKey(options); + + this.encrypted = options.encrypted; + this.xprivkey = options.xprivkey; + this.phrase = options.phrase; + this.passphrase = options.passphrase; this.key = options.key || null; } MasterKey.prototype.decrypt = function decrypt(passphrase) { + var xprivkey; + if (this.key) return this.key; - if (!this.json.encrypted) - return bcoin.hd.fromJSON(this.json); + if (this.encrypted) { + assert(passphrase, 'Passphrase is required.'); + xprivkey = utils.decrypt(this.xprivkey, passphrase); + } else { + xprivkey = this.xprivkey; + } - return bcoin.hd.fromJSON(this.json, passphrase); + return bcoin.hd.PrivateKey.fromRaw(xprivkey); }; -MasterKey.prototype.toJSON = function toJSON() { - return this.json; +MasterKey.prototype.encrypt = function encrypt(passphrase) { + if (this.encrypted) + return; + + this.key = null; + this.encrypted = true; + this.xprivkey = utils.encrypt(this.xprivkey, passphrase); + + if (this.phrase) { + this.phrase = utils.encrypt(this.phrase, passphrase); + this.passphrase = utils.encrypt(this.passphrase, passphrase); + } +}; + +MasterKey.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + + p.writeU8(this.encrypted ? 1 : 0); + + if (this.phrase) { + p.writeU8(1); + p.writeVarBytes(this.phrase); + p.writeVarBytes(this.passphrase); + } else { + p.writeU8(0); + } + + p.writeBytes(this.xprivkey); + + if (!writer) + p = p.render(); + + return p; +}; + +MasterKey.fromRaw = function fromRaw(raw) { + var data = {}; + var p = new BufferReader(raw); + + data.encrypted = p.readU8() === 1; + + if (p.readU8() === 1) { + data.phrase = p.readVarBytes(); + data.passphrase = p.readVarBytes(); + } + + data.xprivkey = p.readBytes(82); + + if (!data.encrypted) + data.key = bcoin.hd.PrivateKey.fromRaw(data.xprivkey); + + return new MasterKey(data); }; MasterKey.fromKey = function fromKey(key) { - return new MasterKey({ - key: key, - json: key.toJSON() - }); + var data = {}; + + data.encrypted = false; + + if (key.mnemonic) { + data.phrase = new Buffer(key.mnemonic.phrase, 'utf8'); + data.passphrase = new Buffer(key.mnemonic.passphrase, 'utf8'); + } + + data.xprivkey = key.toRaw(); + + data.key = key; + + return new MasterKey(data); +}; + +MasterKey.prototype.toJSON = function toJSON() { + var json = {}; + + json.encrypted = this.encrypted; + + if (this.encrypted) { + if (this.phrase) { + json.phrase = this.phrase.toString('hex'); + json.passphrase = this.passphrase.toString('hex'); + } + json.xprivkey = this.xprivkey.toString('hex'); + } else { + if (this.phrase) { + json.phrase = this.phrase.toString('utf8'); + json.passphrase = this.passphrase.toString('utf8'); + } + json.xprivkey = utils.toBase58(this.xprivkey); + } + + return json; }; MasterKey.fromJSON = function fromJSON(json) { - var key; + var data = {}; - if (!json.encrypted) - key = bcoin.hd.fromJSON(json); + data.encrypted = json.encrypted; - return new MasterKey({ - key: key, - json: json - }); + if (json.encrypted) { + if (json.phrase) { + data.phrase = new Buffer(json.phrase, 'hex'); + data.passphrase = new Buffer(json.passphrase, 'hex'); + } + data.xprivkey = new Buffer(json.xprivkey, 'hex'); + } else { + if (json.phrase) { + data.phrase = new Buffer(json.phrase, 'utf8'); + data.passphrase = new Buffer(json.passphrase, 'utf8'); + } + data.xprivkey = utils.fromBase58(json.xprivkey); + } + + if (!data.encrypted) + data.key = bcoin.hd.PrivateKey.fromRaw(data.xprivkey); + + return new MasterKey(data); }; MasterKey.isMasterKey = function isMasterKey(obj) { return obj - && obj.json + && obj.xprivkey && typeof obj.decrypt === 'function'; }; /* - * Expose - */ - -module.exports = Wallet; - -/** - * HD BIP-44/45 wallet - * @exports Wallet - * @constructor - * @param {Object} options - * @param {WalletDB} options.db - * present, no coins will be available. - * @param {(HDPrivateKey|HDPublicKey)?} options.master - Master HD key. If not - * present, it will be generated. - * @param {Boolean?} options.witness - Whether to use witness programs. - * @param {Number?} options.accountIndex - The BIP44 account index (default=0). - * @param {Number?} options.receiveDepth - The index of the _next_ receiving - * address. - * @param {Number?} options.changeDepth - The index of the _next_ change - * address. - * @param {Boolean?} options.copayBIP45 - Use copay-style BIP45 if bip45 - * derivation is used. - * @param {Number?} options.lookahead - Amount of lookahead addresses - * (default=5). - * @param {String?} options.type - Type of wallet (pubkeyhash, multisig) - * (default=pubkeyhash). - * @param {String?} options.derivation - Derivation type (bip44, bip45) - * (default=bip44). - * @param {Boolean?} options.compressed - Whether to use compressed - * public keys (default=true). - * @param {Number?} options.m - `m` value for multisig. - * @param {Number?} options.n - `n` value for multisig. - * @param {String?} options.id - Wallet ID (used for storage) - * (default=account key "address"). + * CWallet */ function CWallet(id, db) { @@ -1805,33 +1831,23 @@ function CWallet(id, db) { this.network = db.network; this.db = db; this.id = id; + this.loaded = false; } utils.inherits(CWallet, EventEmitter); -/** - * Open the wallet, wait for the database to load. - * @param {Function} callback - */ - CWallet.prototype.open = function open(callback) { var self = this; if (this.loaded) return utils.nextTick(callback); - if (this._loading) - return this.once('open', callback); + this.loaded = true; this.db.register(this.id, this); - callback(); -}; -/** - * Close the wallet, wait for the database to close. - * @method - * @param {Function} callback - */ + return utils.nextTick(callback); +}; CWallet.prototype.close = CWallet.prototype.destroy = function destroy(callback) { @@ -1842,283 +1858,108 @@ CWallet.prototype.destroy = function destroy(callback) { this.db.unregister(this.id, this); this.db = null; -}; -/** - * Add a public account/purpose key to the wallet for multisig. - * @param {HDPublicKey|Base58String} key - Account (bip44) or - * Purpose (bip45) key (can be in base58 form). - * @throws Error on non-hdkey/non-accountkey. - */ + return utils.nextTick(callback); +}; CWallet.prototype.addKey = function addKey(key, callback) { this.db.addKey(this.id, key, callback); }; -/** - * Remove a public account/purpose key to the wallet for multisig. - * @param {HDPublicKey|Base58String} key - Account (bip44) or Purpose - * (bip45) key (can be in base58 form). - * @throws Error on non-hdkey/non-accountkey. - */ - CWallet.prototype.removeKey = function removeKey(key, callback) { this.db.removeKey(this.id, key, callback); }; -/** - * Get the wallet ID which is either the passed in `id` - * option, or the account/purpose key converted to an - * address with a prefix of `0x03be04` (`WLT`). - * @returns {Base58String} - */ - CWallet.prototype.getID = function getID() { return this.id; }; -/** - * Create a new receiving address (increments receiveDepth). - * @returns {KeyRing} - */ - CWallet.prototype.createReceive = function createReceive(callback) { return this.db.createAddress(this.id, false, callback); }; -/** - * Create a new change address (increments receiveDepth). - * @returns {KeyRing} - */ - CWallet.prototype.createChange = function createChange(callback) { return this.db.createAddress(this.id, true, callback); }; -/** - * Create a new address (increments depth). - * @param {Boolean} change - * @returns {KeyRing} - */ - CWallet.prototype.createAddress = function createAddress(change, callback) { return this.db.createAddress(this.id, change, callback); }; -/** - * Test whether the wallet posesses an address. - * @param {Base58Address} address - * @returns {Boolean} - */ - CWallet.prototype.hasAddress = function hasAddress(address, callback) { this.db.hasAddress(this.id, address, callback); }; -/** - * Fill a transaction with inputs, estimate - * transaction size, calculate fee, and add a change output. - * @see MTX#selectCoins - * @see MTX#fill - * @param {MTX} tx - _Must_ be a mutable transaction. - * @param {Object?} options - * @param {String?} options.selection - Coin selection priority. Can - * be `age`, `random`, or `all`. (default=age). - * @param {Boolean} options.round - Whether to round to the nearest - * kilobyte for fee calculation. - * See {@link TX#getMinFee} vs. {@link TX#getRoundFee}. - * @param {Rate} options.rate - Rate used for fee calculation. - * @param {Boolean} options.confirmed - Select only confirmed coins. - * @param {Boolean} options.free - Do not apply a fee if the - * transaction priority is high enough to be considered free. - * @param {Amount?} options.fee - Use a hard fee rather than calculating one. - * @param {Number|Boolean} options.subtractFee - Whether to subtract the - * fee from existing outputs rather than adding more inputs. - */ - CWallet.prototype.fill = function fill(tx, options, callback) { this.db.fill(this.id, tx, options, callback); }; -/** - * Fill transaction with coins (accesses db). - * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. - */ - CWallet.prototype.fillCoins = function fillCoins(tx, callback) { this.db.fillHistory(this.id, tx, callback); }; -/** - * Get a coin from the wallet (accesses db). - * @param {Hash} hash - * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}]. - */ - CWallet.prototype.getCoin = function getCoin(hash, index, callback) { this.db.getCoin(hash, index, callback); }; -/** - * Get a transaction from the wallet (accesses db). - * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. - */ - CWallet.prototype.getTX = function getTX(hash, callback) { this.db.getTX(hash, callback); }; -/** - * Build a transaction, fill it with outputs and inputs, - * sort the members according to BIP69, set locktime, - * and sign it (accesses db). - * @param {Object} options - See {@link CWallet#fill options}. - * @param {Object[]} outputs - See {@link Script.createOutputScript}. - * @param {Function} callback - Returns [Error, {@link MTX}]. - */ - CWallet.prototype.createTX = function createTX(options, outputs, callback) { this.db.createTX(this.id, options, outputs, callback); }; -/** - * Get path by address. - * @param {Base58Address} address - Base58 address. - */ - CWallet.prototype.getPath = function getPath(address, callback) { if (!address || typeof address !== 'string') return callback(); this.db.getPath(this.id, address, callback); }; -/** - * Get a redeem script or witness script by hash. - * @param {Hash} hash - Can be a ripemd160 or a sha256. - * @returns {Script} - */ - CWallet.prototype.getRedeem = function getRedeem(hash, callback) { - ; + this.db.getRedeem(this.id, hash, callback); }; -/** - * Zap stale TXs from wallet (accesses db). - * @param {Number} now - Current time (unix time). - * @param {Number} age - Age threshold (unix time, default=72 hours). - * @param {Function} callback - Returns [Error]. - */ - CWallet.prototype.zap = function zap(now, age, callback) { - return this.db.zapWallet(this.id, now, age, callback); + return this.db.zap(this.id, now, age, callback); }; -/** - * Build input scripts templates for a transaction (does not - * sign, only creates signature slots). Only builds scripts - * for inputs that are redeemable by this wallet. - * @param {MTX} tx - * @param {Number?} index - Index of input. If not present, - * it will attempt to sign all redeemable inputs. - * @returns {Number} Total number of scripts built. - */ - CWallet.prototype.scriptInputs = function scriptInputs(tx, callback) { this.db.scriptInputs(this.id, tx, callback); }; -/** - * Build input scripts and sign inputs for a transaction. Only attempts - * to build/sign inputs that are redeemable by this wallet. - * @param {MTX} tx - * @param {Number?} index - Index of input. If not present, - * it will attempt to build and sign all redeemable inputs. - * @param {SighashType?} type - * @returns {Number} Total number of inputs scripts built and signed. - */ - CWallet.prototype.sign = function sign(tx, options, callback) { this.db.sign(this.id, tx, options, callback); }; -/** - * Add a transaction to the wallets TX history (accesses db). - * @param {TX} tx - * @param {Function} callback - */ - CWallet.prototype.addTX = function addTX(tx, callback) { return this.db.addTX(tx, callback); }; -/** - * Get all transactions in transaction history (accesses db). - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - CWallet.prototype.getHistory = function getHistory(callback) { return this.db.getHistory(this.id, callback); }; -/** - * Get all available coins (accesses db). - * @param {Function} callback - Returns [Error, {@link Coin}[]]. - */ - CWallet.prototype.getCoins = function getCoins(callback) { return this.db.getCoins(this.id, callback); }; -/** - * Get all pending/unconfirmed transactions (accesses db). - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - CWallet.prototype.getUnconfirmed = function getUnconfirmed(callback) { return this.db.getUnconfirmed(this.id, callback); }; -/** - * Get wallet balance (accesses db). - * @param {Function} callback - Returns [Error, {@link Balance}]. - */ - CWallet.prototype.getBalance = function getBalance(callback) { return this.db.getBalance(this.id, callback); }; -/** - * Get last timestamp and height this wallet was active - * at (accesses db). Useful for resetting the chain - * to a certain height when in SPV mode. - * @param {Function} callback - Returns [Error, Number(ts), Number(height)]. - */ - CWallet.prototype.getLastTime = function getLastTime(callback) { return this.db.getLastTime(this.id, callback); }; -/** - * Get the last N transactions (accesses db). - * @param {Number} limit - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - CWallet.prototype.getLast = function getLast(limit, callback) { return this.db.getLast(this.id, limit, callback); }; -/** - * Get a range of transactions between two timestamps (accesses db). - * @param {Object} options - * @param {Number} options.start - * @param {Number} options.end - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - CWallet.prototype.getTimeRange = function getTimeRange(options, callback) { return this.db.getTimeRange(this.id, options, callback); }; @@ -2128,28 +1969,13 @@ CWallet.prototype.getReceiveAddress = function getReceiveAddress(callback) { }; CWallet.prototype.getInfo = function getInfo(callback) { - return this.db.get(this.id, function(err, cwallet, wallet) { - if (err) - return callback(err); - return callback(null, wallet); - }); + return this.db.getInfo(this.id, callback); }; -/** - * Convert the wallet to a more inspection-friendly object. - * @returns {Object} - */ - CWallet.prototype.inspect = function inspect() { return ''; }; -/** - * Test an object to see if it is a CWallet. - * @param {Object} obj - * @returns {Boolean} - */ - CWallet.isCWallet = function isCWallet(obj) { return obj && obj.db @@ -2162,5 +1988,5 @@ CWallet.isCWallet = function isCWallet(obj) { */ module.exports = Wallet; -bcoin.cwallet = CWallet; module.exports.CWallet = CWallet; +bcoin.cwallet = CWallet; diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index ec725ff9..4d292bc7 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -8,7 +8,7 @@ /* * Database Layout: * (inherits all from txdb) - * W/[address]/[id] -> dummy (map address to id) + * W/[address] -> id & path data * w/[id] -> wallet */ @@ -17,6 +17,8 @@ var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); var assert = utils.assert; var DUMMY = new Buffer([0]); +var BufferReader = require('./reader'); +var BufferWriter = require('./writer'); /** * WalletDB @@ -40,7 +42,7 @@ function WalletDB(options) { EventEmitter.call(this); - this.providers = []; + this.watchers = []; this.options = options; this.loaded = false; this.network = bcoin.network.get(options.network); @@ -110,9 +112,6 @@ WalletDB.prototype._init = function _init() { this.tx = new bcoin.txdb(this, { network: this.network, - indexExtra: true, - indexAddress: true, - mapAddress: true, verify: this.options.verify, useFilter: true }); @@ -221,60 +220,61 @@ WalletDB.prototype.destroy = function destroy(callback) { }; /** - * Derive an address. + * Helper function to get a wallet. + * @private * @param {WalletID} id - * @param {Boolean} change + * @param {Function} errback * @param {Function} callback */ -WalletDB.prototype.rpc = function rpc(id, callback, method) { +WalletDB.prototype.fetchWallet = function fetchWallet(id, errback, callback) { var self = this; callback = utils.ensure(callback); this.get(id, function(err, _, wallet) { if (err) - return callback(err); + return errback(err); if (!wallet) - return callback(new Error('No wallet.')); + return errback(new Error('No wallet.')); - method(wallet); + callback(wallet); }); }; WalletDB.prototype.syncOutputDepth = function syncOutputDepth(id, tx, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.syncOutputDepth(tx, callback); }); }; WalletDB.prototype.createAddress = function createAddress(id, change, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.createAddress(change, callback); }); }; WalletDB.prototype.getReceiveAddress = function getReceiveAddress(id, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { callback(null, wallet.receiveAddress); }); }; WalletDB.prototype.getChangeAddress = function getChangeAddress(id, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { callback(null, wallet.changeAddress); }); }; WalletDB.prototype.fill = function fill(id, tx, options, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.fill(tx, options, callback); }); }; WalletDB.prototype.scriptInputs = function scriptInputs(id, tx, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.scriptInputs(tx, callback); }); }; @@ -288,29 +288,41 @@ WalletDB.prototype.sign = function sign(id, tx, options, callback) { if (typeof options === 'string' || Buffer.isBuffer(options)) options = { passphrase: options }; - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.sign(tx, options, callback); }); }; WalletDB.prototype.createTX = function createTX(id, options, outputs, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.createTX(options, outputs, callback); }); }; WalletDB.prototype.addKey = function addKey(id, key, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.addKey(key, callback); }); }; WalletDB.prototype.removeKey = function removeKey(id, key, callback) { - this.rpc(id, callback, function(wallet) { + this.fetchWallet(id, callback, function(wallet) { wallet.removeKey(key, callback); }); }; +WalletDB.prototype.getInfo = function getInfo(id, callback) { + this.fetchWallet(id, callback, function(wallet) { + callback(null, wallet); + }); +}; + +WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) { + this.fetchWallet(id, callback, function(wallet) { + wallet.getRedeem(hash, callback); + }); +}; + /** * Save a "naked" (non-instantiated) wallet. Will * also index the address table. @@ -320,8 +332,7 @@ WalletDB.prototype.removeKey = function removeKey(id, key, callback) { */ WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) { - var data = new Buffer(JSON.stringify(json), 'utf8'); - this.db.put('w/' + id, data, callback); + this.db.put('w/' + id, json, callback); }; /** @@ -358,6 +369,9 @@ WalletDB.prototype.removeJSON = function removeJSON(id, callback) { WalletDB.prototype.getJSON = function getJSON(id, callback) { callback = utils.ensure(callback); + if (!id) + return callback(); + this.db.get('w/' + id, function(err, json) { if (err && err.type === 'NotFoundError') return callback(); @@ -366,7 +380,7 @@ WalletDB.prototype.getJSON = function getJSON(id, callback) { return callback(err); try { - json = JSON.parse(json.toString('utf8')); + json = bcoin.wallet.parseRaw(json); } catch (e) { return callback(e); } @@ -376,7 +390,7 @@ WalletDB.prototype.getJSON = function getJSON(id, callback) { }; /** - * Get a wallet from the database, instantiate, decrypt, and setup provider. + * Get a wallet from the database, instantiate, decrypt, and setup watcher. * @param {WalletID} id * @param {Function} callback - Returns [Error, {@link Wallet}]. */ @@ -386,19 +400,18 @@ WalletDB.prototype.get = function get(id, callback) { callback = utils.ensure(callback); - return this.getJSON(id, function(err, options) { + return this.getJSON(id, function(err, json) { var wallet; if (err) return callback(err); - if (!options) + if (!json) return callback(); try { - options = bcoin.wallet.parseJSON(options); - options.db = self; - wallet = new bcoin.wallet(options); + json.db = self; + wallet = new bcoin.wallet(json); } catch (e) { return callback(e); } @@ -425,7 +438,7 @@ WalletDB.prototype.save = function save(wallet, callback) { self.save(wallet, next); }, callback); } - this.saveJSON(wallet.id, wallet.toJSON(), callback); + this.saveJSON(wallet.id, wallet.toRaw(), callback); }; /** @@ -445,27 +458,24 @@ WalletDB.prototype.remove = function remove(id, callback) { }; /** - * Create a new wallet, save to database, setup provider. + * Create a new wallet, save to database, setup watcher. * @param {Object} options - See {@link Wallet}. * @param {Function} callback - Returns [Error, {@link Wallet}]. */ WalletDB.prototype.create = function create(options, callback) { var self = this; + var wallet; - function create(err, json) { - var wallet; + this.has(options.id, function(err, exists) { + if (err) + return callback(err); if (err) return callback(err); - if (json) { - return callback( - new Error('`' + options.id + '` already exists.'), - null, - null, - json); - } + if (exists) + return callback(new Error('Wallet already exists.')); if (self.network.witness) options.witness = options.witness !== false; @@ -480,12 +490,20 @@ WalletDB.prototype.create = function create(options, callback) { return callback(null, new bcoin.cwallet(wallet.id, self), wallet); }); - } + }); +}; - if (!options.id) - return create(); +/** + * Test for the existence of a wallet. + * @param {WalletID?} id + * @param {Function} callback + */ - return this.getJSON(options.id, create); +WalletDB.prototype.has = function has(id, callback) { + if (!id) + return callback(null, false); + + this.db.hash('w/' + id, callback); }; /** @@ -497,41 +515,38 @@ WalletDB.prototype.create = function create(options, callback) { WalletDB.prototype.ensure = function ensure(options, callback) { var self = this; - return this.create(options, function(err, cwallet, wallet, json) { - if (err && !json) + return this.get(options.id, function(err, cwallet, wallet) { + if (err) return callback(err); if (cwallet) - return callback(null, cwallet); + return callback(null, cwallet, wallet); - assert(json); - - try { - options = bcoin.wallet.parseJSON(json); - options.db = self; - wallet = new bcoin.wallet(options); - } catch (e) { - return callback(e); - } - - wallet.init(function(err) { - if (err) - return callback(err); - - return callback(null, new bcoin.cwallet(wallet.id, self), wallet); - }); + self.create(options, callback); }); }; -WalletDB.prototype.saveAddress = function saveAddress(id, address, callback) { +/** + * Save an address to the path map. + * The path map exists in the form of: + * `W/[address-hash] -> {walletid1=path1, walletid2=path2, ...}` + * @param {WalletID} id + * @param {KeyRing[]} addresses + * @param {Function} callback + */ + +WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) { var self = this; var hashes = []; var batch = this.db.batch(); + var i, address; - if (!Array.isArray(address)) - address = [address]; + if (!Array.isArray(addresses)) + addresses = [addresses]; + + for (i = 0; i < addresses.length; i++) { + address = addresses[i]; - address.forEach(function(address) { hashes.push([address.getKeyHash('hex'), address.path]); if (address.type === 'multisig') @@ -539,33 +554,26 @@ WalletDB.prototype.saveAddress = function saveAddress(id, address, callback) { if (address.witness) hashes.push([address.getProgramHash('hex'), address.path]); - }); + } utils.forEach(hashes, function(hash, next) { if (self.tx.filter) self.tx.filter.add(hash[0], 'hex'); - self.db.fetch('W/' + hash[0], function(json) { - return JSON.parse(json.toString('utf8')); - }, function(err, json) { + self.db.fetch('W/' + hash[0], parsePaths, function(err, paths) { if (err) return next(err); - if (!json) { - json = { - wallets: [], - path: hash[1] - }; - } + if (!paths) + paths = {}; - if (json.wallets.indexOf(id) !== -1) + if (paths[id]) return next(); - json.wallets.push(id); + paths[id] = hash[1]; - json = new Buffer(JSON.stringify(json), 'utf8'); + batch.put('W/' + hash[0], serializePaths(paths)); - batch.put('W/' + hash[0], json); next(); }); }, function(err) { @@ -576,39 +584,52 @@ WalletDB.prototype.saveAddress = function saveAddress(id, address, callback) { }); }; +/** + * Test whether an address hash exists in the + * path map and is relevant to the wallet id. + * @param {WalletID} id + * @param {Hash} address + * @param {Function} callback + */ + WalletDB.prototype.hasAddress = function hasAddress(id, address, callback) { - this.getAddress(id, address, function(err, address) { + this.getAddress(address, function(err, paths) { if (err) return callback(err); - return callback(null, !!address); + if (!paths || !paths[id]) + return callback(null, false); + + return callback(null, true); }); }; -WalletDB.prototype.getAddress = function getAddress(id, address, callback) { - var self = this; - this.db.fetch('W/' + address, function(json) { - return JSON.parse(json.toString('utf8')); - }, function(err, address) { - if (err) - return callback(err); +/** + * Get path data for the specified address hash. + * @param {Hash} address + * @param {Function} callback + */ - if (!address || address.wallets.indexOf(id) === -1) - return callback(); - - return callback(null, address); - }); +WalletDB.prototype.getAddress = function getAddress(address, callback) { + this.db.fetch('W/' + address, parsePaths, callback); }; +/** + * Get the corresponding path for an address hash. + * @param {WalletID} id + * @param {Hash} address + * @param {Function} callback + */ + WalletDB.prototype.getPath = function getPath(id, address, callback) { - this.getAddress(id, address, function(err, address) { + this.getAddress(address, function(err, paths) { if (err) return callback(err); - if (!address) + if (!paths || !paths[id]) return callback(); - return callback(null, address.path); + return callback(null, paths[id]); }); }; @@ -637,35 +658,35 @@ WalletDB.prototype.getCoin = function getCoin(hash, index, callback) { }; /** - * @see {@link TXDB#getHistoryByAddress}. + * @see {@link TXDB#getHistory}. */ WalletDB.prototype.getHistory = function getHistory(id, callback) { - return this.tx.getHistoryByAddress(id, callback); + return this.tx.getHistory(id, callback); }; /** - * @see {@link TXDB#getCoinsByAddress}. + * @see {@link TXDB#getCoins}. */ WalletDB.prototype.getCoins = function getCoins(id, callback) { - return this.tx.getCoinsByAddress(id, callback); + return this.tx.getCoins(id, callback); }; /** - * @see {@link TXDB#getUnconfirmedByAddress}. + * @see {@link TXDB#getUnconfirmed}. */ WalletDB.prototype.getUnconfirmed = function getUnconfirmed(id, callback) { - return this.tx.getUnconfirmedByAddress(id, callback); + return this.tx.getUnconfirmed(id, callback); }; /** - * @see {@link TXDB#getBalanceByAddress}. + * @see {@link TXDB#getBalance}. */ WalletDB.prototype.getBalance = function getBalance(id, callback) { - return this.tx.getBalanceByAddress(id, callback); + return this.tx.getBalance(id, callback); }; /** @@ -708,6 +729,15 @@ WalletDB.prototype.fillCoins = function fillCoins(tx, callback) { return this.tx.fillCoins(tx, callback); }; +/** + * Zap all walletdb transactions. + * @see {@link TXDB#zap}. + */ + +WalletDB.prototype.zap = function zap(id, now, age, callback) { + return this.tx.zap(id, now, age, callback); +}; + /** * Notify the database that a block has been * removed (reorg). Unconfirms transactions by height. @@ -748,73 +778,109 @@ WalletDB.prototype.removeBlock = function removeBlock(block, callback) { }; /** - * Zap all walletdb transactions. - * @see {@link TXDB#zap}. + * Register an event emitter with the walletdb. + * @param {WalletID} id + * @param {EventEmitter} watcher */ -WalletDB.prototype.zap = function zap(now, age, callback) { - return this.tx.zap(now, age, callback); +WalletDB.prototype.register = function register(id, watcher) { + if (!this.watchers[id]) + this.watchers[id] = []; + + if (this.watchers[id].indexOf(watcher) === -1) + this.watchers[id].push(watcher); }; /** - * Zap transactions for wallet. - * @see {@link TXDB#zap}. + * Unregister an event emitter with the walletdb. + * @param {WalletID} id + * @param {EventEmitter} watcher */ -WalletDB.prototype.zapWallet = function zapWallet(id, now, age, callback) { - return this.tx.zap(id, now, age, callback); -}; - -WalletDB.prototype.register = function register(id, provider) { - if (!this.providers[id]) - this.providers[id] = []; - - if (this.providers[id].indexOf(provider) === -1) - this.providers[id].push(provider); -}; - -WalletDB.prototype.unregister = function unregister(id, provider) { - var providers = this.providers[id]; +WalletDB.prototype.unregister = function unregister(id, watcher) { + var watchers = this.watchers[id]; var i; - if (!providers) + if (!watchers) return; - i = providers.indexOf(provider); + i = watchers.indexOf(watcher); if (i !== -1) - providers.splice(i, 1); + watchers.splice(i, 1); - if (providers.length === 0) - delete this.providers[id]; + if (watchers.length === 0) + delete this.watchers[id]; }; +/** + * Fire an event for all registered event emitters. + * @param {WalletID} id + * @param {...Object} args + */ + WalletDB.prototype.fire = function fire(id) { var args = Array.prototype.slice.call(arguments, 1); - var providers = this.providers[id]; + var watchers = this.watchers[id]; var i; - if (!providers) + if (!watchers) return; - for (i = 0; i < providers.length; i++) - providers[i].emit.apply(providers[i], args); + for (i = 0; i < watchers.length; i++) + watchers[i].emit.apply(watchers[i], args); }; +/** + * Test for a listener on a registered event emitter. + * @param {WalletID} id + * @param {String} event + * @returns {Boolean} + */ + WalletDB.prototype.hasListener = function hasListener(id, event) { - var providers = this.providers[id]; + var watchers = this.watchers[id]; var i; - if (!providers) + if (!watchers) return false; - for (i = 0; i < providers.length; i++) { - if (providers[i].listeners(event).length !== 0) + for (i = 0; i < watchers.length; i++) { + if (watchers[i].listeners(event).length !== 0) return true; } return false; }; +/* + * Helpers + */ + +function parsePaths(data) { + var p = new BufferReader(data); + var out = {}; + + while (p.left()) + out[p.readVarString('utf8')] = p.readVarString('ascii'); + + return out; +} + +function serializePaths(out) { + var p = new BufferWriter(); + var keys = Object.keys(out); + var i, id, path; + + for (i = 0; i < keys.length; i++) { + id = keys[i]; + path = out[id]; + p.writeVarString(id, 'utf8'); + p.writeVarString(path, 'ascii'); + } + + return p.render(); +} + /* * Expose */