serialization. refactor.

This commit is contained in:
Christopher Jeffrey 2016-05-29 03:04:28 -07:00
parent 663e62639d
commit ab9301ce7e
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
6 changed files with 754 additions and 893 deletions

View File

@ -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

View File

@ -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
*/

View File

@ -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}

View File

@ -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.

File diff suppressed because it is too large Load Diff

View File

@ -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
*/