diff --git a/bws.js b/bws.js index 271cafe..3b166fb 100755 --- a/bws.js +++ b/bws.js @@ -28,30 +28,35 @@ if (config.https) { serverOpts.cert = fs.readFileSync(config.certificateFile || './ssl/certificate.pem'); } -var start = function() { +var start = function(cb) { var server; if (config.cluster) { server = sticky(clusterInstances, function() { - var app = ExpressApp.start(config); - var server = config.https ? serverModule.createServer(serverOpts, app) : + ExpressApp.start(config, function(err, app) { + var server = config.https ? serverModule.createServer(serverOpts, app) : + serverModule.Server(app); + WsApp.start(server, config); + return server; + }); + }); + return cb(server); + } else { + ExpressApp.start(config, function(err, app) { + server = config.https ? serverModule.createServer(serverOpts, app) : serverModule.Server(app); WsApp.start(server, config); - return server; + return cb(server); }); - } else { - var app = ExpressApp.start(config); - server = config.https ? serverModule.createServer(serverOpts, app) : - serverModule.Server(app); - WsApp.start(server, config); - } + }; +}; + +if (config.cluster && !config.lockOpts.lockerServer) + throw 'When running in cluster mode, locker server need to be configured'; + +start(function(server) { server.listen(port, function(err) { if (err) console.log('ERROR: ', err); log.info('Bitcore Wallet Service running on port ' + port); }); -}; - -if (config.cluster && (!config.storageOpts.multiLevel || !config.lockOpts.lockerServer)) - throw 'When running in cluster mode, multilevel and locker server need to be configured'; - -start(); +}); diff --git a/config.js b/config.js index 1507220..b31a895 100644 --- a/config.js +++ b/config.js @@ -1,7 +1,7 @@ var config = { basePath: '/bws/api', disableLogs: false, - port: 3232, + port: 3232, // Uncomment to make BWS a forking server // cluster: true, // Uncomment to use the nr of availalbe CPUs @@ -12,19 +12,17 @@ var config = { // certificateFile: 'cert.pem', storageOpts: { - dbPath: './db', - // Uncomment to use multilevel server - // multiLevel: { - // host: 'localhost', - // port: 3230, - // }, + mongoDb: { + host: 'localhost', + port: 27017, + }, }, lockOpts: { // To use locker-server, uncomment this: - // lockerServer: { - // host: 'localhost', - // port: 3231, - // }, + // lockerServer: { + // host: 'localhost', + // port: 3231, + // }, }, blockchainExplorerOpts: { livenet: { diff --git a/lib/expressapp.js b/lib/expressapp.js index 3e6bbe4..318387e 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -22,11 +22,11 @@ var ExpressApp = function() {}; * @param opts.WalletService options for WalletService class * @param opts.basePath * @param opts.disableLogs + * @param {Callback} cb */ -ExpressApp.start = function(opts) { +ExpressApp.start = function(opts, cb) { opts = opts || {}; - WalletService.initialize(opts); var app = express(); app.use(function(req, res, next) { res.setHeader('Access-Control-Allow-Origin', '*'); @@ -316,7 +316,10 @@ ExpressApp.start = function(opts) { app.use(opts.basePath || '/bws/api', router); - return app; + + WalletService.initialize(opts, function(err) { + return cb(err,app); + }); }; module.exports = ExpressApp; diff --git a/lib/model/address.js b/lib/model/address.js index a5da342..2590caf 100644 --- a/lib/model/address.js +++ b/lib/model/address.js @@ -13,6 +13,7 @@ Address.create = function(opts) { x.createdOn = Math.floor(Date.now() / 1000); x.address = opts.address; + x.walletId = opts.walletId; x.isChange = opts.isChange; x.path = opts.path; x.publicKeys = opts.publicKeys; @@ -24,6 +25,7 @@ Address.fromObj = function(obj) { x.createdOn = obj.createdOn; x.address = obj.address; + x.walletId = obj.walletId; x.isChange = obj.isChange; x.path = obj.path; x.publicKeys = obj.publicKeys; diff --git a/lib/model/copayer.js b/lib/model/copayer.js index d5a3444..1845350 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -60,7 +60,11 @@ Copayer.prototype.createAddress = function(wallet, isChange) { $.checkState(wallet.isComplete()); var path = this.addressManager.getNewAddressPath(isChange); - var address = Address.create(WalletUtils.deriveAddress(wallet.publicKeyRing, path, wallet.m, wallet.network)); + var raw = Address.create(WalletUtils.deriveAddress(wallet.publicKeyRing, path, wallet.m, wallet.network)); + var address = Address.create(_.extend(raw, { + walletId: wallet.id, + })); + address.isChange = isChange; return address; }; diff --git a/lib/model/index.js b/lib/model/index.js new file mode 100644 index 0000000..3a3fcf2 --- /dev/null +++ b/lib/model/index.js @@ -0,0 +1,9 @@ +var Model = {}; + +Model.Wallet = require('./wallet'); +Model.Copayer = require('./copayer'); +Model.TxProposal = require('./txproposal'); +Model.Address = require('./address'); +Model.Notification = require('./notification'); + +module.exports = Model; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 7cbe973..f95eda3 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -145,7 +145,10 @@ Wallet.prototype.createAddress = function(isChange) { $.checkState(this.isComplete()); var path = this.addressManager.getNewAddressPath(isChange); - var address = Address.create(WalletUtils.deriveAddress(this.publicKeyRing, path, this.m, this.network)); + var raw = WalletUtils.deriveAddress(this.publicKeyRing, path, this.m, this.network); + var address = Address.create(_.extend(raw, { + walletId: this.id, + })); address.isChange = isChange; return address; }; diff --git a/lib/server.js b/lib/server.js index 0eb4be8..71caba6 100644 --- a/lib/server.js +++ b/lib/server.js @@ -53,14 +53,44 @@ WalletService.onNotification = function(func) { * @param {Object} opts * @param {Storage} [opts.storage] - The storage provider. * @param {Storage} [opts.blockchainExplorer] - The blockchainExporer provider. + * @param {Callback} cb */ -WalletService.initialize = function(opts) { +WalletService.initialize = function(opts, cb) { + $.shouldBeFunction(cb); + + opts = opts || {}; lock = opts.lock || new Lock(opts.lockOpts); - storage = opts.storage || new Storage(opts.storageOpts); blockchainExplorer = opts.blockchainExplorer; blockchainExplorerOpts = opts.blockchainExplorerOpts; - initialized = true; + + if (initialized) + return cb(); + + if (opts.storage) { + storage = opts.storage; + initialized = true; + return cb(); + } else { + var newStorage = new Storage(); + newStorage.connect(opts.storageOpts, function(err) { + if (err) return cb(err); + storage = newStorage; + initialized = true; + return cb(); + }); + } +}; + + +WalletService.shutDown = function(cb) { + if (initialized) { + storage.disconnect(function(err) { + if (err) return cb(err); + initialized = false; + return cb(); + }); + } }; WalletService.getInstance = function() { @@ -942,7 +972,7 @@ WalletService.prototype.getPendingTxs = function(opts, cb) { * @param {Object} opts.minTs (defaults to 0) * @param {Object} opts.maxTs (defaults to now) * @param {Object} opts.limit - * @returns {TxProposal[]} Transaction proposals, first newer + * @returns {TxProposal[]} Transaction proposals, newer first */ WalletService.prototype.getTxs = function(opts, cb) { var self = this; @@ -1120,7 +1150,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { async.parallel([ function(next) { - self.storage.fetchTxs(self.walletId, opts, function(err, txps) { + self.storage.fetchTxs(self.walletId, {}, function(err, txps) { if (err) return next(err); next(null, txps); }); diff --git a/lib/storage.js b/lib/storage.js index f2bed0b..c1f1a2c 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -1,121 +1,116 @@ 'use strict'; var _ = require('lodash'); -var levelup = require('levelup'); -var multilevel = require('multilevel'); -var net = require('net'); var async = require('async'); var $ = require('preconditions').singleton(); var log = require('npmlog'); -var util = require('util'); log.debug = log.verbose; log.disableColor(); +var util = require('util'); -var Wallet = require('./model/wallet'); -var Copayer = require('./model/copayer'); -var Address = require('./model/address'); -var TxProposal = require('./model/txproposal'); -var Notification = require('./model/notification'); +var mongodb = require('mongodb'); + +var Model = require('./model'); + +var collections = { + WALLETS: 'wallets', + TXS: 'txs', + ADDRESSES: 'addresses', + NOTIFICATIONS: 'notifications', + COPAYERS_LOOKUP: 'copayers_lookup', +}; var Storage = function(opts) { opts = opts || {}; this.db = opts.db; +}; - if (!this.db) { - if (opts.multiLevel) { - this.db = multilevel.client(); - var con = net.connect(opts.multiLevel); - con.pipe(this.db.createRpcStream()).pipe(con); - log.info('Using multilevel server:' + opts.multiLevel.host + ':' + opts.multiLevel.port); - } else { - this.db = levelup(opts.dbPath || './db/bws.db', { - valueEncoding: 'json' - }); +Storage.prototype.connect = function(opts, cb) { + var self = this; + + opts = opts || {}; + + if (this.db) return cb(null); + + var config = opts.mongoDb || {}; + var url = 'mongodb://' + (config.host || 'localhost') + ':' + (config.port ||  27017) + '/bws'; + mongodb.MongoClient.connect(url, function(err, db) { + if (err) { + log.error('Unable to connect to the mongoDB server.'); + return cb(err); } - } + self.db = db; + console.log('Connection established to ', url); + return cb(null); + }); }; -var zeroPad = function(x, length) { - return _.padLeft(parseInt(x), length, '0'); -}; -var walletPrefix = function(id) { - return 'w!' + id; -}; - -var opKey = function(key) { - return key ? '!' + key : ''; -}; - -var MAX_TS = _.repeat('9', 14); - - -var KEY = { - WALLET: function(walletId) { - return walletPrefix(walletId) + '!main'; - }, - COPAYER: function(id) { - return 'copayer!' + id; - }, - TXP: function(walletId, txProposalId) { - return walletPrefix(walletId) + '!txp' + opKey(txProposalId); - }, - NOTIFICATION: function(walletId, notificationId) { - return walletPrefix(walletId) + '!not' + opKey(notificationId); - }, - PENDING_TXP: function(walletId, txProposalId) { - return walletPrefix(walletId) + '!ptxp' + opKey(txProposalId); - }, - ADDRESS: function(walletId, address) { - return walletPrefix(walletId) + '!addr' + opKey(address); - }, +Storage.prototype.disconnect = function(cb) { + var self = this; + this.db.close(true, function(err) { + if (err) return cb(err); + self.db = null; + return cb(); + }); }; Storage.prototype.fetchWallet = function(id, cb) { - this.db.get(KEY.WALLET(id), function(err, data) { - if (err) { - if (err.notFound) return cb(); - return cb(err); - } - return cb(null, Wallet.fromObj(data)); + this.db.collection(collections.WALLETS).findOne({ + id: id + }, function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + return cb(null, Model.Wallet.fromObj(result)); }); }; Storage.prototype.storeWallet = function(wallet, cb) { - this.db.put(KEY.WALLET(wallet.id), wallet, cb); + this.db.collection(collections.WALLETS).update({ + id: wallet.id + }, wallet, { + w: 1, + upsert: true, + }, cb); }; Storage.prototype.storeWalletAndUpdateCopayersLookup = function(wallet, cb) { - var ops = []; - ops.push({ - type: 'put', - key: KEY.WALLET(wallet.id), - value: wallet - }); - _.each(wallet.copayers, function(copayer) { - var value = { + var self = this; + + var copayerLookups = _.map(wallet.copayers, function(copayer) { + return { + copayerId: copayer.id, walletId: wallet.id, requestPubKey: copayer.requestPubKey, }; - ops.push({ - type: 'put', - key: KEY.COPAYER(copayer.id), - value: value + }); + + this.db.collection(collections.COPAYERS_LOOKUP).remove({ + walletId: wallet.id + }, { + w: 1 + }, function(err) { + if (err) return cb(err); + self.db.collection(collections.COPAYERS_LOOKUP).insert(copayerLookups, { + w: 1 + }, function(err) { + if (err) return cb(err); + return self.storeWallet(wallet, cb); }); }); - this.db.batch(ops, cb); }; Storage.prototype.fetchCopayerLookup = function(copayerId, cb) { - this.db.get(KEY.COPAYER(copayerId), function(err, data) { - if (err) { - if (err.notFound) return cb(); - return cb(err); - } - return cb(null, data); + this.db.collection(collections.COPAYERS_LOOKUP).findOne({ + copayerId: copayerId + }, function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + return cb(null, result); }); }; +// TODO: should be done client-side Storage.prototype._completeTxData = function(walletId, txs, cb) { var txList = [].concat(txs); this.fetchWallet(walletId, function(err, wallet) { @@ -132,12 +127,14 @@ Storage.prototype._completeTxData = function(walletId, txs, cb) { Storage.prototype.fetchTx = function(walletId, txProposalId, cb) { var self = this; - this.db.get(KEY.TXP(walletId, txProposalId), function(err, data) { - if (err) { - if (err.notFound) return cb(); - return cb(err); - } - return self._completeTxData(walletId, TxProposal.fromObj(data), cb); + + this.db.collection(collections.TXS).findOne({ + id: txProposalId, + walletId: walletId + }, function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + return self._completeTxData(walletId, Model.TxProposal.fromObj(result), cb); }); }; @@ -145,22 +142,19 @@ Storage.prototype.fetchTx = function(walletId, txProposalId, cb) { Storage.prototype.fetchPendingTxs = function(walletId, cb) { var self = this; - var txs = []; - var key = KEY.PENDING_TXP(walletId); - this.db.createReadStream({ - gte: key, - lt: key + '~' - }) - .on('data', function(data) { - txs.push(TxProposal.fromObj(data.value)); - }) - .on('error', function(err) { - if (err.notFound) return cb(); - return cb(err); - }) - .on('end', function() { - return self._completeTxData(walletId, txs, cb); + this.db.collection(collections.TXS).find({ + walletId: walletId, + isPending: true + }).sort({ + createdOn: -1 + }).toArray(function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + var txs = _.map(result, function(tx) { + return Model.TxProposal.fromObj(tx); }); + return self._completeTxData(walletId, txs, cb); + }); }; /** @@ -174,31 +168,30 @@ Storage.prototype.fetchPendingTxs = function(walletId, cb) { Storage.prototype.fetchTxs = function(walletId, opts, cb) { var self = this; - var txs = []; opts = opts || {}; - opts.limit = _.isNumber(opts.limit) ? parseInt(opts.limit) : -1; - opts.minTs = _.isNumber(opts.minTs) ? zeroPad(opts.minTs, 11) : 0; - opts.maxTs = _.isNumber(opts.maxTs) ? zeroPad(opts.maxTs, 11) : MAX_TS; - var key = KEY.TXP(walletId, opts.minTs); - var endkey = KEY.TXP(walletId, opts.maxTs); + var tsFilter = {}; + if (_.isNumber(opts.minTs)) tsFilter.$gte = opts.minTs; + if (_.isNumber(opts.maxTs)) tsFilter.$lte = opts.maxTs; - this.db.createReadStream({ - gt: key, - lt: endkey + '~', - reverse: true, - limit: opts.limit, - }) - .on('data', function(data) { - txs.push(TxProposal.fromObj(data.value)); - }) - .on('error', function(err) { - if (err.notFound) return cb(); - return cb(err); - }) - .on('end', function() { - return self._completeTxData(walletId, txs, cb); + var filter = { + walletId: walletId + }; + if (!_.isEmpty(tsFilter)) filter.createdOn = tsFilter; + + var mods = {}; + if (_.isNumber(opts.limit)) mods.limit = opts.limit; + + this.db.collection(collections.TXS).find(filter, mods).sort({ + createdOn: -1 + }).toArray(function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + var txs = _.map(result, function(tx) { + return Model.TxProposal.fromObj(tx); }); + return self._completeTxData(walletId, txs, cb); + }); }; @@ -209,183 +202,133 @@ Storage.prototype.fetchTxs = function(walletId, opts, cb) { * @param opts.minTs * @param opts.maxTs * @param opts.limit + * @param opts.reverse */ Storage.prototype.fetchNotifications = function(walletId, opts, cb) { - var txs = []; + var self = this; + opts = opts || {}; - opts.limit = _.isNumber(opts.limit) ? parseInt(opts.limit) : -1; - opts.minTs = _.isNumber(opts.minTs) ? zeroPad(opts.minTs, 11) : 0; - opts.maxTs = _.isNumber(opts.maxTs) ? zeroPad(opts.maxTs, 11) : MAX_TS; - var key = KEY.NOTIFICATION(walletId, opts.minTs); - var endkey = KEY.NOTIFICATION(walletId, opts.maxTs); + var tsFilter = {}; + if (_.isNumber(opts.minTs)) tsFilter.$gte = opts.minTs; + if (_.isNumber(opts.maxTs)) tsFilter.$lte = opts.maxTs; - this.db.createReadStream({ - gt: key, - lt: endkey + '~', - reverse: opts.reverse, - limit: opts.limit, - }) - .on('data', function(data) { - txs.push(Notification.fromObj(data.value)); - }) - .on('error', function(err) { - if (err.notFound) return cb(); - return cb(err); - }) - .on('end', function() { - return cb(null, txs); + var filter = { + walletId: walletId + }; + if (!_.isEmpty(tsFilter)) filter.createdOn = tsFilter; + + var mods = {}; + if (_.isNumber(opts.limit)) mods.limit = opts.limit; + + this.db.collection(collections.NOTIFICATIONS).find(filter, mods).sort({ + id: opts.reverse ? -1 : 1, + }).toArray(function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + var notifications = _.map(result, function(notification) { + return Model.Notification.fromObj(notification); }); + return cb(null, notifications); + }); }; +// TODO: remove walletId from signature Storage.prototype.storeNotification = function(walletId, notification, cb) { - this.db.put(KEY.NOTIFICATION(walletId, notification.id), notification, cb); + this.db.collection(collections.NOTIFICATIONS).insert(notification, { + w: 1 + }, cb); }; - -// TODO should we store only txp.id on keys for indexing -// or the whole txp? For now, the entire record makes sense -// (faster + easier to access) +// TODO: remove walletId from signature Storage.prototype.storeTx = function(walletId, txp, cb) { - var ops = [{ - type: 'put', - key: KEY.TXP(walletId, txp.id), - value: txp, - }]; - - if (txp.isPending()) { - ops.push({ - type: 'put', - key: KEY.PENDING_TXP(walletId, txp.id), - value: txp, - }); - } else { - ops.push({ - type: 'del', - key: KEY.PENDING_TXP(walletId, txp.id), - }); - } - this.db.batch(ops, cb); + txp.isPending = txp.isPending(); // Persist attribute to use when querying + this.db.collection(collections.TXS).update({ + id: txp.id, + walletId: walletId + }, txp, { + w: 1, + upsert: true, + }, cb); }; Storage.prototype.removeTx = function(walletId, txProposalId, cb) { - var ops = [{ - type: 'del', - key: KEY.TXP(walletId, txProposalId), + this.db.collection(collections.TXS).findAndRemove({ + id: txProposalId, + walletId: walletId }, { - type: 'del', - key: KEY.PENDING_TXP(walletId, txProposalId), - }]; - - this.db.batch(ops, cb); -}; - -Storage.prototype._delByKey = function(key, cb) { - var self = this; - var keys = []; - this.db.createKeyStream({ - gte: key, - lt: key + '~', - }) - .on('data', function(key) { - keys.push(key); - }) - .on('error', function(err) { - if (err.notFound) return cb(); - return cb(err); - }) - .on('end', function(err) { - self.db.batch(_.map(keys, function(k) { - return { - key: k, - type: 'del' - }; - }), function(err) { - return cb(err); - }); - }); -}; - -Storage.prototype._removeCopayers = function(walletId, cb) { - var self = this; - - this.fetchWallet(walletId, function(err, w) { - if (err || !w) return cb(err); - - self.db.batch(_.map(w.copayers, function(c) { - return { - type: 'del', - key: KEY.COPAYER(c.id), - }; - }), cb); - }); + w: 1 + }, cb); }; Storage.prototype.removeWallet = function(walletId, cb) { var self = this; - async.series([ + async.parallel([ function(next) { - // This should be the first step. Will check the wallet exists - self._removeCopayers(walletId, next); + self.db.collection(collections.WALLETS).findAndRemove({ + id: walletId + }, next); }, function(next) { - self._delByKey(walletPrefix(walletId), cb); + var otherCollections = _.without(_.values(collections), collections.WALLETS); + async.each(otherCollections, function(col, next) { + self.db.collection(col).remove({ + walletId: walletId + }, next); + }, next); }, ], cb); }; Storage.prototype.fetchAddresses = function(walletId, cb) { - var addresses = []; - var key = KEY.ADDRESS(walletId); - this.db.createReadStream({ - gte: key, - lt: key + '~' - }) - .on('data', function(data) { - addresses.push(Address.fromObj(data.value)); - }) - .on('error', function(err) { - if (err.notFound) return cb(); - return cb(err); - }) - .on('end', function() { - return cb(null, addresses); + var self = this; + + this.db.collection(collections.ADDRESSES).find({ + walletId: walletId, + }).sort({ + createdOn: 1 + }).toArray(function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + var addresses = _.map(result, function(address) { + return Model.Address.fromObj(address); }); + return cb(null, addresses); + }); }; Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) { - var ops = _.map([].concat(addresses), function(address) { - return { - type: 'put', - key: KEY.ADDRESS(wallet.id, address.address), - value: address, - }; + var self = this; + var addresses = [].concat(addresses); + if (addresses.length == 0) return cb(); + this.db.collection(collections.ADDRESSES).insert(addresses, { + w: 1 + }, function(err) { + if (err) return cb(err); + self.storeWallet(wallet, cb); }); - ops.unshift({ - type: 'put', - key: KEY.WALLET(wallet.id), - value: wallet, - }); - - this.db.batch(ops, cb); }; Storage.prototype._dump = function(cb, fn) { fn = fn || console.log; + cb = cb || function() {}; - this.db.readStream() - .on('data', function(data) { - fn(util.inspect(data, { - depth: 10 - })); - }) - .on('end', function() { - if (cb) return cb(); - }); + var self = this; + this.db.collections(function(err, collections) { + if (err) return cb(err); + async.eachSeries(collections, function(col, next) { + col.find().toArray(function(err, items) { + fn('--------', col.s.name); + fn(items); + fn('------------------------------------------------------------------\n\n'); + next(err); + }); + }, cb); + }); }; module.exports = Storage; diff --git a/lib/storage_leveldb.js b/lib/storage_leveldb.js new file mode 100644 index 0000000..8553287 --- /dev/null +++ b/lib/storage_leveldb.js @@ -0,0 +1,383 @@ +'use strict'; + +var _ = require('lodash'); +var levelup = require('levelup'); +var net = require('net'); +var async = require('async'); +var $ = require('preconditions').singleton(); +var log = require('npmlog'); +var util = require('util'); +log.debug = log.verbose; +log.disableColor(); + +var Wallet = require('./model/wallet'); +var Copayer = require('./model/copayer'); +var Address = require('./model/address'); +var TxProposal = require('./model/txproposal'); +var Notification = require('./model/notification'); + +var Storage = function(opts) { + opts = opts || {}; + this.db = opts.db; + + if (!this.db) { + this.db = levelup(opts.dbPath || './db/bws.db', { + valueEncoding: 'json' + }); + } +}; + +var zeroPad = function(x, length) { + return _.padLeft(parseInt(x), length, '0'); +}; + +var walletPrefix = function(id) { + return 'w!' + id; +}; + +var opKey = function(key) { + return key ? '!' + key : ''; +}; + +var MAX_TS = _.repeat('9', 14); + + +var KEY = { + WALLET: function(walletId) { + return walletPrefix(walletId) + '!main'; + }, + COPAYER: function(id) { + return 'copayer!' + id; + }, + TXP: function(walletId, txProposalId) { + return walletPrefix(walletId) + '!txp' + opKey(txProposalId); + }, + NOTIFICATION: function(walletId, notificationId) { + return walletPrefix(walletId) + '!not' + opKey(notificationId); + }, + PENDING_TXP: function(walletId, txProposalId) { + return walletPrefix(walletId) + '!ptxp' + opKey(txProposalId); + }, + ADDRESS: function(walletId, address) { + return walletPrefix(walletId) + '!addr' + opKey(address); + }, +}; + +Storage.prototype.fetchWallet = function(id, cb) { + this.db.get(KEY.WALLET(id), function(err, data) { + if (err) { + if (err.notFound) return cb(); + return cb(err); + } + return cb(null, Wallet.fromObj(data)); + }); +}; + +Storage.prototype.storeWallet = function(wallet, cb) { + this.db.put(KEY.WALLET(wallet.id), wallet, cb); +}; + +Storage.prototype.storeWalletAndUpdateCopayersLookup = function(wallet, cb) { + var ops = []; + ops.push({ + type: 'put', + key: KEY.WALLET(wallet.id), + value: wallet + }); + _.each(wallet.copayers, function(copayer) { + var value = { + walletId: wallet.id, + requestPubKey: copayer.requestPubKey, + }; + ops.push({ + type: 'put', + key: KEY.COPAYER(copayer.id), + value: value + }); + }); + this.db.batch(ops, cb); +}; + +Storage.prototype.fetchCopayerLookup = function(copayerId, cb) { + this.db.get(KEY.COPAYER(copayerId), function(err, data) { + if (err) { + if (err.notFound) return cb(); + return cb(err); + } + return cb(null, data); + }); +}; + +Storage.prototype._completeTxData = function(walletId, txs, cb) { + var txList = [].concat(txs); + this.fetchWallet(walletId, function(err, wallet) { + if (err) return cb(err); + _.each(txList, function(tx) { + tx.creatorName = wallet.getCopayer(tx.creatorId).name; + _.each(tx.actions, function(action) { + action.copayerName = wallet.getCopayer(action.copayerId).name; + }); + }); + return cb(null, txs); + }); +}; + +Storage.prototype.fetchTx = function(walletId, txProposalId, cb) { + var self = this; + this.db.get(KEY.TXP(walletId, txProposalId), function(err, data) { + if (err) { + if (err.notFound) return cb(); + return cb(err); + } + return self._completeTxData(walletId, TxProposal.fromObj(data), cb); + }); +}; + + +Storage.prototype.fetchPendingTxs = function(walletId, cb) { + var self = this; + + var txs = []; + var key = KEY.PENDING_TXP(walletId); + this.db.createReadStream({ + gte: key, + lt: key + '~' + }) + .on('data', function(data) { + txs.push(TxProposal.fromObj(data.value)); + }) + .on('error', function(err) { + if (err.notFound) return cb(); + return cb(err); + }) + .on('end', function() { + return self._completeTxData(walletId, txs, cb); + }); +}; + +/** + * fetchTxs. Times are in UNIX EPOCH (seconds) + * + * @param walletId + * @param opts.minTs + * @param opts.maxTs + * @param opts.limit + */ +Storage.prototype.fetchTxs = function(walletId, opts, cb) { + var self = this; + + var txs = []; + opts = opts || {}; + opts.limit = _.isNumber(opts.limit) ? parseInt(opts.limit) : -1; + opts.minTs = _.isNumber(opts.minTs) ? zeroPad(opts.minTs, 11) : 0; + opts.maxTs = _.isNumber(opts.maxTs) ? zeroPad(opts.maxTs, 11) : MAX_TS; + + var key = KEY.TXP(walletId, opts.minTs); + var endkey = KEY.TXP(walletId, opts.maxTs); + + this.db.createReadStream({ + gt: key, + lt: endkey + '~', + reverse: true, + limit: opts.limit, + }) + .on('data', function(data) { + txs.push(TxProposal.fromObj(data.value)); + }) + .on('error', function(err) { + if (err.notFound) return cb(); + return cb(err); + }) + .on('end', function() { + return self._completeTxData(walletId, txs, cb); + }); +}; + + +/** + * fetchNotifications + * + * @param walletId + * @param opts.minTs + * @param opts.maxTs + * @param opts.limit + */ +Storage.prototype.fetchNotifications = function(walletId, opts, cb) { + var txs = []; + opts = opts || {}; + opts.limit = _.isNumber(opts.limit) ? parseInt(opts.limit) : -1; + opts.minTs = _.isNumber(opts.minTs) ? zeroPad(opts.minTs, 11) : 0; + opts.maxTs = _.isNumber(opts.maxTs) ? zeroPad(opts.maxTs, 11) : MAX_TS; + + var key = KEY.NOTIFICATION(walletId, opts.minTs); + var endkey = KEY.NOTIFICATION(walletId, opts.maxTs); + + this.db.createReadStream({ + gt: key, + lt: endkey + '~', + reverse: opts.reverse, + limit: opts.limit, + }) + .on('data', function(data) { + txs.push(Notification.fromObj(data.value)); + }) + .on('error', function(err) { + if (err.notFound) return cb(); + return cb(err); + }) + .on('end', function() { + return cb(null, txs); + }); +}; + + +Storage.prototype.storeNotification = function(walletId, notification, cb) { + this.db.put(KEY.NOTIFICATION(walletId, notification.id), notification, cb); +}; + + +// TODO should we store only txp.id on keys for indexing +// or the whole txp? For now, the entire record makes sense +// (faster + easier to access) +Storage.prototype.storeTx = function(walletId, txp, cb) { + var ops = [{ + type: 'put', + key: KEY.TXP(walletId, txp.id), + value: txp, + }]; + + if (txp.isPending()) { + ops.push({ + type: 'put', + key: KEY.PENDING_TXP(walletId, txp.id), + value: txp, + }); + } else { + ops.push({ + type: 'del', + key: KEY.PENDING_TXP(walletId, txp.id), + }); + } + this.db.batch(ops, cb); +}; + +Storage.prototype.removeTx = function(walletId, txProposalId, cb) { + var ops = [{ + type: 'del', + key: KEY.TXP(walletId, txProposalId), + }, { + type: 'del', + key: KEY.PENDING_TXP(walletId, txProposalId), + }]; + + this.db.batch(ops, cb); +}; + +Storage.prototype._delByKey = function(key, cb) { + var self = this; + var keys = []; + this.db.createKeyStream({ + gte: key, + lt: key + '~', + }) + .on('data', function(key) { + keys.push(key); + }) + .on('error', function(err) { + if (err.notFound) return cb(); + return cb(err); + }) + .on('end', function(err) { + self.db.batch(_.map(keys, function(k) { + return { + key: k, + type: 'del' + }; + }), function(err) { + return cb(err); + }); + }); +}; + +Storage.prototype._removeCopayers = function(walletId, cb) { + var self = this; + + this.fetchWallet(walletId, function(err, w) { + if (err || !w) return cb(err); + + self.db.batch(_.map(w.copayers, function(c) { + return { + type: 'del', + key: KEY.COPAYER(c.id), + }; + }), cb); + }); +}; + +Storage.prototype.removeWallet = function(walletId, cb) { + var self = this; + + async.series([ + + function(next) { + // This should be the first step. Will check the wallet exists + self._removeCopayers(walletId, next); + }, + function(next) { + self._delByKey(walletPrefix(walletId), cb); + }, + ], cb); +}; + + +Storage.prototype.fetchAddresses = function(walletId, cb) { + var addresses = []; + var key = KEY.ADDRESS(walletId); + this.db.createReadStream({ + gte: key, + lt: key + '~' + }) + .on('data', function(data) { + addresses.push(Address.fromObj(data.value)); + }) + .on('error', function(err) { + if (err.notFound) return cb(); + return cb(err); + }) + .on('end', function() { + return cb(null, addresses); + }); +}; + +Storage.prototype.storeAddressAndWallet = function(wallet, addresses, cb) { + var ops = _.map([].concat(addresses), function(address) { + return { + type: 'put', + key: KEY.ADDRESS(wallet.id, address.address), + value: address, + }; + }); + ops.unshift({ + type: 'put', + key: KEY.WALLET(wallet.id), + value: wallet, + }); + + this.db.batch(ops, cb); +}; + +Storage.prototype._dump = function(cb, fn) { + fn = fn || console.log; + + this.db.readStream() + .on('data', function(data) { + fn(util.inspect(data, { + depth: 10 + })); + }) + .on('end', function() { + if (cb) return cb(); + }); +}; + +module.exports = Storage; diff --git a/multilevel/clientMultilevel.js b/multilevel/clientMultilevel.js deleted file mode 100755 index f2d028a..0000000 --- a/multilevel/clientMultilevel.js +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node - -var multilevel = require('multilevel'); -var net = require('net'); -var moment = require('moment'); - -var PORT = 3230; - -var otherDate; - -//trying to parse optional parameter to get stats on any given date -try { - otherDate = process.argv[2] && moment(process.argv[2]).isValid() ? moment(process.argv[2]) : null; -} catch (e) { - console.log('Enter the date in the format YYYY-MM-DD.'); -} - -var db = multilevel.client(); -var con = net.connect(PORT); -con.pipe(db.createRpcStream()).pipe(con); - - -var Today = otherDate || moment(); -var TotalTx = 0; -var TotalAmount = 0; -var TotalNewWallets = 0; - -var IsToday = function(date) { - if (!date) return false; - var date = moment(date * 1000); - return (date >= Today.startOf('day') && date <= Today.endOf('day')); -} - -var TotalTxpForToday = function(data) { - if (!data) return; - if (data.key.indexOf('!txp!') < 0) return; - if (!data.value || !IsToday(data.value.createdOn)) return; - TotalTx++; - TotalAmount = TotalAmount + data.value.amount; -}; - -var TotalNewWalletForToday = function(data) { - if (!data) return; - if (data.key.indexOf('!main') < 0) return; - if (!data.value || !IsToday(data.value.createdOn)) return; - TotalNewWallets++; -}; - -var PrintStats = function() { - console.log('Stats for date : ', Today.format("YYYY-MM-DD")); - console.log('New wallets : ', TotalNewWallets); - console.log('Total tx : ', TotalTx); - console.log('Total amount in tx (satoshis) : ', TotalAmount); -}; - -var ProcessData = function(data) { - TotalTxpForToday(data); - TotalNewWalletForToday(data); -}; - -// streams -db.createReadStream().on('data', function(data) { - ProcessData(data); -}).on('error', function(err) { - console.log('Error : ', err); - process.exit(code = 1); -}).on('close', function() { - PrintStats(); - process.exit(code = 0); -}); diff --git a/multilevel/multilevel.js b/multilevel/multilevel.js deleted file mode 100755 index 742638a..0000000 --- a/multilevel/multilevel.js +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node - -var multilevel = require('multilevel'); -var net = require('net'); -var level = require('levelup'); - -var db = level('./db', { - valueEncoding: 'json' -}); -var HOST = 'localhost'; -var PORT = 3230; - - -console.log('Server started at port ' + PORT + '...'); -net.createServer(function(con) { - con.pipe(multilevel.server(db)).pipe(con); -}).listen(PORT, HOST); diff --git a/package.json b/package.json index 979d51b..48953f0 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,12 @@ "coveralls": "^2.11.2", "express": "^4.10.0", "inherits": "^2.0.1", - "leveldown": "^0.10.0", - "levelup": "^0.19.0", "locker": "^0.1.0", "locker-server": "^0.1.3", "lodash": "^3.3.1", "mocha-lcov-reporter": "0.0.1", + "mongodb": "^2.0.27", "morgan": "*", - "multilevel": "^6.1.0", "npmlog": "^0.1.1", "preconditions": "^1.0.7", "read": "^1.0.5", @@ -51,7 +49,8 @@ "memdown": "^1.0.0", "mocha": "^1.18.2", "sinon": "^1.10.3", - "supertest": "*" + "supertest": "*", + "tingodb": "^0.3.4" }, "scripts": { "start": "node bws.js", diff --git a/scripts/level2mongo.js b/scripts/level2mongo.js new file mode 100644 index 0000000..125d484 --- /dev/null +++ b/scripts/level2mongo.js @@ -0,0 +1,63 @@ +'use strict'; + +var LevelStorage = require('../lib/storage_leveldb'); +var MongoStorage = require('../lib/storage'); + + +var level = new LevelStorage({ + dbPath: './db/bws.db', +}); + +var mongo = new MongoStorage(); +mongo.connect({ + host: 'localhost', + port: '27017' +}, function(err) { + if (err) throw err; + mongo.db.dropDatabase(function(err) { + if (err) throw err; + run(function(err) { + if (err) throw err; + console.log('All data successfully migrated'); + process.exit(0); + // mongo._dump(function() { + // process.exit(0); + // }); + }); + }); +}); + + +function run(cb) { + level.db.readStream() + .on('data', function(data) { + migrate(data.key, data.value, function(err) { + if (err) throw err; + }); + }) + .on('error', function(err) { + return cb(err); + }) + .on('end', function() { + return cb(); + }); +}; + +function migrate(key, value, cb) { + if (key.match(/^copayer!/)) { + value.copayerId = key.substring(key.indexOf('!') + 1); + mongo.db.collection('copayers_lookup').insert(value, cb); + } else if (key.match(/!addr!/)) { + value.walletId = key.substring(2, key.indexOf('!addr')); + mongo.db.collection('addresses').insert(value, cb); + } else if (key.match(/!not!/)) { + mongo.db.collection('notifications').insert(value, cb); + } else if (key.match(/!p?txp!/)) { + value.isPending = key.indexOf('!ptxp!') != -1; + mongo.db.collection('txs').insert(value, cb); + } else if (key.match(/!main$/)) { + mongo.db.collection('wallets').insert(value, cb); + } else { + return cb(new Error('Invalid key ' + key)); + } +}; diff --git a/test/integration/server.js b/test/integration/server.js index 9e3c728..165fc31 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -7,21 +7,26 @@ var inspect = require('util').inspect; var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); -var levelup = require('levelup'); -var memdown = require('memdown'); var log = require('npmlog'); log.debug = log.verbose; +var fs = require('fs'); +var tingodb = require('tingodb')({ + memStore: true +}); + var Utils = require('../../lib/utils'); var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = WalletUtils.Bitcore; var Storage = require('../../lib/storage'); var BlockchainMonitor = require('../../lib/blockchainmonitor'); -var Wallet = require('../../lib/model/wallet'); -var TxProposal = require('../../lib/model/txproposal'); -var Address = require('../../lib/model/address'); -var Copayer = require('../../lib/model/copayer'); +var Model = require('../../lib/model'); +var Wallet = Model.Wallet; +var TxProposal = Model.TxProposal; +var Address = Model.Address; +var Copayer = Model.Copayer; + var WalletService = require('../../lib/server'); var NotificationBroadcaster = require('../../lib/notificationbroadcaster'); var TestData = require('../testdata'); @@ -208,27 +213,45 @@ helpers.createAddresses = function(server, wallet, main, change, cb) { var db, storage, blockchainExplorer; +function openDb(cb) { + db = new tingodb.Db('./db/test', {}); + return cb(); +}; + +function resetDb(cb) { + if (!db) return cb(); + db.dropDatabase(function(err) { + return cb(); + }); +}; + describe('Wallet service', function() { - beforeEach(function() { - db = levelup(memdown, { - valueEncoding: 'json' + before(function(done) { + openDb(function() { + storage = new Storage({ + db: db + }); + done(); }); - storage = new Storage({ - db: db + }); + beforeEach(function(done) { + resetDb(function() { + blockchainExplorer = sinon.stub(); + WalletService.initialize({ + storage: storage, + blockchainExplorer: blockchainExplorer, + }, function() { + helpers.offset = 0; + done(); + }); }); - blockchainExplorer = sinon.stub(); - - WalletService.initialize({ - storage: storage, - blockchainExplorer: blockchainExplorer, - }); - helpers.offset = 0; + }); + after(function(done) { + WalletService.shutDown(done); }); - describe('#getInstanceWithAuth', function() { - beforeEach(function() {}); it('should get server instance for existing copayer', function(done) { @@ -1172,6 +1195,7 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, _.range(1, 9), function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { + should.not.exist(err); should.exist(tx); txid = tx.id; @@ -1814,9 +1838,7 @@ describe('Wallet service', function() { var server, wallet, clock; beforeEach(function(done) { - if (server) return done(); this.timeout(5000); - console.log('\tCreating TXS...'); clock = sinon.useFakeTimers(); helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; @@ -1824,7 +1846,7 @@ describe('Wallet service', function() { helpers.stubUtxos(server, wallet, _.range(10), function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, null, TestData.copayers[0].privKey_1H_0); async.eachSeries(_.range(10), function(i, next) { - clock.tick(10000); + clock.tick(10 * 1000); server.createTx(txOpts, function(err, tx) { next(); }); @@ -1883,17 +1905,18 @@ describe('Wallet service', function() { }); - it('should txs from times 50 to 70', function(done) { - server.getTxs({ - minTs: 50, - maxTs: 70, - }, function(err, txps) { - should.not.exist(err); - var times = _.pluck(txps, 'createdOn'); - times.should.deep.equal([70, 60, 50]); - done(); + it('should txs from times 50 to 70', + function(done) { + server.getTxs({ + minTs: 50, + maxTs: 70, + }, function(err, txps) { + should.not.exist(err); + var times = _.pluck(txps, 'createdOn'); + times.should.deep.equal([70, 60, 50]); + done(); + }); }); - }); }); describe('Notifications', function() { @@ -2073,62 +2096,110 @@ describe('Wallet service', function() { }); }); }); + it('should delete a wallet', function(done) { - var i = 0; - var count = function() { - return ++i; - }; - server.storage._dump(function() { - i.should.above(1); - server.removeWallet({}, function(err) { - i = 0; - server.storage._dump(function() { - server.storage._dump(); - i.should.equal(0); + server.removeWallet({}, function(err) { + should.not.exist(err); + server.getWallet({}, function(err, w) { + should.exist(err); + err.message.should.equal('Wallet not found'); + should.not.exist(w); + async.parallel([ + + function(next) { + server.storage.fetchAddresses(wallet.id, function(err, items) { + items.length.should.equal(0); + next(); + }); + }, + function(next) { + server.storage.fetchTxs(wallet.id, {}, function(err, items) { + items.length.should.equal(0); + next(); + }); + }, + function(next) { + server.storage.fetchNotifications(wallet.id, {}, function(err, items) { + items.length.should.equal(0); + next(); + }); + }, + ], function(err) { + should.not.exist(err); done(); - }, count); + }); }); - }, count); + }); }); // creates 2 wallet, and deletes only 1. it('should delete a wallet, and only that wallet', function(done) { - var i = 0; - var db = []; - var cat = function(data) { - db.push(data); - }; - server.storage._dump(function() { - var before = _.clone(db); - db.length.should.above(1); + var server2, wallet2; + async.series([ - helpers.offset = 1; - helpers.createAndJoinWallet(2, 3, function(s, w) { - server = s; - wallet = w; + function(next) { + helpers.offset = 1; + helpers.createAndJoinWallet(1, 1, function(s, w) { + server2 = s; + wallet2 = w; - helpers.stubUtxos(server, wallet, _.range(2), function() { - var txOpts = { - toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', - amount: helpers.toSatoshi(0.1), - }; - async.eachSeries(_.range(2), function(i, next) { - server.createTx(txOpts, function(err, tx) { - next(); - }); - }, function() { - server.removeWallet({}, function(err) { - db = []; - server.storage._dump(function() { - var after = _.clone(db); - after.should.deep.equal(before); - done(); - }, cat); - }); - }, cat); + helpers.stubUtxos(server2, wallet2, _.range(1, 3), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.1, 'some message', TestData.copayers[1].privKey_1H_0); + async.eachSeries(_.range(2), function(i, next) { + server2.createTx(txOpts, function(err, tx) { + should.not.exist(err); + next(err); + }); + }, next); + }); }); - }); - }, cat); + }, + function(next) { + server.removeWallet({}, next); + }, + function(next) { + server.getWallet({}, function(err, wallet) { + should.exist(err); + err.message.should.contain('not found'); + next(); + }); + }, + function(next) { + server2.getWallet({}, function(err, wallet) { + should.not.exist(err); + should.exist(wallet); + wallet.id.should.equal(wallet2.id); + next(); + }); + }, + function(next) { + server2.getMainAddresses({}, function(err, addresses) { + should.not.exist(err); + should.exist(addresses); + addresses.length.should.above(0); + next(); + }); + }, + function(next) { + server2.getTxs({}, function(err, txs) { + should.not.exist(err); + should.exist(txs); + txs.length.should.equal(2); + next(); + }); + }, + function(next) { + server2.getNotifications({}, function(err, notifications) { + should.not.exist(err); + should.exist(notifications); + notifications.length.should.above(0); + next(); + }); + }, + ], function(err) { + should.not.exist(err); + done(); + }); }); }); @@ -2967,29 +3038,37 @@ describe('Wallet service', function() { describe('Blockchain monitor', function() { var addressSubscriber; - beforeEach(function() { - db = levelup(memdown, { - valueEncoding: 'json' + before(function(done) { + openDb(function() { + storage = new Storage({ + db: db + }); + done(); }); - storage = new Storage({ - db: db - }); - blockchainExplorer = sinon.stub(); - - WalletService.initialize({ - storage: storage, - blockchainExplorer: blockchainExplorer, - }); - helpers.offset = 0; + }); + beforeEach(function(done) { addressSubscriber = sinon.stub(); addressSubscriber.subscribe = sinon.stub(); sinon.stub(BlockchainMonitor.prototype, '_getAddressSubscriber').onFirstCall().returns(addressSubscriber); - }); + resetDb(function() { + blockchainExplorer = sinon.stub(); + WalletService.initialize({ + storage: storage, + blockchainExplorer: blockchainExplorer, + }, function() { + helpers.offset = 0; + done(); + }); + }); + }); afterEach(function() { BlockchainMonitor.prototype._getAddressSubscriber.restore(); }); + after(function(done) { + WalletService.shutDown(done); + }); it('should subscribe wallet', function(done) { var monitor = new BlockchainMonitor(); diff --git a/test/storage.js b/test/storage.js new file mode 100644 index 0000000..5f5ff87 --- /dev/null +++ b/test/storage.js @@ -0,0 +1,193 @@ +'use strict'; + +var _ = require('lodash'); +var async = require('async'); +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var tingodb = require('tingodb')({ + memStore: true +}); + +var Storage = require('../lib/storage'); +var Model = require('../lib/model'); + +var db, storage; + +function openDb(cb) { + db = new tingodb.Db('./db/test', {}); + return cb(); +}; + +function resetDb(cb) { + if (!db) return cb(); + db.dropDatabase(function(err) { + return cb(); + }); +}; + + +describe('Storage', function() { + before(function(done) { + openDb(function() { + storage = new Storage({ + db: db + }); + done(); + }); + }); + beforeEach(function(done) { + resetDb(done); + }); + + describe('Store & fetch wallet', function() { + it('should correctly store and fetch wallet', function(done) { + var wallet = Model.Wallet.create({ + id: '123', + name: 'my wallet', + m: 2, + n: 3, + }); + should.exist(wallet); + storage.storeWallet(wallet, function(err) { + should.not.exist(err); + storage.fetchWallet('123', function(err, w) { + should.not.exist(err); + should.exist(w); + w.id.should.equal(wallet.id); + w.name.should.equal(wallet.name); + w.m.should.equal(wallet.m); + w.n.should.equal(wallet.n); + done(); + }) + }); + }); + it('should not return error if wallet not found', function(done) { + storage.fetchWallet('123', function(err, w) { + should.not.exist(err); + should.not.exist(w); + done(); + }); + }); + }); + describe('Copayer lookup', function() { + it('should correctly store and fetch copayer lookup', function(done) { + var wallet = Model.Wallet.create({ + id: '123', + name: 'my wallet', + m: 2, + n: 3, + }); + _.each(_.range(3), function(i) { + var copayer = Model.Copayer.create({ + name: 'copayer ' + i, + xPubKey: 'xPubKey ' + i, + requestPubKey: 'requestPubKey ' + i, + }); + wallet.addCopayer(copayer); + }); + + should.exist(wallet); + storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { + should.not.exist(err); + storage.fetchCopayerLookup(wallet.copayers[1].id, function(err, lookup) { + should.not.exist(err); + should.exist(lookup); + lookup.walletId.should.equal('123'); + lookup.requestPubKey.should.equal('requestPubKey 1'); + done(); + }) + }); + }); + it('should not return error if copayer not found', function(done) { + storage.fetchCopayerLookup('2', function(err, lookup) { + should.not.exist(err); + should.not.exist(lookup); + done(); + }); + }); + }); + + describe('Transaction proposals', function() { + var wallet, proposals; + + beforeEach(function(done) { + wallet = Model.Wallet.create({ + id: '123', + name: 'my wallet', + m: 2, + n: 3, + }); + _.each(_.range(3), function(i) { + var copayer = Model.Copayer.create({ + name: 'copayer ' + i, + xPubKey: 'xPubKey ' + i, + requestPubKey: 'requestPubKey ' + i, + }); + wallet.addCopayer(copayer); + }); + should.exist(wallet); + storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { + should.not.exist(err); + + proposals = _.map(_.range(4), function(i) { + var tx = Model.TxProposal.create({ + walletId: '123', + creatorId: wallet.copayers[0].id, + amount: i + 100, + }); + if (i % 2 == 0) { + tx.status = 'rejected'; + tx.isPending().should.be.false; + } + return tx; + }); + async.each(proposals, function(tx, next) { + storage.storeTx('123', tx, next); + }, function(err) { + should.not.exist(err); + done(); + }); + }); + }); + it('should fetch tx', function(done) { + storage.fetchTx('123', proposals[0].id, function(err, tx) { + should.not.exist(err); + should.exist(tx); + tx.id.should.equal(proposals[0].id); + tx.walletId.should.equal(proposals[0].walletId); + tx.creatorName.should.equal('copayer 0'); + done(); + }); + }); + it('should fetch all pending txs', function(done) { + storage.fetchPendingTxs('123', function(err, txs) { + should.not.exist(err); + should.exist(txs); + txs.length.should.equal(2); + txs = _.sortBy(txs, 'amount'); + txs[0].amount.should.equal(101); + txs[1].amount.should.equal(103); + done(); + }); + }); + it('should remove tx', function(done) { + storage.removeTx('123', proposals[0].id, function(err) { + should.not.exist(err); + storage.fetchTx('123', proposals[0].id, function(err, tx) { + should.not.exist(err); + should.not.exist(tx); + storage.fetchTxs('123', {}, function(err, txs) { + should.not.exist(err); + should.exist(txs); + txs.length.should.equal(3); + _.any(txs, { + id: proposals[0].id + }).should.be.false; + done(); + }); + }); + }); + }); + }); +});