wallet refactor.

This commit is contained in:
Christopher Jeffrey 2016-06-24 15:12:47 -07:00
parent 2a18a76973
commit f1376e5a99
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
3 changed files with 261 additions and 326 deletions

View File

@ -2809,25 +2809,14 @@ Script.prototype.isNulldata = function isNulldata(sloppy) {
return true; return true;
} }
if (this.raw.length === 2) { if (this.raw.length === 2)
if (this.raw[1] === opcodes.OP_0) return Script.getSmall(this.raw[1]) !== -1;
return true;
if (this.raw[1] >= opcodes.OP_1 && this.raw[1] <= opcodes.OP_16)
return true;
return false;
}
if (this.raw[1] >= 0x01 && this.raw[1] <= 0x4b) { if (this.raw[1] >= 0x01 && this.raw[1] <= 0x4b)
if (this.raw[1] + 2 !== this.raw.length) return this.raw[1] + 2 === this.raw.length;
return false;
return true;
}
if (this.raw[1] === opcodes.OP_PUSHDATA1) { if (this.raw[1] === opcodes.OP_PUSHDATA1)
if (this.raw[2] + 3 !== this.raw.length) return this.raw[2] > 75 && this.raw[2] + 3 === this.raw.length;
return false;
return true;
}
return false; return false;
}; };

View File

@ -39,23 +39,37 @@ var BufferWriter = require('./writer');
* (default=account key "address"). * (default=account key "address").
*/ */
function Wallet(options) { function Wallet(db, options) {
var master;
if (!(this instanceof Wallet)) if (!(this instanceof Wallet))
return new Wallet(options); return new Wallet(db, options);
EventEmitter.call(this); EventEmitter.call(this);
assert(options, 'Options required.'); assert(db, 'DB required.');
assert(options.db, 'DB required.');
this.options = options; this.db = db;
this.network = bcoin.network.get(options.network); this.network = db.network;
this.db = options.db;
this.locker = new bcoin.locker(this); this.locker = new bcoin.locker(this);
master = options.master; this.id = null;
this.master = null;
this.initialized = false;
this.accountDepth = 0;
this.account = null;
if (options)
this.fromOptions(options);
}
utils.inherits(Wallet, EventEmitter);
Wallet.fromOptions = function fromOptions(db, options) {
return new Wallet(db).fromOptions(options);
};
Wallet.prototype.fromOptions = function fromOptions(options) {
var master = options.master;
if (!master) if (!master)
master = bcoin.hd.fromMnemonic(null, this.network); master = bcoin.hd.fromMnemonic(null, this.network);
@ -66,58 +80,47 @@ function Wallet(options) {
if (!MasterKey.isMasterKey(master)) if (!MasterKey.isMasterKey(master))
master = MasterKey.fromKey(master); master = MasterKey.fromKey(master);
this.id = options.id || null;
this.master = master; this.master = master;
this.initialized = options.initialized || false; this.initialized = options.initialized || false;
this.accountDepth = options.accountDepth || 0; this.accountDepth = options.accountDepth || 0;
this.id = options.id || this.getID();
this.loaded = false; return this;
this.loading = false; };
this.account = null;
if (!this.id) Wallet.prototype.init = function init(options, callback) {
this.id = this.getID(); var self = this;
if (this.options.passphrase) assert(!this.initialized);
this.master.encrypt(this.options.passphrase); this.initialized = true;
}
utils.inherits(Wallet, EventEmitter); if (options.passphrase)
this.master.encrypt(options.passphrase);
/** this.createAccount(options, function(err, account) {
* Open the wallet, register with the database. if (err)
* @param {Function} callback return callback(err);
*/
assert(account);
self.account = account;
return callback();
});
};
Wallet.prototype.open = function open(callback) { Wallet.prototype.open = function open(callback) {
var self = this; var self = this;
assert(this.initialized);
callback = utils.ensure(callback); this.getAccount(0, function(err, account) {
if (err)
if (this.loaded)
return utils.nextTick(callback);
if (this.loading)
return this.once('open', callback);
this.loading = true;
try {
this.db.register(this);
} catch (e) {
this.emit('error', e);
return callback(e);
}
this.init(function(err) {
if (err) {
self.emit('error', err);
return callback(err); return callback(err);
}
self.loading = false; if (!account)
self.loaded = true; return callback(new Error('Default account not found.'));
self.emit('open');
self.account = account;
return callback(); return callback();
}); });
@ -129,15 +132,9 @@ Wallet.prototype.open = function open(callback) {
* @param {Function} callback * @param {Function} callback
*/ */
Wallet.prototype.close =
Wallet.prototype.destroy = function destroy(callback) { Wallet.prototype.destroy = function destroy(callback) {
callback = utils.ensure(callback); callback = utils.ensure(callback);
if (!this.loaded)
return utils.nextTick(callback);
assert(!this.loading);
this.master.destroy(); this.master.destroy();
try { try {
@ -147,8 +144,6 @@ Wallet.prototype.destroy = function destroy(callback) {
return callback(e); return callback(e);
} }
this.loaded = false;
return utils.nextTick(callback); return utils.nextTick(callback);
}; };
@ -160,33 +155,6 @@ Wallet.prototype.destroy = function destroy(callback) {
* @param {Function} callback * @param {Function} callback
*/ */
Wallet.prototype.init = function init(callback) {
var self = this;
function done(err, account) {
if (err)
return callback(err);
if (!account)
return callback(new Error('Account not found.'));
self.account = account;
return callback();
}
this.db.open(function(err) {
if (err)
return callback(err);
if (self.initialized)
return self.getAccount(0, done);
self.initialized = true;
self.createAccount(self.options, done);
});
};
/** /**
* Add a public account key to the wallet (multisig). * Add a public account key to the wallet (multisig).
* Saves the key in the wallet database. * Saves the key in the wallet database.
@ -686,6 +654,34 @@ Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) {
}); });
}; };
Wallet.prototype.getKeyring = function getKeyring(hash, callback) {
var self = this;
var address;
if (!hash)
return callback();
this.getPath(hash, function(err, path) {
if (err)
return callback(err);
if (!path)
return callback();
self.getAccount(path.account, function(err, account) {
if (err)
return callback(err);
if (!account)
return callback();
address = account.deriveAddress(path.change, path.index);
return callback(null, address);
});
});
};
/** /**
* Map input addresses to paths. * Map input addresses to paths.
* @param {TX|Input} tx * @param {TX|Input} tx
@ -1419,14 +1415,13 @@ Wallet.prototype.toJSON = function toJSON() {
* @throws Error on bad decrypt * @throws Error on bad decrypt
*/ */
Wallet.parseJSON = function parseJSON(json) { Wallet.prototype.fromJSON = function fromJSON(json) {
return { this.network = bcoin.network.get(json.network);
network: json.network, this.id = json.id;
id: json.id, this.initialized = json.initialized;
initialized: json.initialized, this.accountDepth = json.accountDepth;
accountDepth: json.accountDepth, this.master = MasterKey.fromJSON(json.master);
master: MasterKey.fromJSON(json.master) return this;
};
}; };
/** /**
@ -1457,21 +1452,14 @@ Wallet.prototype.toRaw = function toRaw(writer) {
* @returns {Object} * @returns {Object}
*/ */
Wallet.parseRaw = function parseRaw(data) { Wallet.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data); var p = new BufferReader(data);
var network = bcoin.network.fromMagic(p.readU32()); this.network = bcoin.network.fromMagic(p.readU32());
var id = p.readVarString('utf8'); this.id = p.readVarString('utf8');
var initialized = p.readU8() === 1; this.initialized = p.readU8() === 1;
var accountDepth = p.readU32(); this.accountDepth = p.readU32();
var master = MasterKey.fromRaw(p.readVarBytes()); this.master = MasterKey.fromRaw(p.readVarBytes());
return this;
return {
network: network.type,
id: id,
initialized: initialized,
accountDepth: accountDepth,
master: master
};
}; };
/** /**
@ -1480,8 +1468,8 @@ Wallet.parseRaw = function parseRaw(data) {
* @returns {Wallet} * @returns {Wallet}
*/ */
Wallet.fromRaw = function fromRaw(data) { Wallet.fromRaw = function fromRaw(db, data) {
return new Wallet(Wallet.parseRaw(data)); return new Wallet(db).fromRaw(data);
}; };
/** /**
@ -1491,8 +1479,8 @@ Wallet.fromRaw = function fromRaw(data) {
* @returns {Wallet} * @returns {Wallet}
*/ */
Wallet.fromJSON = function fromJSON(json) { Wallet.fromJSON = function fromJSON(db, json) {
return new Wallet(Wallet.parseJSON(json)); return new Wallet(db).fromJSON(json);
}; };
/** /**
@ -1531,28 +1519,60 @@ Wallet.isWallet = function isWallet(obj) {
* @param {String?} options.name - Account name * @param {String?} options.name - Account name
*/ */
function Account(options) { function Account(db, options) {
var i; var i;
if (!(this instanceof Account)) if (!(this instanceof Account))
return new Account(options); return new Account(db, options);
EventEmitter.call(this); EventEmitter.call(this);
assert(db, 'Database is required.');
this.db = db;
this.network = db.network;
this.lookahead = Account.LOOKAHEAD;
this.cache = new bcoin.lru(20, 1);
this.loaded = false;
this.loading = false;
this.receiveAddress = null;
this.changeAddress = null;
this.id = null;
this.name = null;
this.witness = false;
this.accountKey = null;
this.accountIndex = 0;
this.receiveDepth = 0;
this.changeDepth = 0;
this.type = null;
this.keys = [];
this.m = 1;
this.n = 1;
this.initialized = false;
if (options)
this.fromOptions(options);
}
utils.inherits(Account, EventEmitter);
Account.fromOptions = function fromOptions(db, options) {
return new Account(db).fromOptions(options);
};
Account.prototype.fromOptions = function fromOptions(options) {
assert(options, 'Options are required.'); assert(options, 'Options are required.');
assert(options.db, 'Database is required.');
assert(options.id, 'Wallet ID is required.'); assert(options.id, 'Wallet ID is required.');
assert(options.accountKey, 'Account key is required.'); assert(options.accountKey, 'Account key is required.');
assert(utils.isNumber(options.accountIndex), 'Account index is required.'); assert(utils.isNumber(options.accountIndex), 'Account index is required.');
this.options = options;
this.network = bcoin.network.get(options.network);
this.db = options.db;
this.lookahead = Account.LOOKAHEAD;
this.id = options.id; this.id = options.id;
this.name = options.name; this.name = options.name;
this.witness = options.witness || false; this.witness = options.witness != null
? options.witness
: this.network.witness;
this.accountKey = options.accountKey; this.accountKey = options.accountKey;
this.accountIndex = options.accountIndex; this.accountIndex = options.accountIndex;
this.receiveDepth = options.receiveDepth || 0; this.receiveDepth = options.receiveDepth || 0;
@ -1563,13 +1583,6 @@ function Account(options) {
this.n = options.n || 1; this.n = options.n || 1;
this.initialized = options.initialized || false; this.initialized = options.initialized || false;
this.loaded = false;
this.loading = false;
this.receiveAddress = null;
this.changeAddress = null;
this.cache = new bcoin.lru(20, 1);
if (this.n > 1) if (this.n > 1)
this.type = 'multisig'; this.type = 'multisig';
@ -1588,9 +1601,9 @@ function Account(options) {
for (i = 0; i < options.keys.length; i++) for (i = 0; i < options.keys.length; i++)
this.pushKey(options.keys[i]); this.pushKey(options.keys[i]);
} }
}
utils.inherits(Account, EventEmitter); return this;
};
/* /*
* Default address lookahead. * Default address lookahead.
@ -1599,58 +1612,6 @@ utils.inherits(Account, EventEmitter);
Account.LOOKAHEAD = 5; Account.LOOKAHEAD = 5;
/**
* Open the account, register with the database.
* @param {Function} callback
*/
Account.prototype.open = function open(callback) {
var self = this;
callback = utils.ensure(callback);
if (this.loaded)
return utils.nextTick(callback);
if (this.loading)
return this.once('open', callback);
this.loading = true;
this.init(function(err) {
if (err) {
self.emit('error', err);
return callback(err);
}
self.loading = false;
self.loaded = true;
self.emit('open');
return callback();
});
};
/**
* Close the account, unregister with the database.
* @method
* @param {Function} callback
*/
Account.prototype.close =
Account.prototype.destroy = function destroy(callback) {
callback = utils.ensure(callback);
if (!this.loaded)
return utils.nextTick(callback);
assert(!this.loading);
this.loaded = false;
return utils.nextTick(callback);
};
/** /**
* Attempt to intialize the account (generating * Attempt to intialize the account (generating
* the first addresses along with the lookahead * the first addresses along with the lookahead
@ -1666,12 +1627,6 @@ Account.prototype.init = function init(callback) {
return this.save(callback); return this.save(callback);
} }
if (this.initialized) {
this.receiveAddress = this.deriveReceive(this.receiveDepth - 1);
this.changeAddress = this.deriveChange(this.changeDepth - 1);
return callback();
}
this.initialized = true; this.initialized = true;
assert(this.receiveDepth === 0); assert(this.receiveDepth === 0);
@ -1680,6 +1635,16 @@ Account.prototype.init = function init(callback) {
this.setDepth(1, 1, callback); this.setDepth(1, 1, callback);
}; };
Account.prototype.open = function open(callback) {
if (!this.initialized)
return callback();
this.receiveAddress = this.deriveReceive(this.receiveDepth - 1);
this.changeAddress = this.deriveChange(this.changeDepth - 1);
return callback();
};
/** /**
* Add a public account key to the account (multisig). * Add a public account key to the account (multisig).
* Does not update the database. * Does not update the database.
@ -1809,6 +1774,7 @@ Account.prototype.addKey = function addKey(key, callback) {
error = e; error = e;
} }
// Try to initialize again.
this.init(function(err) { this.init(function(err) {
if (err) if (err)
return callback(err); return callback(err);
@ -2186,24 +2152,27 @@ Account.prototype.toJSON = function toJSON() {
* @throws Error on bad decrypt * @throws Error on bad decrypt
*/ */
Account.parseJSON = function parseJSON(json) { Account.prototype.fromJSON = function fromJSON(json) {
return { var i;
network: json.network,
id: json.id, this.network = bcoin.network.get(json.network);
name: json.name, this.id = json.id;
initialized: json.initialized, this.name = json.name;
type: json.type, this.initialized = json.initialized;
m: json.m, this.type = json.type;
n: json.n, this.m = json.m;
witness: json.witness, this.n = json.n;
accountIndex: json.accountIndex, this.witness = json.witness;
receiveDepth: json.receiveDepth, this.accountIndex = json.accountIndex;
changeDepth: json.changeDepth, this.receiveDepth = json.receiveDepth;
accountKey: bcoin.hd.fromBase58(json.accountKey), this.changeDepth = json.changeDepth;
keys: json.keys.map(function(key) { this.accountKey = bcoin.hd.fromBase58(json.accountKey);
return bcoin.hd.fromBase58(key); this.keys = [];
})
}; for (i = 0; i < json.keys.length; i++)
this.keys.push(bcoin.hd.fromBase58(key));
return this;
}; };
/** /**
@ -2246,42 +2215,30 @@ Account.prototype.toRaw = function toRaw(writer) {
* @returns {Object} * @returns {Object}
*/ */
Account.parseRaw = function parseRaw(data) { Account.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data); var p = new BufferReader(data);
var network = bcoin.network.fromMagic(p.readU32()); var i, count;
var id = p.readVarString('utf8');
var name = p.readVarString('utf8'); this.network = bcoin.network.fromMagic(p.readU32());
var initialized = p.readU8() === 1; this.id = p.readVarString('utf8');
var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; this.name = p.readVarString('utf8');
var m = p.readU8(); this.initialized = p.readU8() === 1;
var n = p.readU8(); this.type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig';
var witness = p.readU8() === 1; this.m = p.readU8();
var accountIndex = p.readU32(); this.n = p.readU8();
var receiveDepth = p.readU32(); this.witness = p.readU8() === 1;
var changeDepth = p.readU32(); this.accountIndex = p.readU32();
var accountKey = bcoin.hd.fromRaw(p.readBytes(82)); this.receiveDepth = p.readU32();
var count = p.readU8(); this.changeDepth = p.readU32();
var keys = []; this.accountKey = bcoin.hd.fromRaw(p.readBytes(82));
var i; this.keys = [];
count = p.readU8();
for (i = 0; i < count; i++) for (i = 0; i < count; i++)
keys.push(bcoin.hd.fromRaw(p.readBytes(82))); this.keys.push(bcoin.hd.fromRaw(p.readBytes(82)));
return { return this;
network: network.type,
id: id,
name: name,
initialized: initialized,
type: type,
m: m,
n: n,
witness: witness,
accountIndex: accountIndex,
receiveDepth: receiveDepth,
changeDepth: changeDepth,
accountKey: accountKey,
keys: keys
};
}; };
/** /**
@ -2290,8 +2247,8 @@ Account.parseRaw = function parseRaw(data) {
* @returns {Account} * @returns {Account}
*/ */
Account.fromRaw = function fromRaw(data) { Account.fromRaw = function fromRaw(db, data) {
return new Account(Account.parseRaw(data)); return new Account(db).fromRaw(data);
}; };
/** /**
@ -2301,8 +2258,8 @@ Account.fromRaw = function fromRaw(data) {
* @returns {Account} * @returns {Account}
*/ */
Account.fromJSON = function fromJSON(json) { Account.fromJSON = function fromJSON(db, json) {
return new Account(Account.parseJSON(json)); return new Account(db).fromJSON(json);
}; };
/** /**

View File

@ -45,7 +45,7 @@ function WalletDB(options) {
EventEmitter.call(this); EventEmitter.call(this);
this.watchers = []; this.watchers = {};
this.options = options; this.options = options;
this.loaded = false; this.loaded = false;
this.network = bcoin.network.get(options.network); this.network = bcoin.network.get(options.network);
@ -249,19 +249,18 @@ WalletDB.prototype.destroy = function destroy(callback) {
}; };
/** /**
* Register a wallet with the walletdb. * Register an object with the walletdb.
* @param {WalletID} id * @param {Object} object
* @param {Wallet} wallet
*/ */
WalletDB.prototype.register = function register(wallet) { WalletDB.prototype.register = function register(object) {
var id = wallet.id; var id = object.id;
if (!this.watchers[id]) if (!this.watchers[id])
this.watchers[id] = { wallet: wallet, refs: 0 }; this.watchers[id] = { object: object, refs: 0 };
// Should never happen, and if it does, I will cry. // Should never happen, and if it does, I will cry.
assert(this.watchers[id].wallet === wallet, 'I\'m crying.'); assert(this.watchers[id].object === object, 'I\'m crying.');
// We do some reference counting here // We do some reference counting here
// because we're thug like that (police // because we're thug like that (police
@ -270,27 +269,41 @@ WalletDB.prototype.register = function register(wallet) {
}; };
/** /**
* Unregister a wallet with the walletdb. * Unregister a object with the walletdb.
* @param {WalletID} id * @param {Object} object
* @param {Wallet} wallet
*/ */
WalletDB.prototype.unregister = function unregister(wallet) { WalletDB.prototype.unregister = function unregister(object) {
var id = wallet.id; var id = object.id;
var watcher = this.watchers[id]; var watcher = this.watchers[id];
if (!watcher) if (!watcher)
return; return;
assert(watcher.wallet === wallet); assert(watcher.object === object);
assert(watcher.refs !== 0, '`wallet.destroy()` called twice!'); assert(watcher.refs !== 0, '`destroy()` called twice!');
if (--watcher.refs === 0) if (--watcher.refs === 0)
delete this.watchers[id]; delete this.watchers[id];
}; };
/** /**
* Fire an event for a registered wallet. * Watch an object (increment reference count).
* @param {Object} object
*/
WalletDB.prototype.watch = function watch(object) {
var id = object.id;
var watcher = this.watchers[id];
if (!watcher)
return;
watcher.refs++;
};
/**
* Fire an event for a registered object.
* @param {WalletID} id * @param {WalletID} id
* @param {...Object} args * @param {...Object} args
*/ */
@ -307,11 +320,11 @@ WalletDB.prototype.fire = function fire(id) {
for (i = 1; i < arguments.length; i++) for (i = 1; i < arguments.length; i++)
args[i - 1] = arguments[i]; args[i - 1] = arguments[i];
watcher.wallet.emit.apply(watcher.wallet, args); watcher.object.emit.apply(watcher.object, args);
}; };
/** /**
* Test for a listener on a registered wallet. * Test for a listener on a registered object.
* @param {WalletID} id * @param {WalletID} id
* @param {String} event * @param {String} event
* @returns {Boolean} * @returns {Boolean}
@ -323,7 +336,7 @@ WalletDB.prototype.hasListener = function hasListener(id, event) {
if (!watcher) if (!watcher)
return false; return false;
if (watcher.wallet.listeners(event).length !== 0) if (watcher.object.listeners(event).length !== 0)
return true; return true;
return false; return false;
@ -337,14 +350,16 @@ WalletDB.prototype.hasListener = function hasListener(id, event) {
WalletDB.prototype.get = function get(id, callback) { WalletDB.prototype.get = function get(id, callback) {
var self = this; var self = this;
var wallet; var watcher, wallet;
if (!id) if (!id)
return callback(); return callback();
if (this.watchers[id]) { watcher = this.watchers[id];
this.watchers[id].refs++;
return callback(null, this.watchers[id].wallet); if (watcher) {
watcher.refs++;
return callback(null, watcher.object);
} }
this.db.get('w/' + id, function(err, data) { this.db.get('w/' + id, function(err, data) {
@ -355,9 +370,13 @@ WalletDB.prototype.get = function get(id, callback) {
return callback(); return callback();
try { try {
data = bcoin.wallet.parseRaw(data); wallet = bcoin.wallet.fromRaw(self, data);
data.db = self; } catch (e) {
wallet = new bcoin.wallet(data); return callback(e);
}
try {
self.register(wallet);
} catch (e) { } catch (e) {
return callback(e); return callback(e);
} }
@ -371,33 +390,6 @@ WalletDB.prototype.get = function get(id, callback) {
}); });
}; };
/**
* Get raw wallet from the database, do not instantiate.
* @param {WalletID} id
* @param {Function} callback - Returns [Error, {@link Wallet}].
*/
WalletDB.prototype.getRaw = function getRaw(id, callback) {
if (!id)
return callback();
this.db.get('w/' + id, function(err, data) {
if (err && err.type !== 'NotFoundError')
return callback();
if (!data)
return callback();
try {
data = bcoin.wallet.parseRaw(data);
} catch (e) {
return callback(e);
}
return callback(null, data);
});
};
/** /**
* Save a wallet to the database. * Save a wallet to the database.
* @param {Wallet} wallet * @param {Wallet} wallet
@ -436,13 +428,19 @@ WalletDB.prototype.create = function create(options, callback) {
if (exists) if (exists)
return callback(new Error('Wallet already exists.')); return callback(new Error('Wallet already exists.'));
options = utils.merge({}, options); try {
wallet = bcoin.wallet.fromOptions(self, options);
} catch (e) {
return callback(e);
}
options.network = self.network; try {
options.db = self; self.register(wallet);
wallet = new bcoin.wallet(options); } catch (e) {
return callback(e);
}
wallet.open(function(err) { wallet.init(options, function(err) {
if (err) if (err)
return callback(err); return callback(err);
@ -510,9 +508,7 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) {
return callback(); return callback();
try { try {
data = bcoin.account.parseRaw(data); account = bcoin.account.fromRaw(self, data);
data.db = self;
account = new bcoin.account(data);
} catch (e) { } catch (e) {
return callback(e); return callback(e);
} }
@ -620,16 +616,9 @@ WalletDB.prototype.createAccount = function createAccount(options, callback) {
if (exists) if (exists)
return callback(new Error('Account already exists.')); return callback(new Error('Account already exists.'));
options = utils.merge({}, options); account = bcoin.account.fromOptions(self, options);
if (self.network.witness) account.init(function(err) {
options.witness = options.witness !== false;
options.network = self.network;
options.db = self;
account = new bcoin.account(options);
account.open(function(err) {
if (err) if (err)
return callback(err); return callback(err);