wallet map.
This commit is contained in:
parent
86027e9668
commit
5095f78dbf
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user