481 lines
13 KiB
JavaScript
481 lines
13 KiB
JavaScript
const _ = require('lodash/fp')
|
|
const uuid = require('uuid')
|
|
const { COINS } = require('@lamassu/coins')
|
|
const { scopedValue } = require('./admin/config-manager')
|
|
|
|
const GLOBAL = 'global'
|
|
const ALL_CRYPTOS = _.values(COINS).sort()
|
|
const ALL_CRYPTOS_STRING = 'ALL_COINS'
|
|
const ALL_MACHINES = 'ALL_MACHINES'
|
|
|
|
const GLOBAL_SCOPE = {
|
|
crypto: ALL_CRYPTOS,
|
|
machine: GLOBAL
|
|
}
|
|
|
|
function getConfigFields (codes, config) {
|
|
const stringfiedGlobalScope = JSON.stringify(GLOBAL_SCOPE)
|
|
|
|
const fields = config
|
|
.filter(i => codes.includes(i.fieldLocator.code))
|
|
.map(f => {
|
|
const crypto = Array.isArray(f.fieldLocator.fieldScope.crypto)
|
|
? f.fieldLocator.fieldScope.crypto.sort()
|
|
: f.fieldLocator.fieldScope.crypto === GLOBAL
|
|
? ALL_CRYPTOS
|
|
: [f.fieldLocator.fieldScope.crypto]
|
|
const machine = f.fieldLocator.fieldScope.machine
|
|
|
|
return {
|
|
code: f.fieldLocator.code,
|
|
scope: {
|
|
crypto,
|
|
machine
|
|
},
|
|
value: f.fieldValue.value
|
|
}
|
|
})
|
|
.filter(f => f.value != null)
|
|
|
|
const grouped = _.chain(fields)
|
|
.groupBy(f => JSON.stringify(f.scope))
|
|
.value()
|
|
|
|
return {
|
|
global: grouped[stringfiedGlobalScope] || [],
|
|
scoped:
|
|
_.entries(
|
|
_.chain(grouped)
|
|
.omit([stringfiedGlobalScope])
|
|
.value()
|
|
).map(f => {
|
|
const fallbackValues =
|
|
_.difference(codes, f[1].map(v => v.code))
|
|
.map(v => ({
|
|
code: v,
|
|
scope: JSON.parse(f[0]),
|
|
value: scopedValue(f[0].crypto, f[0].machine, v, config)
|
|
}))
|
|
.filter(f => f.value != null)
|
|
|
|
return {
|
|
scope: JSON.parse(f[0]),
|
|
values: f[1].concat(fallbackValues)
|
|
}
|
|
}) || []
|
|
}
|
|
}
|
|
|
|
function migrateCommissions (config) {
|
|
const areArraysEquals = (arr1, arr2) => Array.isArray(arr1) && Array.isArray(arr2) && _.isEmpty(_.xor(arr1, arr2))
|
|
const getMachine = _.get('scope.machine')
|
|
const getCrypto = _.get('scope.crypto')
|
|
const flattenCoins = _.compose(_.flatten, _.map(getCrypto))
|
|
const diffAllCryptos = _.compose(_.difference(ALL_CRYPTOS))
|
|
|
|
const codes = {
|
|
minimumTx: 'minimumTx',
|
|
cashInFee: 'fixedFee',
|
|
cashInCommission: 'cashIn',
|
|
cashOutCommission: 'cashOut'
|
|
}
|
|
|
|
const { global, scoped } = getConfigFields(_.keys(codes), config)
|
|
const defaultCashOutCommissions = { code: 'cashOutCommission', value: 0, scope: global[0].scope }
|
|
const isCashOutDisabled =
|
|
_.isEmpty(_.filter(commissionElement => commissionElement.code === 'cashOutCommission', global))
|
|
const globalWithDefaults =
|
|
isCashOutDisabled ? _.concat(global, defaultCashOutCommissions) : global
|
|
|
|
const machineAndCryptoScoped = scoped.filter(
|
|
f => f.scope.machine !== GLOBAL_SCOPE.machine && f.scope.crypto.length === 1
|
|
)
|
|
const cryptoScoped = scoped.filter(
|
|
f =>
|
|
f.scope.machine === GLOBAL_SCOPE.machine &&
|
|
!areArraysEquals(f.scope.crypto, GLOBAL_SCOPE.crypto)
|
|
)
|
|
const machineScoped = scoped.filter(
|
|
f =>
|
|
f.scope.machine !== GLOBAL_SCOPE.machine &&
|
|
areArraysEquals(f.scope.crypto, GLOBAL_SCOPE.crypto)
|
|
)
|
|
|
|
const withCryptoScoped = machineAndCryptoScoped.concat(cryptoScoped)
|
|
|
|
const filteredMachineScoped = _.map(it => {
|
|
const filterByMachine = _.filter(_.includes(getMachine(it)))
|
|
const unrepeatedCryptos = _.compose(
|
|
diffAllCryptos,
|
|
flattenCoins,
|
|
filterByMachine
|
|
)(withCryptoScoped)
|
|
|
|
return _.set('scope.crypto', unrepeatedCryptos)(it)
|
|
})(machineScoped)
|
|
|
|
const allCommissionsOverrides = withCryptoScoped.concat(filteredMachineScoped)
|
|
|
|
return {
|
|
..._.fromPairs(globalWithDefaults.map(f => [`commissions_${codes[f.code]}`, f.value])),
|
|
...(allCommissionsOverrides.length > 0 && {
|
|
commissions_overrides: allCommissionsOverrides.map(s => ({
|
|
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
|
|
machine: s.scope.machine === GLOBAL ? ALL_MACHINES : s.scope.machine,
|
|
cryptoCurrencies: areArraysEquals(s.scope.crypto, ALL_CRYPTOS) ? [ALL_CRYPTOS_STRING] : s.scope.crypto,
|
|
id: uuid.v4()
|
|
}))
|
|
})
|
|
}
|
|
}
|
|
|
|
function migrateLocales (config) {
|
|
const codes = {
|
|
country: 'country',
|
|
fiatCurrency: 'fiatCurrency',
|
|
machineLanguages: 'languages',
|
|
cryptoCurrencies: 'cryptoCurrencies',
|
|
timezone: 'timezone'
|
|
}
|
|
|
|
const { global, scoped } = getConfigFields(_.keys(codes), config)
|
|
|
|
return {
|
|
..._.fromPairs(global.map(f => [`locale_${codes[f.code]}`, f.value])),
|
|
...(scoped.length > 0 && {
|
|
locale_overrides: scoped.map(s => ({
|
|
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
|
|
machine: s.scope.machine,
|
|
id: uuid.v4()
|
|
}))
|
|
})
|
|
}
|
|
}
|
|
|
|
function migrateCashOut (config) {
|
|
const globalCodes = {
|
|
fudgeFactorActive: 'fudgeFactorActive'
|
|
}
|
|
|
|
const scopedCodes = {
|
|
cashOutEnabled: 'active',
|
|
topCashOutDenomination: 'top',
|
|
bottomCashOutDenomination: 'bottom',
|
|
zeroConfLimit: 'zeroConfLimit'
|
|
}
|
|
|
|
const { global } = getConfigFields(_.keys(globalCodes), config)
|
|
const { scoped } = getConfigFields(_.keys(scopedCodes), config)
|
|
|
|
return {
|
|
..._.fromPairs(
|
|
global.map(f => [`cashOut_${globalCodes[f.code]}`, f.value])
|
|
),
|
|
..._.fromPairs(
|
|
_.flatten(
|
|
scoped.map(s => {
|
|
const fields = s.values.map(f => [
|
|
`cashOut_${f.scope.machine}_${scopedCodes[f.code]}`,
|
|
f.value
|
|
])
|
|
|
|
fields.push([`cashOut_${s.scope.machine}_id`, s.scope.machine])
|
|
|
|
return fields
|
|
})
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
function migrateNotifications (config) {
|
|
const globalCodes = {
|
|
notificationsEmailEnabled: 'email_active',
|
|
notificationsSMSEnabled: 'sms_active',
|
|
cashOutCassette1AlertThreshold: 'fiatBalanceCassette1',
|
|
cashOutCassette2AlertThreshold: 'fiatBalanceCassette2',
|
|
cryptoAlertThreshold: 'cryptoLowBalance'
|
|
}
|
|
|
|
const machineScopedCodes = {
|
|
cashOutCassette1AlertThreshold: 'cassette1',
|
|
cashOutCassette2AlertThreshold: 'cassette2'
|
|
}
|
|
|
|
const cryptoScopedCodes = {
|
|
cryptoAlertThreshold: 'lowBalance'
|
|
}
|
|
|
|
const { global } = getConfigFields(_.keys(globalCodes), config)
|
|
const machineScoped = getConfigFields(
|
|
_.keys(machineScopedCodes),
|
|
config
|
|
).scoped.filter(f => f.scope.crypto === GLOBAL && f.scope.machine !== GLOBAL)
|
|
const cryptoScoped = getConfigFields(
|
|
_.keys(cryptoScopedCodes),
|
|
config
|
|
).scoped.filter(f => f.scope.crypto !== GLOBAL && f.scope.machine === GLOBAL)
|
|
|
|
return {
|
|
..._.fromPairs(
|
|
global.map(f => [`notifications_${globalCodes[f.code]}`, f.value])
|
|
),
|
|
notifications_email_balance: true,
|
|
notifications_email_transactions: true,
|
|
notifications_email_compliance: true,
|
|
notifications_email_errors: true,
|
|
notifications_sms_balance: true,
|
|
notifications_sms_transactions: true,
|
|
notifications_sms_compliance: true,
|
|
notifications_sms_errors: true,
|
|
...(machineScoped.length > 0 && {
|
|
notifications_fiatBalanceOverrides: machineScoped.map(s => ({
|
|
..._.fromPairs(
|
|
s.values.map(f => [machineScopedCodes[f.code], f.value])
|
|
),
|
|
machine: s.scope.machine,
|
|
id: uuid.v4()
|
|
}))
|
|
}),
|
|
...(cryptoScoped.length > 0 && {
|
|
notifications_cryptoBalanceOverrides: cryptoScoped.map(s => ({
|
|
..._.fromPairs(s.values.map(f => [cryptoScopedCodes[f.code], f.value])),
|
|
cryptoCurrency: s.scope.crypto,
|
|
id: uuid.v4()
|
|
}))
|
|
})
|
|
}
|
|
}
|
|
|
|
function migrateWallet (config) {
|
|
const codes = {
|
|
ticker: 'ticker',
|
|
wallet: 'wallet',
|
|
exchange: 'exchange',
|
|
zeroConf: 'zeroConf'
|
|
}
|
|
|
|
const { scoped } = getConfigFields(_.keys(codes), config)
|
|
|
|
return {
|
|
...(scoped.length > 0 &&
|
|
_.fromPairs(
|
|
_.flatten(
|
|
scoped.map(s =>
|
|
s.values.map(f => [
|
|
`wallets_${f.scope.crypto}_${codes[f.code]}`,
|
|
f.value
|
|
])
|
|
)
|
|
)
|
|
))
|
|
}
|
|
}
|
|
|
|
function migrateOperatorInfo (config) {
|
|
const codes = {
|
|
operatorInfoActive: 'active',
|
|
operatorInfoEmail: 'email',
|
|
operatorInfoName: 'name',
|
|
operatorInfoPhone: 'phone',
|
|
operatorInfoWebsite: 'website',
|
|
operatorInfoCompanyNumber: 'companyNumber'
|
|
}
|
|
|
|
const { global } = getConfigFields(_.keys(codes), config)
|
|
|
|
return {
|
|
..._.fromPairs(global.map(f => [`operatorInfo_${codes[f.code]}`, f.value]))
|
|
}
|
|
}
|
|
|
|
function migrateReceiptPrinting (config) {
|
|
const codes = {
|
|
receiptPrintingActive: 'active'
|
|
}
|
|
|
|
const { global } = getConfigFields(_.keys(codes), config)
|
|
|
|
return {
|
|
..._.fromPairs(global.map(f => [`receipt_${codes[f.code]}`, f.value])),
|
|
receipt_operatorWebsite: true,
|
|
receipt_operatorEmail: true,
|
|
receipt_operatorPhone: true,
|
|
receipt_companyRegistration: true,
|
|
receipt_machineLocation: true,
|
|
receipt_customerNameOrPhoneNumber: true,
|
|
receipt_exchangeRate: true,
|
|
receipt_addressQRCode: true
|
|
}
|
|
}
|
|
|
|
function migrateCoinATMRadar (config) {
|
|
const codes = ['coinAtmRadarActive', 'coinAtmRadarShowRates']
|
|
|
|
const { global } = getConfigFields(codes, config)
|
|
const coinAtmRadar = _.fromPairs(global.map(f => [f.code, f.value]))
|
|
|
|
return {
|
|
coinAtmRadar_active: coinAtmRadar.coinAtmRadarActive,
|
|
coinAtmRadar_commissions: coinAtmRadar.coinAtmRadarShowRates,
|
|
coinAtmRadar_limitsAndVerification: coinAtmRadar.coinAtmRadarShowRates
|
|
}
|
|
}
|
|
|
|
function migrateTermsAndConditions (config) {
|
|
const codes = {
|
|
termsScreenActive: 'active',
|
|
termsScreenTitle: 'title',
|
|
termsScreenText: 'text',
|
|
termsAcceptButtonText: 'acceptButtonText',
|
|
termsCancelButtonText: 'cancelButtonText'
|
|
}
|
|
|
|
const { global } = getConfigFields(_.keys(codes), config)
|
|
|
|
return {
|
|
..._.fromPairs(
|
|
global.map(f => [`termsConditions_${codes[f.code]}`, f.value])
|
|
)
|
|
}
|
|
}
|
|
|
|
function migrateComplianceTriggers (config) {
|
|
|
|
const suspensionDays = 1
|
|
|
|
const triggerTypes = {
|
|
amount: 'txAmount',
|
|
velocity: 'txVelocity',
|
|
volume: 'txVolume',
|
|
consecutiveDays: 'consecutiveDays'
|
|
}
|
|
|
|
const requirements = {
|
|
sms: 'sms',
|
|
idData: 'idCardData',
|
|
idPhoto: 'idCardPhoto',
|
|
facePhoto: 'facephoto',
|
|
sanctions: 'sanctions',
|
|
suspend: 'suspend'
|
|
}
|
|
|
|
function createTrigger (
|
|
requirement,
|
|
threshold,
|
|
suspensionDays
|
|
) {
|
|
const triggerConfig = {
|
|
id: uuid.v4(),
|
|
direction: 'both',
|
|
threshold,
|
|
thresholdDays: 1,
|
|
triggerType: triggerTypes.volume,
|
|
requirement
|
|
}
|
|
if (!requirement === 'suspend') return triggerConfig
|
|
return _.assign(triggerConfig, { suspensionDays })
|
|
}
|
|
|
|
const codes = [
|
|
'smsVerificationActive',
|
|
'smsVerificationThreshold',
|
|
'idCardDataVerificationActive',
|
|
'idCardDataVerificationThreshold',
|
|
'idCardPhotoVerificationActive',
|
|
'idCardPhotoVerificationThreshold',
|
|
'frontCameraVerificationActive',
|
|
'frontCameraVerificationThreshold',
|
|
'sanctionsVerificationActive',
|
|
'sanctionsVerificationThreshold',
|
|
'hardLimitVerificationActive',
|
|
'hardLimitVerificationThreshold',
|
|
'rejectAddressReuseActive'
|
|
]
|
|
|
|
const global = _.fromPairs(
|
|
getConfigFields(codes, config).global.map(f => [f.code, f.value])
|
|
)
|
|
|
|
const triggers = []
|
|
if (global.smsVerificationActive && _.isNumber(global.smsVerificationThreshold)) {
|
|
triggers.push(
|
|
createTrigger(requirements.sms, global.smsVerificationThreshold)
|
|
)
|
|
}
|
|
if (global.idCardDataVerificationActive && _.isNumber(global.idCardDataVerificationThreshold)) {
|
|
triggers.push(
|
|
createTrigger(requirements.idData, global.idCardDataVerificationThreshold)
|
|
)
|
|
}
|
|
if (global.idCardPhotoVerificationActive && _.isNumber(global.idCardPhotoVerificationThreshold)) {
|
|
triggers.push(
|
|
createTrigger(requirements.idPhoto, global.idCardPhotoVerificationThreshold)
|
|
)
|
|
}
|
|
if (global.frontCameraVerificationActive && _.isNumber(global.frontCameraVerificationThreshold)) {
|
|
triggers.push(
|
|
createTrigger(requirements.facePhoto, global.frontCameraVerificationThreshold)
|
|
)
|
|
}
|
|
if (global.sanctionsVerificationActive && _.isNumber(global.sanctionsVerificationThreshold)) {
|
|
triggers.push(
|
|
createTrigger(requirements.sanctions, global.sanctionsVerificationThreshold)
|
|
)
|
|
}
|
|
if (global.hardLimitVerificationActive && _.isNumber(global.hardLimitVerificationThreshold)) {
|
|
triggers.push(
|
|
createTrigger(requirements.suspend, global.hardLimitVerificationThreshold, suspensionDays)
|
|
)
|
|
}
|
|
return {
|
|
triggers,
|
|
['compliance_rejectAddressReuse']: global.rejectAddressReuseActive
|
|
}
|
|
}
|
|
|
|
function migrateConfig (config) {
|
|
return {
|
|
...migrateCommissions(config),
|
|
...migrateLocales(config),
|
|
...migrateCashOut(config),
|
|
...migrateNotifications(config),
|
|
...migrateWallet(config),
|
|
...migrateOperatorInfo(config),
|
|
...migrateReceiptPrinting(config),
|
|
...migrateCoinATMRadar(config),
|
|
...migrateTermsAndConditions(config),
|
|
...migrateComplianceTriggers(config)
|
|
}
|
|
}
|
|
|
|
function migrateAccounts (accounts) {
|
|
const accountArray = [
|
|
'bitgo',
|
|
'bitstamp',
|
|
'blockcypher',
|
|
'infura',
|
|
'itbit',
|
|
'kraken',
|
|
'mailgun',
|
|
'twilio'
|
|
]
|
|
|
|
const services = _.keyBy('code', accounts)
|
|
const serviceFields = _.mapValues(({ fields }) => _.keyBy('code', fields))(services)
|
|
const allAccounts = _.mapValues(_.mapValues(_.get('value')))(serviceFields)
|
|
return _.pick(accountArray)(allAccounts)
|
|
}
|
|
|
|
function migrate (config, accounts) {
|
|
return {
|
|
config: migrateConfig(config),
|
|
accounts: migrateAccounts(accounts)
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
migrateConfig,
|
|
migrateAccounts,
|
|
migrate
|
|
}
|