wallet map.

This commit is contained in:
Christopher Jeffrey 2016-06-26 02:40:44 -07:00
parent 86027e9668
commit 5095f78dbf
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 269 additions and 80 deletions

View File

@ -79,6 +79,12 @@ function Network(options) {
Network.primary = null;
Network.main = null;
Network.testnet = null;
Network.regtest = null;
Network.segnet3 = null;
Network.segnet4 = null;
/**
* Update the height of the network.
* @param {Number} height

View File

@ -110,6 +110,131 @@ TXDB.prototype._testFilter = function _testFilter(addresses) {
return false;
};
// Each address can potentially map to multiple
// accounts and wallets due to the fact that
// multisig accounts can have shared addresses.
// An address could map to 2 accounts on different
// wallets, or 2 accounts on the same wallet!
// In summary, bitcoin is hard. Use Bobchain instead.
//
// Table:
// [address-hash] -> [array of Path objects]
// '1edc6b6858fd12c64b26d8bd1e0e50d44b5bafb9':
// [Path {
// id: 'WLTZ3f5mMBsgWr1TcLzAdtLD8pkLcmWuBfPt',
// name: 'default',
// account: 0,
// change: 0,
// index: 0
// }]
//
// What we need:
// Table: Above.
// All: uniqified paths by id/name
// Outputs: Technically uniqified by ID, but id/name works too.
// What they need (api):
// outputs: [ - SAME AS OUTPUTS ABOVE!!!
// // Sum of value:
// { value: 0, id: wallet-id, name: account, index: account }
// ]
function WalletMap(table) {
var i, j, keys, key, paths, path;
this.inputs = [];
this.outputs = [];
this.paths = [];
this.accounts = null;
this.wallets = [];
this.table = null;
keys = Object.keys(table);
// Flatten paths and push all of
// them onto the `paths` array.
for (i = 0; i < keys.length; i++) {
key = keys[i];
paths = table[key];
for (j = 0; j < paths.length; j++) {
path = paths[j];
this.wallets.push(path.id);
this.paths.push(path);
}
}
this.wallets = utils.uniq(this.wallets);
this.accounts = uniq(this.paths);
this.table = table;
}
WalletMap.fromTX = function fromTX(table, tx) {
var map = new WalletMap(table);
var i, input, output;
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
map.inputs.push(MapMember.fromMember(table, input));
}
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
map.outputs.push(MapMember.fromMember(table, output));
}
return map;
};
WalletMap.prototype.hasPaths = function hasPaths(address) {
var paths;
if (!address)
return false;
paths = this.table[address];
return paths && paths.length !== 0;
};
WalletMap.prototype.getPaths = function getPaths(address) {
return this.table[address];
};
function MapMember() {
this.value = 0;
this.hash = null;
this.wallets = [];
this.paths = [];
}
MapMember.fromMember = function fromMember(table, io) {
var hash = io.getHash('hex');
var member = new MapMember();
var i, paths;
member.value = io.coin
? io.coin.value
: io.value || 0;
if (!hash)
return member;
paths = table[hash];
assert(paths);
member.hash = hash;
member.paths = paths;
for (i = 0; i < paths.length; i++)
member.wallets.push(paths[i].id);
member.wallets = utils.uniq(member.wallets);
return member;
};
/**
* Map a transactions' addresses to wallet IDs.
* @param {TX} tx
@ -119,9 +244,7 @@ TXDB.prototype._testFilter = function _testFilter(addresses) {
TXDB.prototype.getMap = function getMap(tx, callback) {
var i, input, output, address, addresses, map;
input = tx.getInputHashes('hex');
output = tx.getOutputHashes('hex');
addresses = utils.uniq(input.concat(output));
addresses = tx.getHashes('hex');
if (!this._testFilter(addresses))
return callback();
@ -130,31 +253,12 @@ TXDB.prototype.getMap = function getMap(tx, callback) {
if (err)
return callback(err);
if (table.count === 0)
if (!table)
return callback();
map = {
table: table,
input: [],
output: [],
all: []
};
map = WalletMap.fromTX(table, tx);
for (i = 0; i < input.length; i++) {
address = input[i];
assert(map.table[address]);
map.input = map.input.concat(map.table[address]);
}
for (i = 0; i < output.length; i++) {
address = output[i];
assert(map.table[address]);
map.output = map.output.concat(map.table[address]);
}
map.input = uniq(map.input);
map.output = uniq(map.output);
map.all = uniq(map.input.concat(map.output));
utils.print(map);
return callback(null, map);
});
@ -168,7 +272,8 @@ TXDB.prototype.getMap = function getMap(tx, callback) {
TXDB.prototype.mapAddresses = function mapAddresses(address, callback) {
var self = this;
var table = { count: 0 };
var table = {};
var count = 0;
var i, keys, values;
return utils.forEachSerial(address, function(address, next) {
@ -190,7 +295,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) {
assert(!table[address]);
table[address] = values;
table.count += values.length;
count += values.length;
return next();
});
@ -198,6 +303,9 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) {
if (err)
return callback(err);
if (count === 0)
return callback();
return callback(null, table);
});
};
@ -338,8 +446,8 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY);
}
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
for (i = 0; i < map.accounts.length; i++) {
path = map.accounts[i];
id = path.id + '/' + path.account;
batch.put('T/' + id + '/' + hash, DUMMY);
if (tx.ts === 0) {
@ -362,7 +470,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
address = input.getHash('hex');
// Only add orphans if this input is ours.
if (!address || !map.table[address].length)
if (!map.hasPaths(address))
return next();
self.getCoin(prevout.hash, prevout.index, function(err, coin) {
@ -385,7 +493,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
updated = true;
paths = map.table[address];
paths = map.getPaths(address);
for (j = 0; j < paths.length; j++) {
path = paths[j];
@ -462,7 +570,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
var coin;
// Do not add unspents for outputs that aren't ours.
if (!address || !map.table[address].length)
if (!map.hasPaths(address))
return next();
if (output.script.isUnspendable())
@ -521,7 +629,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
return next(err);
if (!orphans) {
paths = map.table[address];
paths = map.getPaths(address);
for (j = 0; j < paths.length; j++) {
path = paths[j];
@ -749,8 +857,8 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
batch.del('m/' + pad32(existing.ps) + '/' + hash);
batch.put('m/' + pad32(tx.ts) + '/' + hash, DUMMY);
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
for (i = 0; i < map.accounts.length; i++) {
path = map.accounts[i];
id = path.id + '/' + path.account;
batch.del('P/' + id + '/' + hash);
batch.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY);
@ -762,7 +870,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
var address = output.getHash('hex');
// Only update coins if this output is ours.
if (!address || !map.table[address].length)
if (!map.hasPaths(address))
return next();
self.getCoin(hash, i, function(err, coin) {
@ -888,8 +996,8 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
batch.del('m/' + pad32(tx.ts) + '/' + hash);
}
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
for (i = 0; i < map.accounts.length; i++) {
path = map.accounts[i];
id = path.id + '/' + path.account;
batch.del('T/' + id + '/' + hash);
if (tx.ts === 0) {
@ -916,10 +1024,10 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
if (!input.coin)
continue;
if (!address || !map.table[address].length)
if (!map.hasPaths(address))
continue;
paths = map.table[address];
paths = map.getPaths(address);
for (j = 0; j < paths.length; j++) {
path = paths[j];
@ -937,13 +1045,13 @@ TXDB.prototype._remove = function remove(tx, map, callback, force) {
key = hash + '/' + i;
address = output.getHash('hex');
if (!address || !map.table[address].length)
if (!map.hasPaths(address))
continue;
if (output.script.isUnspendable())
continue;
paths = map.table[address];
paths = map.getPaths(address);
for (j = 0; j < paths.length; j++) {
path = paths[j];
@ -1040,8 +1148,8 @@ TXDB.prototype._unconfirm = function unconfirm(tx, map, callback, force) {
batch.del('m/' + pad32(ts) + '/' + hash);
batch.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY);
for (i = 0; i < map.all.length; i++) {
path = map.all[i];
for (i = 0; i < map.accounts.length; i++) {
path = map.accounts[i];
id = path.id + '/' + path.account;
batch.put('P/' + id + '/' + hash, DUMMY);
batch.del('H/' + id + '/' + pad32(height) + '/' + hash);

View File

@ -125,35 +125,35 @@ WalletDB.prototype._init = function _init() {
this.tx.on('tx', function(tx, map) {
self.emit('tx', tx, map);
map.all.forEach(function(path) {
map.accounts.forEach(function(path) {
self.fire(path.id, 'tx', tx, path.name);
});
});
this.tx.on('conflict', function(tx, map) {
self.emit('conflict', tx, map);
map.all.forEach(function(path) {
map.accounts.forEach(function(path) {
self.fire(path.id, 'conflict', tx, path.name);
});
});
this.tx.on('confirmed', function(tx, map) {
self.emit('confirmed', tx, map);
map.all.forEach(function(path) {
map.accounts.forEach(function(path) {
self.fire(path.id, 'confirmed', tx, path.name);
});
});
this.tx.on('unconfirmed', function(tx, map) {
self.emit('unconfirmed', tx, map);
map.all.forEach(function(path) {
map.accounts.forEach(function(path) {
self.fire(path.id, 'unconfirmed', tx, path.name);
});
});
this.tx.on('updated', function(tx, map) {
self.emit('updated', tx, map);
map.all.forEach(function(path) {
map.accounts.forEach(function(path) {
self.fire(path.id, 'updated', tx, path.name);
});
self.updateBalances(tx, map, function(err, balances) {
@ -186,21 +186,28 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) {
var self = this;
var balances = {};
utils.forEachSerial(map.output, function(path, next) {
if (self.listeners('balance').length === 0
&& !self.hasListener(path.id, 'balance')) {
return next();
}
utils.forEachSerial(map.outputs, function(output, next) {
utils.forEachSerial(output.wallets, function(id, next) {
if (self.listeners('balance').length === 0
&& !self.hasListener(id, 'balance')) {
return next();
}
if (balances[path.id] != null)
return next();
if (balances[id] != null)
return next();
self.getBalance(path.id, function(err, balance) {
self.getBalance(id, function(err, balance) {
if (err)
return next(err);
balances[id] = balance;
next();
});
}, function(err) {
if (err)
return next(err);
balances[path.id] = balance;
next();
});
}, function(err) {
@ -213,12 +220,19 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) {
WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) {
var self = this;
utils.forEachSerial(map.output, function(path, next) {
self.syncOutputDepth(path.id, tx, function(err, receive, change) {
utils.forEachSerial(map.outputs, function(output, next) {
utils.forEachSerial(output.wallets, function(id, next) {
self.syncOutputDepth(id, tx, function(err, receive, change) {
if (err)
return next(err);
self.fire(id, 'address', receive, change);
self.emit('address', receive, change, map);
next();
});
}, function(err) {
if (err)
return next(err);
self.fire(path.id, 'address', receive, change, path.name);
self.emit('address', receive, change, map);
next();
});
}, callback);
@ -702,7 +716,7 @@ WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) {
if (paths[id])
return next();
paths[id] = hash[1];
paths[id] = Path.fromKeyRing(id, hash[1]);
batch.put('W/' + hash[0], serializePaths(paths));
@ -1143,6 +1157,75 @@ WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) {
});
};
/**
* Path
* @constructor
*/
function Path() {
this.id = null;
this.name = null;
this.account = 0;
this.change = 0;
this.index = 0;
this.hash = null;
}
Path.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data);
this.id = p.readVarString('utf8');
this.name = p.readVarString('utf8');
this.account = p.readU32();
this.change = p.readU32();
this.index = p.readU32();
return this;
};
Path.fromRaw = function fromRaw(data) {
return new Path().fromRaw(data);
};
Path.prototype.fromKeyRing = function fromKeyRing(id, address) {
this.id = id;
this.name = address.name;
this.account = address.account;
this.change = address.change;
this.index = address.index;
return this;
};
Path.fromKeyRing = function fromKeyRing(id, address) {
return new Path().fromKeyRing(id, address);
};
Path.prototype.toPath = function() {
return 'm/' + this.account
+ '\'/' + this.change
+ '/' + this.index;
};
Path.prototype.inspect = function() {
return '<Path: ' + this.id
+ '/' + this.name
+ ': ' + this.toPath()
+ '>';
};
Path.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);
p.writeVarString(this.id, 'utf8');
p.writeVarString(this.name, 'utf8');
p.writeU32(this.account);
p.writeU32(this.change);
p.writeU32(this.index);
if (!writer)
p = p.render();
return p;
};
/*
* Helpers
*/
@ -1150,17 +1233,11 @@ WalletDB.prototype.getRedeem = function getRedeem(id, hash, callback) {
function parsePaths(data) {
var p = new BufferReader(data);
var out = {};
var id;
var path;
while (p.left()) {
id = p.readVarString('utf8');
out[id] = {
id: id,
name: p.readVarString('utf8'),
account: p.readU32(),
change: p.readU32(),
index: p.readU32()
};
path = Path.fromRaw(p);
out[path.id] = path;
}
return out;
@ -1174,11 +1251,7 @@ function serializePaths(out) {
for (i = 0; i < keys.length; i++) {
id = keys[i];
path = out[id];
p.writeVarString(id, 'utf8');
p.writeVarString(path.name, 'utf8');
p.writeU32(path.account);
p.writeU32(path.change);
p.writeU32(path.index);
path.toRaw(p);
}
return p.render();

View File

@ -433,8 +433,10 @@ describe('Wallet', function() {
var t3 = bcoin.mtx().addOutput(w2, 15000);
w1.fill(t3, { rate: 10000 }, function(err) {
assert(err);
assert(balance.total === 5460);
cb();
setTimeout(function() {
assert(balance.total === 5460);
cb();
}, 100);
});
});
});