diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index c67b9c59..fd51c9bc 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -126,8 +126,12 @@ Address.prototype.toScript = function toScript() { * @returns {Base58String} */ -Address.prototype.toString = function toString() { - return this.toBase58(); +Address.prototype.toString = function toString(enc) { + if (enc === 'hex') + return this.getHash('hex'); + if (enc === 'base58') + enc = null; + return this.toBase58(enc); }; /** diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index bcbe41f1..45ee4ef7 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -175,7 +175,7 @@ Mnemonic.prototype.toSeed = function toSeed() { if (!this.phrase) this.phrase = this.createMnemonic(); - this.seed = utils.pbkdf2( + this.seed = utils.pbkdf2Sync( nfkd(this.phrase), nfkd('mnemonic' + this.passphrase), 2048, 64, 'sha512'); @@ -1083,9 +1083,9 @@ HDPrivateKey.prototype.fromBase58 = function fromBase58(xkey) { HDPrivateKey.prototype.fromRaw = function fromRaw(raw) { var p = new BufferReader(raw); - var i, type, prefix; + var i, version, type, prefix; - this.version = p.readU32BE(); + version = p.readU32BE(); this.depth = p.readU8(); this.parentFingerPrint = p.readBytes(4); this.childIndex = p.readU32BE(); @@ -1097,7 +1097,7 @@ HDPrivateKey.prototype.fromRaw = function fromRaw(raw) { for (i = 0; i < network.types.length; i++) { type = network.types[i]; prefix = network[type].prefixes.xprivkey; - if (this.version === prefix) + if (version === prefix) break; } @@ -1616,9 +1616,9 @@ HDPublicKey.prototype.fromBase58 = function fromBase58(xkey) { HDPublicKey.prototype.fromRaw = function fromRaw(raw) { var p = new BufferReader(raw); - var i, type, prefix; + var i, version, type, prefix; - this.version = p.readU32BE(); + version = p.readU32BE(); this.depth = p.readU8(); this.parentFingerPrint = p.readBytes(4); this.childIndex = p.readU32BE(); @@ -1629,7 +1629,7 @@ HDPublicKey.prototype.fromRaw = function fromRaw(raw) { for (i = 0; i < network.types.length; i++) { type = network.types[i]; prefix = network[type].prefixes.xpubkey; - if (this.version === prefix) + if (version === prefix) break; } diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index 3a673ab2..57a0f4d7 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -776,7 +776,7 @@ MTX.prototype.maxSize = function maxSize(options, force) { options = {}; if (options instanceof bcoin.wallet) - options = { wallet: options, m: options.m, n: options.n }; + options = { wallet: options }; if (options.wallet) wallet = options.wallet; @@ -791,6 +791,7 @@ MTX.prototype.maxSize = function maxSize(options, force) { if (!wallet) return; + // Hack address = wallet.receiveAddress; if (address.program && hash.length === 20) diff --git a/lib/bcoin/scrypt-async.js b/lib/bcoin/scrypt-async.js index 7ab38242..207854a0 100644 --- a/lib/bcoin/scrypt-async.js +++ b/lib/bcoin/scrypt-async.js @@ -33,7 +33,6 @@ 'use strict'; -var crypto = require('crypto'); var utils = require('./utils'); /** @@ -70,7 +69,7 @@ function scrypt(passwd, salt, N, r, p, len, callback) { XY = new Buffer(256 * r); V = new Buffer(128 * r * N); - crypto.pbkdf2(passwd, salt, 1, p * 128 * r, 'sha256', function(err, B) { + utils.pbkdf2(passwd, salt, 1, p * 128 * r, 'sha256', function(err, B) { if (err) return callback(err); @@ -80,7 +79,7 @@ function scrypt(passwd, salt, N, r, p, len, callback) { if (err) return callback(err); - crypto.pbkdf2(passwd, B, 1, len, 'sha256', callback); + utils.pbkdf2(passwd, B, 1, len, 'sha256', callback); }); }); } diff --git a/lib/bcoin/scrypt.js b/lib/bcoin/scrypt.js index b6e40871..20f38941 100644 --- a/lib/bcoin/scrypt.js +++ b/lib/bcoin/scrypt.js @@ -69,12 +69,12 @@ function scrypt(passwd, salt, N, r, p, len) { XY = new Buffer(256 * r); V = new Buffer(128 * r * N); - B = utils.pbkdf2(passwd, salt, 1, p * 128 * r, 'sha256'); + B = utils.pbkdf2Sync(passwd, salt, 1, p * 128 * r, 'sha256'); for (i = 0; i < p; i++) smix(B, i * 128 * r, r, N, V, XY); - return utils.pbkdf2(passwd, B, 1, len, 'sha256'); + return utils.pbkdf2Sync(passwd, B, 1, len, 'sha256'); } function salsa20_8(B) { diff --git a/lib/bcoin/txdb.js b/lib/bcoin/txdb.js index cdb9742b..44ce9383 100644 --- a/lib/bcoin/txdb.js +++ b/lib/bcoin/txdb.js @@ -201,15 +201,61 @@ WalletMap.prototype.getPaths = function getPaths(address) { return this.table[address]; }; +WalletMap.prototype.toJSON = function toJSON() { + return { + inputs: this.inputs.map(function(input) { + return input.toJSON(); + }), + outputs: this.outputs.map(function(output) { + return output.toJSON(); + }), + paths: this.paths.map(function(path) { + return path.toJSON(); + }), + accounts: this.accounts.map(function(path) { + return { + id: path.id, + name: path.name, + account: path.account + }; + }), + wallets: this.wallets + }; +}; + function MapMember() { this.value = 0; - this.hash = null; - this.wallets = []; + this.address = null; this.paths = []; + this.accounts = []; + this.wallets = []; } +MapMember.prototype.toJSON = function toJSON() { + return { + value: utils.btc(this.value), + address: this.address + ? this.address.toBase58() + : null, + hash: this.address + ? this.address.getHash('hex') + : null, + paths: this.paths.map(function(path) { + return path.toJSON(); + }), + accounts: this.accounts.map(function(path) { + return { + id: path.id, + name: path.name, + account: path.account + }; + }), + wallets: this.wallets + }; +}; + MapMember.fromMember = function fromMember(table, io) { - var hash = io.getHash('hex'); + var address = io.getAddress(); var member = new MapMember(); var i, paths; @@ -217,19 +263,20 @@ MapMember.fromMember = function fromMember(table, io) { ? io.coin.value : io.value || 0; - if (!hash) + if (!address) return member; - paths = table[hash]; + paths = table[address.getHash('hex')]; assert(paths); - member.hash = hash; + member.address = address; member.paths = paths; for (i = 0; i < paths.length; i++) member.wallets.push(paths[i].id); + member.accounts = uniq(member.paths); member.wallets = utils.uniq(member.wallets); return member; @@ -258,7 +305,7 @@ TXDB.prototype.getMap = function getMap(tx, callback) { map = WalletMap.fromTX(table, tx); - utils.print(map); + utils.print(map.toJSON()); return callback(null, map); }); @@ -276,7 +323,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) { var count = 0; var i, keys, values; - return utils.forEachSerial(address, function(address, next) { + utils.forEachSerial(address, function(address, next) { self.walletdb.getAddress(address, function(err, paths) { if (err) return next(err); diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index a33c6620..db28be04 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -358,7 +358,7 @@ utils.hmac = function hmac(alg, data, salt) { * @returns {Buffer} */ -utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { +utils.pbkdf2Sync = function pbkdf2Sync(key, salt, iter, len, alg) { if (typeof key === 'string') key = new Buffer(key, 'utf8'); @@ -381,7 +381,7 @@ utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { * @param {Function} callback */ -utils.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) { +utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg, callback) { var result; if (typeof key === 'string') @@ -403,13 +403,13 @@ utils.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) { }; /** - * Derive a key using pbkdf2 with 25,000 iterations. + * Derive a key using pbkdf2 with 50,000 iterations. * @param {Buffer|String} passphrase * @param {Function} callback */ utils.derive = function derive(passphrase, callback) { - utils.pbkdf2Async(passphrase, 'bcoin', 25000, 32, 'sha256', callback); + utils.pbkdf2(passphrase, 'bcoin', 50000, 32, 'sha256', callback); }; /** @@ -423,6 +423,7 @@ utils.derive = function derive(passphrase, callback) { utils.encrypt = function encrypt(data, passphrase, iv, callback) { assert(Buffer.isBuffer(data)); assert(passphrase, 'No passphrase.'); + assert(Buffer.isBuffer(iv)); utils.derive(passphrase, function(err, key) { if (err) @@ -474,6 +475,7 @@ utils.encipher = function encipher(data, key, iv) { utils.decrypt = function decrypt(data, passphrase, iv, callback) { assert(Buffer.isBuffer(data)); assert(passphrase, 'No passphrase.'); + assert(Buffer.isBuffer(iv)); utils.derive(passphrase, function(err, key) { if (err) diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index ffd2b828..6207338e 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -295,7 +295,7 @@ Wallet.prototype.lock = function lock() { */ Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) { - this.master.toKey(passphrase, timeout, callback); + this.master.unlock(passphrase, timeout, callback); }; /** @@ -340,7 +340,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force callback = utils.wrap(callback, unlock); - this.master.toKey(options.passphrase, options.timeout, function(err, master) { + this.unlock(options.passphrase, options.timeout, function(err, master) { if (err) return callback(err); @@ -918,19 +918,26 @@ Wallet.prototype.getRedeem = function getRedeem(hash, callback) { /** * Scan for active accounts and addresses. Used for importing a wallet. - * @param {Function} getByAddress - Must be a function which accepts + * @param {Function} scanner - Must be a function which accepts * a {@link Base58Address} as well as a callback and returns * transactions by address. * @param {Function} callback - Return [Error, Number] (total number * of addresses allocated). */ -Wallet.prototype.scan = function scan(getByAddress, callback) { +Wallet.prototype.scan = function scan(maxGap, scanner, callback) { var self = this; var total = 0; + var index = 0; var unlock; - unlock = this.locker.lock(scan, [getByAddress, callback]); + if (typeof maxGap === 'function') { + callback = scanner; + scanner = maxGap; + maxGap = null; + } + + unlock = this.locker.lock(scan, [maxGap, scanner, callback]); if (!unlock) return; @@ -940,22 +947,24 @@ Wallet.prototype.scan = function scan(getByAddress, callback) { if (!this.initialized) return callback(new Error('Wallet is not initialized.')); - (function next(err, account) { - if (err) - return callback(err); - - account.scan(getByAddress, function(err, result) { + (function next() { + self.getAccount(index++, function(err, account) { if (err) return callback(err); - if (result === 0) + if (!account) return callback(null, total); - total += result; + account.scan(maxGap, scanner, function(err, result) { + if (err) + return callback(err); - self.createAccount(self.options, next, true); - }); - })(null, this.account); + total += result; + + next(); + }); + }, true); + })(); }; /** @@ -1012,7 +1021,7 @@ Wallet.prototype.sign = function sign(tx, options, callback) { if (err) return callback(err); - self.master.toKey(options.passphrase, options.timeout, function(err, master) { + self.unlock(options.passphrase, options.timeout, function(err, master) { if (err) return callback(err); @@ -1545,7 +1554,7 @@ function Account(db, options) { this.db = db; this.network = db.network; - this.lookahead = Account.LOOKAHEAD; + this.lookahead = Account.MAX_LOOKAHEAD; this.loaded = false; this.loading = false; @@ -1636,7 +1645,14 @@ Account.fromOptions = function fromOptions(db, options) { * @const {Number} */ -Account.LOOKAHEAD = 5; +Account.MAX_LOOKAHEAD = 5; + +/* + * Default address gap for scanning. + * @const {Number} + */ + +Account.MAX_GAP = 20; /** * Attempt to intialize the account (generating @@ -2040,16 +2056,25 @@ Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callba /** * Scan for addresses. - * @param {Function} getByAddress - Must be a callback which accepts + * @param {Function} scanner - Must be a callback which accepts * a callback and returns transactions by address. * @param {Function} callback - Return [Error, Number] (total number * of addresses allocated). */ -Account.prototype.scan = function scan(getByAddress, callback) { +Account.prototype.scan = function scan(maxGap, scanner, callback) { var self = this; var total = 0; + if (typeof maxGap === 'function') { + callback = scanner; + scanner = maxGap; + maxGap = null; + } + + if (maxGap == null) + maxGap = Account.MAX_GAP; + if (!this.initialized) return callback(new Error('Account is not initialized.')); @@ -2068,37 +2093,63 @@ Account.prototype.scan = function scan(getByAddress, callback) { } (function chainCheck(change) { - var address = change ? self.changeAddress : self.receiveAddress; + var depth = change ? self.changeDepth : self.receiveDepth; + var index = 0; var gap = 0; - (function next(err, address) { - if (err) - return callback(err); + function createAddress(callback) { + if (index === depth) + return self.createAddress(change, callback); + return callback(null, self.deriveAddress(change, index++)); + } - getByAddress(address.getAddress(), function(err, txs) { + (function next() { + createAddress(function(err, address) { if (err) return callback(err); - addTX(txs, function(err, result) { + scanner(address.getAddress(), function(err, txs) { if (err) return callback(err); - if (result) { - total++; - gap = 0; - return self.createAddress(change, next); - } + addTX(txs, function(err, result) { + if (err) + return callback(err); - if (++gap < 20) - return self.createAddress(change, next); + // Special case for maxGap=0 + if (maxGap === 0 && index === depth) { + if (!change) + return chainCheck(true); + return callback(null, total); + } - if (!change) - return chainCheck(true); + if (result) { + total++; + gap = 0; + return next(); + } - return callback(null, total); + if (++gap < Account.MAX_GAP) + return next(); + + if (!change) { + self.receiveDepth = Math.max(depth, self.receiveDepth - gap); + self.receiveAddress = self.deriveReceive(self.receiveDepth - 1); + return chainCheck(true); + } + + self.changeDepth = Math.max(depth, self.changeDepth - gap); + self.changeAddress = self.deriveChange(self.changeDepth - 1); + + self.save(function(err) { + if (err) + return callback(err); + return callback(null, total); + }); + }); }); }); - })(null, address); + })(); })(false); }; @@ -2353,11 +2404,11 @@ MasterKey.fromOptions = function fromOptions(options) { * @returns {HDPrivateKey} */ -MasterKey.prototype.toKey = function toKey(passphrase, timeout, callback) { +MasterKey.prototype.unlock = function _unlock(passphrase, timeout, callback) { var self = this; var unlock; - unlock = this.locker.lock(toKey, [passphrase, timeout, callback]); + unlock = this.locker.lock(_unlock, [passphrase, timeout, callback]); if (!unlock) return; @@ -2538,6 +2589,16 @@ MasterKey.prototype.toRaw = function toRaw(writer) { p.writeVarBytes(this.iv); p.writeVarBytes(this.ciphertext); + // Future-proofing: + // algorithm (0=pbkdf2, 1=scrypt) + p.writeU8(0); + // iterations (pbkdf2) / N (scrypt) + p.writeU32(50000); + // r (scrypt) + p.writeU32(0); + // p (scrypt) + p.writeU32(0); + if (!writer) p = p.render(); diff --git a/lib/bcoin/walletdb.js b/lib/bcoin/walletdb.js index c8f71119..35a6952c 100644 --- a/lib/bcoin/walletdb.js +++ b/lib/bcoin/walletdb.js @@ -1211,6 +1211,16 @@ Path.prototype.inspect = function() { + '>'; }; +Path.prototype.toJSON = function toJSON() { + return { + id: this.id, + name: this.name, + account: this.account, + change: this.change, + index: this.index + }; +}; + Path.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); diff --git a/test/wallet-test.js b/test/wallet-test.js index 9d5b4391..9b4a77dd 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -873,6 +873,7 @@ describe('Wallet', function() { assert.ifError(err); w1.master.stop(); w1.master.key = null; + utils.print(w1.toJSON()); // Coinbase var t1 = bcoin.mtx()