lamassu-server/packages/server/lib/new-settings-loader.js

287 lines
7 KiB
JavaScript

const crypto = require('crypto')
const _ = require('lodash/fp')
const db = require('./db')
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({ 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(db, 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],
row => row?.data?.accounts ?? {},
)
}
const loadAccounts = schemaVersion => _loadAccounts(db, schemaVersion)
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)
})
}
const loadLatest = schemaVersion =>
db
.task(t =>
t.batch([
loadLatestConfigOrNoneReturningVersion(t, schemaVersion),
_loadAccounts(t, 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(db, 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(db, 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],
({ data: { config } }) => 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 db.task(t => {
t.batch([loadConfig(t, versionId), _loadAccounts(t)]).then(
([config, accounts]) => ({
config,
version: versionId,
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,
}