@abcpros/bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
2,006 lines (1,834 loc) • 70.1 kB
text/typescript
import * as async from 'async';
import { AnyRecordWithTtl } from 'dns';
import _, { countBy, isNull, isUndefined } from 'lodash';
import moment from 'moment';
import { Db } from 'mongodb';
import * as mongodb from 'mongodb';
import { TokenInfo } from './chain/xec';
import logger from './logger';
import {
Address,
Advertisement,
Email,
Notification,
Preferences,
PushNotificationSub,
Session,
TxConfirmationSub,
TxNote,
TxProposal,
Wallet
} from './model';
import { CoinConfig } from './model/config-swap';
import { ConversionOrder } from './model/conversionOrder';
import { DonationStorage } from './model/donation';
import { MerchantOrder } from './model/merchantorder';
import { Order } from './model/order';
import { OrderInfoNoti } from './model/OrderInfoNoti';
import { IUser } from './model/user';
import { ICoinConfigFilter } from './server';
// import { Order } from './model/order';
const mongoDbQueue = require('../../node_modules/mongodb-queue');
const BCHAddressTranslator = require('./bchaddresstranslator'); // only for migration
const $ = require('preconditions').singleton();
const collections = {
// Duplciated in helpers.. TODO
WALLETS: 'wallets',
USER: 'user',
USER_CONVERSION: 'user_conversion',
COIN_CONFIG: 'coin_config',
KEYS: 'keys',
KEYS_CONVERSION: 'keys_conversion',
TXS: 'txs',
ADDRESSES: 'addresses',
ADVERTISEMENTS: 'advertisements',
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',
DONATION: 'donation',
TOKEN_INFO: 'token_info',
ORDER_INFO: 'order_info',
CONVERSION_ORDER_INFO: 'conversion_order_info',
MERCHANT_ORDER: 'merchant_order',
USER_WATCH_ADDRESS: 'user_watch_address',
ORDER_INFO_NOTI: 'order_info_noti',
ORDER_QUEUE: 'order_queue'
};
const Common = require('./common');
const Constants = Common.Constants;
const Defaults = Common.Defaults;
const ObjectID = mongodb.ObjectID;
var objectIdDate = function(date) {
return Math.floor(date / 1000).toString(16) + '0000000000000000';
};
export class Storage {
static BCHEIGHT_KEY = 'bcheight';
static collections = collections;
db: Db;
queue: any;
orderQueue: any;
conversionOrderQueue: any;
merchantOrderQueue: any;
client: any;
constructor(opts: { db?: Db } = {}) {
opts = opts || {};
this.db = opts.db;
}
static createIndexes(db) {
logger.info('Creating DB indexes');
if (!db.collection) {
console.log('[storage.ts.55] no db.collection'); // TODO
logger.error('DB not ready');
return;
}
db.collection(collections.USER).createIndex({
id: 1
});
db.collection(collections.USER_CONVERSION).createIndex({
id: 1
});
db.collection(collections.COIN_CONFIG).createIndex({
id: 1
});
db.collection(collections.KEYS).createIndex({
id: 1
});
db.collection(collections.KEYS_CONVERSION).createIndex({
id: 1
});
db.collection(collections.WALLETS).createIndex({
id: 1
});
db.collection(collections.DONATION).createIndex({
txidDonation: 1
});
db.collection(collections.TOKEN_INFO).createIndex({
id: 1
});
db.collection(collections.ORDER_INFO).createIndex({
id: 1
});
db.collection(collections.CONVERSION_ORDER_INFO).createIndex({
id: 1
});
db.collection(collections.MERCHANT_ORDER).createIndex({
id: 1
});
db.collection(collections.USER_WATCH_ADDRESS).createIndex({
id: 1
});
db.collection(collections.ORDER_INFO_NOTI).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.ADVERTISEMENTS).createIndex(
{
advertisementId: 1,
title: 1
},
{ unique: true }
);
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';
logger.info('Read operations set to secondaryPreferred');
}
if (!config.dbname) {
logger.error('No dbname at config.');
return cb(new Error('No dbname at config.'));
}
mongodb.MongoClient.connect(config.uri, { useUnifiedTopology: true }, (err, client) => {
if (err) {
logger.error('Unable to connect to the mongoDB. Check the credentials.');
return cb(err);
}
this.db = client.db(config.dbname);
this.client = client;
this.queue = mongoDbQueue(this.db, 'donation_queue');
this.orderQueue = mongoDbQueue(this.db, 'order_queue');
this.conversionOrderQueue = mongoDbQueue(this.db, 'conversion_order_queue');
this.merchantOrderQueue = mongoDbQueue(this.db, 'merchant_order_queue');
logger.info(`Connection established to db: ${config.uri}`);
Storage.createIndexes(this.db);
return cb();
});
}
disconnect(cb) {
if (this.client) {
this.client.close(err => {
if (err) return cb(err);
this.db = null;
this.client = null;
return cb();
});
} else {
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).replaceOne(
{
id: wallet.id
},
wallet.toObject(),
{
w: 1,
upsert: true
},
cb
);
}
storeDonation(donationStorage, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', donationStorage);
return;
}
this.db.collection(collections.DONATION).insertOne(
donationStorage,
{
w: 1
},
cb
);
}
storeTokenInfo(tokenInfo, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', tokenInfo);
return;
}
this.db.collection(collections.TOKEN_INFO).insertOne(
tokenInfo,
{
w: 1
},
cb
);
}
fetchTokenInfoById(tokenId, cb) {
if (!this.db) return cb();
this.db.collection(collections.TOKEN_INFO).findOne(
{
id: tokenId
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchTokenInfo(cb) {
if (!this.db) return cb();
this.db
.collection(collections.TOKEN_INFO)
.find({})
.toArray((err, result: TokenInfo[]) => {
if (err) return cb(err);
return cb(null, result);
});
}
fetchDonationByTxid(txidDonation, cb) {
if (!this.db) return cb();
this.db.collection(collections.DONATION).findOne(
{
txidDonation
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchDonationInToday(cb) {
const start = moment()
.utc()
.startOf('day')
.valueOf();
const end = moment()
.utc()
.endOf('day')
.valueOf();
this.db
.collection(collections.DONATION)
.find({ createdOn: { $gte: start, $lt: end } })
.toArray((err, result: DonationStorage[]) => {
const donationInToday = _.filter(result, item => item.txidDonation);
return cb(null, donationInToday);
});
}
updateDonation(donationInfo, cb) {
this.db.collection(collections.DONATION).updateOne(
{
txidDonation: donationInfo.txidDonation
},
{
$set: {
txidGiveLotus: donationInfo.txidGiveLotus,
isGiven: donationInfo.isGiven,
error: donationInfo.error
}
},
{
upsert: false
},
cb
);
}
storeUser(user, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', user);
return;
}
this.db.collection(collections.USER).update(
{
email: user.email
},
{
$setOnInsert: user
},
{ upsert: true },
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
storeUserConversion(user, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', user);
return;
}
this.db.collection(collections.USER_CONVERSION).update(
{
email: user.email
},
{
$setOnInsert: user
},
{ upsert: true },
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchUserByEmail(email, cb) {
if (!this.db) return cb();
this.db.collection(collections.USER).findOne(
{
email
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb('Can not find user in db');
return cb(null, result);
}
);
}
fetchUserConversionByEmail(email, cb) {
if (!this.db) return cb();
this.db.collection(collections.USER_CONVERSION).findOne(
{
email
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb('Can not find user conversion in db');
return cb(null, result);
}
);
}
storeKeys(keys, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', keys);
return;
}
this.db.collection(collections.KEYS).insertOne(
keys,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
storeKeysConversion(keys, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', keys);
return;
}
this.db.collection(collections.KEYS_CONVERSION).insertOne(
keys,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchKeys(cb) {
if (!this.db) return cb();
this.db.collection(collections.KEYS).findOne({}, (err, result) => {
if (err) return cb(err);
if (!result) return cb(null, null);
return cb(null, result);
});
}
fetchKeysConversion(cb) {
if (!this.db) return cb();
this.db.collection(collections.KEYS_CONVERSION).findOne({}, (err, result) => {
if (err) return cb(err);
if (!result) return cb(null, null);
return cb(null, result);
});
}
updateKeys(keys, cb) {
this.db.collection(collections.KEYS).findOneAndUpdate(
{},
{
$set: {
keyFund: keys.keyFund,
keyReceive: keys.keyReceive,
hashPassword: keys.hashPassword,
hashRecoveryKey: keys.hashRecoveryKey
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update keys'));
return cb(null, result);
}
);
}
updateKeysConversion(keys, cb) {
this.db.collection(collections.KEYS_CONVERSION).findOneAndUpdate(
{},
{
$set: {
keyFund: keys.keyFund,
hashPassword: keys.hashPassword,
hashRecoveryKey: keys.hashRecoveryKey,
lastModified: new Date()
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update key conversion'));
return cb(null, result);
}
);
}
updateOrder(orderInfo: Order, cb) {
this.db.collection(collections.ORDER_INFO).updateOne(
{
id: orderInfo.id
},
{
$set: {
adddressUserDeposit: orderInfo.adddressUserDeposit,
updatedRate: orderInfo.updatedRate,
status: orderInfo.status,
isSentToFund: orderInfo.isSentToFund,
isSentToUser: orderInfo.isSentToUser,
listTxIdUserDeposit: orderInfo.listTxIdUserDeposit,
listTxIdUserReceive: orderInfo.listTxIdUserReceive,
error: orderInfo.error,
pendingReason: orderInfo.pendingReason,
lastModified: new Date(),
isResolve: orderInfo.isResolve,
note: orderInfo.note,
isInQueue: orderInfo.isInQueue,
actualSent: orderInfo.actualSent,
actualReceived: orderInfo.actualReceived
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update order'));
return cb(null, result);
}
);
}
updateOrderById(orderId: string, orderInfo: Order, cb) {
this.db.collection(collections.ORDER_INFO).updateOne(
{
id: orderId
},
{
$set: {
adddressUserDeposit: orderInfo.adddressUserDeposit,
updatedRate: orderInfo.updatedRate,
status: orderInfo.status,
isSentToFund: orderInfo.isSentToFund,
isSentToUser: orderInfo.isSentToUser,
listTxIdUserDeposit: orderInfo.listTxIdUserDeposit,
listTxIdUserReceive: orderInfo.listTxIdUserReceive,
error: orderInfo.error,
pendingReason: orderInfo.pendingReason,
lastModified: new Date(),
isResolve: orderInfo.isResolve,
note: orderInfo.note,
actualSent: orderInfo.actualSent,
actualReceived: orderInfo.actualReceived
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update order'));
return cb(null, result);
}
);
}
updateOrderStatus(id: string, status: string, cb) {
this.db.collection(collections.ORDER_INFO).updateOne(
{
id
},
{
$set: {
status
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update order'));
return cb(null, result);
}
);
}
updateConversionOrder(orderInfo: ConversionOrder, cb) {
this.db.collection(collections.CONVERSION_ORDER_INFO).updateOne(
{
txIdFromUser: orderInfo.txIdFromUser
},
{
$set: {
txIdSentToUser: orderInfo.txIdSentToUser,
lastModified: new Date(),
error: orderInfo.error,
pendingReason: orderInfo.pendingReason,
status: orderInfo.status
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update order'));
return cb(null, result);
}
);
}
updateMerchantOrder(merchantOrder: MerchantOrder, cb) {
this.db.collection(collections.MERCHANT_ORDER).updateOne(
{
txIdFromUser: merchantOrder.txIdFromUser
},
{
$set: {
status: merchantOrder.status,
txIdMerchantPayment: merchantOrder.txIdMerchantPayment,
lastModified: new Date(),
error: merchantOrder.error
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update merchant order'));
return cb(null, result);
}
);
}
updateListCoinConfig(listCoinConfig: CoinConfig[], cb) {
if (!this.db) {
logger.warn('Trying to update list coin config with close DB', listCoinConfig);
return;
}
var bulk = this.db.collection(collections.COIN_CONFIG).initializeUnorderedBulkOp();
for (var i = 0; i < listCoinConfig.length; i++) {
const coinConfig = listCoinConfig[i];
var ObjectId = require('mongodb').ObjectId;
bulk.find({ _id: ObjectId(coinConfig._id) }).update({
$set: {
isEnableSwap: coinConfig.isEnableSwap,
isEnableReceive: coinConfig.isEnableReceive,
min: coinConfig.min,
max: coinConfig.max,
serviceFee: coinConfig.serviceFee,
settleFee: coinConfig.settleFee,
networkFee: coinConfig.networkFee,
isSwap: coinConfig.isSwap,
isReceive: coinConfig.isReceive,
dailyLimit: coinConfig.dailyLimit || 0
}
});
}
bulk
.execute()
.then(result => {
return cb(null, result);
})
.catch(e => {
return cb(e);
});
}
storeOrderInfo(orderInfo, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', orderInfo);
return;
}
this.db.collection(collections.ORDER_INFO).insertOne(
orderInfo,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchOrderinfoById(orderId: string, cb) {
this.db.collection(collections.ORDER_INFO).findOne(
{
id: orderId
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Your order could not be found, please re-enter!'));
return cb(null, result);
}
);
}
fetchConversionOrderInfoByTxIdFromUser(txIdFromUser: string, cb) {
this.db.collection(collections.CONVERSION_ORDER_INFO).findOne(
{
txIdFromUser
},
(err, result) => {
if (err) return cb(err);
return cb(null, result);
}
);
}
fetchMerchantOrderByTxIdFromUser(txIdFromUser: string, cb) {
this.db.collection(collections.MERCHANT_ORDER).findOne(
{
txIdFromUser
},
(err, result) => {
if (err) return cb(err);
return cb(null, result);
}
);
}
storeConversionOrderInfo(conversionOrderInfo, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', conversionOrderInfo);
return;
}
this.db.collection(collections.CONVERSION_ORDER_INFO).insertOne(
conversionOrderInfo,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
storeMerchantOrder(merchantOrder, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store merchant order with close DB', merchantOrder);
return;
}
this.db.collection(collections.MERCHANT_ORDER).insertOne(
merchantOrder,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
storeUserWatchAddress(user, cb) {
// This should only happens in certain tests.
if (!this.db) {
logger.warn('Trying to store a notification with close DB', user);
return;
}
this.db.collection(collections.USER_WATCH_ADDRESS).insertOne(
user,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
updateUserWatchAddress(user, cb) {
this.db.collection(collections.USER_WATCH_ADDRESS).updateOne(
{
msgId: user.msgId
},
{
$set: {
address: user.address
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update order'));
return cb(null, result);
}
);
}
removeUserWatchAddress(userInfo, cb) {
if (!this.db) {
logger.warn('Trying to store a notification with close DB', userInfo);
return;
}
this.db.collection(collections.USER_WATCH_ADDRESS).deleteOne(
userInfo,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchAllAddressByMsgId(msgId: string, cb) {
this.db
.collection(collections.USER_WATCH_ADDRESS)
.find({
msgId
})
.toArray((err, listUserInfo) => {
if (err) return cb(err);
if (!listUserInfo || listUserInfo.length === 0) return cb(null, null);
const listAddress = _.map(listUserInfo, user => user.address);
return cb(null, listAddress);
});
}
fetchAllMsgIdByAddress(address: string, cb) {
this.db
.collection(collections.USER_WATCH_ADDRESS)
.find({
address
})
.toArray((err, listUserInfo) => {
if (err) return cb(err);
if (!listUserInfo || listUserInfo.length === 0) return cb(null, null);
const listMsgId = _.map(listUserInfo, user => user.msgId);
return cb(null, listMsgId);
});
}
storeOrderInfoNoti(orderInfoNoti: OrderInfoNoti, cb) {
if (!this.db) {
logger.warn('Trying to store a orderInfoNoti with close DB', orderInfoNoti);
return;
}
this.db.collection(collections.ORDER_INFO_NOTI).insertOne(
orderInfoNoti,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
fetchOrderInfoNoti(opts, cb) {
if (!this.db) {
logger.warn('Trying to store a orderInfoNoti with close DB');
return;
}
let queryObject = {};
let queryReceivedTxId = null;
let queryPendingReason = null;
let queryError = null;
if (opts) {
if (opts.receivedTxId) {
queryReceivedTxId = {
receivedTxId: opts.receivedTxId
};
} else if (opts.pendingReason) {
queryPendingReason = {
pendingReason: opts.pendingReason
};
} else if (opts.error) {
queryError = {
error: opts.error
};
}
}
queryObject = Object.assign(
{},
{ orderId: opts.orderId },
queryReceivedTxId && { ...queryReceivedTxId },
queryPendingReason && { ...queryPendingReason },
queryError && { ...queryError }
);
this.db.collection(collections.ORDER_INFO_NOTI).findOne(queryObject, (err, result) => {
if (err) return cb(err);
if (!result) return cb(null);
return cb(null, result);
});
}
fetchAllOrderInfo(opts, cb) {
const coinConfigFilter: ICoinConfigFilter = opts.coinConfigFilter || null;
let queryObject = {};
let queryDate = null;
let queryFromCoin = null;
let queryFromNetwork = null;
let queryToNetwork = null;
let queryToCoin = null;
let queryStatus = null;
let queryIsInQueue = null;
let queryOrderId = null;
if (coinConfigFilter) {
if (coinConfigFilter.fromDate && coinConfigFilter.toDate) {
queryDate = {
lastModified: { $gte: new Date(coinConfigFilter.fromDate), $lte: new Date(coinConfigFilter.toDate) }
};
}
if (coinConfigFilter.fromCoinCode) {
queryFromCoin = { fromCoinCode: coinConfigFilter.fromCoinCode };
}
if (coinConfigFilter.fromNetwork) {
queryFromNetwork = { fromNetwork: coinConfigFilter.fromNetwork };
}
if (coinConfigFilter.toCoinCode) {
queryToCoin = { toCoinCode: coinConfigFilter.toCoinCode };
}
if (coinConfigFilter.toNetwork) {
queryToNetwork = { toNetwork: coinConfigFilter.toNetwork };
}
if (coinConfigFilter.status) {
queryStatus = { status: coinConfigFilter.status };
}
if (!isUndefined(coinConfigFilter.isInQueue) && !isNull(coinConfigFilter.isInQueue)) {
queryIsInQueue = { status: coinConfigFilter.status };
}
if (coinConfigFilter.orderId && coinConfigFilter.orderId.length > 0) {
queryOrderId = { id: coinConfigFilter.orderId };
}
queryObject = Object.assign(
{},
queryDate && { ...queryDate },
queryFromCoin && { ...queryFromCoin },
queryToCoin && { ...queryToCoin },
queryStatus && { ...queryStatus },
queryFromNetwork && { ...queryFromNetwork },
queryToNetwork && { ...queryToNetwork },
queryIsInQueue && { ...queryIsInQueue },
queryOrderId && { ...queryOrderId }
);
}
this.db
.collection(collections.ORDER_INFO)
.find(queryObject)
.sort(opts.query)
.limit(opts.limit)
.skip(opts.skip)
.toArray((err, listOrderInfo) => {
if (err) return cb(err);
if (listOrderInfo.length === 0) return cb(new Error('Not found any order'));
else return cb(null, listOrderInfo);
});
}
fetchAllOrderInfoNotInQueue(cb) {
this.db
.collection(collections.ORDER_INFO)
.find({
$or: [{ status: 'waiting' }, { status: 'processing' }]
})
.sort({ lastModified: 1 })
.toArray((err, listOrderInfo) => {
if (err) return cb(err);
else return cb(null, listOrderInfo);
});
}
fetchAllOrderInfoInQueue(cb) {
this.db
.collection(collections.ORDER_QUEUE)
.find()
.toArray((err, listOrderInfo) => {
if (err) return cb(err);
else return cb(null, listOrderInfo);
});
}
countAllOrderInfo(opts) {
const coinConfigFilter: ICoinConfigFilter = opts.coinConfigFilter || null;
let queryObject = {};
let queryDate = null;
let queryFromCoin = null;
let queryFromNetwork = null;
let queryToNetwork = null;
let queryToCoin = null;
let queryStatus = null;
let queryIsInQueue = null;
let queryOrderId = null;
if (coinConfigFilter) {
if (coinConfigFilter.fromDate && coinConfigFilter.toDate) {
queryDate = {
lastModified: { $gte: new Date(coinConfigFilter.fromDate), $lte: new Date(coinConfigFilter.toDate) }
};
}
if (coinConfigFilter.fromCoinCode) {
queryFromCoin = { fromCoinCode: coinConfigFilter.fromCoinCode };
}
if (coinConfigFilter.fromNetwork) {
queryFromNetwork = { fromNetwork: coinConfigFilter.fromNetwork };
}
if (coinConfigFilter.toCoinCode) {
queryToCoin = { toCoinCode: coinConfigFilter.toCoinCode };
}
if (coinConfigFilter.toNetwork) {
queryToNetwork = { toNetwork: coinConfigFilter.toNetwork };
}
if (coinConfigFilter.status) {
queryStatus = { status: coinConfigFilter.status };
}
if (!isUndefined(coinConfigFilter.isInQueue) && !isNull(coinConfigFilter.isInQueue)) {
queryIsInQueue = { status: coinConfigFilter.status };
}
if (coinConfigFilter.orderId && coinConfigFilter.orderId.length > 0) {
queryOrderId = { id: coinConfigFilter.orderId };
}
queryObject = Object.assign(
{},
queryDate && { ...queryDate },
queryFromCoin && { ...queryFromCoin },
queryToCoin && { ...queryToCoin },
queryStatus && { ...queryStatus },
queryFromNetwork && { ...queryFromNetwork },
queryToNetwork && { ...queryToNetwork },
queryIsInQueue && { ...queryIsInQueue },
queryOrderId && { ...queryOrderId }
);
}
return this.db
.collection(collections.ORDER_INFO)
.find(queryObject)
.sort(opts.query)
.count();
}
fetchAllConversionOrderInfo(opts, cb) {
this.db
.collection(collections.CONVERSION_ORDER_INFO)
.find({})
.sort({ _id: -1 })
.toArray((err, listConversionOrderInfo) => {
if (err) return cb(err);
if (listConversionOrderInfo.length === 0) return cb(new Error('Not found any conversion order'));
else return cb(null, listConversionOrderInfo);
});
}
countAllConversionOrderInfo(opts) {
return this.db
.collection(collections.CONVERSION_ORDER_INFO)
.find({})
.sort({ _id: -1 })
.count();
}
fetchAllCoinConfig(cb) {
this.db
.collection(collections.COIN_CONFIG)
.find()
.toArray((err, listCoinConfig) => {
if (err) return cb(err);
else return cb(null, listCoinConfig);
});
}
storeListCoinConfig(listCoinConfig, cb) {
if (!this.db) {
logger.warn('Trying to store a notification with close DB', listCoinConfig);
return;
}
this.db.collection(collections.COIN_CONFIG).insertMany(
listCoinConfig,
{
w: 1
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, result);
}
);
}
updateCoinConfig(coinConfig, cb) {
this.db.collection(collections.COIN_CONFIG).updateOne(
{
code: coinConfig.code,
network: coinConfig.network
},
{
$set: {
isSupport: coinConfig.isSupport
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update coin config'));
return cb(null, result);
}
);
}
updateDailyLimitCoinConfig(coinConfig, cb) {
this.db.collection(collections.COIN_CONFIG).updateOne(
{
code: coinConfig.code,
network: coinConfig.network
},
{
$set: {
dailyLimit: coinConfig.dailyLimit,
dailyLimitUsage: coinConfig.dailyLimitUsage
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update daily limit for coin config'));
return cb(null, result);
}
);
}
resetAllDailyLimitUsageInCoinConfig(cb) {
this.db.collection(collections.COIN_CONFIG).updateMany(
{},
{
$set: {
dailyLimitUsage: 0
}
},
{
upsert: false
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb(new Error('Can not update daily limit for coin config'));
return cb(null, result);
}
);
}
storeWalletAndUpdateCopayersLookup(wallet, cb) {
const copayerLookups = _.map(wallet.copayers, copayer => {
try {
$.checkState(
copayer.requestPubKeys,
'Failed state: copayer.requestPubkeys undefined at <storeWalletAndUpdateCopayersLookup()>'
);
} catch (e) {
return cb(e);
}
return {
copayerId: copayer.id,
walletId: wallet.id,
requestPubKeys: copayer.requestPubKeys
};
});
this.db.collection(collections.COPAYERS_LOOKUP).deleteMany(
{
walletId: wallet.id
},
{
w: 1
},
err => {
if (err) return cb(err);
this.db.collection(collections.COPAYERS_LOOKUP).insertMany(
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);
}
);
}
fetchAllAddressInUserWatchAddress(cb) {
this.db
.collection(collections.USER_WATCH_ADDRESS)
.distinct('address')
.then(listAddress => {
return cb(null, listAddress);
})
.catch(e => {
return cb(e);
});
}
// 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);
});
}
fetchEthPendingTxs(multisigTxpsInfo) {
return new Promise((resolve, reject) => {
this.db
.collection(collections.TXS)
.find({
txid: { $in: multisigTxpsInfo.map(txpInfo => txpInfo.transactionHash) }
})
.sort({
createdOn: -1
})
.toArray(async (err, result) => {
if (err) return reject(err);
if (!result) return reject();
const multisigTxpsInfoByTransactionHash: any = _.groupBy(multisigTxpsInfo, 'transactionHash');
const actionsById = {};
const txs = _.compact(
_.map(result, tx => {
if (!tx.multisigContractAddress) {
return undefined;
}
tx.status = 'pending';
tx.multisigTxId = multisigTxpsInfoByTransactionHash[tx.txid][0].transactionId;
tx.actions.forEach(action => {
if (_.some(multisigTxpsInfoByTransactionHash[tx.txid], { event: 'ExecutionFailure' })) {
action.type = 'failed';
}
});
if (tx.amount === 0) {
actionsById[tx.multisigTxId] = [...tx.actions, ...(actionsById[tx.multisigTxId] || [])];
return undefined;
}
return TxProposal.fromObj(tx);
})
);
txs.forEach((tx: TxProposal) => {
if (actionsById[tx.multisigTxId]) {
tx.actions = [...tx.actions, ...(actionsById[tx.multisigTxId] || [])];
}
});
return resolve(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) {
logger.warn('Trying to store a notification with close DB', notification);
return;
}
this.db.collection(collections.NOTIFICATIONS).insertOne(
notification,
{
w: 1
},
cb
);
}
// TODO: remove walletId from signature
storeTx(walletId, txp, cb) {
this.db.collection(collections.TXS).replaceOne(
{
id: txp.id,
walletId
},
txp.toObject(),
{
w: 1,
upsert: true
},
cb
);
}
removeTx(walletId, txProposalId, cb) {
this.db.collection(collections.TXS).deleteOne(
{
id: txProposalId,
walletId
},
{
w: 1
},
cb
);
}
removeWallet(walletId, cb) {
async.parallel(
[
next => {
this.db.collection(collections.WALLETS).deleteOne(
{
id: walletId
},
next
);
},
next => {
const otherCollections: string[] = _.without(_.values(collections), collections.WALLETS);
async.each(
otherCollections,
(col, next) => {
this.db.collection(col).deleteMany(
{
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));
});
}
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).updateMany({ _id: doc._id }, { $set: { address: x } });
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).replaceOne(
{
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).updateOne(
{
id: walletId
},
{ $set: { beRegistered: null } },
{
w: 1,
upsert: false
},
() => {
this.db.collection(collections.ADDRESSES).updateMany(
{
walletId
},
{ $set: { beRegistered: null } },
{
w: 1,
upsert: false
},
() => {
this.clearWalletCache(walletId, cb);
}
);
}
);
}
storeAddressAndWallet(wallet, addresses, cb) {
const clonedAddresses = [].concat(addresses);
if (_.isEmpty(addresses)) return cb();
let duplicate;
this.db.collection(collections.ADDRESSES).insertMany(
clonedAddresses,
{
w: 1
},
err => {
// duplicate address?
if (err) {
if (!err.toString().match(/E11000/)) {
return cb(err);
} else {
// just return it
duplicate = true;
logger.warn('Found duplicate address: ' + _.join(_.map(clonedAddresses, 'address'), ','));
}
}
this.storeWallet(wallet, err => {
return cb(err, duplicate);
});
}
);
}
fetchAddressWithWalletId(walletId, cb) {
this.db.collection(collections.ADDRESSES).findOne(
{
walletId
},
(err, result) => {
if (err) return cb(err);
if (!result) return cb();
return cb(null, Address.fromObj(result));
}
);
}
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).replaceOne(
{
walletId: preferences.walletId,
copayerId: preferences.copayerId
},
preferences,
{
w: 1,
upsert: true
},
cb
);
}
storeEmail(email, cb) {
this.db.collection(collections.EMAIL_QUEUE).replaceOne(
{
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