const crypto = require('crypto') const _ = require('lodash/fp') const db = require('./db') const { asyncLocalStorage } = require('./async-storage') const { getOperatorId } = require('./operator') const { getTermsConditions, setTermsConditions } = require('./new-config-manager') const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2 const PASSWORD_FILLED = 'PASSWORD_FILLED' const SECRET_FIELDS = [ 'bitgo.BTCWalletPassphrase', 'bitgo.LTCWalletPassphrase', 'bitgo.ZECWalletPassphrase', 'bitgo.BCHWalletPassphrase', 'bitgo.DASHWalletPassphrase', 'bitstamp.secret', 'itbit.clientSecret', 'kraken.privateKey', 'binanceus.privateKey', 'cex.privateKey', 'binance.privateKey', 'twilio.authToken', 'telnyx.apiKey', 'vonage.apiSecret', 'inforu.apiKey', 'galoy.walletId', 'galoy.apiSecret', 'bitfinex.secret', 'sumsub.apiToken', 'sumsub.privateKey' ] /* * JSON.stringify isn't necessarily deterministic so this function may compute * different hashes for the same object. */ const md5hash = text => crypto .createHash('MD5') .update(text) .digest('hex') const addTermsHash = configs => { const terms = _.omit(['hash'], getTermsConditions(configs)) return !terms?.text ? configs : _.flow( _.get('text'), md5hash, hash => _.set('hash', hash, terms), setTermsConditions, _.assign(configs), )(terms) } const notifyReload = (dbOrTx, operatorId) => dbOrTx.none( 'NOTIFY $1:name, $2', ['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })] ) function saveAccounts (accounts) { if (!accounts) { return Promise.resolve() } const accountsSql = `UPDATE user_config SET data = $1, valid = TRUE, schema_version = $2 WHERE type = 'accounts'; INSERT INTO user_config (type, data, valid, schema_version) SELECT 'accounts', $1, TRUE, $2 WHERE 'accounts' NOT IN (SELECT type FROM user_config)` return Promise.all([loadAccounts(), getOperatorId('middleware')]) .then(([currentAccounts, operatorId]) => { const newAccounts = _.merge(currentAccounts, accounts) // Only allow one wallet scoring active at a time if (accounts.elliptic?.enabled && newAccounts.scorechain) { newAccounts.scorechain.enabled = false } if (accounts.scorechain?.enabled && newAccounts.elliptic) { newAccounts.elliptic.enabled = false } return db.tx(t => t.none(accountsSql, [{ accounts: newAccounts }, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(() => notifyReload(t, operatorId)) ).catch(console.error) }) } function loadAccounts (schemaVersion) { const sql = `SELECT data FROM user_config WHERE type = $1 AND schema_version = $2 AND valid ORDER BY id DESC LIMIT 1` return db.oneOrNone(sql, ['accounts', schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(_.compose(_.defaultTo({}), _.get('data.accounts'))) } function hideSecretFields (accounts) { return _.flow( _.filter(path => !_.isEmpty(_.get(path, accounts))), _.reduce( (accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts), accounts ) )(SECRET_FIELDS) } function showAccounts (schemaVersion) { return loadAccounts(schemaVersion) .then(hideSecretFields) } const insertConfigRow = (dbOrTx, data) => dbOrTx.none( "INSERT INTO user_config (type, data, valid, schema_version) VALUES ('config', $1, TRUE, $2)", [data, NEW_SETTINGS_LOADER_SCHEMA_VERSION] ) function saveConfig (config) { return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')]) .then(([currentConfig, operatorId]) => { const newConfig = addTermsHash(_.assign(currentConfig, config)) return db.tx(t => insertConfigRow(t, { config: newConfig }) .then(() => notifyReload(t, operatorId)) ).catch(console.error) }) } function removeFromConfig (fields) { return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')]) .then(([currentConfig, operatorId]) => { const newConfig = _.omit(fields, currentConfig) return db.tx(t => insertConfigRow(t, { config: newConfig }) .then(() => notifyReload(t, operatorId)) ).catch(console.error) }) } function migrationSaveConfig (config) { return loadLatestConfigOrNone() .then(currentConfig => { const newConfig = _.assign(currentConfig, config) return insertConfigRow(db, { config: newConfig }) .catch(console.error) }) } function loadLatest (schemaVersion) { return Promise.all([loadLatestConfigOrNoneReturningVersion(schemaVersion), loadAccounts(schemaVersion)]) .then(([configObj, accounts]) => ({ config: configObj.config, accounts, version: configObj.version })) } function loadLatestConfig () { const sql = `SELECT data FROM user_config WHERE type = 'config' AND schema_version = $1 AND valid ORDER BY id DESC LIMIT 1` return db.oneOrNone(sql, [NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row ? row.data.config : {}) .catch(err => { throw err }) } function loadLatestConfigOrNoneReturningVersion (schemaVersion) { const sql = `SELECT data, id FROM user_config WHERE type = 'config' AND schema_version = $1 AND valid ORDER BY id DESC LIMIT 1` return db.oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row ? { config: row.data.config, version: row.id } : {}) } function loadLatestConfigOrNone (schemaVersion) { const sql = `SELECT data FROM user_config WHERE type = 'config' AND schema_version = $1 ORDER BY id DESC LIMIT 1` return db.oneOrNone(sql, [schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row ? row.data.config : {}) } function loadConfig (versionId) { const sql = `SELECT data FROM user_config WHERE id = $1 AND type = 'config' AND schema_version = $2 AND valid` return db.one(sql, [versionId, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) .then(row => row.data.config) .catch(err => { if (err.name === 'QueryResultError') { throw new Error('No such config version: ' + versionId) } throw err }) } function load (versionId) { if (!versionId) Promise.reject('versionId is required') return Promise.all([loadConfig(versionId), loadAccounts()]) .then(([config, accounts]) => ({ config, accounts })) } const fetchCurrentConfigVersion = () => { const sql = `SELECT id FROM user_config WHERE type = 'config' AND valid ORDER BY id DESC LIMIT 1` return db.one(sql).then(row => row.id) } module.exports = { saveConfig, migrationSaveConfig, saveAccounts, loadAccounts, showAccounts, loadLatest, loadLatestConfig, loadLatestConfigOrNone, load, removeFromConfig, fetchCurrentConfigVersion, }