From f1376e5a99a27b8d8acc32eb6cffd48df499c0de Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 24 Jun 2016 15:12:47 -0700 Subject: [PATCH] wallet refactor. --- lib/bcoin/script.js | 23 +-- lib/bcoin/wallet.js | 435 +++++++++++++++++++----------------------- lib/bcoin/walletdb.js | 129 ++++++------- 3 files changed, 261 insertions(+), 326 deletions(-) diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index eee2c0d7..a5291dc7 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -2809,25 +2809,14 @@ Script.prototype.isNulldata = function isNulldata(sloppy) { return true; } - if (this.raw.length === 2) { - if (this.raw[1] === opcodes.OP_0) - return true; - if (this.raw[1] >= opcodes.OP_1 && this.raw[1] <= opcodes.OP_16) - return true; - return false; - } + if (this.raw.length === 2) + return Script.getSmall(this.raw[1]) !== -1; - if (this.raw[1] >= 0x01 && this.raw[1] <= 0x4b) { - if (this.raw[1] + 2 !== this.raw.length) - return false; - return true; - } + if (this.raw[1] >= 0x01 && this.raw[1] <= 0x4b) + return this.raw[1] + 2 === this.raw.length; - if (this.raw[1] === opcodes.OP_PUSHDATA1) { - if (this.raw[2] + 3 !== this.raw.length) - return false; - return true; - } + if (this.raw[1] === opcodes.OP_PUSHDATA1) + return this.raw[2] > 75 && this.raw[2] + 3 === this.raw.length; return false; }; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index d7c45787..5f168595 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -39,23 +39,37 @@ var BufferWriter = require('./writer'); * (default=account key "address"). */ -function Wallet(options) { - var master; - +function Wallet(db, options) { if (!(this instanceof Wallet)) - return new Wallet(options); + return new Wallet(db, options); EventEmitter.call(this); - assert(options, 'Options required.'); - assert(options.db, 'DB required.'); + assert(db, 'DB required.'); - this.options = options; - this.network = bcoin.network.get(options.network); - this.db = options.db; + this.db = db; + this.network = db.network; 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) master = bcoin.hd.fromMnemonic(null, this.network); @@ -66,58 +80,47 @@ function Wallet(options) { if (!MasterKey.isMasterKey(master)) master = MasterKey.fromKey(master); - this.id = options.id || null; this.master = master; this.initialized = options.initialized || false; this.accountDepth = options.accountDepth || 0; + this.id = options.id || this.getID(); - this.loaded = false; - this.loading = false; - this.account = null; + return this; +}; - if (!this.id) - this.id = this.getID(); +Wallet.prototype.init = function init(options, callback) { + var self = this; - if (this.options.passphrase) - this.master.encrypt(this.options.passphrase); -} + assert(!this.initialized); + this.initialized = true; -utils.inherits(Wallet, EventEmitter); + if (options.passphrase) + this.master.encrypt(options.passphrase); -/** - * Open the wallet, register with the database. - * @param {Function} callback - */ + this.createAccount(options, function(err, account) { + if (err) + return callback(err); + + assert(account); + + self.account = account; + + return callback(); + }); +}; Wallet.prototype.open = function open(callback) { var self = this; + assert(this.initialized); - callback = utils.ensure(callback); - - 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); + this.getAccount(0, function(err, account) { + if (err) return callback(err); - } - self.loading = false; - self.loaded = true; - self.emit('open'); + if (!account) + return callback(new Error('Default account not found.')); + + self.account = account; return callback(); }); @@ -129,15 +132,9 @@ Wallet.prototype.open = function open(callback) { * @param {Function} callback */ -Wallet.prototype.close = Wallet.prototype.destroy = function destroy(callback) { callback = utils.ensure(callback); - if (!this.loaded) - return utils.nextTick(callback); - - assert(!this.loading); - this.master.destroy(); try { @@ -147,8 +144,6 @@ Wallet.prototype.destroy = function destroy(callback) { return callback(e); } - this.loaded = false; - return utils.nextTick(callback); }; @@ -160,33 +155,6 @@ Wallet.prototype.destroy = function destroy(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). * 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. * @param {TX|Input} tx @@ -1419,14 +1415,13 @@ Wallet.prototype.toJSON = function toJSON() { * @throws Error on bad decrypt */ -Wallet.parseJSON = function parseJSON(json) { - return { - network: json.network, - id: json.id, - initialized: json.initialized, - accountDepth: json.accountDepth, - master: MasterKey.fromJSON(json.master) - }; +Wallet.prototype.fromJSON = function fromJSON(json) { + this.network = bcoin.network.get(json.network); + this.id = json.id; + this.initialized = json.initialized; + this.accountDepth = json.accountDepth; + this.master = MasterKey.fromJSON(json.master); + return this; }; /** @@ -1457,21 +1452,14 @@ Wallet.prototype.toRaw = function toRaw(writer) { * @returns {Object} */ -Wallet.parseRaw = function parseRaw(data) { +Wallet.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - var network = bcoin.network.fromMagic(p.readU32()); - var id = p.readVarString('utf8'); - var initialized = p.readU8() === 1; - var accountDepth = p.readU32(); - var master = MasterKey.fromRaw(p.readVarBytes()); - - return { - network: network.type, - id: id, - initialized: initialized, - accountDepth: accountDepth, - master: master - }; + this.network = bcoin.network.fromMagic(p.readU32()); + this.id = p.readVarString('utf8'); + this.initialized = p.readU8() === 1; + this.accountDepth = p.readU32(); + this.master = MasterKey.fromRaw(p.readVarBytes()); + return this; }; /** @@ -1480,8 +1468,8 @@ Wallet.parseRaw = function parseRaw(data) { * @returns {Wallet} */ -Wallet.fromRaw = function fromRaw(data) { - return new Wallet(Wallet.parseRaw(data)); +Wallet.fromRaw = function fromRaw(db, data) { + return new Wallet(db).fromRaw(data); }; /** @@ -1491,8 +1479,8 @@ Wallet.fromRaw = function fromRaw(data) { * @returns {Wallet} */ -Wallet.fromJSON = function fromJSON(json) { - return new Wallet(Wallet.parseJSON(json)); +Wallet.fromJSON = function fromJSON(db, json) { + return new Wallet(db).fromJSON(json); }; /** @@ -1531,28 +1519,60 @@ Wallet.isWallet = function isWallet(obj) { * @param {String?} options.name - Account name */ -function Account(options) { +function Account(db, options) { var i; if (!(this instanceof Account)) - return new Account(options); + return new Account(db, options); 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.db, 'Database is required.'); assert(options.id, 'Wallet ID is required.'); assert(options.accountKey, 'Account key 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.name = options.name; - this.witness = options.witness || false; + this.witness = options.witness != null + ? options.witness + : this.network.witness; this.accountKey = options.accountKey; this.accountIndex = options.accountIndex; this.receiveDepth = options.receiveDepth || 0; @@ -1563,13 +1583,6 @@ function Account(options) { this.n = options.n || 1; 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) this.type = 'multisig'; @@ -1588,9 +1601,9 @@ function Account(options) { for (i = 0; i < options.keys.length; i++) this.pushKey(options.keys[i]); } -} -utils.inherits(Account, EventEmitter); + return this; +}; /* * Default address lookahead. @@ -1599,58 +1612,6 @@ utils.inherits(Account, EventEmitter); 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 * the first addresses along with the lookahead @@ -1666,12 +1627,6 @@ Account.prototype.init = function init(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; assert(this.receiveDepth === 0); @@ -1680,6 +1635,16 @@ Account.prototype.init = function init(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). * Does not update the database. @@ -1809,6 +1774,7 @@ Account.prototype.addKey = function addKey(key, callback) { error = e; } + // Try to initialize again. this.init(function(err) { if (err) return callback(err); @@ -2186,24 +2152,27 @@ Account.prototype.toJSON = function toJSON() { * @throws Error on bad decrypt */ -Account.parseJSON = function parseJSON(json) { - return { - network: json.network, - id: json.id, - name: json.name, - initialized: json.initialized, - type: json.type, - m: json.m, - n: json.n, - witness: json.witness, - accountIndex: json.accountIndex, - receiveDepth: json.receiveDepth, - changeDepth: json.changeDepth, - accountKey: bcoin.hd.fromBase58(json.accountKey), - keys: json.keys.map(function(key) { - return bcoin.hd.fromBase58(key); - }) - }; +Account.prototype.fromJSON = function fromJSON(json) { + var i; + + this.network = bcoin.network.get(json.network); + this.id = json.id; + this.name = json.name; + this.initialized = json.initialized; + this.type = json.type; + this.m = json.m; + this.n = json.n; + this.witness = json.witness; + this.accountIndex = json.accountIndex; + this.receiveDepth = json.receiveDepth; + this.changeDepth = json.changeDepth; + this.accountKey = bcoin.hd.fromBase58(json.accountKey); + 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} */ -Account.parseRaw = function parseRaw(data) { +Account.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - var network = bcoin.network.fromMagic(p.readU32()); - var id = p.readVarString('utf8'); - var name = p.readVarString('utf8'); - var initialized = p.readU8() === 1; - 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 accountKey = bcoin.hd.fromRaw(p.readBytes(82)); - var count = p.readU8(); - var keys = []; - var i; + var i, count; + + this.network = bcoin.network.fromMagic(p.readU32()); + this.id = p.readVarString('utf8'); + this.name = p.readVarString('utf8'); + this.initialized = p.readU8() === 1; + this.type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; + this.m = p.readU8(); + this.n = p.readU8(); + this.witness = p.readU8() === 1; + this.accountIndex = p.readU32(); + this.receiveDepth = p.readU32(); + this.changeDepth = p.readU32(); + this.accountKey = bcoin.hd.fromRaw(p.readBytes(82)); + this.keys = []; + + count = p.readU8(); for (i = 0; i < count; i++) - keys.push(bcoin.hd.fromRaw(p.readBytes(82))); + this.keys.push(bcoin.hd.fromRaw(p.readBytes(82))); - return { - 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 - }; + return this; }; /** @@ -2290,8 +2247,8 @@ Account.parseRaw = function parseRaw(data) { * @returns {Account} */ -Account.fromRaw = function fromRaw(data) { - return new Account(Account.parseRaw(data)); +Account.fromRaw = function fromRaw(db, data) { + return new Account(db).fromRaw(data); }; /** @@ -2301,8 +2258,8 @@ Account.fromRaw = function fromRaw(data) { * @returns {Account} */ -Account.fromJSON = function fromJSON(json) { - return new Account(Account.parseJSON(json)); +Account.fromJSON = function fromJSON(db, json) { + return new Account(db).fromJSON(json); }; /** diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index 38afaa5e..58796099 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -45,7 +45,7 @@ function WalletDB(options) { EventEmitter.call(this); - this.watchers = []; + this.watchers = {}; this.options = options; this.loaded = false; this.network = bcoin.network.get(options.network); @@ -249,19 +249,18 @@ WalletDB.prototype.destroy = function destroy(callback) { }; /** - * Register a wallet with the walletdb. - * @param {WalletID} id - * @param {Wallet} wallet + * Register an object with the walletdb. + * @param {Object} object */ -WalletDB.prototype.register = function register(wallet) { - var id = wallet.id; +WalletDB.prototype.register = function register(object) { + var id = object.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. - assert(this.watchers[id].wallet === wallet, 'I\'m crying.'); + assert(this.watchers[id].object === object, 'I\'m crying.'); // We do some reference counting here // because we're thug like that (police @@ -270,27 +269,41 @@ WalletDB.prototype.register = function register(wallet) { }; /** - * Unregister a wallet with the walletdb. - * @param {WalletID} id - * @param {Wallet} wallet + * Unregister a object with the walletdb. + * @param {Object} object */ -WalletDB.prototype.unregister = function unregister(wallet) { - var id = wallet.id; +WalletDB.prototype.unregister = function unregister(object) { + var id = object.id; var watcher = this.watchers[id]; if (!watcher) return; - assert(watcher.wallet === wallet); - assert(watcher.refs !== 0, '`wallet.destroy()` called twice!'); + assert(watcher.object === object); + assert(watcher.refs !== 0, '`destroy()` called twice!'); if (--watcher.refs === 0) 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 {...Object} args */ @@ -307,11 +320,11 @@ WalletDB.prototype.fire = function fire(id) { for (i = 1; i < arguments.length; 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 {String} event * @returns {Boolean} @@ -323,7 +336,7 @@ WalletDB.prototype.hasListener = function hasListener(id, event) { if (!watcher) return false; - if (watcher.wallet.listeners(event).length !== 0) + if (watcher.object.listeners(event).length !== 0) return true; return false; @@ -337,14 +350,16 @@ WalletDB.prototype.hasListener = function hasListener(id, event) { WalletDB.prototype.get = function get(id, callback) { var self = this; - var wallet; + var watcher, wallet; if (!id) return callback(); - if (this.watchers[id]) { - this.watchers[id].refs++; - return callback(null, this.watchers[id].wallet); + watcher = this.watchers[id]; + + if (watcher) { + watcher.refs++; + return callback(null, watcher.object); } this.db.get('w/' + id, function(err, data) { @@ -355,9 +370,13 @@ WalletDB.prototype.get = function get(id, callback) { return callback(); try { - data = bcoin.wallet.parseRaw(data); - data.db = self; - wallet = new bcoin.wallet(data); + wallet = bcoin.wallet.fromRaw(self, data); + } catch (e) { + return callback(e); + } + + try { + self.register(wallet); } catch (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. * @param {Wallet} wallet @@ -436,13 +428,19 @@ WalletDB.prototype.create = function create(options, callback) { if (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; - options.db = self; - wallet = new bcoin.wallet(options); + try { + self.register(wallet); + } catch (e) { + return callback(e); + } - wallet.open(function(err) { + wallet.init(options, function(err) { if (err) return callback(err); @@ -510,9 +508,7 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) { return callback(); try { - data = bcoin.account.parseRaw(data); - data.db = self; - account = new bcoin.account(data); + account = bcoin.account.fromRaw(self, data); } catch (e) { return callback(e); } @@ -620,16 +616,9 @@ WalletDB.prototype.createAccount = function createAccount(options, callback) { if (exists) return callback(new Error('Account already exists.')); - options = utils.merge({}, options); + account = bcoin.account.fromOptions(self, options); - if (self.network.witness) - options.witness = options.witness !== false; - - options.network = self.network; - options.db = self; - account = new bcoin.account(options); - - account.open(function(err) { + account.init(function(err) { if (err) return callback(err);