add spv node. add .bcoin dir. add file storage for wallet.

This commit is contained in:
Christopher Jeffrey 2016-02-20 16:48:07 -08:00
parent 86a368dddf
commit 6e2eb039ac
13 changed files with 537 additions and 150 deletions

View File

@ -20,6 +20,8 @@ if (process.env.BCOIN_DEBUG) {
bcoin.debug = +bcoin.debug === 1; bcoin.debug = +bcoin.debug === 1;
} }
bcoin.dir = process.env.HOME + '/.bcoin';
if (!bcoin.isBrowser) { if (!bcoin.isBrowser) {
bcoin.fs = require('f' + 's'); bcoin.fs = require('f' + 's');
bcoin.crypto = require('cry' + 'pto'); bcoin.crypto = require('cry' + 'pto');
@ -27,6 +29,14 @@ if (!bcoin.isBrowser) {
bcoin.cp = require('child_' + 'process'); 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.inherits = inherits;
bcoin.elliptic = elliptic; bcoin.elliptic = elliptic;
bcoin.bn = bn; bcoin.bn = bn;

View File

@ -31,7 +31,6 @@ function Address(options) {
options = {}; options = {};
this.options = options; this.options = options;
this.storage = options.storage;
this.label = options.label || ''; this.label = options.label || '';
this.derived = !!options.derived; this.derived = !!options.derived;
this.addressMap = null; this.addressMap = null;
@ -67,8 +66,6 @@ function Address(options) {
if (options.redeem || options.script) if (options.redeem || options.script)
this.setRedeem(options.redeem || options.script); this.setRedeem(options.redeem || options.script);
this.prefix = 'bt/address/' + this.getID() + '/';
} }
inherits(Address, EventEmitter); inherits(Address, EventEmitter);
@ -559,7 +556,7 @@ Address.getType = function getType(addr) {
return prefix; return prefix;
}; };
Address.prototype.toJSON = function toJSON(encrypt) { Address.prototype.toJSON = function toJSON(passphrase) {
return { return {
v: 1, v: 1,
name: 'address', name: 'address',
@ -570,7 +567,7 @@ Address.prototype.toJSON = function toJSON(encrypt) {
index: this.index, index: this.index,
path: this.path, path: this.path,
address: this.getAddress(), address: this.getAddress(),
key: this.key.toJSON(encrypt), key: this.key.toJSON(passphrase),
type: this.type, type: this.type,
redeem: this.redeem ? utils.toHex(this.redeem) : null, redeem: this.redeem ? utils.toHex(this.redeem) : null,
keys: this.keys.map(utils.toBase58), 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; var w;
assert.equal(json.v, 1); assert.equal(json.v, 1);
@ -594,7 +591,7 @@ Address.fromJSON = function fromJSON(json, decrypt) {
derived: json.derived, derived: json.derived,
index: json.index, index: json.index,
path: json.path, path: json.path,
key: bcoin.keypair.fromJSON(json.key, decrypt), key: bcoin.keypair.fromJSON(json.key, passphrase),
type: json.type, type: json.type,
redeem: json.redeem ? utils.toArray(json.redeem, 'hex') : null, redeem: json.redeem ? utils.toArray(json.redeem, 'hex') : null,
keys: json.keys.map(utils.fromBase58), keys: json.keys.map(utils.fromBase58),

View File

@ -31,7 +31,7 @@ function BlockDB(options) {
this.file = options.indexFile; this.file = options.indexFile;
if (!this.file) if (!this.file)
this.file = process.env.HOME + '/bcoin-index-' + network.type + '.db'; this.file = bcoin.dir + '/index-' + network.type + '.db';
this.options = options; this.options = options;
@ -1095,7 +1095,7 @@ function BlockData(options) {
this.file = options.blockFile; this.file = options.blockFile;
if (!this.file) 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.bufferPool = { used: {} };
this.size = 0; this.size = 0;

View File

@ -752,10 +752,14 @@ Chain.prototype._addEntry = function _addEntry(entry, block, callback) {
}; };
Chain.prototype.resetHeight = function resetHeight(height, force) { Chain.prototype.resetHeight = function resetHeight(height, force) {
var self = this;
if (height === this.db.getSize() - 1) if (height === this.db.getSize() - 1)
return; return;
this.db.resetHeightSync(height); this.db.resetHeightSync(height, function(entry) {
self.emit('remove entry', entry);
});
// Reset the orphan map completely. There may // Reset the orphan map completely. There may
// have been some orphans on a forked chain we // have been some orphans on a forked chain we
@ -791,6 +795,8 @@ Chain.prototype.resetHeightAsync = function resetHeightAsync(height, callback, f
self.purgePending(); self.purgePending();
return done(); return done();
}, function(entry) {
self.emit('remove entry', entry);
}); });
}; };

View File

@ -35,7 +35,7 @@ function ChainDB(chain, options) {
this.file = options.file; this.file = options.file;
if (!this.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.heightLookup = {};
this.queue = {}; 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 self = this;
var osize = this.size; var osize = this.size;
var ohighest = this.highest; var ohighest = this.highest;
@ -453,6 +453,10 @@ ChainDB.prototype.resetHeightSync = function resetHeightSync(height) {
assert(existing); assert(existing);
// Emit the blocks we remove.
if (emit)
emit(existing);
// Warn of potential race condition // Warn of potential race condition
// (handled with _onFlush). // (handled with _onFlush).
if (this.queue[i]) 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 self = this;
var osize = this.size; var osize = this.size;
var ohighest = this.highest; var ohighest = this.highest;
@ -538,6 +542,10 @@ ChainDB.prototype.resetHeightAsync = function resetHeightAsync(height, callback)
assert(existing); assert(existing);
// Emit the blocks we remove.
if (emit)
emit(existing);
// Warn of potential race condition // Warn of potential race condition
// (handled with _onFlush). // (handled with _onFlush).
if (self.queue[i]) if (self.queue[i])

View File

@ -837,25 +837,25 @@ HDPrivateKey.prototype.deriveString = function deriveString(path) {
}, this); }, this);
}; };
HDPrivateKey.prototype.toJSON = function toJSON(encrypt) { HDPrivateKey.prototype.toJSON = function toJSON(passphrase) {
var json = { var json = {
v: 1, v: 1,
name: 'hdkey', name: 'hdkey',
encrypted: encrypt ? true : false encrypted: passphrase ? true : false
}; };
if (this instanceof HDPrivateKey) { if (this instanceof HDPrivateKey) {
if (this.seed) { if (this.seed) {
json.mnemonic = encrypt json.mnemonic = passphrase
? encrypt(this.seed.mnemonic) ? utils.encrypt(this.seed.mnemonic, passphrase)
: this.seed.mnemonic; : this.seed.mnemonic;
json.passphrase = encrypt json.passphrase = passphrase
? encrypt(this.seed.passphrase) ? utils.encrypt(this.seed.passphrase, passphrase)
: this.seed.passphrase; : this.seed.passphrase;
return json; return json;
} }
json.xprivkey = encrypt json.xprivkey = passphrase
? encrypt(this.xprivkey) ? utils.encrypt(this.xprivkey, passphrase)
: this.xprivkey; : this.xprivkey;
return json; return json;
} }
@ -865,21 +865,21 @@ HDPrivateKey.prototype.toJSON = function toJSON(encrypt) {
return json; return json;
}; };
HDPrivateKey.fromJSON = function fromJSON(json, decrypt) { HDPrivateKey.fromJSON = function fromJSON(json, passphrase) {
assert.equal(json.v, 1); assert.equal(json.v, 1);
assert.equal(json.name, 'hdkey'); assert.equal(json.name, 'hdkey');
if (json.encrypted && !decrypt) if (json.encrypted && !passphrase)
throw new Error('Cannot decrypt address'); throw new Error('Cannot decrypt address');
if (json.mnemonic) { if (json.mnemonic) {
return new HDPrivateKey({ return new HDPrivateKey({
seed: new HDSeed({ seed: new HDSeed({
mnemonic: json.encrypted mnemonic: json.encrypted
? decrypt(json.mnemonic) ? utils.decrypt(json.mnemonic, passphrase)
: json.mnemonic, : json.mnemonic,
passphrase: json.encrypted passphrase: json.encrypted
? decrypt(json.passphrase) ? utils.decrypt(json.passphrase, passphrase)
: json.passphrase : json.passphrase
}) })
}); });
@ -888,7 +888,7 @@ HDPrivateKey.fromJSON = function fromJSON(json, decrypt) {
if (json.xprivkey) { if (json.xprivkey) {
return new HDPrivateKey({ return new HDPrivateKey({
xkey: json.encrypted xkey: json.encrypted
? decrypt(json.xprivkey) ? utils.decrypt(json.xprivkey, passphrase)
: json.xprivkey : json.xprivkey
}); });
} }

View File

@ -195,16 +195,16 @@ KeyPair.sign = function sign(msg, key) {
return bcoin.ecdsa.sign(msg, key.priv); return bcoin.ecdsa.sign(msg, key.priv);
}; };
KeyPair.prototype.toJSON = function toJSON(encrypt) { KeyPair.prototype.toJSON = function toJSON(passphrase) {
var json = { var json = {
v: 1, v: 1,
name: 'keypair', name: 'keypair',
encrypted: encrypt ? true : false encrypted: passphrase ? true : false
}; };
if (this.pair.priv) { if (this.pair.priv) {
json.privateKey = encrypt json.privateKey = passphrase
? encrypt(this.toSecret()) ? utils.encrypt(this.toSecret(), passphrase)
: this.toSecret(); : this.toSecret();
return json; return json;
} }
@ -214,19 +214,19 @@ KeyPair.prototype.toJSON = function toJSON(encrypt) {
return json; return json;
}; };
KeyPair.fromJSON = function fromJSON(json, decrypt) { KeyPair.fromJSON = function fromJSON(json, passphrase) {
var privateKey, publicKey, compressed; var privateKey, publicKey, compressed;
assert.equal(json.v, 1); assert.equal(json.v, 1);
assert.equal(json.name, 'keypair'); assert.equal(json.name, 'keypair');
if (json.encrypted && !decrypt) if (json.encrypted && !passphrase)
throw new Error('Cannot decrypt address'); throw new Error('Cannot decrypt address');
if (json.privateKey) { if (json.privateKey) {
privateKey = json.privateKey; privateKey = json.privateKey;
if (json.encrypted) if (json.encrypted)
privateKey = decrypt(privateKey); privateKey = utils.decrypt(privateKey, passphrase);
return KeyPair.fromSecret(privateKey); return KeyPair.fromSecret(privateKey);
} }

View File

@ -40,6 +40,9 @@ function Node(options) {
this.pool = null; this.pool = null;
this.chain = null; this.chain = null;
this.miner = null; this.miner = null;
this.wallet = null;
this.loading = false;
Node.global = this; Node.global = this;
@ -51,9 +54,14 @@ inherits(Node, EventEmitter);
Node.prototype._init = function _init() { Node.prototype._init = function _init() {
var self = this; var self = this;
this.loading = true;
if (!this.options.pool) if (!this.options.pool)
this.options.pool = {}; this.options.pool = {};
if (!this.options.miner)
this.options.miner = {};
this.blockdb = new bcoin.blockdb(this.options.blockdb); this.blockdb = new bcoin.blockdb(this.options.blockdb);
this.mempool = new bcoin.mempool(this, this.options.mempool); 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.pool = new bcoin.pool(this.options.pool);
this.chain = this.pool.chain; this.chain = this.pool.chain;
this.miner = new bcoin.miner(this.pool, this.options.miner); this.miner = new bcoin.miner(this.pool, this.options.miner);
this.mempool.on('error', function(err) { this.mempool.on('error', function(err) {
@ -73,7 +82,51 @@ Node.prototype._init = function _init() {
self.emit('error', err); 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) { Node.prototype.getCoin = function getCoin(hash, index, callback) {

View File

@ -1185,11 +1185,11 @@ Pool.prototype.isWatched = function(tx, bloom) {
return false; return false;
}; };
Pool.prototype.addWallet = function addWallet(wallet) { Pool.prototype.addWallet = function addWallet(wallet, callback) {
var self = this; var self = this;
if (this.loading) 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) if (this.wallets.indexOf(wallet) !== -1)
return false; return false;
@ -1206,16 +1206,16 @@ Pool.prototype.addWallet = function addWallet(wallet) {
}); });
if (!self.options.spv) if (!self.options.spv)
return; return callback();
if (self._pendingSearch) if (self._pendingSearch)
return; return callback();
self._pendingSearch = true; self._pendingSearch = true;
utils.nextTick(function() { utils.nextTick(function() {
self._pendingSearch = false; self._pendingSearch = false;
self.searchWallet(); self.searchWallet(callback);
}); });
} }
@ -1288,7 +1288,7 @@ Pool.prototype.unwatchWallet = function unwatchWallet(wallet) {
delete wallet._poolOnRemove; delete wallet._poolOnRemove;
}; };
Pool.prototype.searchWallet = function(ts, height) { Pool.prototype.searchWallet = function(ts, height, callback) {
var self = this; var self = this;
var wallet; var wallet;
@ -1297,7 +1297,8 @@ Pool.prototype.searchWallet = function(ts, height) {
if (!this.options.spv) if (!this.options.spv)
return; return;
if (ts == null) { if (ts == null || typeof ts === 'function') {
callback = ts;
height = this.wallets.reduce(function(height, wallet) { height = this.wallets.reduce(function(height, wallet) {
if (wallet.lastHeight < height) if (wallet.lastHeight < height)
return wallet.lastHeight; return wallet.lastHeight;
@ -1311,6 +1312,7 @@ Pool.prototype.searchWallet = function(ts, height) {
}, Infinity); }, Infinity);
assert(ts !== Infinity); assert(ts !== Infinity);
} else if (typeof ts !== 'number') { } else if (typeof ts !== 'number') {
callback = height;
wallet = ts; wallet = ts;
if (wallet.loading) { if (wallet.loading) {
wallet.once('load', function() { wallet.once('load', function() {
@ -1322,6 +1324,8 @@ Pool.prototype.searchWallet = function(ts, height) {
height = wallet.lastHeight; height = wallet.lastHeight;
} }
callback = utils.asyncify(callback);
// Always prefer height // Always prefer height
if (height > 0) { if (height > 0) {
// Back one week // Back one week
@ -1329,8 +1333,10 @@ Pool.prototype.searchWallet = function(ts, height) {
height = this.chain.height - (7 * 24 * 6); height = this.chain.height - (7 * 24 * 6);
this.chain.resetHeightAsync(height, function(err) { this.chain.resetHeightAsync(height, function(err) {
if (err) if (err) {
throw err; utils.debug('Failed to reset height: %s', err.stack + '');
return callback(err);
}
utils.debug('Wallet height: %s', height); utils.debug('Wallet height: %s', height);
utils.debug( utils.debug(
@ -1338,6 +1344,8 @@ Pool.prototype.searchWallet = function(ts, height) {
self.chain.height, self.chain.height,
new Date(self.chain.tip.ts * 1000) new Date(self.chain.tip.ts * 1000)
); );
callback();
}); });
return; return;
@ -1347,8 +1355,10 @@ Pool.prototype.searchWallet = function(ts, height) {
ts = utils.now() - 7 * 24 * 3600; ts = utils.now() - 7 * 24 * 3600;
this.chain.resetTimeAsync(ts, function(err) { this.chain.resetTimeAsync(ts, function(err) {
if (err) if (err) {
throw err; utils.debug('Failed to reset time: %s', err.stack + '');
return callback(err);
}
utils.debug('Wallet time: %s', new Date(ts * 1000)); utils.debug('Wallet time: %s', new Date(ts * 1000));
utils.debug( utils.debug(
@ -1356,6 +1366,8 @@ Pool.prototype.searchWallet = function(ts, height) {
self.chain.height, self.chain.height,
new Date(self.chain.tip.ts * 1000) new Date(self.chain.tip.ts * 1000)
); );
callback();
}); });
}; };

117
lib/bcoin/spvnode.js Normal file
View File

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

View File

@ -15,17 +15,15 @@ var EventEmitter = require('events').EventEmitter;
* TXPool * TXPool
*/ */
function TXPool(wallet) { function TXPool(wallet, txs) {
var self = this; var self = this;
if (!(this instanceof TXPool)) if (!(this instanceof TXPool))
return new TXPool(wallet); return new TXPool(wallet, txs);
EventEmitter.call(this); EventEmitter.call(this);
this._wallet = wallet; this._wallet = wallet;
this._storage = wallet.storage;
this._prefix = wallet.prefix + 'tx/';
this._all = {}; this._all = {};
this._unspent = {}; this._unspent = {};
this._orphans = {}; this._orphans = {};
@ -37,44 +35,29 @@ function TXPool(wallet) {
this._received = new bn(0); this._received = new bn(0);
this._balance = new bn(0); this._balance = new bn(0);
// Load TXs from storage this._init(txs);
this._init();
} }
inherits(TXPool, EventEmitter); inherits(TXPool, EventEmitter);
TXPool.prototype._init = function init() { TXPool.prototype._init = function _init(txs) {
var self = this; var self = this;
if (!this._storage) { if (!txs)
utils.nextTick(function() {
self._loaded = true;
self.emit('load', self._lastTs, self._lastHeight);
});
return; return;
}
var s = this._storage.createReadStream({ utils.nextTick(function() {
keys: false, self.populate(txs);
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);
}); });
}; };
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 hash = tx.hash('hex');
var updated = false; var updated = false;
var i, j, input, output, coin, unspent, index, orphan; 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; var key = hash + '/' + i;
if (this._unspent[key]) if (this._unspent[key])
this._unspent[key].height = tx.height; this._unspent[key].height = tx.height;
}); }, this);
this._storeTX(hash, tx, noWrite); this._storeTX(hash, tx, noWrite);
this._lastTs = Math.max(tx.ts, this._lastTs); this._lastTs = Math.max(tx.ts, this._lastTs);
this._lastHeight = Math.max(tx.height, this._lastHeight); 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) { TXPool.prototype._storeTX = function _storeTX(hash, tx, noWrite) {
var self = this; var self = this;
if (!this._storage || noWrite) if (noWrite)
return; return;
this._storage.put(this._prefix + hash, tx.toJSON(), function(err) { this._wallet.saveFile();
if (err)
self.emit('error', err);
});
}; };
TXPool.prototype._removeTX = function _removeTX(tx, noWrite) { TXPool.prototype._removeTX = function _removeTX(tx, noWrite) {
var self = this; var self = this;
var hash = tx.hash('hex'); 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; key = hash + '/' + i;
if (this._unspent[key]) { if (this._unspent[key]) {
delete this._unspent[key]; delete this._unspent[key];
@ -250,13 +230,73 @@ TXPool.prototype._removeTX = function _removeTX(tx, noWrite) {
// delete this._all[hash]; // delete this._all[hash];
if (!this._storage || noWrite) if (noWrite)
return; return;
this._storage.del(this._prefix + tx.hash('hex'), function(err) { this._wallet.saveFile();
if (err) };
self.emit('error', err);
}); 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) { 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.pending = TXPool.prototype.getPending;
TXPool.prototype.balance = TXPool.prototype.getBalance; 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 * Expose
*/ */

View File

@ -217,6 +217,57 @@ utils.sha512hmac = function sha512hmac(data, salt) {
return utils.toArray(result); 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) { utils.writeAscii = function writeAscii(dst, str, off) {
var i = 0; var i = 0;
var c; var c;

View File

@ -13,12 +13,14 @@ var utils = bcoin.utils;
var assert = utils.assert; var assert = utils.assert;
var constants = bcoin.protocol.constants; var constants = bcoin.protocol.constants;
var network = bcoin.protocol.network; var network = bcoin.protocol.network;
var fs = bcoin.fs;
/** /**
* Wallet * Wallet
*/ */
function Wallet(options) { function Wallet(options) {
var self = this;
var key, receiving; var key, receiving;
if (!(this instanceof Wallet)) if (!(this instanceof Wallet))
@ -87,7 +89,6 @@ function Wallet(options) {
: this.master.deriveAccount44(this.accountIndex); : this.master.deriveAccount44(this.accountIndex);
} }
this.storage = options.storage;
this.loading = true; this.loading = true;
this.lastTs = 0; this.lastTs = 0;
this.lastHeight = 0; this.lastHeight = 0;
@ -131,15 +132,16 @@ Wallet.prototype._init = function _init() {
assert(!this.receiveAddress.change); assert(!this.receiveAddress.change);
assert(this.changeAddress.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.file || this.file === true) {
this.file = bcoin.dir + '/wallet-'
if (this.tx._loaded) { + this.id + '-' + network.type + '.json';
this.loading = false;
return;
} }
this.tx = new bcoin.txPool(this);
// Notify owners about new accepted transactions // Notify owners about new accepted transactions
this.tx.on('update', function(lastTs, lastHeight, tx) { this.tx.on('update', function(lastTs, lastHeight, tx) {
var b = this.getBalance(); var b = this.getBalance();
@ -163,16 +165,22 @@ Wallet.prototype._init = function _init() {
self.emit('confirmed', tx); 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) { this.tx.on('error', function(err) {
self.emit('error', 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) { Wallet.prototype.addKey = function addKey(key) {
@ -277,6 +285,9 @@ Wallet.prototype._finalizeKeys = function _finalizeKeys(key) {
// bip45: Purpose key address // bip45: Purpose key address
// bip44: Account key address // bip44: Account key address
Wallet.prototype.getID = function getID() { Wallet.prototype.getID = function getID() {
if (this.options.id)
return this.options.id;
return bcoin.address.compile(this.accountKey.publicKey); return bcoin.address.compile(this.accountKey.publicKey);
}; };
@ -846,11 +857,12 @@ Wallet.prototype.__defineGetter__('address', function() {
return this.getAddress(); return this.getAddress();
}); });
Wallet.prototype.toJSON = function toJSON(encrypt) { Wallet.prototype.toJSON = function toJSON(noPool) {
return { return {
v: 3, v: 3,
name: 'wallet', name: 'wallet',
network: network.type, network: network.type,
id: this.id,
type: this.type, type: this.type,
m: this.m, m: this.m,
n: this.n, n: this.n,
@ -859,17 +871,19 @@ Wallet.prototype.toJSON = function toJSON(encrypt) {
accountIndex: this.accountIndex, accountIndex: this.accountIndex,
receiveDepth: this.receiveDepth, receiveDepth: this.receiveDepth,
changeDepth: this.changeDepth, changeDepth: this.changeDepth,
master: this.master ? this.master.toJSON(encrypt) : null, master: this.master.toJSON(this.options.passphrase),
addressMap: this.addressMap, addressMap: this.addressMap,
keys: this.keys.map(function(key) { keys: this.keys.map(function(key) {
return key.xpubkey; return key.xpubkey;
}), }),
balance: utils.btc(this.getBalance()), 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; var wallet;
assert.equal(json.v, 3); assert.equal(json.v, 3);
@ -878,7 +892,8 @@ Wallet.fromJSON = function fromJSON(json, decrypt) {
if (json.network) if (json.network)
assert.equal(json.network, network.type); assert.equal(json.network, network.type);
wallet = new Wallet({ return {
id: json.id,
type: json.type, type: json.type,
m: json.m, m: json.m,
n: json.n, n: json.n,
@ -887,16 +902,130 @@ Wallet.fromJSON = function fromJSON(json, decrypt) {
accountIndex: json.accountIndex, accountIndex: json.accountIndex,
receiveDepth: json.receiveDepth, receiveDepth: json.receiveDepth,
changeDepth: json.changeDepth, changeDepth: json.changeDepth,
master: json.master master: bcoin.hd.fromJSON(json.master, passphrase),
? bcoin.hd.fromJSON(json.master, decrypt)
: null,
addressMap: json.addressMap, 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);
});
}; };
/** /**