async wallet.

This commit is contained in:
Christopher Jeffrey 2016-05-28 05:48:50 -07:00
parent 7bb67aa449
commit 420d72d647
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 682 additions and 783 deletions

View File

@ -37,7 +37,6 @@ function KeyRing(options) {
if (!options)
options = {};
this.options = options;
this.addressMap = null;
this.network = bcoin.network.get(options.network);

View File

@ -197,17 +197,13 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) {
});
}
this.db.iterate({
gte: 'W/' + address,
lte: 'W/' + address + '~',
transform: function(key) {
return key.split('/')[2];
}
}, function(err, keys) {
this.db.fetch('W/' + address, function(json) {
return JSON.parse(json.toString('utf8'));
}, function(err, data) {
if (err)
return callback(err);
table[address] = keys;
table[address] = data ? data.wallets : [];
return callback(null, table);
});
@ -575,8 +571,13 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
self.emit('tx', tx, map);
if (updated) {
if (tx.ts !== 0)
self.emit('confirmed', tx, map);
if (tx.ts !== 0) {
self.emit('confirmed', tx, map, function() {
self.emit('updated', tx, map);
return callback(null, true);
});
return;
}
self.emit('updated', tx, map);
}
@ -763,10 +764,11 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
if (err)
return callback(err);
self.emit('confirmed', tx, map);
self.emit('tx', tx, map);
self.emit('confirmed', tx, map, function() {
self.emit('tx', tx, map);
return callback(null, true);
return callback(null, true);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -141,7 +141,7 @@ WalletDB.prototype._init = function _init() {
});
});
this.tx.on('confirmed', function(tx, map) {
this.tx.on('confirmed', function(tx, map, callback) {
self.emit('confirmed', tx, map);
map.all.forEach(function(id) {
self.fire(id, 'confirmed', tx);
@ -151,6 +151,8 @@ WalletDB.prototype._init = function _init() {
}, function(err) {
if (err)
self.emit('error', err);
if (callback)
callback();
});
});
@ -240,13 +242,23 @@ WalletDB.prototype.syncOutputDepth = function syncOutputDepth(id, tx, callback)
if (!wallet)
return callback(new Error('No wallet.'));
wallet.syncOutputDepth(tx);
self.save(wallet, function(err) {
if (err)
wallet.syncOutputDepth(tx, function(err) {
if (err) {
wallet.destroy();
return callback(err);
}
self.emit('sync output depth', id, tx);
wallet.destroy();
if (!self.providers[id])
return callback();
self.providers[id].forEach(function(provider) {
provider.receiveDepth = wallet.receiveDepth;
provider.changeDepth = wallet.changeDepth;
provider.receiveAddress = wallet.receiveAddress;
provider.changeAddress = wallet.changeAddress;
});
callback();
});
@ -266,20 +278,32 @@ WalletDB.prototype.createAddress = function createAddress(id, change, callback)
callback = utils.ensure(callback);
this.get(id, function(err, json) {
this.get(id, function(err, wallet) {
if (err)
return callback(err);
if (!wallet)
return callback(new Error('No wallet.'));
address = wallet.createAddress(change);
self.save(wallet, function(err) {
if (err)
wallet.createAddress(change, function(err) {
if (err) {
wallet.destroy();
return callback(err);
}
return callback(null, address);
wallet.destroy();
if (!self.providers[id])
return callback();
self.providers[id].forEach(function(provider) {
provider.receiveDepth = wallet.receiveDepth;
provider.changeDepth = wallet.changeDepth;
provider.receiveAddress = wallet.receiveAddress;
provider.changeAddress = wallet.changeAddress;
});
callback();
});
});
};
@ -304,16 +328,33 @@ WalletDB.prototype.modifyKey = function modifyKey(id, key, remove, callback) {
if (!wallet)
return callback(new Error('No wallet.'));
try {
if (!remove)
wallet.addKey(key);
else
wallet.removeKey(key);
} catch (e) {
return callback(e);
function done(err) {
if (err) {
wallet.destroy();
return callback(err);
}
wallet.destroy();
if (!self.providers[id])
return callback();
self.providers[id].forEach(function(provider) {
provider.keys = wallet.keys.slice();
provider.initialized = wallet.initialized;
});
callback();
}
self.save(wallet, callback);
try {
if (!remove)
wallet.addKey(key, done);
else
wallet.removeKey(key, done);
} catch (e) {
return done(e);
}
});
};
@ -326,29 +367,8 @@ WalletDB.prototype.modifyKey = function modifyKey(id, key, remove, callback) {
*/
WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) {
var self = this;
var data = new Buffer(JSON.stringify(json), 'utf8');
var batch;
this.db.put('w/' + id, data, function(err) {
if (err)
return callback(err);
batch = self.db.batch();
Object.keys(json.addressMap).forEach(function(address) {
if (self.tx.filter)
self.tx.filter.add(address, 'hex');
batch.put('W/' + address + '/' + json.id, DUMMY);
});
return batch.write(function(err) {
if (err)
return callback(err);
return callback(null, json);
});
});
this.db.put('w/' + id, data, callback);
};
/**
@ -359,7 +379,6 @@ WalletDB.prototype.saveJSON = function saveJSON(id, json, callback) {
WalletDB.prototype.removeJSON = function removeJSON(id, callback) {
var self = this;
var batch;
callback = utils.ensure(callback);
@ -367,22 +386,11 @@ WalletDB.prototype.removeJSON = function removeJSON(id, callback) {
if (err)
return callback(err);
batch = self.db.batch();
Object.keys(json.addressMap).forEach(function(address) {
batch.del('W/' + address + '/' + json.id);
});
batch.write(function(err) {
if (err)
self.db.del('w/' + id, function(err) {
if (err && err.type !== 'NotFoundError')
return callback(err);
self.db.del(key, function(err) {
if (err && err.type !== 'NotFoundError')
return callback(err);
return callback(null, json);
});
return callback(null, json);
});
});
};
@ -436,13 +444,18 @@ WalletDB.prototype.get = function get(id, callback) {
try {
options = bcoin.wallet.parseJSON(options);
options.provider = new Provider(self);
options.db = self;
wallet = new bcoin.wallet(options);
} catch (e) {
return callback(e);
}
return callback(null, wallet);
wallet.open(function(err) {
if (err)
return callback(err);
return callback(null, wallet);
});
});
};
@ -503,11 +516,11 @@ WalletDB.prototype.create = function create(options, callback) {
if (self.network.witness)
options.witness = options.witness !== false;
options.provider = new Provider(self);
options.network = self.network;
options.db = self;
wallet = new bcoin.wallet(options);
self.save(wallet, function(err) {
wallet.open(function(err) {
if (err)
return callback(err);
@ -541,13 +554,107 @@ WalletDB.prototype.ensure = function ensure(options, callback) {
try {
options = bcoin.wallet.parseJSON(json);
options.provider = new Provider(self);
options.db = self;
wallet = new bcoin.wallet(options);
} catch (e) {
return callback(e);
}
return callback(null, wallet);
wallet.open(function(err) {
if (err)
return callback(err);
return callback(null, wallet);
});
});
};
WalletDB.prototype.saveAddress = function saveAddress(id, address, callback) {
var self = this;
var hashes = [];
var batch = this.db.batch();
if (!Array.isArray(address))
address = [address];
address.forEach(function(address) {
hashes.push([address.getKeyHash('hex'), address.path]);
if (address.type === 'multisig')
hashes.push([address.getScriptHash('hex'), address.path]);
if (address.witness)
hashes.push([address.getProgramHash('hex'), address.path]);
});
utils.forEach(hashes, function(hash, next) {
if (self.tx.filter)
self.tx.filter.add(hash[0], 'hex');
self.db.fetch('W/' + hash[0], function(json) {
return JSON.parse(json.toString('utf8'));
}, function(err, json) {
if (err)
return next(err);
if (!json) {
json = {
wallets: [],
path: hash[1]
};
}
if (json.wallets.indexOf(id) !== -1)
return next();
json.wallets.push(id);
json = new Buffer(JSON.stringify(json), 'utf8');
batch.put('W/' + hash[0], json);
next();
});
}, function(err) {
if (err)
return callback(err);
batch.write(callback);
});
};
WalletDB.prototype.hasAddress = function hasAddress(id, address, callback) {
this.getAddress(id, address, function(err, address) {
if (err)
return callback(err);
return callback(null, !!address);
});
};
WalletDB.prototype.getAddress = function getAddress(id, address, callback) {
var self = this;
this.db.fetch('W/' + address, function(json) {
return JSON.parse(json.toString('utf8'));
}, function(err, address) {
if (err)
return callback(err);
if (!address || address.wallets.indexOf(id) === -1)
return callback();
return callback(null, address);
});
};
WalletDB.prototype.getPath = function getPath(id, address, callback) {
this.getAddress(id, address, function(err, address) {
if (err)
return callback(err);
if (!address)
return callback();
return callback(null, address.path);
});
};
@ -704,15 +811,6 @@ WalletDB.prototype.zapWallet = function zapWallet(id, now, age, callback) {
return this.tx.zap(id, now, age, callback);
};
/**
* Instantiate a {@link Provider}.
* @returns {Provider}
*/
WalletDB.prototype.provider = function provider() {
return new Provider(this);
};
WalletDB.prototype.register = function register(id, provider) {
if (!this.providers[id])
this.providers[id] = [];
@ -763,280 +861,10 @@ WalletDB.prototype.hasListener = function hasListener(id, event) {
return false;
};
/**
* Represents {@link Wallet} Provider. This is what
* allows the {@link Wallet} object to access
* transactions and utxos, as well as listen for
* events like confirmations, etc. Any object that
* follows this model can be used as a wallet provider.
* @exports Provider
* @constructor
* @param {WalletDB} db
* @property {WalletDB} db
* @property {WalletID?} id
*/
function Provider(db) {
if (!(this instanceof Provider))
return new Provider(db);
EventEmitter.call(this);
this.loaded = false;
this.db = db;
this.id = null;
this._init();
}
utils.inherits(Provider, EventEmitter);
Provider.prototype._init = function _init() {
var self = this;
if (this.db.loaded) {
this.loaded = true;
return;
}
this.db.once('open', function() {
self.loaded = true;
self.emit('open');
});
};
/**
* Open the provider, wait for the database to load.
* @param {Function} callback
*/
Provider.prototype.open = function open(callback) {
return this.db.open(callback);
};
/**
* Set the ID, telling the provider backend
* which wallet we want to listen for events on.
* @param {WalletID}
*/
Provider.prototype.setID = function setID(id) {
var self = this;
assert(!this.id, 'ID has already been set.');
this.id = id;
this.db.register(this.id, this);
};
/**
* Close the provider, unlisten on wallet.
* @method
* @param {Function} callback
*/
Provider.prototype.close =
Provider.prototype.destroy = function destroy(callback) {
callback = utils.ensure(callback);
if (!this.id)
return utils.nextTick(callback);
this.db.unregister(this.id, this);
this.db = null;
this.id = null;
return utils.nextTick(callback);
};
/**
* Get all transactions for wallet.
* @param {Function} callback - Returns [Error, {@link TX}[]].
*/
Provider.prototype.getHistory = function getHistory(callback) {
return this.db.getHistory(this.id, callback);
};
/**
* Get all coins for wallet.
* @param {Function} callback - Returns [Error, {@link Coin}[]].
*/
Provider.prototype.getCoins = function getCoins(callback) {
return this.db.getCoins(this.id, callback);
};
/**
* Get all unconfirmed transactions for wallet.
* @param {Function} callback - Returns [Error, {@link TX}[]].
*/
Provider.prototype.getUnconfirmed = function getUnconfirmed(callback) {
return this.db.getUnconfirmed(this.id, callback);
};
/**
* Calculate wallet balance.
* @param {Function} callback - Returns [Error, {@link Balance}].
*/
Provider.prototype.getBalance = function getBalance(callback) {
return this.db.getBalance(this.id, callback);
};
/**
* Get last active timestamp and height.
* @param {Function} callback - Returns [Error, Number(ts), Number(height)].
*/
Provider.prototype.getLastTime = function getLastTime(callback) {
return this.db.getLastTime(this.id, callback);
};
/**
* Get last N transactions.
* @param {Number} limit - Max number of transactions.
* @param {Function} callback - Returns [Error, {@link TX}[]].
*/
Provider.prototype.getLast = function getLast(limit, callback) {
return this.db.getLast(this.id, limit, callback);
};
/**
* Get transactions by timestamp range.
* @param {Object} options
* @param {Number} options.start - Start time.
* @param {Number} options.end - End time.
* @param {Number?} options.limit - Max number of records.
* @param {Boolean?} options.reverse - Reverse order.
* @param {Function} callback - Returns [Error, {@link TX}[]].
*/
Provider.prototype.getRange = function getRange(options, callback) {
return this.db.getRange(this.id, options, callback);
};
/**
* Get transaction.
* @param {Hash} hash
* @param {Function} callback - Returns [Error, {@link TX}].
*/
Provider.prototype.getTX = function getTX(hash, callback) {
return this.db.getTX(hash, callback);
};
/**
* Get coin.
* @param {Hash} hash
* @param {Number} index
* @param {Function} callback - Returns [Error, {@link Coin}].
*/
Provider.prototype.getCoin = function getCoin(hash, index, callback) {
return this.db.getCoin(hash, index, callback);
};
/**
* Fill a transaction with coins (all historical coins).
* @param {TX} tx
* @param {Function} callback - Returns [Error, {@link TX}].
*/
Provider.prototype.fillHistory = function fillHistory(tx, callback) {
return this.db.fillHistory(tx, callback);
};
/**
* Fill a transaction with coins.
* @param {TX} tx
* @param {Function} callback - Returns [Error, {@link TX}].
*/
Provider.prototype.fillCoins = function fillCoins(tx, callback) {
return this.db.fillCoins(tx, callback);
};
/**
* Add a transaction to the provider backend (not
* technically necessary if you're implementing a provider).
* @param {TX} tx
* @param {Function} callback
*/
Provider.prototype.addTX = function addTX(tx, callback) {
return this.db.tx.add(tx, callback);
};
/**
* Notify the provider backend that a new address was
* derived (not technically necessary if you're
* implementing a provider).
* @param {Wallet} wallet
* @param {Address} address
*/
Provider.prototype.save = function save(wallet, callback) {
return this.db.save(wallet, callback);
};
/**
* Add a key to the wallet.
* @param {HDPublicKey} key
* @param {Function} callback
*/
Provider.prototype.addKey = function addKey(key, callback) {
return this.db.addKey(this.id, key, false, callback);
};
/**
* Remove a key from the wallet.
* @param {HDPublicKey} key
* @param {Function} callback
*/
Provider.prototype.removeKey = function removeKey(key, callback) {
return this.db.addKey(this.id, key, true, callback);
};
/**
* Create a receiving address.
* @param {Function} callback
*/
Provider.prototype.createReceive = function createReceive(callback) {
return this.db.createAddress(this.id, false, callback);
};
/**
* Create a change address.
* @param {Function} callback
*/
Provider.prototype.createChange = function createChange(callback) {
return this.db.createAddress(this.id, true, callback);
};
/**
* Zap stale transactions.
* @param {Number} now - Current time.
* @param {Number} age - Age delta (delete transactions older than `now - age`).
* @param {Function} callback
*/
Provider.prototype.zap = function zap(now, age, callback) {
return this.db.zapWallet(this.id, now, age, callback);
};
/*
* Expose
*/
exports = WalletDB;
exports.Provider = Provider;
module.exports = exports;

View File

@ -60,9 +60,12 @@ describe('Wallet', function() {
it('should generate new key and address', function() {
var w = bcoin.wallet();
var addr = w.getAddress();
assert(addr);
assert(bcoin.address.validate(addr));
w.open(function(err) {
assert.ifError(err);
var addr = w.getAddress();
assert(addr);
assert(bcoin.address.validate(addr));
});
});
it('should validate existing address', function() {
@ -101,18 +104,16 @@ describe('Wallet', function() {
});
src.addInput(dummyInput);
assert(w.ownOutput(src));
assert(w.ownOutput(src.outputs[0]));
assert(!w.ownOutput(src.outputs[1]));
var tx = bcoin.mtx()
.addInput(src, 0)
.addOutput(w.getAddress(), 5460);
w.sign(tx);
assert(tx.verify(null, true, flags));
cb();
w.sign(tx, function(err) {
assert.ifError(err);
assert(tx.verify(null, true, flags));
cb();
});
});
}
@ -135,33 +136,36 @@ describe('Wallet', function() {
m: 1,
n: 2
});
var k2 = bcoin.hd.fromMnemonic().deriveAccount44(0).hdPublicKey;
w.addKey(k2);
w.open(function(err) {
assert.ifError(err);
var k2 = bcoin.hd.fromMnemonic().deriveAccount44(0).hdPublicKey;
w.addKey(k2, function(err) {
assert.ifError(err);
// Input transcation
var src = bcoin.mtx({
outputs: [{
value: 5460 * 2,
m: 1,
keys: [ w.getPublicKey(), k2.derive('m/0/0').publicKey ]
}, {
value: 5460 * 2,
address: bcoin.address.fromData(new Buffer([])).toBase58()
}]
});
src.addInput(dummyInput);
// Input transcation
var src = bcoin.mtx({
outputs: [{
value: 5460 * 2,
m: 1,
keys: [ w.getPublicKey(), k2.derive('m/0/0').publicKey ]
}, {
value: 5460 * 2,
address: bcoin.address.fromData(new Buffer([])).toBase58()
}]
var tx = bcoin.mtx()
.addInput(src, 0)
.addOutput(w.getAddress(), 5460);
var maxSize = tx.maxSize();
w.sign(tx, function(err) {
assert.ifError(err);
assert(tx.render().length <= maxSize);
assert(tx.verify());
});
});
});
src.addInput(dummyInput);
assert(w.ownOutput(src));
assert(w.ownOutput(src.outputs[0]));
assert(!w.ownOutput(src.outputs[1]));
var tx = bcoin.mtx()
.addInput(src, 0)
.addOutput(w.getAddress(), 5460);
var maxSize = tx.maxSize();
w.sign(tx);
assert(tx.render().length <= maxSize);
assert(tx.verify());
});
var dw, di;
@ -176,32 +180,44 @@ describe('Wallet', function() {
var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000);
t1.addInput(dummyInput);
// balance: 51000
w.sign(t1);
// w.sign(t1);
w.sign(t1, function(err) {
assert.ifError(err);
var t2 = bcoin.mtx().addInput(t1, 0) // 50000
.addOutput(w, 24000)
.addOutput(w, 24000);
di = t2.inputs[0];
// balance: 49000
w.sign(t2);
// w.sign(t2);
w.sign(t2, function(err) {
assert.ifError(err);
var t3 = bcoin.mtx().addInput(t1, 1) // 1000
.addInput(t2, 0) // 24000
.addOutput(w, 23000);
// balance: 47000
w.sign(t3);
// w.sign(t3);
w.sign(t3, function(err) {
assert.ifError(err);
var t4 = bcoin.mtx().addInput(t2, 1) // 24000
.addInput(t3, 0) // 23000
.addOutput(w, 11000)
.addOutput(w, 11000);
// balance: 22000
w.sign(t4);
// w.sign(t4);
w.sign(t4, function(err) {
assert.ifError(err);
var f1 = bcoin.mtx().addInput(t4, 1) // 11000
.addOutput(f, 10000);
// balance: 11000
w.sign(f1);
// w.sign(f1);
w.sign(f1, function(err) {
assert.ifError(err);
var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed)
.addOutput(w, 500);
// Script inputs but do not sign
w.scriptInputs(fake);
// w.scriptInputs(fake);
w.scriptInputs(fake, function(err) {
assert.ifError(err);
// Fake signature
fake.inputs[0].script.code[0] = new Buffer([0,0,0,0,0,0,0,0,0]);
// balance: 11000
@ -265,6 +281,12 @@ describe('Wallet', function() {
});
});
});
});
});
});
});
});
});
});
});
@ -304,7 +326,8 @@ describe('Wallet', function() {
var t2 = bcoin.mtx().addOutput(w2, 5460);
w1.fill(t2, { rate: 10000, round: true }, function(err) {
assert.ifError(err);
w1.sign(t2);
w1.sign(t2, function(err) {
assert.ifError(err);
assert(t2.verify());
assert.equal(t2.getInputValue(), 16380);
@ -322,6 +345,7 @@ describe('Wallet', function() {
cb();
});
});
});
});
});
});
@ -350,7 +374,8 @@ describe('Wallet', function() {
var t2 = bcoin.mtx().addOutput(w2, 5460);
w1.fill(t2, { rate: 10000 }, function(err) {
assert.ifError(err);
w1.sign(t2);
w1.sign(t2, function(err) {
assert.ifError(err);
assert(t2.verify());
assert.equal(t2.getInputValue(), 16380);
@ -374,6 +399,7 @@ describe('Wallet', function() {
cb();
});
});
});
});
});
});
@ -444,24 +470,36 @@ describe('Wallet', function() {
tx.outputs[tx.outputs.length - 1].value = left;
// Sign transaction
assert.equal(w1.sign(tx), 2);
assert.equal(w2.sign(tx), 1);
w1.sign(tx, function(err, total) {
assert.ifError(err);
assert.equal(total, 2);
w2.sign(tx, function(err, total) {
assert.ifError(err);
assert.equal(total, 1);
// Verify
assert.equal(tx.verify(), true);
// Verify
assert.equal(tx.verify(), true);
// Sign transaction using `inputs` and `off` params.
tx.inputs.length = 0;
tx.addInput(coins1[1]);
tx.addInput(coins1[2]);
tx.addInput(coins2[1]);
assert.equal(w1.sign(tx), 2);
assert.equal(w2.sign(tx), 1);
// Sign transaction using `inputs` and `off` params.
tx.inputs.length = 0;
tx.addInput(coins1[1]);
tx.addInput(coins1[2]);
tx.addInput(coins2[1]);
w1.sign(tx, function(err, total) {
assert.ifError(err);
assert.equal(total, 2);
w2.sign(tx, function(err, total) {
assert.ifError(err);
assert.equal(total, 1);
// Verify
assert.equal(tx.verify(), true);
// Verify
assert.equal(tx.verify(), true);
cb();
cb();
});
});
});
});
});
});
});
@ -520,18 +558,13 @@ describe('Wallet', function() {
], function(err) {
assert.ifError(err);
w1.addKey(w2);
w1.addKey(w3);
w2.addKey(w1);
w2.addKey(w3);
w3.addKey(w1);
w3.addKey(w2);
utils.serial([
wdb.save.bind(wdb, w1),
wdb.save.bind(wdb, w2),
wdb.save.bind(wdb, w3),
wdb.save.bind(wdb, receive)
w1.addKey.bind(w1, w2),
w1.addKey.bind(w1, w3),
w2.addKey.bind(w2, w1),
w2.addKey.bind(w2, w3),
w3.addKey.bind(w3, w1),
w3.addKey.bind(w3, w2)
], function(err) {
assert.ifError(err);
@ -563,8 +596,6 @@ describe('Wallet', function() {
utx.addInput(dummyInput);
assert(w1.ownOutput(utx.outputs[0]));
// Simulate a confirmation
utx.ps = 0;
utx.ts = 1;
@ -595,10 +626,12 @@ describe('Wallet', function() {
w1.fill(send, { rate: 10000, round: true }, function(err) {
assert.ifError(err);
w1.sign(send);
w1.sign(send, function(err) {
assert.ifError(err);
assert(!send.verify(null, true, flags));
w2.sign(send);
w2.sign(send, function(err) {
assert.ifError(err);
assert(send.verify(null, true, flags));
@ -641,14 +674,16 @@ describe('Wallet', function() {
w3 = bcoin.wallet.fromJSON(w3.toJSON());
assert.equal(w3.receiveDepth, 2);
assert.equal(w3.changeDepth, 2);
assert.equal(w3.getAddress(), addr);
assert.equal(w3.changeAddress.getAddress(), change);
//assert.equal(w3.getAddress(), addr);
//assert.equal(w3.changeAddress.getAddress(), change);
cb();
});
});
});
});
});
});
});
});
});