UNPKG

@ducatus/ducatus-wallet-service-rev

Version:
1,541 lines (1,405 loc) 35.3 kB
import * as async from 'async'; import _ from 'lodash'; import { Db } from 'mongodb'; import * as mongodb from 'mongodb'; import { Address, Email, Notification, Preferences, PushNotificationSub, Session, TxConfirmationSub, TxNote, TxProposal, Wallet } from './model'; const BCHAddressTranslator = require('./bchaddresstranslator'); // only for migration const $ = require('preconditions').singleton(); let log = require('npmlog'); log.debug = log.verbose; log.disableColor(); const collections = { // Duplciated in helpers.. TODO WALLETS: 'wallets', TXS: 'txs', ADDRESSES: 'addresses', NOTIFICATIONS: 'notifications', COPAYERS_LOOKUP: 'copayers_lookup', PREFERENCES: 'preferences', EMAIL_QUEUE: 'email_queue', CACHE: 'cache', FIAT_RATES2: 'fiat_rates2', TX_NOTES: 'tx_notes', SESSIONS: 'sessions', PUSH_NOTIFICATION_SUBS: 'push_notification_subs', TX_CONFIRMATION_SUBS: 'tx_confirmation_subs', LOCKS: 'locks' }; export class Storage { static BCHEIGHT_KEY = 'bcheight'; static collections = collections; db: Db; constructor(opts: { db?: Db } = {}) { opts = opts || {}; this.db = opts.db; } static createIndexes(db) { log.info('Creating DB indexes'); db.collection(collections.WALLETS).createIndex({ id: 1 }); db.collection(collections.COPAYERS_LOOKUP).createIndex({ copayerId: 1 }); db.collection(collections.COPAYERS_LOOKUP).createIndex({ walletId: 1 }); db.collection(collections.TXS).createIndex({ walletId: 1, id: 1 }); db.collection(collections.TXS).createIndex({ walletId: 1, isPending: 1, txid: 1 }); db.collection(collections.TXS).createIndex({ walletId: 1, createdOn: -1 }); db.collection(collections.TXS).createIndex({ txid: 1 }); db.collection(collections.NOTIFICATIONS).createIndex({ walletId: 1, id: 1 }); db.collection(collections.ADDRESSES).createIndex({ walletId: 1, createdOn: 1 }); db.collection(collections.ADDRESSES).createIndex( { address: 1 }, { unique: true } ); db.collection(collections.ADDRESSES).createIndex({ address: 1, beRegistered: 1 }); db.collection(collections.ADDRESSES).createIndex({ walletId: 1, address: 1 }); db.collection(collections.EMAIL_QUEUE).createIndex({ id: 1 }); db.collection(collections.EMAIL_QUEUE).createIndex({ notificationId: 1 }); db.collection(collections.CACHE).createIndex({ walletId: 1, type: 1, key: 1 }); db.collection(collections.TX_NOTES).createIndex({ walletId: 1, txid: 1 }); db.collection(collections.PREFERENCES).createIndex({ walletId: 1 }); db.collection(collections.FIAT_RATES2).createIndex({ coin: 1, code: 1, ts: 1 }); db.collection(collections.PUSH_NOTIFICATION_SUBS).createIndex({ copayerId: 1 }); db.collection(collections.TX_CONFIRMATION_SUBS).createIndex({ copayerId: 1, txid: 1 }); db.collection(collections.TX_CONFIRMATION_SUBS).createIndex({ isActive: 1, copayerId: 1 }); db.collection(collections.SESSIONS).createIndex({ copayerId: 1 }); } connect(opts, cb) { opts = opts || {}; if (this.db) return cb(); const config = opts.mongoDb || {}; if (opts.secondaryPreferred) { if (config.uri.indexOf('?') > 0) { config.uri = config.uri + '&'; } else { config.uri = config.uri + '?'; } config.uri = config.uri + 'readPreference=secondaryPreferred'; log.info('Read operations set to secondaryPreferred'); } mongodb.MongoClient.connect(config.uri, (err, db) => { if (err) { log.error('Unable to connect to the mongoDB. Check the credentials.'); return cb(err); } this.db = db; log.info('Connection established to mongoDB:' + config.uri); Storage.createIndexes(db); return cb(); }); } disconnect(cb) { this.db.close(true, err => { if (err) return cb(err); this.db = null; return cb(); }); } fetchWallet(id, cb: (err?: any, wallet?: Wallet) => void) { if (!this.db) return cb('not ready'); this.db.collection(collections.WALLETS).findOne( { id }, (err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, Wallet.fromObj(result)); } ); } storeWallet(wallet, cb) { this.db.collection(collections.WALLETS).update( { id: wallet.id }, wallet.toObject(), { w: 1, upsert: true }, cb ); } storeWalletAndUpdateCopayersLookup(wallet, cb) { const copayerLookups = _.map(wallet.copayers, copayer => { try { $.checkState(copayer.requestPubKeys); } catch (e) { return cb(e); } return { copayerId: copayer.id, walletId: wallet.id, requestPubKeys: copayer.requestPubKeys }; }); this.db.collection(collections.COPAYERS_LOOKUP).remove( { walletId: wallet.id }, { w: 1 }, err => { if (err) return cb(err); this.db.collection(collections.COPAYERS_LOOKUP).insert( copayerLookups, { w: 1 }, err => { if (err) return cb(err); return this.storeWallet(wallet, cb); } ); } ); } fetchCopayerLookup(copayerId, cb) { this.db.collection(collections.COPAYERS_LOOKUP).findOne( { copayerId }, (err, result) => { if (err) return cb(err); if (!result) return cb(); if (!result.requestPubKeys) { result.requestPubKeys = [ { key: result.requestPubKey, signature: result.signature } ]; } return cb(null, result); } ); } // TODO: should be done client-side _completeTxData(walletId, txs, cb) { this.fetchWallet(walletId, (err, wallet) => { if (err) return cb(err); _.each([].concat(txs), tx => { tx.derivationStrategy = wallet.derivationStrategy || 'BIP45'; tx.creatorName = wallet.getCopayer(tx.creatorId).name; _.each(tx.actions, action => { action.copayerName = wallet.getCopayer(action.copayerId).name; }); if (tx.status == 'accepted') tx.raw = tx.getRawTx(); }); return cb(null, txs); }); } // TODO: remove walletId from signature fetchTx(walletId, txProposalId, cb) { if (!this.db) return cb(); this.db.collection(collections.TXS).findOne( { id: txProposalId, walletId }, (err, result) => { if (err) return cb(err); if (!result) return cb(); return this._completeTxData(walletId, TxProposal.fromObj(result), cb); } ); } fetchTxByHash(hash, cb) { if (!this.db) return cb(); this.db.collection(collections.TXS).findOne( { txid: hash }, (err, result) => { if (err) return cb(err); if (!result) return cb(); return this._completeTxData(result.walletId, TxProposal.fromObj(result), cb); } ); } fetchLastTxs(walletId, creatorId, limit, cb) { this.db .collection(collections.TXS) .find( { walletId, creatorId }, { limit: limit || 5 } ) .sort({ createdOn: -1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const txs = _.map(result, tx => { return TxProposal.fromObj(tx); }); return cb(null, txs); }); } fetchPendingTxs(walletId, cb) { this.db .collection(collections.TXS) .find({ walletId, isPending: true }) .sort({ createdOn: -1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const txs = _.map(result, tx => { return TxProposal.fromObj(tx); }); return this._completeTxData(walletId, txs, cb); }); } /** * fetchTxs. Times are in UNIX EPOCH (seconds) * * @param walletId * @param opts.minTs * @param opts.maxTs * @param opts.limit */ fetchTxs(walletId, opts, cb) { opts = opts || {}; const tsFilter: { $gte?: number; $lte?: number } = {}; if (_.isNumber(opts.minTs)) tsFilter.$gte = opts.minTs; if (_.isNumber(opts.maxTs)) tsFilter.$lte = opts.maxTs; const filter: { walletId: string; createdOn?: typeof tsFilter } = { walletId }; if (!_.isEmpty(tsFilter)) filter.createdOn = tsFilter; const mods: { limit?: number } = {}; if (_.isNumber(opts.limit)) mods.limit = opts.limit; this.db .collection(collections.TXS) .find(filter, mods) .sort({ createdOn: -1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const txs = _.map(result, tx => { return TxProposal.fromObj(tx); }); return this._completeTxData(walletId, txs, cb); }); } /** * fetchBroadcastedTxs. Times are in UNIX EPOCH (seconds) * * @param walletId * @param opts.minTs * @param opts.maxTs * @param opts.limit */ fetchBroadcastedTxs(walletId, opts, cb) { opts = opts || {}; const tsFilter: { $gte?: number; $lte?: number } = {}; if (_.isNumber(opts.minTs)) tsFilter.$gte = opts.minTs; if (_.isNumber(opts.maxTs)) tsFilter.$lte = opts.maxTs; const filter: { walletId: string; status: string; broadcastedOn?: typeof tsFilter; } = { walletId, status: 'broadcasted' }; if (!_.isEmpty(tsFilter)) filter.broadcastedOn = tsFilter; const mods: { limit?: number } = {}; if (_.isNumber(opts.limit)) mods.limit = opts.limit; this.db .collection(collections.TXS) .find(filter, mods) .sort({ createdOn: -1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const txs = _.map(result, tx => { return TxProposal.fromObj(tx); }); return this._completeTxData(walletId, txs, cb); }); } /** * Retrieves notifications after a specific id or from a given ts (whichever is more recent). * * @param {String} notificationId * @param {Number} minTs * @returns {Notification[]} Notifications */ fetchNotifications(walletId, notificationId, minTs, cb) { function makeId(timestamp) { return _.padStart(timestamp, 14, '0') + _.repeat('0', 4); } let minId = makeId(minTs); if (notificationId) { minId = notificationId > minId ? notificationId : minId; } this.db .collection(collections.NOTIFICATIONS) .find({ walletId, id: { $gt: minId } }) .sort({ id: 1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const notifications = _.map(result, notification => { return Notification.fromObj(notification); }); return cb(null, notifications); }); } // TODO: remove walletId from signature storeNotification(walletId, notification, cb) { // This should only happens in certain tests. if (!this.db) { log.warn('Trying to store a notification with close DB', notification); return; } this.db.collection(collections.NOTIFICATIONS).insert( notification, { w: 1 }, cb ); } // TODO: remove walletId from signature storeTx(walletId, txp, cb) { this.db.collection(collections.TXS).update( { id: txp.id, walletId }, txp.toObject(), { w: 1, upsert: true }, cb ); } removeTx(walletId, txProposalId, cb) { this.db.collection(collections.TXS).remove( { id: txProposalId, walletId }, { w: 1 }, cb ); } removeWallet(walletId, cb) { async.parallel( [ next => { this.db.collection(collections.WALLETS).remove( { id: walletId }, next ); }, next => { const otherCollections: string[] = _.without(_.values(collections), collections.WALLETS); async.each( otherCollections, (col, next) => { this.db.collection(col).remove( { walletId }, next ); }, next ); } ], cb ); } fetchAddresses(walletId, cb) { this.db .collection(collections.ADDRESSES) .find({ walletId }) .sort({ createdOn: 1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, result.map(Address.fromObj)); }); } fetchAddress(address, cb) { this.db .collection(collections.ADDRESSES) .find({ address }) .sort({ createdOn: 1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, result.map(Address.fromObj)); }); } migrateToCashAddr(walletId, cb) { const cursor = this.db.collection(collections.ADDRESSES).find({ walletId }); cursor.on('end', () => { console.log(`Migration to cash address of ${walletId} Finished`); return this.clearWalletCache(walletId, cb); }); cursor.on('err', err => { return cb(err); }); cursor.on('data', doc => { cursor.pause(); let x; try { x = BCHAddressTranslator.translate(doc.address, 'cashaddr'); } catch (e) { return cb(e); } this.db.collection(collections.ADDRESSES).update({ _id: doc._id }, { $set: { address: x } }, { multi: true }); cursor.resume(); }); } fetchUnsyncAddresses(walletId, cb) { this.db .collection(collections.ADDRESSES) .find({ walletId, beRegistered: null }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, result); }); } fetchNewAddresses(walletId, fromTs, cb) { this.db .collection(collections.ADDRESSES) .find({ walletId, createdOn: { $gte: fromTs } }) .sort({ createdOn: 1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, result.map(Address.fromObj)); }); } storeAddress(address, cb) { this.db.collection(collections.ADDRESSES).update( { walletId: address.walletId, address: address.address }, address, { w: 1, upsert: false }, cb ); } markSyncedAddresses(addresses, cb) { this.db.collection(collections.ADDRESSES).updateMany( { address: { $in: addresses } }, { $set: { beRegistered: true } }, { w: 1, upsert: false }, cb ); } deregisterWallet(walletId, cb) { this.db.collection(collections.WALLETS).update( { id: walletId }, { $set: { beRegistered: null } }, { w: 1, upsert: false }, () => { this.db.collection(collections.ADDRESSES).update( { walletId }, { $set: { beRegistered: null } }, { w: 1, upsert: false, multi: true }, () => { this.clearWalletCache(walletId, cb); } ); } ); } storeAddressAndWallet(wallet, addresses, cb) { const clonedAddresses = [].concat(addresses); if (_.isEmpty(addresses)) return cb(); let duplicate; this.db.collection(collections.ADDRESSES).insert( clonedAddresses, { w: 1 }, err => { // duplicate address? if (err) { if (!err.toString().match(/E11000/)) { return cb(err); } else { // just return it duplicate = true; log.warn('Found duplicate address: ' + _.join(_.map(clonedAddresses, 'address'), ',')); } } this.storeWallet(wallet, err => { return cb(err, duplicate); }); } ); } fetchAddressByWalletId(walletId, address, cb) { this.db.collection(collections.ADDRESSES).findOne( { walletId, address }, (err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, Address.fromObj(result)); } ); } fetchAddressesByWalletId(walletId, addresses, cb) { this.db .collection(collections.ADDRESSES) .find( { walletId, address: { $in: addresses } }, {} ) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, result); }); } fetchAddressByCoin(coin, address, cb) { if (!this.db) return cb(); this.db .collection(collections.ADDRESSES) .find({ address }) .toArray((err, result) => { if (err) return cb(err); if (!result || _.isEmpty(result)) return cb(); if (result.length > 1) { result = _.find(result, address => { return coin == (address.coin || 'btc'); }); } else { result = _.head(result); } if (!result) return cb(); return cb(null, Address.fromObj(result)); }); } fetchPreferences(walletId, copayerId, cb) { this.db .collection(collections.PREFERENCES) .find({ walletId }) .toArray((err, result) => { if (err) return cb(err); if (copayerId) { result = _.find(result, { copayerId }); } if (!result) return cb(); const preferences = _.map([].concat(result), r => { return Preferences.fromObj(r); }); if (copayerId) { // TODO: review if returs are correct return cb(null, preferences[0]); } else { return cb(null, preferences); } }); } storePreferences(preferences, cb) { this.db.collection(collections.PREFERENCES).update( { walletId: preferences.walletId, copayerId: preferences.copayerId }, preferences, { w: 1, upsert: true }, cb ); } storeEmail(email, cb) { this.db.collection(collections.EMAIL_QUEUE).update( { id: email.id }, email, { w: 1, upsert: true }, cb ); } fetchUnsentEmails(cb) { this.db .collection(collections.EMAIL_QUEUE) .find({ status: 'fail' }) .toArray((err, result) => { if (err) return cb(err); if (!result || _.isEmpty(result)) return cb(null, []); const emails = _.map(result, x => { return Email.fromObj(x); }); return cb(null, emails); }); } fetchEmailByNotification(notificationId, cb) { this.db.collection(collections.EMAIL_QUEUE).findOne( { notificationId }, (err, result) => { if (err) return cb(err); if (!result) return cb(); return cb(null, Email.fromObj(result)); } ); } getTxHistoryCacheStatusV8(walletId, cb) { this.db.collection(collections.CACHE).findOne( { walletId, type: 'historyCacheStatusV8', key: null }, (err, result) => { if (err) return cb(err); if (!result) return cb(null, { tipId: null, tipIndex: null }); return cb(null, { updatedOn: result.updatedOn, updatedHeight: result.updatedHeight, tipIndex: result.tipIndex, tipTxId: result.tipTxId, tipHeight: result.tipHeight }); } ); } getWalletAddressChecked(walletId, cb) { this.db.collection(collections.CACHE).findOne( { walletId, type: 'addressChecked', key: null }, (err, result) => { if (err || !result) return cb(err); return cb(null, result.totalAddresses); } ); } setWalletAddressChecked(walletId, totalAddresses, cb) { this.db.collection(collections.CACHE).update( { walletId, type: 'addressChecked', key: null }, { walletId, type: 'addressChecked', key: null, totalAddresses }, { w: 1, upsert: true }, cb ); } // Since cache TX are "hard confirmed" skip, and limit // should be reliable to query the database. // // // skip=0 -> Latest TX of the wallet. (heights heigth, -1 doest not count because this // are confirmed TXs). // // In a query, tipIndex - skip - limit would be the oldest tx to be queried. getTxHistoryCacheV8(walletId, skip, limit, cb) { $.checkArgument(skip >= 0); $.checkArgument(limit >= 0); this.getTxHistoryCacheStatusV8(walletId, (err, cacheStatus) => { if (err) return cb(err); if (_.isNull(cacheStatus.tipId)) return cb(null, []); // console.log('Cache status in GET:', cacheStatus); //TODO let firstPosition = cacheStatus.tipIndex - skip - limit + 1; const lastPosition = cacheStatus.tipIndex - skip + 1; if (firstPosition < 0) firstPosition = 0; if (lastPosition <= 0) return cb(null, []); // console.log('[storage.js.750:first/lastPosition:]',firstPosition + '/'+lastPosition); //TODO this.db .collection(collections.CACHE) .find({ walletId, type: 'historyCacheV8', key: { $gte: firstPosition, $lt: lastPosition } }) .sort({ key: -1 }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const txs = _.map(result, 'tx'); return cb(null, txs); }); }); } clearWalletCache(walletId, cb) { this.db.collection(collections.CACHE).deleteMany( { walletId }, {}, cb ); } /* * This represent a ongoing query stream from a Wallet client */ storeTxHistoryStreamV8(walletId, streamKey, items, cb) { // only 1 per wallet is allowed this.db.collection(collections.CACHE).update( { walletId, type: 'historyStream', key: null }, { walletId, type: 'historyStream', key: null, streamKey, items }, { w: 1, upsert: true }, cb ); } clearTxHistoryStreamV8(walletId, cb) { this.db.collection(collections.CACHE).deleteMany( { walletId, type: 'historyStream', key: null }, {}, cb ); } getTxHistoryStreamV8(walletId, cb) { this.db.collection(collections.CACHE).findOne( { walletId, type: 'historyStream', key: null }, (err, result) => { if (err || !result) return cb(err); return cb(null, result); } ); } /* * @param {string} [opts.walletId] - The wallet id to use as current wallet * @param {tipIndex} [integer] - Last tx index of the current cache * @param {array} [items] - The items (transactions) to store * @param {updateHeight} [integer] - The blockchain height up to which the transactions where queried, with CONFIRMATIONS_TO_START_CACHING subtracted. * * */ storeTxHistoryCacheV8(walletId, tipIndex, items, updateHeight, cb) { let index = _.isNull(tipIndex) ? 0 : tipIndex + 1; let pos; // `items` must be ordeder: first item [0]: most recent. // // In cache: // pos = 0; oldest one. // pos = tipIndex (item[0] => most recent). _.each(items.reverse(), item => { item.position = index++; }); async.each( items, (item: { position: number; code: string; value: string }, next) => { pos = item.position; delete item.position; // console.log('STORING [storage.js.804:at:]',pos, item.blockheight); this.db.collection(collections.CACHE).insert( { walletId, type: 'historyCacheV8', key: pos, tx: item }, next ); }, err => { if (err) return cb(err); interface CacheItem { txid?: string; blockheight?: number; } const first: CacheItem = _.first(items); const last: CacheItem = _.last(items); try { $.checkState(last.txid, 'missing txid in tx to be cached'); $.checkState(last.blockheight, 'missing blockheight in tx to be cached'); $.checkState(first.blockheight, 'missing blockheight in tx to be cached'); $.checkState(last.blockheight >= 0, 'blockheight <=0 om tx to be cached'); // note there is a .reverse before. $.checkState( first.blockheight <= last.blockheight, 'tx to be cached are in wrong order (lastest should be first)' ); } catch (e) { return cb(e); } log.debug(`Cache Last Item: ${last.txid} blockh: ${last.blockheight} updatedh: ${updateHeight}`); this.db.collection(collections.CACHE).update( { walletId, type: 'historyCacheStatusV8', key: null }, { walletId, type: 'historyCacheStatusV8', key: null, updatedOn: Date.now(), updatedHeight: updateHeight, tipIndex: pos, tipTxId: last.txid, tipHeight: last.blockheight }, { w: 1, upsert: true }, cb ); } ); } storeFiatRate(coin, rates, cb) { const now = Date.now(); async.each( rates, (rate: { code: string; value: string }, next) => { let i = { ts: now, coin, code: rate.code, value: rate.value }; this.db.collection(collections.FIAT_RATES2).insert( i, { w: 1 }, next ); }, cb ); } fetchFiatRate(coin, code, ts, cb) { this.db .collection(collections.FIAT_RATES2) .find({ coin, code, ts: { $lte: ts } }) .sort({ ts: -1 }) .limit(1) .toArray((err, result) => { if (err || _.isEmpty(result)) return cb(err); return cb(null, result[0]); }); } fetchHistoricalRates(coin, code, ts, cb) { this.db .collection(collections.FIAT_RATES2) .find({ coin, code, ts: { $gte: ts } }) .sort({ ts: -1 }) .toArray((err, result) => { if (err || _.isEmpty(result)) return cb(err); return cb(null, result); }); } fetchTxNote(walletId, txid, cb) { this.db.collection(collections.TX_NOTES).findOne( { walletId, txid }, (err, result) => { if (err) return cb(err); if (!result) return cb(); return this._completeTxNotesData(walletId, TxNote.fromObj(result), cb); } ); } // TODO: should be done client-side _completeTxNotesData(walletId, notes, cb) { this.fetchWallet(walletId, (err, wallet) => { if (err) return cb(err); _.each([].concat(notes), note => { note.editedByName = wallet.getCopayer(note.editedBy).name; }); return cb(null, notes); }); } /** * fetchTxNotes. Times are in UNIX EPOCH (seconds) * * @param walletId * @param opts.minTs */ fetchTxNotes(walletId, opts, cb) { const filter: { walletId: string; editedOn?: { $gte: number } } = { walletId }; if (_.isNumber(opts.minTs)) filter.editedOn = { $gte: opts.minTs }; this.db .collection(collections.TX_NOTES) .find(filter) .toArray((err, result) => { if (err) return cb(err); const notes = _.compact( _.map(result, note => { return TxNote.fromObj(note); }) ); return this._completeTxNotesData(walletId, notes, cb); }); } storeTxNote(txNote, cb) { this.db.collection(collections.TX_NOTES).update( { txid: txNote.txid, walletId: txNote.walletId }, txNote.toObject(), { w: 1, upsert: true }, cb ); } getSession(copayerId, cb) { this.db.collection(collections.SESSIONS).findOne( { copayerId }, (err, result) => { if (err || !result) return cb(err); return cb(null, Session.fromObj(result)); } ); } storeSession(session, cb) { this.db.collection(collections.SESSIONS).update( { copayerId: session.copayerId }, session.toObject(), { w: 1, upsert: true }, cb ); } fetchPushNotificationSubs(copayerId, cb) { this.db .collection(collections.PUSH_NOTIFICATION_SUBS) .find({ copayerId }) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const tokens = _.map([].concat(result), r => { return PushNotificationSub.fromObj(r); }); return cb(null, tokens); }); } storePushNotificationSub(pushNotificationSub, cb) { this.db.collection(collections.PUSH_NOTIFICATION_SUBS).update( { copayerId: pushNotificationSub.copayerId, token: pushNotificationSub.token }, pushNotificationSub, { w: 1, upsert: true }, cb ); } removePushNotificationSub(copayerId, token, cb) { this.db.collection(collections.PUSH_NOTIFICATION_SUBS).remove( { copayerId, token }, { w: 1 }, cb ); } fetchActiveTxConfirmationSubs(copayerId, cb) { // This should only happens in certain tests. if (!this.db) { log.warn('Trying to fetch notifications with closed DB'); return; } const filter: { isActive: boolean; copayerId?: string } = { isActive: true }; if (copayerId) filter.copayerId = copayerId; this.db .collection(collections.TX_CONFIRMATION_SUBS) .find(filter) .toArray((err, result) => { if (err) return cb(err); if (!result) return cb(); const subs = _.map([].concat(result), r => { return TxConfirmationSub.fromObj(r); }); return cb(null, subs); }); } storeTxConfirmationSub(txConfirmationSub, cb) { this.db.collection(collections.TX_CONFIRMATION_SUBS).update( { copayerId: txConfirmationSub.copayerId, txid: txConfirmationSub.txid }, txConfirmationSub, { w: 1, upsert: true }, cb ); } removeTxConfirmationSub(copayerId, txid, cb) { this.db.collection(collections.TX_CONFIRMATION_SUBS).remove( { copayerId, txid }, { w: 1 }, cb ); } _dump(cb, fn) { fn = fn || console.log; cb = cb || function() {}; this.db.collections((err, collections) => { if (err) return cb(err); async.eachSeries( collections, (col: any, next) => { col.find().toArray((err, items) => { fn('--------', col.s.name); fn(items); fn('------------------------------------------------------------------\n\n'); next(err); }); }, cb ); }); } // key: 'feeLevel' + JSON.stringify(opts); // duration: FEE_LEVEL_DURATION // checkAndUseGlobalCache(key, duration, cb) { const now = Date.now(); this.db.collection(collections.CACHE).findOne( { key, walletId: null, type: null }, (err, ret) => { if (err) return cb(err); if (!ret) return cb(); const validFor = ret.ts + duration - now; // always return the value as a 3 param anyways. return cb(null, validFor > 0 ? ret.result : null, ret.result); } ); } storeGlobalCache(key, values, cb) { const now = Date.now(); this.db.collection(collections.CACHE).update( { key, walletId: null, type: null }, { $set: { ts: now, result: values } }, { w: 1, upsert: true }, cb ); } clearGlobalCache(key, cb) { this.db.collection(collections.CACHE).remove( { key, walletId: null, type: null }, { w: 1 }, cb ); } walletCheck = async params => { const { walletId } = params; return new Promise(resolve => { const addressStream = this.db.collection(collections.ADDRESSES).find({ walletId }); let sum = 0; let lastAddress; addressStream.on('data', walletAddress => { if (walletAddress.address) { lastAddress = walletAddress.address.replace(/:.*$/, ''); const addressSum = Buffer.from(lastAddress).reduce((tot, cur) => (tot + cur) % Number.MAX_SAFE_INTEGER); sum = (sum + addressSum) % Number.MAX_SAFE_INTEGER; } }); addressStream.on('end', () => { resolve({ lastAddress, sum }); }); }); }; acquireLock(key, expireTs, cb) { this.db.collection(collections.LOCKS).insert( { _id: key, expireOn: expireTs }, {}, cb ); } releaseLock(key, cb) { this.db.collection(collections.LOCKS).remove( { _id: key }, {}, cb ); } clearExpiredLock(key, cb) { this.db.collection(collections.LOCKS).findOne( { _id: key }, (err, ret) => { if (err || !ret) return; if (ret.expireOn < Date.now()) { log.info('Releasing expired lock : ' + key); return this.releaseLock(key, cb); } return cb(); } ); } }