Merge branch 'releases/v10.1' into releases/v10.0.7

This commit is contained in:
Rafael Taranto 2024-12-24 10:11:57 +00:00 committed by GitHub
commit 5075e90c87
62 changed files with 742 additions and 603 deletions

View file

@ -92,7 +92,7 @@ function loadLatestConfig (filterSchemaVersion = true) {
order by id desc
limit 1`
return db.one(sql, ['config', configValidate.SETTINGS_LOADER_SCHEMA_VERSION])
return db.oneOrNone(sql, ['config', configValidate.SETTINGS_LOADER_SCHEMA_VERSION])
.then(row => row.data.config)
.then(configValidate.validate)
.catch(err => {
@ -240,6 +240,7 @@ module.exports = {
loadRecentConfig,
load,
loadLatest,
loadLatestConfig,
save,
loadFixture,
mergeValues,

View file

@ -30,37 +30,37 @@ const BINARIES = {
BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-27.1/bin'
url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-28.0/bin'
},
ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz',
dir: 'geth-linux-amd64-1.14.8-a9523b64'
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.12-293a300d.tar.gz',
dir: 'geth-linux-amd64-1.14.12-293a300d'
},
ZEC: {
url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz',
dir: 'zcash-5.9.0/bin'
url: 'https://download.z.cash/downloads/zcash-6.0.0-linux64-debian-bullseye.tar.gz',
dir: 'zcash-6.0.0/bin'
},
DASH: {
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
defaultDir: 'dashcore-18.1.0/bin',
url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz',
dir: 'dashcore-21.1.0/bin'
url: 'https://github.com/dashpay/dash/releases/download/v21.1.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz',
dir: 'dashcore-21.1.1/bin'
},
LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
defaultDir: 'litecoin-0.18.1/bin',
url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz',
dir: 'litecoin-0.21.3/bin'
url: 'https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz',
dir: 'litecoin-0.21.4/bin'
},
BCH: {
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-27.1.0/bin',
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.0/bitcoin-cash-node-28.0.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-28.0.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
},
XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.18.3.3',
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2',
dir: 'monero-x86_64-linux-gnu-v0.18.3.4',
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
}
}

View file

@ -1,8 +1,16 @@
const axios = require("axios");
const getSatBEstimateFee = () => {
return axios.get('https://mempool.space/api/v1/fees/recommended')
.then(r => r.data.hourFee)
}
module.exports = { getSatBEstimateFee }
const axios = require("axios");
const getSatBEstimateFee = () => {
return axios.get('https://mempool.space/api/v1/fees/recommended')
.then(r => r.data.hourFee)
}
const getSatBEstimateFees = () => {
return axios.get('https://mempool.space/api/v1/fees/recommended')
.then(r => r.data)
}
module.exports = {
getSatBEstimateFees,
getSatBEstimateFee
}

View file

@ -36,7 +36,7 @@ function post (machineTx, pi) {
let addressReuse = false
let walletScore = {}
const promises = [settingsLoader.loadLatest()]
const promises = [settingsLoader.loadLatestConfig()]
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
if (isFirstPost) {
@ -44,7 +44,7 @@ function post (machineTx, pi) {
}
return Promise.all(promises)
.then(([{ config }, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
walletScore = fetchedWalletScore

View file

@ -474,8 +474,4 @@ function migrate (config, accounts) {
}
}
module.exports = {
migrateConfig,
migrateAccounts,
migrate
}
module.exports = { migrate }

View file

@ -51,6 +51,7 @@ const CASH_UNIT_CAPACITY = {
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
const CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS = 6
const AUTHENTICATOR_ISSUER_ENTITY = 'Lamassu'
const AUTH_TOKEN_EXPIRATION_TIME = '30 minutes'
const REGISTRATION_TOKEN_EXPIRATION_TIME = '30 minutes'
@ -85,6 +86,7 @@ module.exports = {
CONFIRMATION_CODE,
CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS,
WALLET_SCORE_THRESHOLD,
RECEIPT,
PSQL_URL,

View file

@ -3,6 +3,7 @@ const nmd = require('nano-markdown')
const plugins = require('../plugins')
const configManager = require('../new-config-manager')
const settingsLoader = require('../new-settings-loader')
const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
const state = require('../middlewares/state')
const { getMachine } = require('../machine-loader')
@ -323,8 +324,7 @@ const terms = (parent, { currentConfigVersion, currentHash }, { deviceId, settin
const isHashNew = hash !== currentHash
const text = isHashNew ? latestTerms.text : null
return plugins(settings, deviceId)
.fetchCurrentConfigVersion()
return settingsLoader.fetchCurrentConfigVersion()
.catch(() => null)
.then(configVersion => isHashNew || _.isNil(currentConfigVersion) || currentConfigVersion < configVersion)
.then(isVersionNew => isVersionNew ? _.omit(['text'], latestTerms) : null)

View file

@ -29,7 +29,7 @@ type OperatorInfo {
}
type MachineInfo {
deviceId: String!
deviceId: String! @deprecated(reason: "unused by the machine")
deviceName: String
numberOfCassettes: Int
numberOfRecyclers: Int
@ -107,7 +107,7 @@ type Trigger {
suspensionDays: Float
threshold: Int
thresholdDays: Int
customInfoRequestId: String
customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id")
customInfoRequest: CustomInfoRequest
externalService: String
}

View file

@ -11,7 +11,6 @@ const pairing = require('./pairing')
const { checkPings, checkStuckScreen } = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
const notifierUtils = require('./notifier/utils')
const notifierQueries = require('./notifier/queries')
const { ApolloError } = require('apollo-server-errors');
@ -94,9 +93,7 @@ function getUnpairedMachines () {
}
function getConfig (defaultConfig) {
if (defaultConfig) return Promise.resolve(defaultConfig)
return settingsLoader.loadLatest().config
return defaultConfig ? Promise.resolve(defaultConfig) : loadLatestConfig()
}
const getStatus = (ping, stuck) => {
@ -529,7 +526,6 @@ module.exports = {
updateNetworkHeartbeat,
getNetworkPerformance,
getNetworkHeartbeat,
getConfig,
getMachineIds,
emptyMachineUnits,
refillMachineUnits,

View file

@ -62,6 +62,7 @@ const ALL_ACCOUNTS = [
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDT_TRON, TRX] },
{ code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDT_TRON, TRX, ZEC] },
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
{ code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },

View file

@ -7,10 +7,7 @@ const resolvers = {
},
Mutation: {
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
// resetAccounts: (...[, { schemaVersion }]) => settingsLoader.resetAccounts(schemaVersion),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config),
// resetConfig: (...[, { schemaVersion }]) => settingsLoader.resetConfig(schemaVersion),
// migrateConfigAndAccounts: () => settingsLoader.migrate()
}
}

View file

@ -8,10 +8,7 @@ const typeDef = gql`
type Mutation {
saveAccounts(accounts: JSONObject): JSONObject @auth
# resetAccounts(schemaVersion: Int): JSONObject @auth
saveConfig(config: JSONObject): JSONObject @auth
# resetConfig(schemaVersion: Int): JSONObject @auth
# migrateConfigAndAccounts: JSONObject @auth
}
`

View file

@ -56,39 +56,45 @@ const addTermsHash = configs => {
)(terms)
}
const accountsSql = `update user_config set data = $2, valid = $3, schema_version = $4 where type = $1;
insert into user_config (type, data, valid, schema_version)
select $1, $2, $3, $4 where $1 not in (select type from user_config)`
const notifyReload = (dbOrTx, operatorId) =>
dbOrTx.none(
'NOTIFY $1:name, $2',
['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]
)
function saveAccounts (accounts) {
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)
return db.tx(t => {
return t.none(accountsSql, ['accounts', { accounts: newAccounts }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
.then(() => t.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]))
}).catch(console.error)
// 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 resetAccounts (schemaVersion) {
return db.none(
accountsSql,
[
'accounts',
{ accounts: NEW_SETTINGS_LOADER_SCHEMA_VERSION ? {} : [] },
true,
schemaVersion
]
)
}
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`
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')))
@ -106,15 +112,20 @@ function showAccounts (schemaVersion) {
})
}
const configSql = 'insert into user_config (type, data, valid, schema_version) values ($1, $2, $3, $4)'
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 => {
return t.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
.then(() => t.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]))
}).catch(console.error)
return db.tx(t =>
insertConfigRow(t, { config: newConfig })
.then(() => notifyReload(t, operatorId))
).catch(console.error)
})
}
@ -122,10 +133,10 @@ function removeFromConfig (fields) {
return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')])
.then(([currentConfig, operatorId]) => {
const newConfig = _.omit(fields, currentConfig)
return db.tx(t => {
return t.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
.then(() => t.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]))
}).catch(console.error)
return db.tx(t =>
insertConfigRow(t, { config: newConfig })
.then(() => notifyReload(t, operatorId))
).catch(console.error)
})
}
@ -133,23 +144,11 @@ function migrationSaveConfig (config) {
return loadLatestConfigOrNone()
.then(currentConfig => {
const newConfig = _.assign(currentConfig, config)
return db.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
return insertConfigRow(db, { config: newConfig })
.catch(console.error)
})
}
function resetConfig (schemaVersion) {
return db.none(
configSql,
[
'config',
{ config: schemaVersion === NEW_SETTINGS_LOADER_SCHEMA_VERSION ? {} : [] },
true,
schemaVersion
]
)
}
function loadLatest (schemaVersion) {
return Promise.all([loadLatestConfigOrNoneReturningVersion(schemaVersion), loadAccounts(schemaVersion)])
.then(([configObj, accounts]) => ({
@ -160,15 +159,15 @@ function loadLatest (schemaVersion) {
}
function loadLatestConfig () {
const sql = `select data
from user_config
where type=$1
and schema_version=$2
and valid
order by id desc
limit 1`
const sql = `SELECT data
FROM user_config
WHERE type = 'config'
AND schema_version = $1
AND valid
ORDER BY id DESC
LIMIT 1`
return db.one(sql, ['config', NEW_SETTINGS_LOADER_SCHEMA_VERSION])
return db.oneOrNone(sql, [NEW_SETTINGS_LOADER_SCHEMA_VERSION])
.then(row => row ? row.data.config : {})
.catch(err => {
throw err
@ -176,38 +175,39 @@ function loadLatestConfig () {
}
function loadLatestConfigOrNoneReturningVersion (schemaVersion) {
const sql = `select data, id
from user_config
where type=$1
and schema_version=$2
order by id desc
limit 1`
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, ['config', schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION])
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=$1
and schema_version=$2
order by id desc
limit 1`
const sql = `SELECT data
FROM user_config
WHERE type = 'config'
AND schema_version = $1
ORDER BY id DESC
LIMIT 1`
return db.oneOrNone(sql, ['config', schemaVersion || NEW_SETTINGS_LOADER_SCHEMA_VERSION])
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=$2
and schema_version=$3
and valid`
const sql = `SELECT data
FROM user_config
WHERE id = $1
AND type = 'config'
AND schema_version = $2
AND valid`
return db.one(sql, [versionId, 'config', NEW_SETTINGS_LOADER_SCHEMA_VERSION])
return db.one(sql, [versionId, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
.then(row => row.data.config)
.catch(err => {
if (err.name === 'QueryResultError') {
@ -228,29 +228,25 @@ function load (versionId) {
}))
}
function migrate () {
return loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION)
.then(res => {
const migrated = migration.migrate(res.config, res.accounts)
saveConfig(migrated.config)
saveAccounts(migrated.accounts)
return migrated
})
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,
resetConfig,
saveAccounts,
resetAccounts,
loadAccounts,
showAccounts,
loadLatest,
loadLatestConfig,
loadLatestConfigOrNone,
load,
migrate,
removeFromConfig
removeFromConfig,
fetchCurrentConfigVersion,
}

View file

@ -6,6 +6,7 @@ const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE'
const HIGH_CRYPTO_BALANCE = 'HIGH_CRYPTO_BALANCE'
const CASH_BOX_FULL = 'CASH_BOX_FULL'
const LOW_CASH_OUT = 'LOW_CASH_OUT'
const LOW_RECYCLER_STACKER = 'LOW_RECYCLER_STACKER'
const SECURITY = 'SECURITY'
const CODES_DISPLAY = {
@ -41,6 +42,7 @@ module.exports = {
HIGH_CRYPTO_BALANCE,
CASH_BOX_FULL,
LOW_CASH_OUT,
LOW_RECYCLER_STACKER,
SECURITY,
CODES_DISPLAY,
NETWORK_DOWN_TIME,

View file

@ -10,6 +10,7 @@ const {
HIGH_CRYPTO_BALANCE,
CASH_BOX_FULL,
LOW_CASH_OUT,
LOW_RECYCLER_STACKER,
SECURITY
} = require('./codes')
@ -80,6 +81,8 @@ function emailAlert (alert) {
return `Cash box full on ${alert.machineName} [${alert.notes} banknotes]`
case LOW_CASH_OUT:
return `Cassette for ${alert.denomination} ${alert.fiatCode} low [${alert.notes} banknotes]`
case LOW_RECYCLER_STACKER:
return `Recycler for ${alert.denomination} ${alert.fiatCode} low [${alert.notes} banknotes]`
case SECURITY:
return `Cashbox removed on ${alert.machineName}`
}

View file

@ -130,8 +130,8 @@ function checkStuckScreen (deviceEvents, machine) {
}
function transactionNotify (tx, rec) {
return settingsLoader.loadLatest().then(settings => {
const notifSettings = configManager.getGlobalNotifications(settings.config)
return settingsLoader.loadLatestConfig().then(config => {
const notifSettings = configManager.getGlobalNotifications(config)
const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity)
const isCashOut = tx.direction === 'cashOut'
@ -147,7 +147,7 @@ function transactionNotify (tx, rec) {
}
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
const walletSettings = configManager.getWalletSettings(tx.cryptoCode, settings.config)
const walletSettings = configManager.getWalletSettings(tx.cryptoCode, config)
const zeroConfLimit = walletSettings.zeroConfLimit || 0
const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit)
const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions
@ -308,8 +308,8 @@ function cashboxNotify (deviceId) {
// for notification center, check if type of notification is active before calling the respective notify function
const notifyIfActive = (type, fnName, ...args) => {
return settingsLoader.loadLatest().then(settings => {
const notificationSettings = configManager.getGlobalNotifications(settings.config).notificationCenter
return settingsLoader.loadLatestConfig().then(config => {
const notificationSettings = configManager.getGlobalNotifications(config).notificationCenter
if (!notificationCenter[fnName]) return Promise.reject(new Error(`Notification function ${fnName} for type ${type} does not exist`))
if (!(notificationSettings.active && notificationSettings[type])) return Promise.resolve()
return notificationCenter[fnName](...args)

View file

@ -2,20 +2,27 @@ const _ = require('lodash/fp')
const queries = require('./queries')
const utils = require('./utils')
const codes = require('./codes')
const customers = require('../customers')
const {
NOTIFICATION_TYPES: {
SECURITY,
COMPLIANCE,
CRYPTO_BALANCE,
FIAT_BALANCE,
ERROR,
HIGH_VALUE_TX,
NORMAL_VALUE_TX
},
const { NOTIFICATION_TYPES: {
SECURITY,
COMPLIANCE,
CRYPTO_BALANCE,
FIAT_BALANCE,
ERROR,
HIGH_VALUE_TX,
NORMAL_VALUE_TX }
} = codes
STALE,
PING,
const { STALE, PING } = codes
HIGH_CRYPTO_BALANCE,
LOW_CRYPTO_BALANCE,
CASH_BOX_FULL,
LOW_CASH_OUT,
LOW_RECYCLER_STACKER,
} = require('./codes')
const sanctionsNotify = (customer, phone) => {
const code = 'SANCTIONS'
@ -71,9 +78,13 @@ const fiatBalancesNotify = (fiatWarnings) => {
const { cassette, deviceId } = o.detail
return cassette === balance.cassette && deviceId === balance.deviceId
}, notInvalidated)) return
const message = balance.code === 'LOW_CASH_OUT' ?
const message = balance.code === LOW_CASH_OUT ?
`Cash-out cassette ${balance.cassette} low or empty!` :
`Cash box full or almost full!`
balance.code === LOW_RECYCLER_STACKER ?
`Recycler ${balance.cassette} low or empty!` :
balance.code === CASH_BOX_FULL ?
`Cash box full or almost full!` :
`Cash box full or almost full!` /* Shouldn't happen */
const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette })
return queries.addNotification(FIAT_BALANCE, message, detailB)
})
@ -105,7 +116,7 @@ const cryptoBalancesNotify = (cryptoWarnings) => {
}, notInvalidated)) return
const fiat = utils.formatCurrency(balance.fiatBalance.balance, balance.fiatCode)
const message = `${balance.code === 'HIGH_CRYPTO_BALANCE' ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]`
const message = `${balance.code === HIGH_CRYPTO_BALANCE ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]`
const detailB = utils.buildDetail({ cryptoCode: balance.cryptoCode, code: balance.code })
return queries.addNotification(CRYPTO_BALANCE, message, detailB)
})
@ -113,8 +124,8 @@ const cryptoBalancesNotify = (cryptoWarnings) => {
}
const balancesNotify = (balances) => {
const isCryptoCode = c => _.includes(c, ['HIGH_CRYPTO_BALANCE', 'LOW_CRYPTO_BALANCE'])
const isFiatCode = c => _.includes(c, ['LOW_CASH_OUT', 'CASH_BOX_FULL'])
const isCryptoCode = c => _.includes(c, [HIGH_CRYPTO_BALANCE, LOW_CRYPTO_BALANCE])
const isFiatCode = c => _.includes(c, [LOW_CASH_OUT, CASH_BOX_FULL, LOW_RECYCLER_STACKER])
const by = o =>
isCryptoCode(o) ? 'crypto' :
isFiatCode(o) ? 'fiat' :

View file

@ -11,6 +11,7 @@ const logger = require('./logger')
const logs = require('./logs')
const T = require('./time')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
const ticker = require('./ticker')
const wallet = require('./wallet')
const walletScoring = require('./wallet-scoring')
@ -23,7 +24,13 @@ const commissionMath = require('./commission-math')
const loyalty = require('./loyalty')
const transactionBatching = require('./tx-batching')
const { CASH_UNIT_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
const {
CASH_OUT_DISPENSE_READY,
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS,
CASH_UNIT_CAPACITY,
CONFIRMATION_CODE,
} = require('./constants')
const notifier = require('./notifier')
@ -237,17 +244,6 @@ function plugins (settings, deviceId) {
.then(([cassettes, recyclers]) => ({ cassettes: cassettes.cassettes, recyclers: recyclers.recyclers }))
}
function fetchCurrentConfigVersion () {
const sql = `select id from user_config
where type=$1
and valid
order by id desc
limit 1`
return db.one(sql, ['config'])
.then(row => row.id)
}
function mapCoinSettings (coinParams) {
const [ cryptoCode, cryptoNetwork ] = coinParams
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
@ -289,7 +285,7 @@ function plugins (settings, deviceId) {
return Promise.all([
buildAvailableCassettes(),
buildAvailableRecyclers(),
fetchCurrentConfigVersion(),
settingsLoader.fetchCurrentConfigVersion(),
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
loyalty.getNumberOfAvailablePromoCodes(),
Promise.all(supportsBatchingPromise),
@ -692,169 +688,73 @@ function plugins (settings, deviceId) {
}
function checkDeviceCashBalances (fiatCode, device) {
const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config)
const denomination1 = cashOutConfig.cassette1
const denomination2 = cashOutConfig.cassette2
const denomination3 = cashOutConfig.cassette3
const denomination4 = cashOutConfig.cassette4
const denominationRecycler1 = cashOutConfig.recycler1
const denominationRecycler2 = cashOutConfig.recycler2
const denominationRecycler3 = cashOutConfig.recycler3
const denominationRecycler4 = cashOutConfig.recycler4
const denominationRecycler5 = cashOutConfig.recycler5
const denominationRecycler6 = cashOutConfig.recycler6
const cashOutEnabled = cashOutConfig.active
const isUnitLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit
// const isUnitHigh = (have, max, limit) => cashOutEnabled && ((have / max) * 100) > limit
// const isUnitOutOfBounds = (have, max, lowerBound, upperBound) => isUnitLow(have, max, lowerBound) || isUnitHigh(have, max, upperBound)
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
const deviceId = device.deviceId
const machineName = device.name
const notifications = configManager.getNotifications(null, deviceId, settings.config)
const cashInAlert = device.cashUnits.cashbox > notifications.cashInAlertThreshold
? {
const cashInAlerts = device.cashUnits.cashbox > notifications.cashInAlertThreshold
? [{
code: 'CASH_BOX_FULL',
machineName,
deviceId: device.deviceId,
deviceId,
notes: device.cashUnits.cashbox
}
: null
}]
: []
const cassette1Alert = device.numberOfCassettes >= 1 && isUnitLow(device.cashUnits.cassette1, getCashUnitCapacity(device.model, 'cassette'), notifications.fillingPercentageCassette1)
? {
code: 'LOW_CASH_OUT',
cassette: 1,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.cassette1,
denomination: denomination1,
fiatCode
}
: null
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
const cashOutEnabled = cashOutConfig.active
const isUnitLow = (have, max, limit) => ((have / max) * 100) < limit
const cassette2Alert = device.numberOfCassettes >= 2 && isUnitLow(device.cashUnits.cassette2, getCashUnitCapacity(device.model, 'cassette'), notifications.fillingPercentageCassette2)
? {
code: 'LOW_CASH_OUT',
cassette: 2,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.cassette2,
denomination: denomination2,
fiatCode
}
: null
if (!cashOutEnabled)
return cashInAlerts
const cassette3Alert = device.numberOfCassettes >= 3 && isUnitLow(device.cashUnits.cassette3, getCashUnitCapacity(device.model, 'cassette'), notifications.fillingPercentageCassette3)
? {
code: 'LOW_CASH_OUT',
cassette: 3,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.cassette3,
denomination: denomination3,
fiatCode
}
: null
const cassetteCapacity = getCashUnitCapacity(device.model, 'cassette')
const cassetteAlerts = Array(Math.min(device.numberOfCassettes ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES))
.fill(null)
.flatMap((_elem, idx) => {
const nth = idx + 1
const cassetteField = `cassette${nth}`
const notes = device.cashUnits[cassetteField]
const denomination = cashOutConfig[cassetteField]
const cassette4Alert = device.numberOfCassettes >= 4 && isUnitLow(device.cashUnits.cassette4, getCashUnitCapacity(device.model, 'cassette'), notifications.fillingPercentageCassette4)
? {
code: 'LOW_CASH_OUT',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.cassette4,
denomination: denomination4,
fiatCode
}
: null
const limit = notifications[`fillingPercentageCassette${nth}`]
return isUnitLow(notes, cassetteCapacity, limit) ?
[{
code: 'LOW_CASH_OUT',
cassette: nth,
machineName,
deviceId,
notes,
denomination,
fiatCode
}] :
[]
})
const recycler1Alert = device.numberOfRecyclers >= 1 && isUnitLow(device.cashUnits.recycler1, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler1)
? {
code: 'LOW_RECYCLER_STACKER',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.recycler1,
denomination: denominationRecycler1,
fiatCode
}
: null
const recyclerCapacity = getCashUnitCapacity(device.model, 'recycler')
const recyclerAlerts = Array(Math.min(device.numberOfRecyclers ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS))
.fill(null)
.flatMap((_elem, idx) => {
const nth = idx + 1
const recyclerField = `recycler${nth}`
const notes = device.cashUnits[recyclerField]
const denomination = cashOutConfig[recyclerField]
const recycler2Alert = device.numberOfRecyclers >= 2 && isUnitLow(device.cashUnits.recycler2, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler2)
? {
code: 'LOW_RECYCLER_STACKER',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.recycler2,
denomination: denominationRecycler2,
fiatCode
}
: null
const limit = notifications[`fillingPercentageRecycler${nth}`]
return isUnitLow(notes, recyclerCapacity, limit) ?
[{
code: 'LOW_RECYCLER_STACKER',
cassette: nth, // @see DETAIL_TEMPLATE in /lib/notifier/utils.js
machineName,
deviceId,
notes,
denomination,
fiatCode
}] :
[]
})
const recycler3Alert = device.numberOfRecyclers >= 3 && isUnitLow(device.cashUnits.recycler3, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler3)
? {
code: 'LOW_RECYCLER_STACKER',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.recycler3,
denomination: denominationRecycler3,
fiatCode
}
: null
const recycler4Alert = device.numberOfRecyclers >= 4 && isUnitLow(device.cashUnits.recycler4, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler4)
? {
code: 'LOW_RECYCLER_STACKER',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.recycler4,
denomination: denominationRecycler4,
fiatCode
}
: null
const recycler5Alert = device.numberOfRecyclers >= 5 && isUnitLow(device.cashUnits.recycler5, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler5)
? {
code: 'LOW_RECYCLER_STACKER',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.recycler5,
denomination: denominationRecycler5,
fiatCode
}
: null
const recycler6Alert = device.numberOfRecyclers >= 6 && isUnitLow(device.cashUnits.recycler6, getCashUnitCapacity(device.model, 'recycler'), notifications.fillingPercentageRecycler6)
? {
code: 'LOW_RECYCLER_STACKER',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cashUnits.recycler6,
denomination: denominationRecycler6,
fiatCode
}
: null
return _.compact([
cashInAlert,
cassette1Alert,
cassette2Alert,
cassette3Alert,
cassette4Alert,
recycler1Alert,
recycler2Alert,
recycler3Alert,
recycler4Alert,
recycler5Alert,
recycler6Alert
])
return [].concat(cashInAlerts, cassetteAlerts, recyclerAlerts)
}
function checkCryptoBalances (fiatCode, devices) {
@ -1036,7 +936,6 @@ function plugins (settings, deviceId) {
sell,
getNotificationConfig,
notifyOperator,
fetchCurrentConfigVersion,
pruneMachinesHeartbeat,
rateAddress,
rateTransaction,

View file

@ -0,0 +1,95 @@
const { AML } = require('elliptic-sdk')
const _ = require('lodash/fp')
const NAME = 'Elliptic'
const HOLLISTIC_COINS = {
BTC: 'BTC',
ETH: 'ETH',
USDT: 'USDT',
USDT_TRON: 'USDT',
LTC: 'LTC',
TRX: 'TRX'
}
const SINGLE_ASSET_COINS = {
ZEC: {
asset: 'ZEC',
blockchain: 'zcash'
},
BCH: {
asset: 'BCH',
blockchain: 'bitcoin_cash'
}
}
const TYPE = {
TRANSACTION: 'transaction',
ADDRESS: 'address'
}
const SUPPORTED_COINS = { ...HOLLISTIC_COINS, ...SINGLE_ASSET_COINS }
function rate (account, objectType, cryptoCode, objectId) {
return isWalletScoringEnabled(account, cryptoCode).then(isEnabled => {
if (!isEnabled) return Promise.resolve(null)
const aml = new AML({
key: account.apiKey,
secret: account.apiSecret
})
const isHolistic = Object.keys(HOLLISTIC_COINS).includes(cryptoCode)
const requestBody = {
subject: {
asset: isHolistic ? 'holistic' : SINGLE_ASSET_COINS[cryptoCode].asset,
blockchain: isHolistic ? 'holistic' : SINGLE_ASSET_COINS[cryptoCode].blockchain,
type: objectType,
hash: objectId
},
type: objectType === TYPE.ADDRESS ? 'wallet_exposure' : 'source_of_funds'
}
const threshold = account.scoreThreshold
const endpoint = objectType === TYPE.ADDRESS ? '/v2/wallet/synchronous' : '/v2/analysis/synchronous'
return aml.client
.post(endpoint, requestBody)
.then((res) => {
const resScore = res.data?.risk_score
// elliptic returns 0-1 score, but we're accepting 0-100 config
// normalize score to 0-10 where 0 is the lowest risk
// elliptic score can be null and contains decimals
return {score: (resScore || 0) * 10, isValid: ((resScore || 0) * 100) < threshold}
})
})
}
function rateTransaction (account, cryptoCode, transactionId) {
return rate(account, TYPE.TRANSACTION, cryptoCode, transactionId)
}
function rateAddress (account, cryptoCode, address) {
return rate(account, TYPE.ADDRESS, cryptoCode, address)
}
function isWalletScoringEnabled (account, cryptoCode) {
const isAccountEnabled = !_.isNil(account) && account.enabled
if (!isAccountEnabled) return Promise.resolve(false)
if (!Object.keys(SUPPORTED_COINS).includes(cryptoCode)) {
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
}
return Promise.resolve(true)
}
module.exports = {
NAME,
rateAddress,
rateTransaction,
isWalletScoringEnabled
}

View file

@ -219,5 +219,6 @@ module.exports = {
sendCoinsBatch,
checkBlockchainStatus,
getTxHashesByAddress,
fetch,
SUPPORTS_BATCHING
}

View file

@ -42,6 +42,10 @@ const SWEEP_QUEUE = new PQueue({
interval: 250,
})
const SEND_QUEUE = new PQueue({
concurrency: 1,
})
const infuraCalls = {}
const pify = _function => {
@ -78,18 +82,20 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
const { toAddress, cryptoAtoms, cryptoCode } = tx
const isErc20Token = coins.utils.isErc20Token(cryptoCode)
return (isErc20Token ? generateErc20Tx : generateTx)(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode)
.then(pify(web3.eth.sendSignedTransaction))
.then(txid => {
return pify(web3.eth.getTransaction)(txid)
.then(tx => {
if (!tx) return { txid }
return SEND_QUEUE.add(() =>
(isErc20Token ? generateErc20Tx : generateTx)(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode)
.then(pify(web3.eth.sendSignedTransaction))
.then(txid => {
return pify(web3.eth.getTransaction)(txid)
.then(tx => {
if (!tx) return { txid }
const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0)
const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0)
return { txid, fee }
})
})
return { txid, fee }
})
})
)
}
function checkCryptoCode (cryptoCode) {

View file

@ -4,8 +4,6 @@ const base = require('../geth/base')
const T = require('../../../time')
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('../../../constants')
const REGULAR_TX_POLLING = 5 * T.seconds
const NAME = 'infura'
function run (account) {
@ -27,21 +25,13 @@ function shouldGetStatus (tx) {
const timePassedSinceTx = Date.now() - new Date(tx.created)
const timePassedSinceReq = Date.now() - new Date(txsCache.get(tx.id).lastReqTime)
// Allow for infura to gradually lower the amount of requests based on the time passed since the transaction
// Until first 5 minutes - 1/2 regular polling speed
// Until first 10 minutes - 1/4 regular polling speed
// Until first hour - 1/8 polling speed
// Until first two hours - 1/12 polling speed
// Until first four hours - 1/16 polling speed
// Until first day - 1/24 polling speed
// After first day - 1/32 polling speed
if (timePassedSinceTx < 5 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * REGULAR_TX_POLLING
if (timePassedSinceTx < 10 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 4 * REGULAR_TX_POLLING
if (timePassedSinceTx < 1 * T.hour) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 8 * REGULAR_TX_POLLING
if (timePassedSinceTx < 2 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 12 * REGULAR_TX_POLLING
if (timePassedSinceTx < 4 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 16 * REGULAR_TX_POLLING
if (timePassedSinceTx < 1 * T.day) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 24 * REGULAR_TX_POLLING
return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 32 * REGULAR_TX_POLLING
if (timePassedSinceTx < 3 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 10 * T.seconds
if (timePassedSinceTx < 5 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 20 * T.seconds
if (timePassedSinceTx < 30 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.minute
if (timePassedSinceTx < 1 * T.hour) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * T.minute
if (timePassedSinceTx < 3 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 5 * T.minute
if (timePassedSinceTx < 1 * T.day) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour
return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour
}
// Override geth's getStatus function to allow for different polling timing

View file

@ -54,7 +54,7 @@ app.use(compression({ threshold: 500 }))
app.use(helmet())
app.use(nocache())
app.use(express.json({ limit: '2mb' }))
app.use(morgan(':method :url :status :response-time ms - :res[content-length]', { stream: logger.stream }))
app.use(morgan(':method :url :status :response-time ms -- :req[content-length]/:res[content-length] b', { stream: logger.stream }))
// app /pair and /ca routes
app.use('/', pairingRoutes)

View file

@ -311,6 +311,7 @@ function getOrAddCustomerPhone (req, res, next) {
}
function getOrAddCustomerEmail (req, res, next) {
const deviceId = req.deviceId
const customerData = req.body
const pi = plugins(req.settings, req.deviceId)
@ -318,7 +319,7 @@ function getOrAddCustomerEmail (req, res, next) {
return pi.getEmailCode(email)
.then(code => {
return addOrUpdateCustomer(customerData, req.settings.config, true)
return addOrUpdateCustomer(customerData, deviceId, req.settings.config, true)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {

View file

@ -4,7 +4,7 @@ const nmd = require('nano-markdown')
const router = express.Router()
const configManager = require('../new-config-manager')
const plugins = require('../plugins')
const settingsLoader = require('../new-settings-loader')
const createTerms = terms => (terms.active && terms.text) ? ({
delay: terms.delay,
@ -18,15 +18,10 @@ const createTerms = terms => (terms.active && terms.text) ? ({
function getTermsConditions (req, res, next) {
const deviceId = req.deviceId
const settings = req.settings
const terms = configManager.getTermsConditions(settings.config)
const pi = plugins(settings, deviceId)
return pi.fetchCurrentConfigVersion().then(version => {
return res.json({ terms: createTerms(terms), version })
})
const { config } = req.settings
const terms = configManager.getTermsConditions(config)
return settingsLoader.fetchCurrentConfigVersion()
.then(version => res.json({ terms: createTerms(terms), version }))
.catch(next)
}

View file

@ -1,14 +1,23 @@
const ph = require('./plugin-helper')
const argv = require('minimist')(process.argv.slice(2))
const configManager = require('./new-config-manager')
function loadWalletScoring (settings, cryptoCode) {
const pluginCode = argv.mockScoring ? 'mock-scoring' : 'scorechain'
const wallet = cryptoCode ? ph.load(ph.WALLET, configManager.getWalletSettings(cryptoCode, settings.config).wallet) : null
const plugin = ph.load(ph.WALLET_SCORING, pluginCode)
const account = settings.accounts[pluginCode]
// TODO - This function should be rolled back after UI is created for this feature
function loadWalletScoring (settings) {
if (argv.mockScoring) {
const mock = ph.load(ph.WALLET_SCORING, 'mock-scoring')
return { plugin: mock, account: {} }
}
return { plugin, account, wallet }
const scorechainAccount = settings.accounts['scorechain']
if (scorechainAccount?.enabled) {
const scorechain = ph.load(ph.WALLET_SCORING, 'scorechain')
return { plugin: scorechain, account: scorechainAccount}
}
const ellipticAccount = settings.accounts['elliptic']
const elliptic = ph.load(ph.WALLET_SCORING, 'elliptic')
return { plugin: elliptic, account: ellipticAccount }
}
function rateTransaction (settings, cryptoCode, address) {