diff --git a/lib/bcoin.js b/lib/bcoin.js index 3e41532c..ebdc86cf 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -20,6 +20,8 @@ if (process.env.BCOIN_DEBUG) { bcoin.debug = +bcoin.debug === 1; } +bcoin.dir = process.env.HOME + '/.bcoin'; + if (!bcoin.isBrowser) { bcoin.fs = require('f' + 's'); bcoin.crypto = require('cry' + 'pto'); @@ -27,6 +29,14 @@ if (!bcoin.isBrowser) { bcoin.cp = require('child_' + 'process'); } +if (bcoin.fs) { + try { + bcoin.fs.statSync(bcoin.dir, 0o750); + } catch (e) { + bcoin.fs.mkdirSync(bcoin.dir); + } +} + bcoin.inherits = inherits; bcoin.elliptic = elliptic; bcoin.bn = bn; diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index 4fd51b51..06652802 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -31,7 +31,6 @@ function Address(options) { options = {}; this.options = options; - this.storage = options.storage; this.label = options.label || ''; this.derived = !!options.derived; this.addressMap = null; @@ -67,8 +66,6 @@ function Address(options) { if (options.redeem || options.script) this.setRedeem(options.redeem || options.script); - - this.prefix = 'bt/address/' + this.getID() + '/'; } inherits(Address, EventEmitter); @@ -559,7 +556,7 @@ Address.getType = function getType(addr) { return prefix; }; -Address.prototype.toJSON = function toJSON(encrypt) { +Address.prototype.toJSON = function toJSON(passphrase) { return { v: 1, name: 'address', @@ -570,7 +567,7 @@ Address.prototype.toJSON = function toJSON(encrypt) { index: this.index, path: this.path, address: this.getAddress(), - key: this.key.toJSON(encrypt), + key: this.key.toJSON(passphrase), type: this.type, redeem: this.redeem ? utils.toHex(this.redeem) : null, keys: this.keys.map(utils.toBase58), @@ -579,7 +576,7 @@ Address.prototype.toJSON = function toJSON(encrypt) { }; }; -Address.fromJSON = function fromJSON(json, decrypt) { +Address.fromJSON = function fromJSON(json, passphrase) { var w; assert.equal(json.v, 1); @@ -594,7 +591,7 @@ Address.fromJSON = function fromJSON(json, decrypt) { derived: json.derived, index: json.index, path: json.path, - key: bcoin.keypair.fromJSON(json.key, decrypt), + key: bcoin.keypair.fromJSON(json.key, passphrase), type: json.type, redeem: json.redeem ? utils.toArray(json.redeem, 'hex') : null, keys: json.keys.map(utils.fromBase58), diff --git a/lib/bcoin/blockdb.js b/lib/bcoin/blockdb.js index 07f8f234..45cf4d2d 100644 --- a/lib/bcoin/blockdb.js +++ b/lib/bcoin/blockdb.js @@ -31,7 +31,7 @@ function BlockDB(options) { this.file = options.indexFile; if (!this.file) - this.file = process.env.HOME + '/bcoin-index-' + network.type + '.db'; + this.file = bcoin.dir + '/index-' + network.type + '.db'; this.options = options; @@ -1095,7 +1095,7 @@ function BlockData(options) { this.file = options.blockFile; if (!this.file) - this.file = process.env.HOME + '/bcoin-block-' + network.type + '.db'; + this.file = bcoin.dir + '/block-' + network.type + '.db'; this.bufferPool = { used: {} }; this.size = 0; diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index ecb64348..c29c53ea 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -752,10 +752,14 @@ Chain.prototype._addEntry = function _addEntry(entry, block, callback) { }; Chain.prototype.resetHeight = function resetHeight(height, force) { + var self = this; + if (height === this.db.getSize() - 1) return; - this.db.resetHeightSync(height); + this.db.resetHeightSync(height, function(entry) { + self.emit('remove entry', entry); + }); // Reset the orphan map completely. There may // have been some orphans on a forked chain we @@ -791,6 +795,8 @@ Chain.prototype.resetHeightAsync = function resetHeightAsync(height, callback, f self.purgePending(); return done(); + }, function(entry) { + self.emit('remove entry', entry); }); }; diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index 11f9f730..32b7b3f2 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -35,7 +35,7 @@ function ChainDB(chain, options) { this.file = options.file; if (!this.file) - this.file = process.env.HOME + '/bcoin-chain-' + network.type + '.db'; + this.file = bcoin.dir + '/chain-' + network.type + '.db'; this.heightLookup = {}; this.queue = {}; @@ -426,7 +426,7 @@ ChainDB.prototype.saveAsync = function saveAsync(entry, callback) { }); }; -ChainDB.prototype.resetHeightSync = function resetHeightSync(height) { +ChainDB.prototype.resetHeightSync = function resetHeightSync(height, emit) { var self = this; var osize = this.size; var ohighest = this.highest; @@ -453,6 +453,10 @@ ChainDB.prototype.resetHeightSync = function resetHeightSync(height) { assert(existing); + // Emit the blocks we remove. + if (emit) + emit(existing); + // Warn of potential race condition // (handled with _onFlush). if (this.queue[i]) @@ -489,7 +493,7 @@ ChainDB.prototype.resetHeightSync = function resetHeightSync(height) { }); }; -ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback) { +ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback, emit) { var self = this; var osize = this.size; var ohighest = this.highest; @@ -538,6 +542,10 @@ ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback) assert(existing); + // Emit the blocks we remove. + if (emit) + emit(existing); + // Warn of potential race condition // (handled with _onFlush). if (self.queue[i]) diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index c6f07851..1ab8eafc 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -837,25 +837,25 @@ HDPrivateKey.prototype.deriveString = function deriveString(path) { }, this); }; -HDPrivateKey.prototype.toJSON = function toJSON(encrypt) { +HDPrivateKey.prototype.toJSON = function toJSON(passphrase) { var json = { v: 1, name: 'hdkey', - encrypted: encrypt ? true : false + encrypted: passphrase ? true : false }; if (this instanceof HDPrivateKey) { if (this.seed) { - json.mnemonic = encrypt - ? encrypt(this.seed.mnemonic) + json.mnemonic = passphrase + ? utils.encrypt(this.seed.mnemonic, passphrase) : this.seed.mnemonic; - json.passphrase = encrypt - ? encrypt(this.seed.passphrase) + json.passphrase = passphrase + ? utils.encrypt(this.seed.passphrase, passphrase) : this.seed.passphrase; return json; } - json.xprivkey = encrypt - ? encrypt(this.xprivkey) + json.xprivkey = passphrase + ? utils.encrypt(this.xprivkey, passphrase) : this.xprivkey; return json; } @@ -865,21 +865,21 @@ HDPrivateKey.prototype.toJSON = function toJSON(encrypt) { return json; }; -HDPrivateKey.fromJSON = function fromJSON(json, decrypt) { +HDPrivateKey.fromJSON = function fromJSON(json, passphrase) { assert.equal(json.v, 1); assert.equal(json.name, 'hdkey'); - if (json.encrypted && !decrypt) + if (json.encrypted && !passphrase) throw new Error('Cannot decrypt address'); if (json.mnemonic) { return new HDPrivateKey({ seed: new HDSeed({ mnemonic: json.encrypted - ? decrypt(json.mnemonic) + ? utils.decrypt(json.mnemonic, passphrase) : json.mnemonic, passphrase: json.encrypted - ? decrypt(json.passphrase) + ? utils.decrypt(json.passphrase, passphrase) : json.passphrase }) }); @@ -888,7 +888,7 @@ HDPrivateKey.fromJSON = function fromJSON(json, decrypt) { if (json.xprivkey) { return new HDPrivateKey({ xkey: json.encrypted - ? decrypt(json.xprivkey) + ? utils.decrypt(json.xprivkey, passphrase) : json.xprivkey }); } diff --git a/lib/bcoin/keypair.js b/lib/bcoin/keypair.js index 8308f133..32af7e8b 100644 --- a/lib/bcoin/keypair.js +++ b/lib/bcoin/keypair.js @@ -195,16 +195,16 @@ KeyPair.sign = function sign(msg, key) { return bcoin.ecdsa.sign(msg, key.priv); }; -KeyPair.prototype.toJSON = function toJSON(encrypt) { +KeyPair.prototype.toJSON = function toJSON(passphrase) { var json = { v: 1, name: 'keypair', - encrypted: encrypt ? true : false + encrypted: passphrase ? true : false }; if (this.pair.priv) { - json.privateKey = encrypt - ? encrypt(this.toSecret()) + json.privateKey = passphrase + ? utils.encrypt(this.toSecret(), passphrase) : this.toSecret(); return json; } @@ -214,19 +214,19 @@ KeyPair.prototype.toJSON = function toJSON(encrypt) { return json; }; -KeyPair.fromJSON = function fromJSON(json, decrypt) { +KeyPair.fromJSON = function fromJSON(json, passphrase) { var privateKey, publicKey, compressed; assert.equal(json.v, 1); assert.equal(json.name, 'keypair'); - if (json.encrypted && !decrypt) + if (json.encrypted && !passphrase) throw new Error('Cannot decrypt address'); if (json.privateKey) { privateKey = json.privateKey; if (json.encrypted) - privateKey = decrypt(privateKey); + privateKey = utils.decrypt(privateKey, passphrase); return KeyPair.fromSecret(privateKey); } diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index 1db5b47c..d23ba68d 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -40,6 +40,9 @@ function Node(options) { this.pool = null; this.chain = null; this.miner = null; + this.wallet = null; + + this.loading = false; Node.global = this; @@ -51,9 +54,14 @@ inherits(Node, EventEmitter); Node.prototype._init = function _init() { var self = this; + this.loading = true; + if (!this.options.pool) this.options.pool = {}; + if (!this.options.miner) + this.options.miner = {}; + this.blockdb = new bcoin.blockdb(this.options.blockdb); this.mempool = new bcoin.mempool(this, this.options.mempool); @@ -63,6 +71,7 @@ Node.prototype._init = function _init() { this.pool = new bcoin.pool(this.options.pool); this.chain = this.pool.chain; + this.miner = new bcoin.miner(this.pool, this.options.miner); this.mempool.on('error', function(err) { @@ -73,7 +82,51 @@ Node.prototype._init = function _init() { self.emit('error', err); }); - this.pool.startSync(); + if (!this.options.wallet) + this.options.wallet = {}; + + if (!this.options.wallet.id) + this.options.wallet.id = 'primary'; + + bcoin.wallet.load(this.options.wallet, function(err, wallet) { + if (err) + throw err; + + self.wallet = wallet; + + utils.debug('Loaded wallet with id=%s address=%s', + wallet.getID(), wallet.getAddress()); + + self.chain.on('block', function(block) { + block.txs.forEach(function(tx) { + self.wallet.addTX(tx); + }); + }); + + self.mempool.on('tx', function(tx) { + self.wallet.addTX(tx); + }); + + self.miner.address = self.wallet.getAddress(); + + // Handle forks + self.chain.on('remove entry', function(entry) { + self.wallet.tx.getAll().forEach(function(tx) { + if (tx.block === entry.hash || tx.height >= entry.height) + self.wallet.tx.unconfirm(tx); + }); + }); + + self.pool.addWallet(self.wallet, function(err) { + if (err) + throw err; + + self.pool.startSync(); + + self.loading = false; + self.emit('load'); + }); + }); }; Node.prototype.getCoin = function getCoin(hash, index, callback) { diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index c9b8ae33..4f5375fb 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -1185,11 +1185,11 @@ Pool.prototype.isWatched = function(tx, bloom) { return false; }; -Pool.prototype.addWallet = function addWallet(wallet) { +Pool.prototype.addWallet = function addWallet(wallet, callback) { var self = this; if (this.loading) - return this.once('load', this.addWallet.bind(this, wallet)); + return this.once('load', this.addWallet.bind(this, wallet, callback)); if (this.wallets.indexOf(wallet) !== -1) return false; @@ -1206,16 +1206,16 @@ Pool.prototype.addWallet = function addWallet(wallet) { }); if (!self.options.spv) - return; + return callback(); if (self._pendingSearch) - return; + return callback(); self._pendingSearch = true; utils.nextTick(function() { self._pendingSearch = false; - self.searchWallet(); + self.searchWallet(callback); }); } @@ -1288,7 +1288,7 @@ Pool.prototype.unwatchWallet = function unwatchWallet(wallet) { delete wallet._poolOnRemove; }; -Pool.prototype.searchWallet = function(ts, height) { +Pool.prototype.searchWallet = function(ts, height, callback) { var self = this; var wallet; @@ -1297,7 +1297,8 @@ Pool.prototype.searchWallet = function(ts, height) { if (!this.options.spv) return; - if (ts == null) { + if (ts == null || typeof ts === 'function') { + callback = ts; height = this.wallets.reduce(function(height, wallet) { if (wallet.lastHeight < height) return wallet.lastHeight; @@ -1311,6 +1312,7 @@ Pool.prototype.searchWallet = function(ts, height) { }, Infinity); assert(ts !== Infinity); } else if (typeof ts !== 'number') { + callback = height; wallet = ts; if (wallet.loading) { wallet.once('load', function() { @@ -1322,6 +1324,8 @@ Pool.prototype.searchWallet = function(ts, height) { height = wallet.lastHeight; } + callback = utils.asyncify(callback); + // Always prefer height if (height > 0) { // Back one week @@ -1329,8 +1333,10 @@ Pool.prototype.searchWallet = function(ts, height) { height = this.chain.height - (7 * 24 * 6); this.chain.resetHeightAsync(height, function(err) { - if (err) - throw err; + if (err) { + utils.debug('Failed to reset height: %s', err.stack + ''); + return callback(err); + } utils.debug('Wallet height: %s', height); utils.debug( @@ -1338,6 +1344,8 @@ Pool.prototype.searchWallet = function(ts, height) { self.chain.height, new Date(self.chain.tip.ts * 1000) ); + + callback(); }); return; @@ -1347,8 +1355,10 @@ Pool.prototype.searchWallet = function(ts, height) { ts = utils.now() - 7 * 24 * 3600; this.chain.resetTimeAsync(ts, function(err) { - if (err) - throw err; + if (err) { + utils.debug('Failed to reset time: %s', err.stack + ''); + return callback(err); + } utils.debug('Wallet time: %s', new Date(ts * 1000)); utils.debug( @@ -1356,6 +1366,8 @@ Pool.prototype.searchWallet = function(ts, height) { self.chain.height, new Date(self.chain.tip.ts * 1000) ); + + callback(); }); }; diff --git a/lib/bcoin/spvnode.js b/lib/bcoin/spvnode.js new file mode 100644 index 00000000..8a87c445 --- /dev/null +++ b/lib/bcoin/spvnode.js @@ -0,0 +1,117 @@ +/** + * spvnode.js - spv node for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var inherits = require('inherits'); +var EventEmitter = require('events').EventEmitter; +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; +var fs = bcoin.fs; + +/** + * SPVNode + */ + +function SPVNode(options) { + if (!(this instanceof SPVNode)) + return new SPVNode(options); + + EventEmitter.call(this); + + if (!options) + options = {}; + + this.options = options; + + if (this.options.debug) + bcoin.debug = this.options.debug; + + if (this.options.network) + network.set(this.options.network); + + this.pool = null; + this.chain = null; + this.wallet = null; + + this.loading = false; + + SPVNode.global = this; + + this._init(); +} + +inherits(SPVNode, EventEmitter); + +SPVNode.prototype._init = function _init() { + var self = this; + + this.loading = true; + + if (!this.options.pool) + this.options.pool = {}; + + this.options.pool.spv = true; + this.options.pool.preload = this.options.pool.preload !== false; + + this.pool = new bcoin.pool(this.options.pool); + this.chain = this.pool.chain; + + this.pool.on('error', function(err) { + self.emit('error', err); + }); + + this.pool.on('tx', function(tx) { + self.wallet.addTX(tx); + }); + + if (!this.options.wallet) + this.options.wallet = {}; + + if (!this.options.wallet.id) + this.options.wallet.id = 'primary'; + + bcoin.wallet.load(this.options.wallet, function(err, wallet) { + if (err) + throw err; + + self.wallet = wallet; + + utils.debug('Loaded wallet with id=%s address=%s', + wallet.getID(), wallet.getAddress()); + + // Handle forks + self.chain.on('remove entry', function(entry) { + self.wallet.tx.getAll().forEach(function(tx) { + if (tx.block === entry.hash || tx.height >= entry.height) + self.wallet.tx.unconfirm(tx); + }); + }); + + self.pool.startSync(); + + self.loading = false; + self.emit('load'); + return; + self.pool.addWallet(this.wallet, function(err) { + if (err) + throw err; + + self.pool.startSync(); + + self.loading = false; + self.emit('load'); + }); + }); +}; + +/** + * Expose + */ + +module.exports = SPVNode; diff --git a/lib/bcoin/tx-pool.js b/lib/bcoin/tx-pool.js index 7a8ff4c2..32bc88ae 100644 --- a/lib/bcoin/tx-pool.js +++ b/lib/bcoin/tx-pool.js @@ -15,17 +15,15 @@ var EventEmitter = require('events').EventEmitter; * TXPool */ -function TXPool(wallet) { +function TXPool(wallet, txs) { var self = this; if (!(this instanceof TXPool)) - return new TXPool(wallet); + return new TXPool(wallet, txs); EventEmitter.call(this); this._wallet = wallet; - this._storage = wallet.storage; - this._prefix = wallet.prefix + 'tx/'; this._all = {}; this._unspent = {}; this._orphans = {}; @@ -37,44 +35,29 @@ function TXPool(wallet) { this._received = new bn(0); this._balance = new bn(0); - // Load TXs from storage - this._init(); + this._init(txs); } inherits(TXPool, EventEmitter); -TXPool.prototype._init = function init() { +TXPool.prototype._init = function _init(txs) { var self = this; - if (!this._storage) { - utils.nextTick(function() { - self._loaded = true; - self.emit('load', self._lastTs, self._lastHeight); - }); + if (!txs) return; - } - var s = this._storage.createReadStream({ - keys: false, - start: this._prefix, - end: this._prefix + 'z' - }); - - s.on('data', function(data) { - self.add(bcoin.tx.fromJSON(data), true); - }); - - s.on('error', function(err) { - self.emit('error', err); - }); - - s.on('end', function() { - self._loaded = true; - self.emit('load', self._lastTs, self._lastHeight); + utils.nextTick(function() { + self.populate(txs); }); }; -TXPool.prototype.add = function add(tx, noWrite, strict) { +TXPool.prototype.populate = function populate(txs) { + txs.forEach(function(tx) { + this.add(tx, true); + }, this); +}; + +TXPool.prototype.add = function add(tx, noWrite) { var hash = tx.hash('hex'); var updated = false; var i, j, input, output, coin, unspent, index, orphan; @@ -103,7 +86,7 @@ TXPool.prototype.add = function add(tx, noWrite, strict) { var key = hash + '/' + i; if (this._unspent[key]) this._unspent[key].height = tx.height; - }); + }, this); this._storeTX(hash, tx, noWrite); this._lastTs = Math.max(tx.ts, this._lastTs); this._lastHeight = Math.max(tx.height, this._lastHeight); @@ -226,21 +209,18 @@ TXPool.prototype.getCoin = function getCoin(hash, index) { TXPool.prototype._storeTX = function _storeTX(hash, tx, noWrite) { var self = this; - if (!this._storage || noWrite) + if (noWrite) return; - this._storage.put(this._prefix + hash, tx.toJSON(), function(err) { - if (err) - self.emit('error', err); - }); + this._wallet.saveFile(); }; TXPool.prototype._removeTX = function _removeTX(tx, noWrite) { var self = this; var hash = tx.hash('hex'); - var key; + var key, i; - for (var i = 0; i < tx.outputs.length; i++) { + for (i = 0; i < tx.outputs.length; i++) { key = hash + '/' + i; if (this._unspent[key]) { delete this._unspent[key]; @@ -250,13 +230,73 @@ TXPool.prototype._removeTX = function _removeTX(tx, noWrite) { // delete this._all[hash]; - if (!this._storage || noWrite) + if (noWrite) return; - this._storage.del(this._prefix + tx.hash('hex'), function(err) { - if (err) - self.emit('error', err); - }); + this._wallet.saveFile(); +}; + +TXPool.prototype.removeTX = function removeTX(hash) { + var tx, input, prev, updated; + + if (hash.hash) + hash = hash('hex'); + + tx = this._all[hash]; + + if (!tx) + return false; + + this._removeTX(tx, false); + + delete this._all[hash]; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + if (!input.output || !this._wallet.ownOutput(input.output)) + continue; + + this._removeInput(input); + + this._unspent[key] = input.output; + updated = true; + } + + if (updated) + this.emit('update', this._lastTs, this._lastHeight); +}; + +TXPool.prototype.unconfirm = function unconfirm(hash) { + var tx; + + if (hash.hash) + hash = hash('hex'); + + tx = this._all[hash]; + + if (!tx) + return false; + + if (this._lastHeight >= tx.height) + this._lastHeight = tx.height; + + if (this._lastTs >= tx.ts) + this._lastTs = tx.ts; + + tx.ps = utils.now(); + tx.ts = 0; + tx.block = null; + tx.height = -1; + tx.outputs.forEach(function(output, i) { + var key = hash + '/' + i; + if (this._unspent[key]) + this._unspent[key].height = -1; + }, this); + this._storeTX(hash, tx, noWrite); + this._lastTs = Math.max(tx.ts, this._lastTs); + this._lastHeight = Math.max(tx.height, this._lastHeight); + this.emit('update', this._lastTs, this._lastHeight, tx); + this.emit('unconfirmed', tx); }; TXPool.prototype._addOutput = function _addOutput(tx, i, remove) { @@ -426,42 +466,6 @@ TXPool.prototype.unspent = TXPool.prototype.getUnspent; TXPool.prototype.pending = TXPool.prototype.getPending; TXPool.prototype.balance = TXPool.prototype.getBalance; -TXPool.prototype.toJSON = function toJSON() { - return { - v: 1, - type: 'tx-pool', - txs: Object.keys(this._all).map(function(hash) { - return this._all[hash].toJSON(); - }, this) - }; -}; - -TXPool.prototype.fromJSON = function fromJSON(json) { - assert.equal(json.v, 1); - assert.equal(json.type, 'tx-pool'); - - json.txs.forEach(function(tx) { - this.add(bcoin.tx.fromJSON(tx)); - }, this); -}; - -TXPool.fromJSON = function fromJSON(wallet, json) { - var txPool; - - assert.equal(json.v, 1); - assert.equal(json.type, 'tx-pool'); - - txPool = new TXPool(wallet); - - utils.nextTick(function() { - json.txs.forEach(function(tx) { - txPool.add(bcoin.tx.fromJSON(tx)); - }); - }); - - return txPool; -}; - /** * Expose */ diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 2598ec44..4cc7ccb4 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -217,6 +217,57 @@ utils.sha512hmac = function sha512hmac(data, salt) { return utils.toArray(result); }; +utils.salt = 'bcoin:'; + +utils.encrypt = function encrypt(data, passphrase) { + var cipher, out; + + if (!bcoin.crypto) + return data; + + if (data[0] === ':') + return data; + + if (!passphrase) + throw new Error('No passphrase.'); + + cipher = bcoin.crypto.createCipher('aes-256-cbc', passphrase); + + out = ''; + out += cipher.update(utils.salt + data, 'utf8', 'hex'); + out += cipher.final('hex'); + + return ':' + out; +}; + +utils.decrypt = function decrypt(data, passphrase) { + var decipher, out; + + if (!bcoin.crypto) + return data; + + if (data[0] !== ':') + return data; + + if (!passphrase) + throw new Error('No passphrase.'); + + data = data.substring(1); + + decipher = bcoin.crypto.createDecipher('aes-256-cbc', passphrase); + + out = ''; + out += decipher.update(data, 'hex', 'utf8'); + out += decipher.final('utf8'); + + if (out.indexOf(utils.salt) !== 0) + throw new Error('Decrypt failed.'); + + out = out.substring(utils.salt.length); + + return out; +}; + utils.writeAscii = function writeAscii(dst, str, off) { var i = 0; var c; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index e3cf7d92..cf0266cd 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -13,12 +13,14 @@ var utils = bcoin.utils; var assert = utils.assert; var constants = bcoin.protocol.constants; var network = bcoin.protocol.network; +var fs = bcoin.fs; /** * Wallet */ function Wallet(options) { + var self = this; var key, receiving; if (!(this instanceof Wallet)) @@ -87,7 +89,6 @@ function Wallet(options) { : this.master.deriveAccount44(this.accountIndex); } - this.storage = options.storage; this.loading = true; this.lastTs = 0; this.lastHeight = 0; @@ -131,15 +132,16 @@ Wallet.prototype._init = function _init() { assert(!this.receiveAddress.change); assert(this.changeAddress.change); - this.prefix = 'bt/wallet/' + this.getID() + '/'; + this.id = this.getID(); + this.file = options.file; - this.tx = options.tx || bcoin.txPool(this); - - if (this.tx._loaded) { - this.loading = false; - return; + if (!this.file || this.file === true) { + this.file = bcoin.dir + '/wallet-' + + this.id + '-' + network.type + '.json'; } + this.tx = new bcoin.txPool(this); + // Notify owners about new accepted transactions this.tx.on('update', function(lastTs, lastHeight, tx) { var b = this.getBalance(); @@ -163,16 +165,22 @@ Wallet.prototype._init = function _init() { self.emit('confirmed', tx); }); - this.tx.once('load', function(ts, height) { - self.loading = false; - self.lastTs = ts; - self.lastHeight = height; - self.emit('load', ts); - }); - this.tx.on('error', function(err) { self.emit('error', err); }); + + if (options.txs) + this.tx.populate(options.txs); + + this.lastTs = this.tx._lastTs; + this.lastHeight = this.tx._lastHeight; + + this.saveFile(); + + utils.nextTick(function() { + self.loading = false; + self.emit('load', self.lastTs); + }); }; Wallet.prototype.addKey = function addKey(key) { @@ -277,6 +285,9 @@ Wallet.prototype._finalizeKeys = function _finalizeKeys(key) { // bip45: Purpose key address // bip44: Account key address Wallet.prototype.getID = function getID() { + if (this.options.id) + return this.options.id; + return bcoin.address.compile(this.accountKey.publicKey); }; @@ -846,11 +857,12 @@ Wallet.prototype.__defineGetter__('address', function() { return this.getAddress(); }); -Wallet.prototype.toJSON = function toJSON(encrypt) { +Wallet.prototype.toJSON = function toJSON(noPool) { return { v: 3, name: 'wallet', network: network.type, + id: this.id, type: this.type, m: this.m, n: this.n, @@ -859,17 +871,19 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { accountIndex: this.accountIndex, receiveDepth: this.receiveDepth, changeDepth: this.changeDepth, - master: this.master ? this.master.toJSON(encrypt) : null, + master: this.master.toJSON(this.options.passphrase), addressMap: this.addressMap, keys: this.keys.map(function(key) { return key.xpubkey; }), balance: utils.btc(this.getBalance()), - tx: this.tx.toJSON() + txs: noPool ? [] : this.tx.getAll().map(function(tx) { + return tx.toJSON(); + }) }; }; -Wallet.fromJSON = function fromJSON(json, decrypt) { +Wallet._fromJSON = function _fromJSON(json, passphrase) { var wallet; assert.equal(json.v, 3); @@ -878,7 +892,8 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { if (json.network) assert.equal(json.network, network.type); - wallet = new Wallet({ + return { + id: json.id, type: json.type, m: json.m, n: json.n, @@ -887,16 +902,130 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { accountIndex: json.accountIndex, receiveDepth: json.receiveDepth, changeDepth: json.changeDepth, - master: json.master - ? bcoin.hd.fromJSON(json.master, decrypt) - : null, + master: bcoin.hd.fromJSON(json.master, passphrase), addressMap: json.addressMap, - keys: json.keys + keys: json.keys, + txs: json.txs.map(function(json) { + return bcoin.tx.fromJSON(json); + }) + }; +}; + +Wallet.fromJSON = function fromJSON(json, passphrase) { + return new Wallet(Wallet._fromJSON(json, passphrase)); +}; + +Wallet.prototype.saveFile = function saveFile(callback) { + callback = utils.asyncify(callback); + + if (!this.options.file) + return callback(); + + return this.toFile(this.file, this.options.passphrase, callback); +}; + +Wallet.prototype.toFile = function toFile(file, callback) { + var json, options; + + if (typeof file === 'function') { + callback = file; + file = null; + } + + if (!file) + file = this.file; + + callback = utils.asyncify(callback); + + if (!bcoin.fs) + return callback(); + + json = JSON.stringify(this.toJSON(this.options.passphrase), null, 2); + + options = { + encoding: 'utf8', + mode: 0o600 + }; + + fs.writeFile(file, json, options, function(err) { + if (err) + return callback(err); + + return callback(null, file); }); +}; - wallet.tx.fromJSON(json.tx); +Wallet._fromFile = function _fromFile(file, passphrase, callback) { + if (typeof passphrase === 'function') { + callback = passphrase; + passphrase = null; + } - return wallet; + callback = utils.asyncify(callback); + + if (!bcoin.fs) + return callback(); + + if (!file) + return callback(); + + fs.readFile(file, 'utf8', function(err, json) { + var options; + + if (err && err.code === 'ENOENT') + return callback(); + + if (err) + return callback(err); + + try { + options = Wallet._fromJSON(JSON.parse(json)); + } catch (e) { + return callback(e); + } + + return callback(null, options); + }); +}; + +Wallet.fromFile = function fromFile(file, passphrase, callback) { + if (typeof passphrase === 'function') { + callback = passphrase; + passphrase = null; + } + + return Wallet._fromFile(file, passphrase, function(err, options) { + if (err) + return callback(err); + + if (!options) + return callback(); + + return callback(null, new Wallet(options)); + }); +}; + +Wallet.load = function load(options, callback) { + var file; + + if (options.id) { + file = bcoin.dir + '/wallet-' + + options.id + '-' + network.type + '.json'; + options.file = true; + } + + if (typeof options.file === 'string') + file = options.file; + + return Wallet.fromFile(file, options.passphrase, function(err, wallet) { + if (err) + return callback(err); + + if (!wallet) + wallet = new Wallet(options); + + return callback(null, wallet); + }); }; /**