1749 lines
35 KiB
JavaScript
1749 lines
35 KiB
JavaScript
/*!
|
|
* walletdb.js - storage for wallets
|
|
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
|
|
* Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var AsyncObject = require('../utils/async');
|
|
var utils = require('../utils/utils');
|
|
var co = require('../utils/co');
|
|
var Locker = require('../utils/locker');
|
|
var LRU = require('../utils/lru');
|
|
var crypto = require('../crypto/crypto');
|
|
var assert = require('assert');
|
|
var constants = require('../protocol/constants');
|
|
var Network = require('../protocol/network');
|
|
var BufferReader = require('../utils/reader');
|
|
var BufferWriter = require('../utils/writer');
|
|
var Path = require('./path');
|
|
var Wallet = require('./wallet');
|
|
var Account = require('./account');
|
|
var ldb = require('../db/ldb');
|
|
var Bloom = require('../utils/bloom');
|
|
var Logger = require('../node/logger');
|
|
var TX = require('../primitives/tx');
|
|
var TXDB = require('./txdb');
|
|
|
|
/*
|
|
* Database Layout:
|
|
* p[addr-hash] -> wallet ids
|
|
* P[wid][addr-hash] -> path data
|
|
* w[wid] -> wallet
|
|
* l[id] -> wid
|
|
* a[wid][index] -> account
|
|
* i[wid][name] -> account index
|
|
* t[wid]* -> txdb
|
|
* R -> tip
|
|
* b[hash] -> wallet block
|
|
* e[hash] -> tx->wid map
|
|
*/
|
|
|
|
var layout = {
|
|
p: function(hash) {
|
|
var key = new Buffer(1 + (hash.length / 2));
|
|
key[0] = 0x70;
|
|
key.write(hash, 1, 'hex');
|
|
return key;
|
|
},
|
|
pp: function(key) {
|
|
return key.toString('hex', 1);
|
|
},
|
|
P: function(wid, hash) {
|
|
var key = new Buffer(1 + 4 + (hash.length / 2));
|
|
key[0] = 0x50;
|
|
key.writeUInt32BE(wid, 1, true);
|
|
key.write(hash, 5, 'hex');
|
|
return key;
|
|
},
|
|
Pp: function(key) {
|
|
return key.toString('hex', 5);
|
|
},
|
|
w: function(wid) {
|
|
var key = new Buffer(5);
|
|
key[0] = 0x77;
|
|
key.writeUInt32BE(wid, 1, true);
|
|
return key;
|
|
},
|
|
ww: function(key) {
|
|
return key.readUInt32BE(1, true);
|
|
},
|
|
l: function(id) {
|
|
var len = Buffer.byteLength(id, 'ascii');
|
|
var key = new Buffer(1 + len);
|
|
key[0] = 0x6c;
|
|
if (len > 0)
|
|
key.write(id, 1, 'ascii');
|
|
return key;
|
|
},
|
|
ll: function(key) {
|
|
return key.toString('ascii', 1);
|
|
},
|
|
a: function a(wid, index) {
|
|
var key = new Buffer(9);
|
|
key[0] = 0x61;
|
|
key.writeUInt32BE(wid, 1, true);
|
|
key.writeUInt32BE(index, 5, true);
|
|
return key;
|
|
},
|
|
i: function i(wid, name) {
|
|
var len = Buffer.byteLength(name, 'ascii');
|
|
var key = new Buffer(5 + len);
|
|
key[0] = 0x69;
|
|
key.writeUInt32BE(wid, 1, true);
|
|
if (len > 0)
|
|
key.write(name, 5, 'ascii');
|
|
return key;
|
|
},
|
|
ii: function ii(key) {
|
|
return [key.readUInt32BE(1, true), key.toString('ascii', 5)];
|
|
},
|
|
R: new Buffer([0x52]),
|
|
b: function b(hash) {
|
|
var key = new Buffer(33);
|
|
key[0] = 0x62;
|
|
key.write(hash, 1, 'hex');
|
|
return key;
|
|
},
|
|
e: function e(hash) {
|
|
var key = new Buffer(33);
|
|
key[0] = 0x65;
|
|
key.write(hash, 1, 'hex');
|
|
return key;
|
|
}
|
|
};
|
|
|
|
if (utils.isBrowser)
|
|
layout = require('./browser').walletdb;
|
|
|
|
/**
|
|
* WalletDB
|
|
* @exports WalletDB
|
|
* @constructor
|
|
* @param {Object} options
|
|
* @param {String?} options.name - Database name.
|
|
* @param {String?} options.location - Database file location.
|
|
* @param {String?} options.db - Database backend (`"leveldb"` by default).
|
|
* @param {Boolean?} options.verify - Verify transactions as they
|
|
* come in (note that this will not happen on the worker pool).
|
|
* @property {Boolean} loaded
|
|
*/
|
|
|
|
function WalletDB(options) {
|
|
if (!(this instanceof WalletDB))
|
|
return new WalletDB(options);
|
|
|
|
if (!options)
|
|
options = {};
|
|
|
|
AsyncObject.call(this);
|
|
|
|
this.options = options;
|
|
this.network = Network.get(options.network);
|
|
this.fees = options.fees;
|
|
this.logger = options.logger || Logger.global;
|
|
|
|
this.tip = this.network.genesis.hash;
|
|
this.height = 0;
|
|
this.depth = 0;
|
|
this.wallets = {};
|
|
|
|
// We need one read lock for `get` and `create`.
|
|
// It will hold locks specific to wallet ids.
|
|
this.readLock = new Locker.Mapped();
|
|
this.writeLock = new Locker();
|
|
this.txLock = new Locker();
|
|
|
|
this.widCache = new LRU(10000);
|
|
this.pathMapCache = new LRU(100000);
|
|
|
|
// Try to optimize for up to 1m addresses.
|
|
// We use a regular bloom filter here
|
|
// because we never want members to
|
|
// lose membership, even if quality
|
|
// degrades.
|
|
// Memory used: 1.7mb
|
|
this.filter = this.options.useFilter !== false
|
|
? Bloom.fromRate(1000000, 0.001, -1)
|
|
: null;
|
|
|
|
this.db = ldb({
|
|
location: this.options.location,
|
|
db: this.options.db,
|
|
maxOpenFiles: this.options.maxFiles,
|
|
cacheSize: 8 << 20,
|
|
writeBufferSize: 4 << 20,
|
|
bufferKeys: !utils.isBrowser
|
|
});
|
|
|
|
this._init();
|
|
}
|
|
|
|
utils.inherits(WalletDB, AsyncObject);
|
|
|
|
/**
|
|
* Database layout.
|
|
* @type {Object}
|
|
*/
|
|
|
|
WalletDB.layout = layout;
|
|
|
|
/**
|
|
* Initialize wallet db.
|
|
* @private
|
|
*/
|
|
|
|
WalletDB.prototype._init = function _init() {
|
|
;
|
|
};
|
|
|
|
/**
|
|
* Open the walletdb, wait for the database to load.
|
|
* @alias WalletDB#open
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._open = co(function* open() {
|
|
yield this.db.open();
|
|
yield this.db.checkVersion('V', 3);
|
|
yield this.writeGenesis();
|
|
|
|
this.depth = yield this.getDepth();
|
|
|
|
this.logger.info(
|
|
'WalletDB loaded (depth=%d, height=%d).',
|
|
this.depth, this.height);
|
|
|
|
yield this.loadFilter();
|
|
});
|
|
|
|
/**
|
|
* Close the walletdb, wait for the database to close.
|
|
* @alias WalletDB#close
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._close = co(function* close() {
|
|
var keys = Object.keys(this.wallets);
|
|
var i, key, wallet;
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
key = keys[i];
|
|
wallet = this.wallets[key];
|
|
yield wallet.destroy();
|
|
}
|
|
|
|
yield this.db.close();
|
|
});
|
|
|
|
/**
|
|
* Backup the wallet db.
|
|
* @param {String} path
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.backup = function backup(path) {
|
|
return this.db.backup(path);
|
|
};
|
|
|
|
/**
|
|
* Get current wallet wid depth.
|
|
* @private
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getDepth = co(function* getDepth() {
|
|
var iter, item, depth;
|
|
|
|
// This may seem like a strange way to do
|
|
// this, but updating a global state when
|
|
// creating a new wallet is actually pretty
|
|
// damn tricky. There would be major atomicity
|
|
// issues if updating a global state inside
|
|
// a "scoped" state. So, we avoid all the
|
|
// nonsense of adding a global lock to
|
|
// walletdb.create by simply seeking to the
|
|
// highest wallet wid.
|
|
iter = this.db.iterator({
|
|
gte: layout.w(0x00000000),
|
|
lte: layout.w(0xffffffff),
|
|
reverse: true
|
|
});
|
|
|
|
item = yield iter.next();
|
|
|
|
if (!item)
|
|
return 1;
|
|
|
|
yield iter.end();
|
|
|
|
depth = layout.ww(item.key);
|
|
|
|
return depth + 1;
|
|
});
|
|
|
|
/**
|
|
* Start batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
*/
|
|
|
|
WalletDB.prototype.start = function start(wallet) {
|
|
assert(!wallet.current, 'Batch already started.');
|
|
wallet.current = this.db.batch();
|
|
return wallet.current;
|
|
};
|
|
|
|
/**
|
|
* Drop batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
*/
|
|
|
|
WalletDB.prototype.drop = function drop(wallet) {
|
|
var batch = this.batch(wallet);
|
|
wallet.current = null;
|
|
batch.clear();
|
|
};
|
|
|
|
/**
|
|
* Clear batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
*/
|
|
|
|
WalletDB.prototype.clear = function clear(wallet) {
|
|
var batch = this.batch(wallet);
|
|
batch.clear();
|
|
};
|
|
|
|
/**
|
|
* Get batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
* @returns {Leveldown.Batch}
|
|
*/
|
|
|
|
WalletDB.prototype.batch = function batch(wallet) {
|
|
assert(wallet.current, 'Batch does not exist.');
|
|
return wallet.current;
|
|
};
|
|
|
|
/**
|
|
* Save batch.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.commit = function commit(wallet) {
|
|
var batch = wallet.current;
|
|
wallet.current = null;
|
|
return batch.write();
|
|
};
|
|
|
|
/**
|
|
* Load the bloom filter into memory.
|
|
* @private
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.loadFilter = co(function* loadFilter() {
|
|
var iter, item, hash;
|
|
|
|
if (!this.filter)
|
|
return;
|
|
|
|
iter = this.db.iterator({
|
|
gte: layout.p(constants.NULL_HASH),
|
|
lte: layout.p(constants.HIGH_HASH)
|
|
});
|
|
|
|
for (;;) {
|
|
item = yield iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
hash = layout.pp(item.key);
|
|
this.filter.add(hash, 'hex');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Test the bloom filter against an array of address hashes.
|
|
* @private
|
|
* @param {Hash[]} hashes
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
WalletDB.prototype.testFilter = function testFilter(hash) {
|
|
if (!this.filter)
|
|
return true;
|
|
|
|
return this.filter.test(hash, 'hex');
|
|
};
|
|
|
|
/**
|
|
* Dump database (for debugging).
|
|
* @returns {Promise} - Returns Object.
|
|
*/
|
|
|
|
WalletDB.prototype.dump = function dump() {
|
|
return this.db.dump();
|
|
};
|
|
|
|
/**
|
|
* Register an object with the walletdb.
|
|
* @param {Object} object
|
|
*/
|
|
|
|
WalletDB.prototype.register = function register(wallet) {
|
|
assert(!this.wallets[wallet.wid]);
|
|
this.wallets[wallet.wid] = wallet;
|
|
};
|
|
|
|
/**
|
|
* Unregister a object with the walletdb.
|
|
* @param {Object} object
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
WalletDB.prototype.unregister = function unregister(wallet) {
|
|
assert(this.wallets[wallet.wid]);
|
|
delete this.wallets[wallet.wid];
|
|
};
|
|
|
|
/**
|
|
* Map wallet label to wallet id.
|
|
* @param {String} label
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getWalletID = co(function* getWalletID(id) {
|
|
var wid, data;
|
|
|
|
if (!id)
|
|
return;
|
|
|
|
if (typeof id === 'number')
|
|
return id;
|
|
|
|
wid = this.widCache.get(id);
|
|
|
|
if (wid)
|
|
return wid;
|
|
|
|
data = yield this.db.get(layout.l(id));
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
wid = data.readUInt32LE(0, true);
|
|
|
|
this.widCache.set(id, wid);
|
|
|
|
return wid;
|
|
});
|
|
|
|
/**
|
|
* Get a wallet from the database, setup watcher.
|
|
* @param {WalletID} wid
|
|
* @returns {Promise} - Returns {@link Wallet}.
|
|
*/
|
|
|
|
WalletDB.prototype.get = co(function* get(id) {
|
|
var wid = yield this.getWalletID(id);
|
|
var unlock;
|
|
|
|
if (!wid)
|
|
return;
|
|
|
|
unlock = yield this.readLock.lock(wid);
|
|
|
|
try {
|
|
return yield this._get(wid);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get a wallet from the database without a lock.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
* @returns {Promise} - Returns {@link Wallet}.
|
|
*/
|
|
|
|
WalletDB.prototype._get = co(function* get(wid) {
|
|
var wallet = this.wallets[wid];
|
|
var data;
|
|
|
|
if (wallet)
|
|
return wallet;
|
|
|
|
data = yield this.db.get(layout.w(wid));
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
wallet = Wallet.fromRaw(this, data);
|
|
|
|
yield wallet.open();
|
|
|
|
this.register(wallet);
|
|
|
|
return wallet;
|
|
});
|
|
|
|
/**
|
|
* Save a wallet to the database.
|
|
* @param {Wallet} wallet
|
|
*/
|
|
|
|
WalletDB.prototype.save = function save(wallet) {
|
|
var wid = wallet.wid;
|
|
var id = wallet.id;
|
|
var batch = this.batch(wallet);
|
|
var buf = new Buffer(4);
|
|
|
|
this.widCache.set(id, wid);
|
|
|
|
batch.put(layout.w(wid), wallet.toRaw());
|
|
|
|
buf.writeUInt32LE(wid, 0, true);
|
|
batch.put(layout.l(id), buf);
|
|
};
|
|
|
|
/**
|
|
* Rename a wallet.
|
|
* @param {Wallet} wallet
|
|
* @param {String} id
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.rename = co(function* rename(wallet, id) {
|
|
var unlock = yield this.writeLock.lock();
|
|
try {
|
|
return yield this._rename(wallet, id);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Rename a wallet without a lock.
|
|
* @private
|
|
* @param {Wallet} wallet
|
|
* @param {String} id
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._rename = co(function* _rename(wallet, id) {
|
|
var old = wallet.id;
|
|
var i, paths, path, batch;
|
|
|
|
if (!utils.isName(id))
|
|
throw new Error('Bad wallet ID.');
|
|
|
|
if (yield this.has(id))
|
|
throw new Error('ID not available.');
|
|
|
|
batch = this.start(wallet);
|
|
batch.del(layout.l(old));
|
|
|
|
wallet.id = id;
|
|
|
|
this.save(wallet);
|
|
|
|
yield this.commit(wallet);
|
|
|
|
this.widCache.remove(old);
|
|
|
|
paths = wallet.pathCache.values();
|
|
|
|
for (i = 0; i < paths.length; i++) {
|
|
path = paths[i];
|
|
path.id = id;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Rename an account.
|
|
* @param {Account} account
|
|
* @param {String} name
|
|
*/
|
|
|
|
WalletDB.prototype.renameAccount = function renameAccount(account, name) {
|
|
var wallet = account.wallet;
|
|
var batch = this.batch(wallet);
|
|
batch.del(layout.i(account.wid, account.name));
|
|
account.name = name;
|
|
this.saveAccount(account);
|
|
};
|
|
|
|
/**
|
|
* Test an api key against a wallet's api key.
|
|
* @param {WalletID} wid
|
|
* @param {String} token
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.auth = co(function* auth(wid, token) {
|
|
var wallet = yield this.get(wid);
|
|
if (!wallet)
|
|
return;
|
|
|
|
if (typeof token === 'string') {
|
|
if (!utils.isHex(token))
|
|
throw new Error('Authentication error.');
|
|
token = new Buffer(token, 'hex');
|
|
}
|
|
|
|
// Compare in constant time:
|
|
if (!crypto.ccmp(token, wallet.token))
|
|
throw new Error('Authentication error.');
|
|
|
|
return wallet;
|
|
});
|
|
|
|
/**
|
|
* Create a new wallet, save to database, setup watcher.
|
|
* @param {Object} options - See {@link Wallet}.
|
|
* @returns {Promise} - Returns {@link Wallet}.
|
|
*/
|
|
|
|
WalletDB.prototype.create = co(function* create(options) {
|
|
var unlock = yield this.writeLock.lock();
|
|
|
|
if (!options)
|
|
options = {};
|
|
|
|
try {
|
|
return yield this._create(options);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Create a new wallet, save to database without a lock.
|
|
* @private
|
|
* @param {Object} options - See {@link Wallet}.
|
|
* @returns {Promise} - Returns {@link Wallet}.
|
|
*/
|
|
|
|
WalletDB.prototype._create = co(function* create(options) {
|
|
var exists = yield this.has(options.id);
|
|
var wallet;
|
|
|
|
if (exists)
|
|
throw new Error('Wallet already exists.');
|
|
|
|
wallet = Wallet.fromOptions(this, options);
|
|
wallet.wid = this.depth++;
|
|
|
|
yield wallet.init(options);
|
|
|
|
this.register(wallet);
|
|
|
|
this.logger.info('Created wallet %s.', wallet.id);
|
|
|
|
return wallet;
|
|
});
|
|
|
|
/**
|
|
* Test for the existence of a wallet.
|
|
* @param {WalletID} id
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.has = co(function* has(id) {
|
|
var wid = yield this.getWalletID(id);
|
|
return wid != null;
|
|
});
|
|
|
|
/**
|
|
* Attempt to create wallet, return wallet if already exists.
|
|
* @param {Object} options - See {@link Wallet}.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.ensure = co(function* ensure(options) {
|
|
var wallet = yield this.get(options.id);
|
|
if (wallet)
|
|
return wallet;
|
|
return yield this.create(options);
|
|
});
|
|
|
|
/**
|
|
* Get an account from the database by wid.
|
|
* @private
|
|
* @param {WalletID} wid
|
|
* @param {Number} index - Account index.
|
|
* @returns {Promise} - Returns {@link Wallet}.
|
|
*/
|
|
|
|
WalletDB.prototype.getAccount = co(function* getAccount(wid, index) {
|
|
var data = yield this.db.get(layout.a(wid, index));
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
return Account.fromRaw(this, data);
|
|
});
|
|
|
|
/**
|
|
* List account names and indexes from the db.
|
|
* @param {WalletID} wid
|
|
* @returns {Promise} - Returns Array.
|
|
*/
|
|
|
|
WalletDB.prototype.getAccounts = co(function* getAccounts(wid) {
|
|
var map = [];
|
|
var i, items, item, name, index, accounts;
|
|
|
|
items = yield this.db.range({
|
|
gte: layout.i(wid, '\x00'),
|
|
lte: layout.i(wid, '\xff')
|
|
});
|
|
|
|
for (i = 0; i < items.length; i++) {
|
|
item = items[i];
|
|
name = layout.ii(item.key)[1];
|
|
index = item.value.readUInt32LE(0, true);
|
|
map[index] = name;
|
|
}
|
|
|
|
// Get it out of hash table mode.
|
|
accounts = [];
|
|
|
|
for (i = 0; i < map.length; i++) {
|
|
assert(map[i] != null);
|
|
accounts.push(map[i]);
|
|
}
|
|
|
|
return accounts;
|
|
});
|
|
|
|
/**
|
|
* Lookup the corresponding account name's index.
|
|
* @param {WalletID} wid
|
|
* @param {String|Number} name - Account name/index.
|
|
* @returns {Promise} - Returns Number.
|
|
*/
|
|
|
|
WalletDB.prototype.getAccountIndex = co(function* getAccountIndex(wid, name) {
|
|
var index = yield this.db.get(layout.i(wid, name));
|
|
|
|
if (!index)
|
|
return -1;
|
|
|
|
return index.readUInt32LE(0, true);
|
|
});
|
|
|
|
/**
|
|
* Save an account to the database.
|
|
* @param {Account} account
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.saveAccount = function saveAccount(account) {
|
|
var wid = account.wid;
|
|
var wallet = account.wallet;
|
|
var index = account.accountIndex;
|
|
var name = account.name;
|
|
var batch = this.batch(wallet);
|
|
var buf = new Buffer(4);
|
|
|
|
buf.writeUInt32LE(index, 0, true);
|
|
|
|
batch.put(layout.a(wid, index), account.toRaw());
|
|
batch.put(layout.i(wid, name), buf);
|
|
|
|
wallet.accountCache.set(index, account);
|
|
};
|
|
|
|
/**
|
|
* Test for the existence of an account.
|
|
* @param {WalletID} wid
|
|
* @param {String|Number} acct
|
|
* @returns {Promise} - Returns Boolean.
|
|
*/
|
|
|
|
WalletDB.prototype.hasAccount = co(function* hasAccount(wid, index) {
|
|
return yield this.db.has(layout.a(wid, index));
|
|
});
|
|
|
|
/**
|
|
* Lookup the corresponding account name's index.
|
|
* @param {WalletID} wid
|
|
* @param {String|Number} name - Account name/index.
|
|
* @returns {Promise} - Returns Number.
|
|
*/
|
|
|
|
WalletDB.prototype.getWalletsByHash = co(function* getWalletsByHash(hash) {
|
|
var wallets = this.pathMapCache.get(hash);
|
|
var data;
|
|
|
|
if (wallets)
|
|
return wallets;
|
|
|
|
data = yield this.db.get(layout.p(hash));
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
wallets = parseWallets(data);
|
|
|
|
this.pathMapCache.get(hash, wallets);
|
|
|
|
return wallets;
|
|
});
|
|
|
|
/**
|
|
* Save an address to the path map.
|
|
* @param {WalletID} wid
|
|
* @param {KeyRing[]} ring
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.saveKey = function saveKey(wallet, ring) {
|
|
return this.savePath(wallet, ring.toPath());
|
|
};
|
|
|
|
/**
|
|
* Save a path to the path map.
|
|
*
|
|
* The path map exists in the form of:
|
|
* - `p[address-hash] -> wids`
|
|
* - `P[wid][address-hash] -> path`
|
|
*
|
|
* @param {WalletID} wid
|
|
* @param {Path[]} path
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.savePath = co(function* savePath(wallet, path) {
|
|
var wid = wallet.wid;
|
|
var hash = path.hash;
|
|
var batch = this.batch(wallet);
|
|
var wallets, result;
|
|
|
|
if (this.filter)
|
|
this.filter.add(hash, 'hex');
|
|
|
|
this.emit('path', path);
|
|
|
|
wallets = yield this.getWalletsByHash(hash);
|
|
|
|
if (!wallets)
|
|
wallets = [];
|
|
|
|
// Keep these motherfuckers sorted.
|
|
result = utils.binaryInsert(wallets, wid, cmp, true);
|
|
|
|
if (result === -1)
|
|
return;
|
|
|
|
this.pathMapCache.set(hash, wallets);
|
|
wallet.pathCache.set(hash, path);
|
|
|
|
batch.put(layout.p(hash), serializeWallets(wallets));
|
|
batch.put(layout.P(wid, hash), path.toRaw());
|
|
});
|
|
|
|
/**
|
|
* Retrieve path by hash.
|
|
* @param {WalletID} wid
|
|
* @param {Hash} hash
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getPath = co(function* getPath(wid, hash) {
|
|
var data = yield this.db.get(layout.P(wid, hash));
|
|
var path;
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
path = Path.fromRaw(data);
|
|
path.wid = wid;
|
|
path.hash = hash;
|
|
|
|
return path;
|
|
});
|
|
|
|
/**
|
|
* Get all address hashes.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getHashes = function getHashes() {
|
|
return this.db.keys({
|
|
gte: layout.p(constants.NULL_HASH),
|
|
lte: layout.p(constants.HIGH_HASH),
|
|
parse: layout.pp
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get all address hashes.
|
|
* @param {WalletID} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getWalletHashes = function getWalletHashes(wid) {
|
|
return this.db.keys({
|
|
gte: layout.P(wid, constants.NULL_HASH),
|
|
lte: layout.P(wid, constants.HIGH_HASH),
|
|
parse: layout.Pp
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get all paths for a wallet.
|
|
* @param {WalletID} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getWalletPaths = co(function* getWalletPaths(wid) {
|
|
var i, item, items, hash, path;
|
|
|
|
items = yield this.db.range({
|
|
gte: layout.P(wid, constants.NULL_HASH),
|
|
lte: layout.P(wid, constants.HIGH_HASH)
|
|
});
|
|
|
|
for (i = 0; i < items.length; i++) {
|
|
item = items[i];
|
|
hash = layout.Pp(item.key);
|
|
path = Path.fromRaw(item.value);
|
|
|
|
path.hash = hash;
|
|
path.wid = wid;
|
|
|
|
items[i] = path;
|
|
}
|
|
|
|
return items;
|
|
});
|
|
|
|
/**
|
|
* Get all wallet ids.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getWallets = function getWallets() {
|
|
return this.db.keys({
|
|
gte: layout.l('\x00'),
|
|
lte: layout.l('\xff'),
|
|
parse: layout.ll
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Encrypt all imported keys for a wallet.
|
|
* @param {WalletID} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.encryptKeys = co(function* encryptKeys(wallet, key) {
|
|
var wid = wallet.wid;
|
|
var paths = yield wallet.getPaths();
|
|
var batch = this.batch(wallet);
|
|
var i, path, iv;
|
|
|
|
for (i = 0; i < paths.length; i++) {
|
|
path = paths[i];
|
|
|
|
if (!path.data)
|
|
continue;
|
|
|
|
assert(!path.encrypted);
|
|
|
|
iv = new Buffer(path.hash, 'hex');
|
|
iv = iv.slice(0, 16);
|
|
|
|
path.data = crypto.encipher(path.data, key, iv);
|
|
path.encrypted = true;
|
|
|
|
wallet.pathCache.set(path.hash, path);
|
|
|
|
batch.put(layout.P(wid, path.hash), path.toRaw());
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Decrypt all imported keys for a wallet.
|
|
* @param {WalletID} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.decryptKeys = co(function* decryptKeys(wallet, key) {
|
|
var wid = wallet.wid;
|
|
var paths = yield wallet.getPaths();
|
|
var batch = this.batch(wallet);
|
|
var i, path, iv;
|
|
|
|
for (i = 0; i < paths.length; i++) {
|
|
path = paths[i];
|
|
|
|
if (!path.data)
|
|
continue;
|
|
|
|
assert(path.encrypted);
|
|
|
|
iv = new Buffer(path.hash, 'hex');
|
|
iv = iv.slice(0, 16);
|
|
|
|
path.data = crypto.decipher(path.data, key, iv);
|
|
path.encrypted = false;
|
|
|
|
wallet.pathCache.set(path.hash, path);
|
|
|
|
batch.put(layout.P(wid, path.hash), path.toRaw());
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Rescan the blockchain.
|
|
* @param {ChainDB} chaindb
|
|
* @param {Number} height
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.rescan = co(function* rescan(chaindb, height) {
|
|
var unlock = yield this.txLock.lock();
|
|
try {
|
|
return yield this._rescan(chaindb, height);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Rescan the blockchain without a lock.
|
|
* @private
|
|
* @param {ChainDB} chaindb
|
|
* @param {Number} height
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._rescan = co(function* rescan(chaindb, height) {
|
|
var self = this;
|
|
var hashes;
|
|
|
|
if (height == null)
|
|
height = this.height;
|
|
|
|
hashes = yield this.getHashes();
|
|
|
|
this.logger.info('Scanning for %d addresses.', hashes.length);
|
|
|
|
yield chaindb.scan(height, hashes, function(block, txs) {
|
|
return self._addBlock(block, txs);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Get keys of all pending transactions
|
|
* in the wallet db (for resending).
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getPendingKeys = co(function* getPendingKeys() {
|
|
var layout = TXDB.layout;
|
|
var dummy = new Buffer(0);
|
|
var keys = [];
|
|
var iter, item;
|
|
|
|
iter = yield this.db.iterator({
|
|
gte: layout.prefix(0x00000000, dummy),
|
|
lte: layout.prefix(0xffffffff, dummy)
|
|
});
|
|
|
|
for (;;) {
|
|
item = yield iter.next();
|
|
|
|
if (!item)
|
|
break;
|
|
|
|
if (item.key[5] === 0x70)
|
|
keys.push(item.key);
|
|
}
|
|
|
|
return keys;
|
|
});
|
|
|
|
/**
|
|
* Get keys of all pending transactions
|
|
* in the wallet db (for resending).
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getPendingTX = co(function* getPendingTX() {
|
|
var layout = TXDB.layout;
|
|
var keys = yield this.getPendingKeys();
|
|
var uniq = {};
|
|
var result = [];
|
|
var i, key, wid, hash;
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
key = keys[i];
|
|
|
|
wid = layout.pre(key);
|
|
hash = layout.pp(key);
|
|
|
|
if (uniq[hash])
|
|
continue;
|
|
|
|
uniq[hash] = true;
|
|
|
|
key = layout.prefix(wid, layout.t(hash));
|
|
result.push(key);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
/**
|
|
* Get all wallet IDs with pending txs in them.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getPendingWallets = co(function* getPendingWallets() {
|
|
var layout = TXDB.layout;
|
|
var keys = yield this.getPendingKeys();
|
|
var uniq = {};
|
|
var result = [];
|
|
var i, key, wid;
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
key = keys[i];
|
|
|
|
wid = layout.pre(key);
|
|
|
|
if (uniq[wid])
|
|
continue;
|
|
|
|
uniq[wid] = true;
|
|
|
|
result.push(wid);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
/**
|
|
* Resend all pending transactions.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.resend = co(function* resend() {
|
|
var keys = yield this.getPendingTX();
|
|
var i, key, data, tx;
|
|
|
|
if (keys.length > 0)
|
|
this.logger.info('Rebroadcasting %d transactions.', keys.length);
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
key = keys[i];
|
|
data = yield this.db.get(key);
|
|
|
|
if (!data)
|
|
continue;
|
|
|
|
tx = TX.fromExtended(data);
|
|
|
|
if (tx.isCoinbase())
|
|
continue;
|
|
|
|
this.emit('send', tx);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get all wallet ids by multiple address hashes.
|
|
* @param {Hash[]} hashes
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getWalletsByHashes = co(function* getWalletsByHashes(hashes) {
|
|
var result = [];
|
|
var i, j, hash, wids;
|
|
|
|
for (i = 0; i < hashes.length; i++) {
|
|
hash = hashes[i];
|
|
|
|
if (!this.testFilter(hash))
|
|
continue;
|
|
|
|
wids = yield this.getWalletsByHash(hash);
|
|
|
|
if (!wids)
|
|
continue;
|
|
|
|
for (j = 0; j < wids.length; j++)
|
|
utils.binaryInsert(result, wids[j], cmp, true);
|
|
}
|
|
|
|
if (result.length === 0)
|
|
return;
|
|
|
|
return result;
|
|
});
|
|
|
|
/**
|
|
* Write the genesis block as the best hash.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.writeGenesis = co(function* writeGenesis() {
|
|
var block = yield this.getTip();
|
|
if (block) {
|
|
this.tip = block.hash;
|
|
this.height = block.height;
|
|
return;
|
|
}
|
|
yield this.setTip(this.network.genesis.hash, 0);
|
|
});
|
|
|
|
/**
|
|
* Get the best block hash.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getTip = co(function* getTip() {
|
|
var data = yield this.db.get(layout.R);
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
return WalletBlock.fromTip(data);
|
|
});
|
|
|
|
/**
|
|
* Write the best block hash.
|
|
* @param {Hash} hash
|
|
* @param {Number} height
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.setTip = co(function* setTip(hash, height) {
|
|
var block = new WalletBlock(hash, height);
|
|
|
|
yield this.db.put(layout.R, block.toTip());
|
|
|
|
this.tip = block.hash;
|
|
this.height = block.height;
|
|
});
|
|
|
|
/**
|
|
* Connect a block.
|
|
* @param {WalletBlock} block
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.writeBlock = function writeBlock(block, matches) {
|
|
var batch = this.db.batch();
|
|
var i, hash, wallets;
|
|
|
|
batch.put(layout.R, block.toTip());
|
|
|
|
if (block.hashes.length === 0)
|
|
return batch.write();
|
|
|
|
batch.put(layout.b(block.hash), block.toRaw());
|
|
|
|
for (i = 0; i < block.hashes.length; i++) {
|
|
hash = block.hashes[i];
|
|
wallets = matches[i];
|
|
batch.put(layout.e(hash), serializeWallets(wallets));
|
|
}
|
|
|
|
return batch.write();
|
|
};
|
|
|
|
/**
|
|
* Disconnect a block.
|
|
* @param {WalletBlock} block
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.unwriteBlock = function unwriteBlock(block) {
|
|
var batch = this.db.batch();
|
|
var prev = new WalletBlock(block.prevBlock, block.height - 1);
|
|
|
|
batch.put(layout.R, prev.toTip());
|
|
batch.del(layout.b(block.hash));
|
|
|
|
return batch.write();
|
|
};
|
|
|
|
/**
|
|
* Get a wallet block (with hashes).
|
|
* @param {Hash} hash
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getBlock = co(function* getBlock(hash) {
|
|
var data = yield this.db.get(layout.b(hash));
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
return WalletBlock.fromRaw(hash, data);
|
|
});
|
|
|
|
/**
|
|
* Get a TX->Wallet map.
|
|
* @param {Hash} hash
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.getWalletsByTX = co(function* getWalletsByTX(hash) {
|
|
var data = yield this.db.get(layout.e(hash));
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
return parseWallets(data);
|
|
});
|
|
|
|
/**
|
|
* Add a block's transactions and write the new best hash.
|
|
* @param {ChainEntry} entry
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.addBlock = co(function* addBlock(entry, txs) {
|
|
var unlock = yield this.txLock.lock();
|
|
try {
|
|
return yield this._addBlock(entry, txs);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Add a block's transactions without a lock.
|
|
* @private
|
|
* @param {ChainEntry} entry
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) {
|
|
var i, block, matches, hash, tx, wallets;
|
|
|
|
if (this.options.useCheckpoints) {
|
|
if (entry.height <= this.network.checkpoints.lastHeight) {
|
|
yield this.setTip(entry.hash, entry.height);
|
|
return;
|
|
}
|
|
}
|
|
|
|
block = WalletBlock.fromEntry(entry);
|
|
matches = [];
|
|
|
|
// Update these early so transactions
|
|
// get correct confirmation calculations.
|
|
this.tip = block.hash;
|
|
this.height = block.height;
|
|
|
|
// Atomicity doesn't matter here. If we crash
|
|
// during this loop, the automatic rescan will get
|
|
// the database back into the correct state.
|
|
for (i = 0; i < txs.length; i++) {
|
|
tx = txs[i];
|
|
|
|
wallets = yield this._addTX(tx);
|
|
|
|
if (!wallets)
|
|
continue;
|
|
|
|
hash = tx.hash('hex');
|
|
block.hashes.push(hash);
|
|
matches.push(wallets);
|
|
}
|
|
|
|
if (block.hashes.length > 0) {
|
|
this.logger.info('Connecting block %s (%d txs).',
|
|
utils.revHex(block.hash), block.hashes.length);
|
|
}
|
|
|
|
yield this.writeBlock(block, matches);
|
|
});
|
|
|
|
/**
|
|
* Unconfirm a block's transactions
|
|
* and write the new best hash (SPV version).
|
|
* @param {ChainEntry} entry
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.removeBlock = co(function* removeBlock(entry) {
|
|
var unlock = yield this.txLock.lock();
|
|
try {
|
|
return yield this._removeBlock(entry);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Unconfirm a block's transactions.
|
|
* @private
|
|
* @param {ChainEntry} entry
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._removeBlock = co(function* removeBlock(entry) {
|
|
var block = WalletBlock.fromEntry(entry);
|
|
var i, data, hash;
|
|
|
|
// If we crash during a reorg, there's not much to do.
|
|
// Reorgs cannot be rescanned. The database will be
|
|
// in an odd state, with some txs being confirmed
|
|
// when they shouldn't be. That being said, this
|
|
// should eventually resolve itself when a new block
|
|
// comes in.
|
|
data = yield this.getBlock(block.hash);
|
|
|
|
if (data)
|
|
block.hashes = data.hashes;
|
|
|
|
if (block.hashes.length > 0) {
|
|
this.logger.warning('Disconnecting block %s (%d txs).',
|
|
utils.revHex(block.hash), block.hashes.length);
|
|
}
|
|
|
|
// Unwrite the tip as fast as we can.
|
|
yield this.unwriteBlock(block);
|
|
|
|
for (i = 0; i < block.hashes.length; i++) {
|
|
hash = block.hashes[i];
|
|
yield this._unconfirmTX(hash);
|
|
}
|
|
|
|
this.tip = block.hash;
|
|
this.height = block.height;
|
|
});
|
|
|
|
/**
|
|
* Add a transaction to the database, map addresses
|
|
* to wallet IDs, potentially store orphans, resolve
|
|
* orphans, or confirm a transaction.
|
|
* @param {TX} tx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.addTX = co(function* addTX(tx) {
|
|
var unlock = yield this.txLock.lock();
|
|
try {
|
|
return yield this._addTX(tx);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Add a transaction to the database without a lock.
|
|
* @private
|
|
* @param {TX} tx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._addTX = co(function* addTX(tx) {
|
|
var i, hashes, wallets, wid, wallet;
|
|
|
|
assert(!tx.mutable, 'Cannot add mutable TX to wallet.');
|
|
|
|
hashes = tx.getHashes('hex');
|
|
wallets = yield this.getWalletsByHashes(hashes);
|
|
|
|
if (!wallets)
|
|
return;
|
|
|
|
this.logger.info(
|
|
'Incoming transaction for %d wallets (%s).',
|
|
wallets.length, tx.rhash);
|
|
|
|
for (i = 0; i < wallets.length; i++) {
|
|
wid = wallets[i];
|
|
wallet = yield this.get(wid);
|
|
|
|
assert(wallet);
|
|
|
|
this.logger.debug('Adding tx to wallet: %s', wallet.id);
|
|
|
|
yield wallet.add(tx);
|
|
}
|
|
|
|
return wallets;
|
|
});
|
|
|
|
/**
|
|
* Unconfirm a transaction from all relevant wallets.
|
|
* @param {Hash} hash
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.unconfirmTX = co(function* unconfirmTX(hash) {
|
|
var unlock = yield this.txLock.lock();
|
|
try {
|
|
return yield this._unconfirmTX(hash);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Unconfirm a transaction from all
|
|
* relevant wallets without a lock.
|
|
* @private
|
|
* @param {Hash} hash
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._unconfirmTX = co(function* unconfirmTX(hash) {
|
|
var wallets = yield this.getWalletsByTX(hash);
|
|
var i, wid, wallet;
|
|
|
|
if (!wallets)
|
|
return;
|
|
|
|
for (i = 0; i < wallets.length; i++) {
|
|
wid = wallets[i];
|
|
wallet = yield this.get(wid);
|
|
assert(wallet);
|
|
yield wallet.unconfirm(hash);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Zap stale transactions.
|
|
* @param {Number} age
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype.zap = co(function* zap(age) {
|
|
var unlock = yield this.txLock.lock();
|
|
try {
|
|
return yield this._zap(age);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Zap stale transactions without a lock.
|
|
* @private
|
|
* @param {Number} age
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
WalletDB.prototype._zap = co(function* zap(age) {
|
|
var wallets = yield this.getPendingWallets();
|
|
var i, wid, wallet;
|
|
|
|
for (i = 0; i < wallets.length; i++) {
|
|
wid = wallets[i];
|
|
wallet = yield this.get(wid);
|
|
assert(wallet);
|
|
yield wallet.zap(age);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Wallet Block
|
|
* @constructor
|
|
* @param {Hash} hash
|
|
* @param {Number} height
|
|
*/
|
|
|
|
function WalletBlock(hash, height) {
|
|
if (!(this instanceof WalletBlock))
|
|
return new WalletBlock(hash, height);
|
|
|
|
this.hash = hash || constants.NULL_HASH;
|
|
this.height = height != null ? height : -1;
|
|
this.prevBlock = constants.NULL_HASH;
|
|
this.hashes = [];
|
|
}
|
|
|
|
/**
|
|
* Instantiate wallet block from chain entry.
|
|
* @private
|
|
* @param {ChainEntry} entry
|
|
*/
|
|
|
|
WalletBlock.prototype.fromEntry = function fromEntry(entry) {
|
|
this.hash = entry.hash;
|
|
this.height = entry.height;
|
|
this.prevBlock = entry.prevBlock;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from json object.
|
|
* @private
|
|
* @param {Object} json
|
|
*/
|
|
|
|
WalletBlock.prototype.fromJSON = function fromJSON(json) {
|
|
this.hash = utils.revHex(json.hash);
|
|
this.height = json.height;
|
|
if (json.prevBlock)
|
|
this.prevBlock = utils.revHex(json.prevBlock);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from serialized data.
|
|
* @private
|
|
* @param {Hash} hash
|
|
* @param {Buffer} data
|
|
*/
|
|
|
|
WalletBlock.prototype.fromRaw = function fromRaw(hash, data) {
|
|
var p = new BufferReader(data);
|
|
this.hash = hash;
|
|
this.height = p.readU32();
|
|
while (p.left())
|
|
this.hashes.push(p.readHash('hex'));
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from serialized tip data.
|
|
* @private
|
|
* @param {Buffer} data
|
|
*/
|
|
|
|
WalletBlock.prototype.fromTip = function fromTip(data) {
|
|
var p = new BufferReader(data);
|
|
this.hash = p.readHash('hex');
|
|
this.height = p.readU32();
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from chain entry.
|
|
* @param {ChainEntry} entry
|
|
* @returns {WalletBlock}
|
|
*/
|
|
|
|
WalletBlock.fromEntry = function fromEntry(entry) {
|
|
return new WalletBlock().fromEntry(entry);
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from json object.
|
|
* @param {Object} json
|
|
* @returns {WalletBlock}
|
|
*/
|
|
|
|
WalletBlock.fromJSON = function fromJSON(json) {
|
|
return new WalletBlock().fromJSON(json);
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from serialized data.
|
|
* @param {Hash} hash
|
|
* @param {Buffer} data
|
|
* @returns {WalletBlock}
|
|
*/
|
|
|
|
WalletBlock.fromRaw = function fromRaw(hash, data) {
|
|
return new WalletBlock().fromRaw(hash, data);
|
|
};
|
|
|
|
/**
|
|
* Instantiate wallet block from serialized tip data.
|
|
* @private
|
|
* @param {Buffer} data
|
|
*/
|
|
|
|
WalletBlock.fromTip = function fromTip(data) {
|
|
return new WalletBlock().fromTip(data);
|
|
};
|
|
|
|
/**
|
|
* Serialize the wallet block as a tip (hash and height).
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
WalletBlock.prototype.toTip = function toTip() {
|
|
var p = new BufferWriter();
|
|
p.writeHash(this.hash);
|
|
p.writeU32(this.height);
|
|
return p.render();
|
|
};
|
|
|
|
/**
|
|
* Serialize the wallet block as a block.
|
|
* Contains matching transaction hashes.
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
WalletBlock.prototype.toRaw = function toRaw() {
|
|
var p = new BufferWriter();
|
|
var i;
|
|
|
|
p.writeU32(this.height);
|
|
|
|
for (i = 0; i < this.hashes.length; i++)
|
|
p.writeHash(this.hashes[i]);
|
|
|
|
return p.render();
|
|
};
|
|
|
|
/**
|
|
* Convert the block to a more json-friendly object.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
WalletBlock.prototype.toJSON = function toJSON() {
|
|
return {
|
|
hash: utils.revHex(this.hash),
|
|
height: this.height
|
|
};
|
|
};
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
function parseWallets(data) {
|
|
var p = new BufferReader(data);
|
|
var wallets = [];
|
|
while (p.left())
|
|
wallets.push(p.readU32());
|
|
return wallets;
|
|
}
|
|
|
|
function serializeWallets(wallets) {
|
|
var p = new BufferWriter();
|
|
var i, wid;
|
|
|
|
for (i = 0; i < wallets.length; i++) {
|
|
wid = wallets[i];
|
|
p.writeU32(wid);
|
|
}
|
|
|
|
return p.render();
|
|
}
|
|
|
|
function cmp(a, b) {
|
|
return a - b;
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = WalletDB;
|