Merge pull request #1689 from RafaelTaranto/chore/sumsub-rebase-simplified
chore: sumsub rebase simplified
This commit is contained in:
commit
09c3fb8a70
29 changed files with 828 additions and 73 deletions
80
lib/compliance-external.js
Normal file
80
lib/compliance-external.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const logger = require('./logger')
|
||||||
|
const configManager = require('./new-config-manager')
|
||||||
|
const ph = require('./plugin-helper')
|
||||||
|
|
||||||
|
const getPlugin = (settings, pluginCode) => {
|
||||||
|
const account = settings.accounts[pluginCode]
|
||||||
|
const plugin = ph.load(ph.COMPLIANCE, pluginCode)
|
||||||
|
|
||||||
|
return ({ plugin, account })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatus = (settings, service, customerId) => {
|
||||||
|
try {
|
||||||
|
const { plugin, account } = getPlugin(settings, service)
|
||||||
|
|
||||||
|
return plugin.getApplicantStatus(account, customerId)
|
||||||
|
.then((status) => ({
|
||||||
|
service,
|
||||||
|
status
|
||||||
|
}))
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response.status !== 404) logger.error(`Error getting applicant for service ${service}:`, error.message)
|
||||||
|
return {
|
||||||
|
service: service,
|
||||||
|
status: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error loading plugin for service ${service}:`, error)
|
||||||
|
return Promise.resolve({
|
||||||
|
service: service,
|
||||||
|
status: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusMap = (settings, customerExternalCompliance) => {
|
||||||
|
const triggers = configManager.getTriggers(settings.config)
|
||||||
|
const services = _.flow(
|
||||||
|
_.map('externalService'),
|
||||||
|
_.compact,
|
||||||
|
_.uniq
|
||||||
|
)(triggers)
|
||||||
|
|
||||||
|
const applicantPromises = _.map(service => {
|
||||||
|
return getStatus(settings, service, customerExternalCompliance)
|
||||||
|
})(services)
|
||||||
|
|
||||||
|
return Promise.all(applicantPromises)
|
||||||
|
.then((applicantResults) => {
|
||||||
|
return _.reduce((map, result) => {
|
||||||
|
if (result.status) map[result.service] = result.status
|
||||||
|
return map
|
||||||
|
}, {})(applicantResults)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createApplicant = (settings, externalService, customerId) => {
|
||||||
|
const account = settings.accounts[externalService]
|
||||||
|
const { plugin } = getPlugin(settings, externalService)
|
||||||
|
|
||||||
|
return plugin.createApplicant(account, customerId, account.applicantLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLink = (settings, externalService, customerId) => {
|
||||||
|
const account = settings.accounts[externalService]
|
||||||
|
const { plugin } = getPlugin(settings, externalService)
|
||||||
|
|
||||||
|
return plugin.createLink(account, customerId, account.applicantLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getStatusMap,
|
||||||
|
getStatus,
|
||||||
|
createApplicant,
|
||||||
|
createLink
|
||||||
|
}
|
||||||
106
lib/customers.js
106
lib/customers.js
|
|
@ -17,6 +17,9 @@ const NUM_RESULTS = 1000
|
||||||
const sms = require('./sms')
|
const sms = require('./sms')
|
||||||
const settingsLoader = require('./new-settings-loader')
|
const settingsLoader = require('./new-settings-loader')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
const externalCompliance = require('./compliance-external')
|
||||||
|
|
||||||
|
const { APPROVED, RETRY } = require('./plugins/compliance/consts')
|
||||||
|
|
||||||
const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel', 'scoreThresholdReached', 'walletScoringError']
|
const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel', 'scoreThresholdReached', 'walletScoringError']
|
||||||
|
|
||||||
|
|
@ -243,7 +246,7 @@ function deleteEditedData (id, data) {
|
||||||
'id_card_data',
|
'id_card_data',
|
||||||
'id_card_photo',
|
'id_card_photo',
|
||||||
'us_ssn',
|
'us_ssn',
|
||||||
'subcriber_info',
|
'subscriber_info',
|
||||||
'name'
|
'name'
|
||||||
]
|
]
|
||||||
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data))
|
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data))
|
||||||
|
|
@ -322,6 +325,7 @@ function getById (id) {
|
||||||
return db.oneOrNone(sql, [id])
|
return db.oneOrNone(sql, [id])
|
||||||
.then(assignCustomerData)
|
.then(assignCustomerData)
|
||||||
.then(getCustomInfoRequestsData)
|
.then(getCustomInfoRequestsData)
|
||||||
|
.then(getExternalComplianceMachine)
|
||||||
.then(camelize)
|
.then(camelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,7 +346,11 @@ function camelize (customer) {
|
||||||
function camelizeDeep (customer) {
|
function camelizeDeep (customer) {
|
||||||
return _.flow(
|
return _.flow(
|
||||||
camelize,
|
camelize,
|
||||||
it => ({ ...it, notes: (it.notes ?? []).map(camelize) })
|
it => ({
|
||||||
|
...it,
|
||||||
|
notes: (it.notes ?? []).map(camelize),
|
||||||
|
externalCompliance: (it.externalCompliance ?? []).map(camelize)
|
||||||
|
})
|
||||||
)(customer)
|
)(customer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,6 +595,7 @@ function getCustomerById (id) {
|
||||||
return db.oneOrNone(sql, [passableErrorCodes, id])
|
return db.oneOrNone(sql, [passableErrorCodes, id])
|
||||||
.then(assignCustomerData)
|
.then(assignCustomerData)
|
||||||
.then(getCustomInfoRequestsData)
|
.then(getCustomInfoRequestsData)
|
||||||
|
.then(getExternalCompliance)
|
||||||
.then(camelizeDeep)
|
.then(camelizeDeep)
|
||||||
.then(formatSubscriberInfo)
|
.then(formatSubscriberInfo)
|
||||||
}
|
}
|
||||||
|
|
@ -927,6 +936,95 @@ function updateLastAuthAttempt (customerId) {
|
||||||
return db.none(sql, [customerId])
|
return db.none(sql, [customerId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExternalComplianceMachine (customer) {
|
||||||
|
return settingsLoader.loadLatest()
|
||||||
|
.then(settings => externalCompliance.getStatusMap(settings, customer.id))
|
||||||
|
.then(statusMap => {
|
||||||
|
return updateExternalComplianceByMap(customer.id, statusMap)
|
||||||
|
.then(() => customer.externalCompliance = statusMap)
|
||||||
|
.then(() => customer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExternalCompliance(customerId, service, status) {
|
||||||
|
const sql = `
|
||||||
|
UPDATE customer_external_compliance SET last_known_status = $1, last_updated = now()
|
||||||
|
WHERE customer_id=$2 AND service=$3
|
||||||
|
`
|
||||||
|
return db.none(sql, [status, customerId, service])
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExternalComplianceByMap(customerId, serviceMap) {
|
||||||
|
const sql = `
|
||||||
|
UPDATE customer_external_compliance SET last_known_status = $1, last_updated = now()
|
||||||
|
WHERE customer_id=$2 AND service=$3
|
||||||
|
`
|
||||||
|
const pairs = _.toPairs(serviceMap)
|
||||||
|
const promises = _.map(([service, status]) => db.none(sql, [status.answer, customerId, service]))(pairs)
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExternalCompliance(customer) {
|
||||||
|
const sql = `SELECT external_id, service, last_known_status, last_updated
|
||||||
|
FROM customer_external_compliance where customer_id=$1`
|
||||||
|
return db.manyOrNone(sql, [customer.id])
|
||||||
|
.then(compliance => {
|
||||||
|
customer.externalCompliance = compliance
|
||||||
|
})
|
||||||
|
.then(() => customer)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenExternalCompliance() {
|
||||||
|
const sql = `SELECT customer_id, service, last_known_status FROM customer_external_compliance where last_known_status in ('PENDING', 'RETRY') or last_known_status is null`
|
||||||
|
return db.manyOrNone(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyRetryExternalCompliance(settings, customerId, service) {
|
||||||
|
const sql = 'SELECT phone FROM customers WHERE id=$1'
|
||||||
|
const promises = [db.one(sql, [customerId]), externalCompliance.createLink(settings, service, customerId)]
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
.then(([toNumber, link]) => {
|
||||||
|
const body = `Your external compliance verification has failed. Please try again. Link for retry: ${link}`
|
||||||
|
|
||||||
|
return sms.sendMessage(settings, { toNumber, body })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyApprovedExternalCompliance(settings, customerId) {
|
||||||
|
const sql = 'SELECT phone FROM customers WHERE id=$1'
|
||||||
|
return db.one(sql, [customerId])
|
||||||
|
.then((toNumber) => {
|
||||||
|
const body = 'Your external compliance verification has been approved.'
|
||||||
|
|
||||||
|
return sms.sendMessage(settings, { toNumber, body })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkExternalCompliance(settings) {
|
||||||
|
return getOpenExternalCompliance()
|
||||||
|
.then(externals => {
|
||||||
|
console.log(externals)
|
||||||
|
const promises = _.map(external => {
|
||||||
|
return externalCompliance.getStatus(settings, external.service, external.customer_id)
|
||||||
|
.then(status => {
|
||||||
|
console.log('status', status, external.customer_id, external.service)
|
||||||
|
if (status.status.answer === RETRY) notifyRetryExternalCompliance(settings, external.customer_id, status.service)
|
||||||
|
if (status.status.answer === APPROVED) notifyApprovedExternalCompliance(settings, external.customer_id)
|
||||||
|
|
||||||
|
return updateExternalCompliance(external.customer_id, external.service, status.status.answer)
|
||||||
|
})
|
||||||
|
}, externals)
|
||||||
|
return Promise.all(promises)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addExternalCompliance(customerId, service, id) {
|
||||||
|
const sql = `INSERT INTO customer_external_compliance (customer_id, external_id, service) VALUES ($1, $2, $3)`
|
||||||
|
return db.none(sql, [customerId, id, service])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
add,
|
add,
|
||||||
addWithEmail,
|
addWithEmail,
|
||||||
|
|
@ -950,5 +1048,7 @@ module.exports = {
|
||||||
updateTxCustomerPhoto,
|
updateTxCustomerPhoto,
|
||||||
enableTestCustomer,
|
enableTestCustomer,
|
||||||
disableTestCustomer,
|
disableTestCustomer,
|
||||||
updateLastAuthAttempt
|
updateLastAuthAttempt,
|
||||||
|
addExternalCompliance,
|
||||||
|
checkExternalCompliance
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ type Trigger {
|
||||||
thresholdDays: Int
|
thresholdDays: Int
|
||||||
customInfoRequestId: String
|
customInfoRequestId: String
|
||||||
customInfoRequest: CustomInfoRequest
|
customInfoRequest: CustomInfoRequest
|
||||||
|
externalService: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type TermsDetails {
|
type TermsDetails {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const ID_VERIFIER = 'idVerifier'
|
||||||
const EMAIL = 'email'
|
const EMAIL = 'email'
|
||||||
const ZERO_CONF = 'zeroConf'
|
const ZERO_CONF = 'zeroConf'
|
||||||
const WALLET_SCORING = 'wallet_scoring'
|
const WALLET_SCORING = 'wallet_scoring'
|
||||||
|
const COMPLIANCE = 'compliance'
|
||||||
|
|
||||||
const ALL_ACCOUNTS = [
|
const ALL_ACCOUNTS = [
|
||||||
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
|
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
|
||||||
|
|
@ -61,7 +62,9 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
|
{ 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: '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: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDT_TRON, TRX] },
|
||||||
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true }
|
{ 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 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const devMode = require('minimist')(process.argv.slice(2)).dev
|
const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ const typeDef = gql`
|
||||||
customInfoRequests: [CustomRequestData]
|
customInfoRequests: [CustomRequestData]
|
||||||
notes: [CustomerNote]
|
notes: [CustomerNote]
|
||||||
isTestCustomer: Boolean
|
isTestCustomer: Boolean
|
||||||
|
externalCompliance: [JSONObject]
|
||||||
}
|
}
|
||||||
|
|
||||||
input CustomerInput {
|
input CustomerInput {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ const SECRET_FIELDS = [
|
||||||
'inforu.apiKey',
|
'inforu.apiKey',
|
||||||
'galoy.walletId',
|
'galoy.walletId',
|
||||||
'galoy.apiSecret',
|
'galoy.apiSecret',
|
||||||
'bitfinex.secret'
|
'bitfinex.secret',
|
||||||
|
'sumsub.apiToken',
|
||||||
|
'sumsub.privateKey'
|
||||||
]
|
]
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ const pluginCodes = {
|
||||||
LAYER2: 'layer2',
|
LAYER2: 'layer2',
|
||||||
SMS: 'sms',
|
SMS: 'sms',
|
||||||
EMAIL: 'email',
|
EMAIL: 'email',
|
||||||
ZERO_CONF: 'zero-conf'
|
ZERO_CONF: 'zero-conf',
|
||||||
|
COMPLIANCE: 'compliance'
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = _.assign({load}, pluginCodes)
|
module.exports = _.assign({load}, pluginCodes)
|
||||||
|
|
|
||||||
6
lib/plugins/compliance/consts.js
Normal file
6
lib/plugins/compliance/consts.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
RETRY: 'RETRY',
|
||||||
|
APPROVED: 'APPROVED',
|
||||||
|
REJECTED: 'REJECTED'
|
||||||
|
}
|
||||||
31
lib/plugins/compliance/mock-compliance/mock-compliance.js
Normal file
31
lib/plugins/compliance/mock-compliance/mock-compliance.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
|
const {APPROVED} = require('../consts')
|
||||||
|
|
||||||
|
const CODE = 'mock-compliance'
|
||||||
|
|
||||||
|
const createLink = (settings, userId, level) => {
|
||||||
|
return `this is a mock external link, ${userId}, ${level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApplicantStatus = (account, userId) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
service: CODE,
|
||||||
|
status: {
|
||||||
|
level: account.applicantLevel, answer: APPROVED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createApplicant = () => {
|
||||||
|
return Promise.resolve({
|
||||||
|
id: uuid.v4()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CODE,
|
||||||
|
createApplicant,
|
||||||
|
getApplicantStatus,
|
||||||
|
createLink
|
||||||
|
}
|
||||||
34
lib/plugins/compliance/sumsub/request.js
Normal file
34
lib/plugins/compliance/sumsub/request.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
const axios = require('axios')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const FormData = require('form-data')
|
||||||
|
|
||||||
|
const axiosConfig = {
|
||||||
|
baseURL: 'https://api.sumsub.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSigBuilder = (apiToken, secretKey) => config => {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000)
|
||||||
|
const signature = crypto.createHmac('sha256', secretKey)
|
||||||
|
|
||||||
|
signature.update(`${timestamp}${_.toUpper(config.method)}${config.url}`)
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
signature.update(config.data.getBuffer())
|
||||||
|
} else if (config.data) {
|
||||||
|
signature.update(JSON.stringify(config.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
config.headers['X-App-Token'] = apiToken
|
||||||
|
config.headers['X-App-Access-Sig'] = signature.digest('hex')
|
||||||
|
config.headers['X-App-Access-Ts'] = timestamp
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = ((account, config) => {
|
||||||
|
const instance = axios.create(axiosConfig)
|
||||||
|
instance.interceptors.request.use(getSigBuilder(account.apiToken, account.secretKey), Promise.reject)
|
||||||
|
return instance(config)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = request
|
||||||
98
lib/plugins/compliance/sumsub/sumsub.api.js
Normal file
98
lib/plugins/compliance/sumsub/sumsub.api.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
const request = require('./request')
|
||||||
|
|
||||||
|
const createApplicant = (account, userId, level) => {
|
||||||
|
if (!userId || !level) {
|
||||||
|
return Promise.reject(`Missing required fields: userId: ${userId}, level: ${level}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `/resources/applicants?levelName=${level}`,
|
||||||
|
data: {
|
||||||
|
externalUserId: userId,
|
||||||
|
sourceKey: 'lamassu'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(account, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLink = (account, userId, level) => {
|
||||||
|
if (!userId || !level) {
|
||||||
|
return Promise.reject(`Missing required fields: userId: ${userId}, level: ${level}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `/resources/sdkIntegrations/levels/${level}/websdkLink?ttlInSecs=${600}&externalUserId=${userId}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(account, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApplicantByExternalId = (account, id) => {
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject('Missing required fields: id')
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/resources/applicants/-;externalUserId=${id}/one`,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(account, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApplicantStatus = (account, id) => {
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject(`Missing required fields: id`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/resources/applicants/${id}/status`,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(account, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApplicantById = (account, id) => {
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject(`Missing required fields: id`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/resources/applicants/${id}/one`,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return request(account, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createLink,
|
||||||
|
createApplicant,
|
||||||
|
getApplicantByExternalId,
|
||||||
|
getApplicantById,
|
||||||
|
getApplicantStatus
|
||||||
|
}
|
||||||
52
lib/plugins/compliance/sumsub/sumsub.js
Normal file
52
lib/plugins/compliance/sumsub/sumsub.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const sumsubApi = require('./sumsub.api')
|
||||||
|
const { PENDING, RETRY, APPROVED, REJECTED } = require('../consts')
|
||||||
|
|
||||||
|
const CODE = 'sumsub'
|
||||||
|
|
||||||
|
const getApplicantByExternalId = (account, userId) => {
|
||||||
|
return sumsubApi.getApplicantByExternalId(account, userId)
|
||||||
|
.then(r => r.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createApplicant = (account, userId, level) => {
|
||||||
|
return sumsubApi.createApplicant(account, userId, level)
|
||||||
|
.then(r => r.data)
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response.status === 409) return getApplicantByExternalId(account, userId)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLink = (account, userId, level) => {
|
||||||
|
return sumsubApi.createLink(account, userId, level)
|
||||||
|
.then(r => r.data.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getApplicantStatus = (account, userId) => {
|
||||||
|
return sumsubApi.getApplicantByExternalId(account, userId)
|
||||||
|
.then(r => {
|
||||||
|
const levelName = _.get('data.review.levelName', r)
|
||||||
|
const reviewStatus = _.get('data.review.reviewStatus', r)
|
||||||
|
const reviewAnswer = _.get('data.review.reviewResult.reviewAnswer', r)
|
||||||
|
const reviewRejectType = _.get('data.review.reviewResult.reviewRejectType', r)
|
||||||
|
|
||||||
|
// if last review was from a different level, return the current level and RETRY
|
||||||
|
if (levelName !== account.applicantLevel) return { level: account.applicantLevel, answer: RETRY }
|
||||||
|
|
||||||
|
let answer = PENDING
|
||||||
|
if (reviewAnswer === 'GREEN' && reviewStatus === 'completed') answer = APPROVED
|
||||||
|
if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY
|
||||||
|
if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') answer = REJECTED
|
||||||
|
|
||||||
|
return { level: levelName, answer }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CODE,
|
||||||
|
createApplicant,
|
||||||
|
getApplicantStatus,
|
||||||
|
createLink
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ const T = require('./time')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const cashOutTx = require('./cash-out/cash-out-tx')
|
const cashOutTx = require('./cash-out/cash-out-tx')
|
||||||
const cashInTx = require('./cash-in/cash-in-tx')
|
const cashInTx = require('./cash-in/cash-in-tx')
|
||||||
|
const customers = require('./customers')
|
||||||
const sanctionsUpdater = require('./ofac/update')
|
const sanctionsUpdater = require('./ofac/update')
|
||||||
const sanctions = require('./ofac/index')
|
const sanctions = require('./ofac/index')
|
||||||
const coinAtmRadar = require('./coinatmradar/coinatmradar')
|
const coinAtmRadar = require('./coinatmradar/coinatmradar')
|
||||||
|
|
@ -31,6 +32,7 @@ const RADAR_UPDATE_INTERVAL = 5 * T.minutes
|
||||||
const PRUNE_MACHINES_HEARTBEAT = 1 * T.day
|
const PRUNE_MACHINES_HEARTBEAT = 1 * T.day
|
||||||
const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes
|
const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes
|
||||||
const TICKER_RATES_INTERVAL = 59 * T.seconds
|
const TICKER_RATES_INTERVAL = 59 * T.seconds
|
||||||
|
const EXTERNAL_COMPLIANCE_INTERVAL = 1 * T.minutes
|
||||||
|
|
||||||
const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds
|
const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds
|
||||||
const PENDING_INTERVAL = 10 * T.seconds
|
const PENDING_INTERVAL = 10 * T.seconds
|
||||||
|
|
@ -127,6 +129,10 @@ function updateCoinAtmRadar () {
|
||||||
.then(rates => coinAtmRadar.update(rates, settings()))
|
.then(rates => coinAtmRadar.update(rates, settings()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function checkExternalCompliance (settings) {
|
||||||
|
// return customers.checkExternalCompliance(settings)
|
||||||
|
// }
|
||||||
|
|
||||||
function initializeEachSchema (schemas = ['public']) {
|
function initializeEachSchema (schemas = ['public']) {
|
||||||
// for each schema set "thread variables" and do polling
|
// for each schema set "thread variables" and do polling
|
||||||
return _.forEach(schema => {
|
return _.forEach(schema => {
|
||||||
|
|
@ -190,6 +196,7 @@ function doPolling (schema) {
|
||||||
pi().sweepHd()
|
pi().sweepHd()
|
||||||
notifier.checkNotification(pi())
|
notifier.checkNotification(pi())
|
||||||
updateCoinAtmRadar()
|
updateCoinAtmRadar()
|
||||||
|
// checkExternalCompliance(settings())
|
||||||
|
|
||||||
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST)
|
||||||
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
|
||||||
|
|
@ -206,6 +213,7 @@ function doPolling (schema) {
|
||||||
addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW)
|
addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW)
|
||||||
addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW)
|
addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW)
|
||||||
addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings)
|
addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings)
|
||||||
|
// addToQueue(checkExternalCompliance, EXTERNAL_COMPLIANCE_INTERVAL, schema, QUEUE.SLOW, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup (schemasToAdd = [], schemasToRemove = []) {
|
function setup (schemasToAdd = [], schemasToRemove = []) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const plugins = require('../plugins')
|
||||||
const Tx = require('../tx')
|
const Tx = require('../tx')
|
||||||
const loyalty = require('../loyalty')
|
const loyalty = require('../loyalty')
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
const externalCompliance = require('../compliance-external')
|
||||||
|
|
||||||
function updateCustomerCustomInfoRequest (customerId, patch) {
|
function updateCustomerCustomInfoRequest (customerId, patch) {
|
||||||
const promise = _.isNil(patch.data) ?
|
const promise = _.isNil(patch.data) ?
|
||||||
|
|
@ -234,6 +235,28 @@ function sendSmsReceipt (req, res, next) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExternalComplianceLink (req, res, next) {
|
||||||
|
const customerId = req.query.customer
|
||||||
|
const triggerId = req.query.trigger
|
||||||
|
const isRetry = req.query.isRetry
|
||||||
|
if (_.isNil(customerId) || _.isNil(triggerId)) return next(httpError('Not Found', 404))
|
||||||
|
|
||||||
|
const settings = req.settings
|
||||||
|
const triggers = configManager.getTriggers(settings.config)
|
||||||
|
const trigger = _.find(it => it.id === triggerId)(triggers)
|
||||||
|
const externalService = trigger.externalService
|
||||||
|
|
||||||
|
if (isRetry) {
|
||||||
|
return externalCompliance.createLink(settings, externalService, customerId)
|
||||||
|
.then(url => respond(req, res, { url }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return externalCompliance.createApplicant(settings, externalService, customerId)
|
||||||
|
.then(applicant => customers.addExternalCompliance(customerId, externalService, applicant.id))
|
||||||
|
.then(() => externalCompliance.createLink(settings, externalService, customerId))
|
||||||
|
.then(url => respond(req, res, { url }))
|
||||||
|
}
|
||||||
|
|
||||||
function addOrUpdateCustomer (customerData, config, isEmailAuth) {
|
function addOrUpdateCustomer (customerData, config, isEmailAuth) {
|
||||||
const triggers = configManager.getTriggers(config)
|
const triggers = configManager.getTriggers(config)
|
||||||
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
||||||
|
|
@ -311,6 +334,7 @@ router.patch('/:id/suspend', triggerSuspend)
|
||||||
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
||||||
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
||||||
router.post('/:id/smsreceipt', sendSmsReceipt)
|
router.post('/:id/smsreceipt', sendSmsReceipt)
|
||||||
|
router.get('/external', getExternalComplianceLink)
|
||||||
router.post('/phone_code', getOrAddCustomerPhone)
|
router.post('/phone_code', getOrAddCustomerPhone)
|
||||||
router.post('/email_code', getOrAddCustomerEmail)
|
router.post('/email_code', getOrAddCustomerEmail)
|
||||||
|
|
||||||
|
|
|
||||||
21
migrations/1718464437502-integrate-sumsub.js
Normal file
21
migrations/1718464437502-integrate-sumsub.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
let sql = [
|
||||||
|
`CREATE TYPE EXTERNAL_COMPLIANCE_STATUS AS ENUM('PENDING', 'APPROVED', 'REJECTED', 'RETRY')`,
|
||||||
|
`CREATE TABLE CUSTOMER_EXTERNAL_COMPLIANCE (
|
||||||
|
customer_id UUID NOT NULL REFERENCES customers(id),
|
||||||
|
service TEXT NOT NULL,
|
||||||
|
external_id TEXT NOT NULL,
|
||||||
|
last_known_status EXTERNAL_COMPLIANCE_STATUS,
|
||||||
|
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (customer_id, service)
|
||||||
|
)`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
53
new-lamassu-admin/src/components/buttons/DeleteButton.js
Normal file
53
new-lamassu-admin/src/components/buttons/DeleteButton.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
|
import { ReactComponent as DeleteIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
|
||||||
|
import { zircon, zircon2, comet, fontColor, white } from 'src/styling/variables'
|
||||||
|
|
||||||
|
const { p } = typographyStyles
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
button: {
|
||||||
|
extend: p,
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: zircon,
|
||||||
|
cursor: 'pointer',
|
||||||
|
outline: 0,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: 167,
|
||||||
|
height: 48,
|
||||||
|
color: fontColor,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: zircon2
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
backgroundColor: comet,
|
||||||
|
color: white,
|
||||||
|
'& svg g *': {
|
||||||
|
stroke: white
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& svg': {
|
||||||
|
marginRight: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const SimpleButton = memo(({ className, children, ...props }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={classnames(classes.button, className)} {...props}>
|
||||||
|
<DeleteIcon />
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default SimpleButton
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import ActionButton from './ActionButton'
|
import ActionButton from './ActionButton'
|
||||||
import AddButton from './AddButton'
|
import AddButton from './AddButton'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
|
import DeleteButton from './DeleteButton'
|
||||||
import FeatureButton from './FeatureButton'
|
import FeatureButton from './FeatureButton'
|
||||||
import IDButton from './IDButton'
|
import IDButton from './IDButton'
|
||||||
import IconButton from './IconButton'
|
import IconButton from './IconButton'
|
||||||
|
|
@ -19,5 +20,6 @@ export {
|
||||||
IDButton,
|
IDButton,
|
||||||
AddButton,
|
AddButton,
|
||||||
SupportLinkButton,
|
SupportLinkButton,
|
||||||
SubpageButton
|
SubpageButton,
|
||||||
|
DeleteButton
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const Photo = ({ show, src }) => {
|
||||||
|
|
||||||
const CustomerData = ({
|
const CustomerData = ({
|
||||||
locale,
|
locale,
|
||||||
customer,
|
customer = {},
|
||||||
updateCustomer,
|
updateCustomer,
|
||||||
replacePhoto,
|
replacePhoto,
|
||||||
editCustomer,
|
editCustomer,
|
||||||
|
|
@ -399,6 +399,33 @@ const CustomerData = ({
|
||||||
})
|
})
|
||||||
}, R.keys(smsData) ?? [])
|
}, R.keys(smsData) ?? [])
|
||||||
|
|
||||||
|
const externalCompliance = R.map(it => ({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'externalId',
|
||||||
|
label: 'Third Party ID',
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastKnownStatus',
|
||||||
|
label: 'Last Known Status',
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastUpdated',
|
||||||
|
label: 'Last Updated',
|
||||||
|
editable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||||
|
title: `External Info [${it.service}]`,
|
||||||
|
initialValues: it ?? {
|
||||||
|
externalId: '',
|
||||||
|
lastKnownStatus: '',
|
||||||
|
lastUpdated: ''
|
||||||
|
}
|
||||||
|
}))(customer.externalCompliance ?? [])
|
||||||
|
|
||||||
const editableCard = (
|
const editableCard = (
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
|
|
@ -440,6 +467,24 @@ const CustomerData = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nonEditableCard = (
|
||||||
|
{ title, state, titleIcon, fields, hasImage, initialValues, children },
|
||||||
|
idx
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<EditableCard
|
||||||
|
title={title}
|
||||||
|
key={idx}
|
||||||
|
state={state}
|
||||||
|
children={children}
|
||||||
|
initialValues={initialValues}
|
||||||
|
titleIcon={titleIcon}
|
||||||
|
editable={false}
|
||||||
|
hasImage={hasImage}
|
||||||
|
fields={fields}></EditableCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const visibleCards = getVisibleCards(cards)
|
const visibleCards = getVisibleCards(cards)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -514,6 +559,25 @@ const CustomerData = ({
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!R.isEmpty(externalCompliance) && (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<span className={classes.separator}>
|
||||||
|
External compliance information
|
||||||
|
</span>
|
||||||
|
<Grid container>
|
||||||
|
<Grid container direction="column" item xs={6}>
|
||||||
|
{externalCompliance.map((elem, idx) => {
|
||||||
|
return isEven(idx) ? nonEditableCard(elem, idx) : null
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="column" item xs={6}>
|
||||||
|
{externalCompliance.map((elem, idx) => {
|
||||||
|
return !isEven(idx) ? nonEditableCard(elem, idx) : null
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{retrieveAdditionalDataDialog}
|
{retrieveAdditionalDataDialog}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ const GET_CUSTOMER = gql`
|
||||||
isTestCustomer
|
isTestCustomer
|
||||||
subscriberInfo
|
subscriberInfo
|
||||||
phoneOverride
|
phoneOverride
|
||||||
|
externalCompliance
|
||||||
customFields {
|
customFields {
|
||||||
id
|
id
|
||||||
label
|
label
|
||||||
|
|
@ -153,6 +154,7 @@ const SET_CUSTOMER = gql`
|
||||||
lastTxClass
|
lastTxClass
|
||||||
subscriberInfo
|
subscriberInfo
|
||||||
phoneOverride
|
phoneOverride
|
||||||
|
externalCompliance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { useState, React } from 'react'
|
||||||
import ErrorMessage from 'src/components/ErrorMessage'
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||||
import { MainStatus } from 'src/components/Status'
|
import { MainStatus } from 'src/components/Status'
|
||||||
// import { HoverableTooltip } from 'src/components/Tooltip'
|
|
||||||
import { ActionButton } from 'src/components/buttons'
|
import { ActionButton } from 'src/components/buttons'
|
||||||
import { Label1, P, H3 } from 'src/components/typography'
|
import { Label1, P, H3 } from 'src/components/typography'
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
import { useQuery } from '@apollo/react-hooks'
|
import { useQuery } from '@apollo/react-hooks'
|
||||||
import { makeStyles } from '@material-ui/core'
|
import { makeStyles } from '@material-ui/core'
|
||||||
import Grid from '@material-ui/core/Grid'
|
import Grid from '@material-ui/core/Grid'
|
||||||
|
|
@ -6,7 +5,7 @@ import BigNumber from 'bignumber.js'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { Label2 } from 'src/components/typography'
|
import { Label2 } from 'src/components/typography'
|
||||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||||
|
|
@ -38,7 +37,7 @@ const Footer = () => {
|
||||||
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
|
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const config = R.path(['config'])(data) ?? {}
|
const config = R.path(['config'])(data) ?? {}
|
||||||
const canExpand = R.keys(withCommissions).length > 4
|
// const canExpand = R.keys(withCommissions).length > 4
|
||||||
|
|
||||||
const wallets = fromNamespace('wallets')(config)
|
const wallets = fromNamespace('wallets')(config)
|
||||||
const cryptoCurrencies = R.path(['cryptoCurrencies'])(data) ?? []
|
const cryptoCurrencies = R.path(['cryptoCurrencies'])(data) ?? []
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import itbit from './itbit'
|
||||||
import kraken from './kraken'
|
import kraken from './kraken'
|
||||||
import mailgun from './mailgun'
|
import mailgun from './mailgun'
|
||||||
import scorechain from './scorechain'
|
import scorechain from './scorechain'
|
||||||
|
import sumsub from './sumsub'
|
||||||
import telnyx from './telnyx'
|
import telnyx from './telnyx'
|
||||||
import trongrid from './trongrid'
|
import trongrid from './trongrid'
|
||||||
import twilio from './twilio'
|
import twilio from './twilio'
|
||||||
|
|
@ -35,5 +36,6 @@ export default {
|
||||||
[scorechain.code]: scorechain,
|
[scorechain.code]: scorechain,
|
||||||
[trongrid.code]: trongrid,
|
[trongrid.code]: trongrid,
|
||||||
[binance.code]: binance,
|
[binance.code]: binance,
|
||||||
[bitfinex.code]: bitfinex
|
[bitfinex.code]: bitfinex,
|
||||||
|
[sumsub.code]: sumsub
|
||||||
}
|
}
|
||||||
|
|
|
||||||
44
new-lamassu-admin/src/pages/Services/schemas/sumsub.js
Normal file
44
new-lamassu-admin/src/pages/Services/schemas/sumsub.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
|
import { SecretInput, TextInput } from 'src/components/inputs/formik'
|
||||||
|
|
||||||
|
import { secretTest } from './helper'
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
code: 'sumsub',
|
||||||
|
name: 'Sumsub',
|
||||||
|
title: 'Sumsub (Compliance)',
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
code: 'apiToken',
|
||||||
|
display: 'API Token',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'secretKey',
|
||||||
|
display: 'Secret Key',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'applicantLevel',
|
||||||
|
display: 'Applicant Level',
|
||||||
|
component: TextInput,
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
apiToken: Yup.string('The API token must be a string')
|
||||||
|
.max(100, 'The API token is too long')
|
||||||
|
.test(secretTest(account?.apiToken, 'API token')),
|
||||||
|
secretKey: Yup.string('The secret key must be a string')
|
||||||
|
.max(100, 'The secret key is too long')
|
||||||
|
.test(secretTest(account?.secretKey, 'secret key')),
|
||||||
|
applicantLevel: Yup.string('The applicant level must be a string')
|
||||||
|
.max(100, 'The applicant level is too long')
|
||||||
|
.required('The applicant level is required')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
@ -28,8 +28,9 @@ const TriggerView = ({
|
||||||
config,
|
config,
|
||||||
toggleWizard,
|
toggleWizard,
|
||||||
addNewTriger,
|
addNewTriger,
|
||||||
customInfoRequests,
|
emailAuth,
|
||||||
emailAuth
|
complianceServices,
|
||||||
|
customInfoRequests
|
||||||
}) => {
|
}) => {
|
||||||
const currency = R.path(['fiatCurrency'])(
|
const currency = R.path(['fiatCurrency'])(
|
||||||
fromNamespace(namespaces.LOCALE)(config)
|
fromNamespace(namespaces.LOCALE)(config)
|
||||||
|
|
@ -78,7 +79,9 @@ const TriggerView = ({
|
||||||
save={add}
|
save={add}
|
||||||
onClose={() => toggleWizard(true)}
|
onClose={() => toggleWizard(true)}
|
||||||
customInfoRequests={customInfoRequests}
|
customInfoRequests={customInfoRequests}
|
||||||
|
complianceServices={complianceServices}
|
||||||
emailAuth={emailAuth}
|
emailAuth={emailAuth}
|
||||||
|
triggers={triggers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{R.isEmpty(triggers) && (
|
{R.isEmpty(triggers) && (
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@ const GET_CONFIG = gql`
|
||||||
query getData {
|
query getData {
|
||||||
config
|
config
|
||||||
accounts
|
accounts
|
||||||
|
accountsConfig {
|
||||||
|
code
|
||||||
|
display
|
||||||
|
class
|
||||||
|
cryptos
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -75,6 +81,9 @@ const Triggers = () => {
|
||||||
const emailAuth =
|
const emailAuth =
|
||||||
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
|
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
|
||||||
|
|
||||||
|
const complianceServices = R.filter(R.propEq('class', 'compliance'))(
|
||||||
|
data?.accountsConfig || []
|
||||||
|
)
|
||||||
const triggers = fromServer(data?.config?.triggers ?? [])
|
const triggers = fromServer(data?.config?.triggers ?? [])
|
||||||
const complianceConfig =
|
const complianceConfig =
|
||||||
data?.config && fromNamespace('compliance')(data.config)
|
data?.config && fromNamespace('compliance')(data.config)
|
||||||
|
|
@ -135,7 +144,7 @@ const Triggers = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TitleSection
|
<TitleSection
|
||||||
title="Compliance Triggers"
|
title="Compliance triggers"
|
||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
text: 'Advanced settings',
|
text: 'Advanced settings',
|
||||||
|
|
@ -219,8 +228,9 @@ const Triggers = () => {
|
||||||
config={data?.config ?? {}}
|
config={data?.config ?? {}}
|
||||||
toggleWizard={toggleWizard('newTrigger')}
|
toggleWizard={toggleWizard('newTrigger')}
|
||||||
addNewTriger={addNewTriger}
|
addNewTriger={addNewTriger}
|
||||||
customInfoRequests={enabledCustomInfoRequests}
|
|
||||||
emailAuth={emailAuth}
|
emailAuth={emailAuth}
|
||||||
|
complianceServices={complianceServices}
|
||||||
|
customInfoRequests={enabledCustomInfoRequests}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!loading && subMenu === 'advancedSettings' && (
|
{!loading && subMenu === 'advancedSettings' && (
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,27 @@ const styles = {
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const getStep = (step, currency, customInfoRequests, emailAuth) => {
|
const getStep = (
|
||||||
|
{ step, config },
|
||||||
|
currency,
|
||||||
|
customInfoRequests,
|
||||||
|
complianceServices,
|
||||||
|
emailAuth,
|
||||||
|
triggers
|
||||||
|
) => {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
// case 1:
|
// case 1:
|
||||||
// return txDirection
|
// return txDirection
|
||||||
case 1:
|
case 1:
|
||||||
return type(currency)
|
return type(currency)
|
||||||
case 2:
|
case 2:
|
||||||
return requirements(customInfoRequests, emailAuth)
|
return requirements(
|
||||||
|
config,
|
||||||
|
triggers,
|
||||||
|
customInfoRequests,
|
||||||
|
complianceServices,
|
||||||
|
emailAuth
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return Fragment
|
return Fragment
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +179,8 @@ const getRequirementText = (config, classes) => {
|
||||||
return <>blocked</>
|
return <>blocked</>
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return <>asked to fulfill a custom requirement</>
|
return <>asked to fulfill a custom requirement</>
|
||||||
|
case 'external':
|
||||||
|
return <>redirected to an external verification process</>
|
||||||
default:
|
default:
|
||||||
return orUnderline(null, classes)
|
return orUnderline(null, classes)
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +225,9 @@ const Wizard = ({
|
||||||
error,
|
error,
|
||||||
currency,
|
currency,
|
||||||
customInfoRequests,
|
customInfoRequests,
|
||||||
emailAuth
|
complianceServices,
|
||||||
|
emailAuth,
|
||||||
|
triggers
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -220,7 +237,14 @@ const Wizard = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLastStep = step === LAST_STEP
|
const isLastStep = step === LAST_STEP
|
||||||
const stepOptions = getStep(step, currency, customInfoRequests, emailAuth)
|
const stepOptions = getStep(
|
||||||
|
{ step, config },
|
||||||
|
currency,
|
||||||
|
customInfoRequests,
|
||||||
|
complianceServices,
|
||||||
|
emailAuth,
|
||||||
|
triggers
|
||||||
|
)
|
||||||
|
|
||||||
const onContinue = async it => {
|
const onContinue = async it => {
|
||||||
const newConfig = R.merge(config, stepOptions.schema.cast(it))
|
const newConfig = R.merge(config, stepOptions.schema.cast(it))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { NumberInput, RadioGroup, Dropdown } from 'src/components/inputs/formik'
|
||||||
import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography'
|
import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography'
|
||||||
import { errorColor } from 'src/styling/variables'
|
import { errorColor } from 'src/styling/variables'
|
||||||
import { transformNumber } from 'src/utils/number'
|
import { transformNumber } from 'src/utils/number'
|
||||||
|
import { onlyFirstToUpper } from 'src/utils/string'
|
||||||
|
|
||||||
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||||
// import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
// import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
|
|
@ -82,6 +83,14 @@ const useStyles = makeStyles({
|
||||||
dropdownField: {
|
dropdownField: {
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
minWidth: 155
|
minWidth: 155
|
||||||
|
},
|
||||||
|
externalFields: {
|
||||||
|
'& > *': {
|
||||||
|
marginRight: 15
|
||||||
|
},
|
||||||
|
'& > *:last-child': {
|
||||||
|
marginRight: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -488,6 +497,13 @@ const requirementSchema = Yup.object()
|
||||||
otherwise: Yup.string()
|
otherwise: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.transform(() => '')
|
.transform(() => '')
|
||||||
|
}),
|
||||||
|
externalService: Yup.string().when('requirement', {
|
||||||
|
is: value => value === 'external',
|
||||||
|
then: Yup.string(),
|
||||||
|
otherwise: Yup.string()
|
||||||
|
.nullable()
|
||||||
|
.transform(() => '')
|
||||||
})
|
})
|
||||||
}).required()
|
}).required()
|
||||||
})
|
})
|
||||||
|
|
@ -502,6 +518,10 @@ const requirementSchema = Yup.object()
|
||||||
return requirement.requirement === type
|
return requirement.requirement === type
|
||||||
? !R.isNil(requirement.customInfoRequestId)
|
? !R.isNil(requirement.customInfoRequestId)
|
||||||
: true
|
: true
|
||||||
|
case 'external':
|
||||||
|
return requirement.requirement === type
|
||||||
|
? !R.isNil(requirement.externalService)
|
||||||
|
: true
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -518,6 +538,12 @@ const requirementSchema = Yup.object()
|
||||||
path: 'requirement',
|
path: 'requirement',
|
||||||
message: 'You must select an item'
|
message: 'You must select an item'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (requirement && !requirementValidator(requirement, 'external'))
|
||||||
|
return context.createError({
|
||||||
|
path: 'requirement',
|
||||||
|
message: 'You must select an item'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const requirementOptions = [
|
const requirementOptions = [
|
||||||
|
|
@ -530,7 +556,8 @@ const requirementOptions = [
|
||||||
{ display: 'US SSN', code: 'usSsn' },
|
{ display: 'US SSN', code: 'usSsn' },
|
||||||
// { display: 'Super user', code: 'superuser' },
|
// { display: 'Super user', code: 'superuser' },
|
||||||
{ display: 'Suspend', code: 'suspend' },
|
{ display: 'Suspend', code: 'suspend' },
|
||||||
{ display: 'Block', code: 'block' }
|
{ display: 'Block', code: 'block' },
|
||||||
|
{ display: 'External Verification', code: 'external' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const hasRequirementError = (errors, touched, values) =>
|
const hasRequirementError = (errors, touched, values) =>
|
||||||
|
|
@ -545,7 +572,18 @@ const hasCustomRequirementError = (errors, touched, values) =>
|
||||||
(!values.requirement?.customInfoRequestId ||
|
(!values.requirement?.customInfoRequestId ||
|
||||||
!R.isNil(values.requirement?.customInfoRequestId))
|
!R.isNil(values.requirement?.customInfoRequestId))
|
||||||
|
|
||||||
const Requirement = ({ customInfoRequests, emailAuth }) => {
|
const hasExternalRequirementError = (errors, touched, values) =>
|
||||||
|
!!errors.requirement &&
|
||||||
|
!!touched.requirement?.externalService &&
|
||||||
|
!values.requirement?.externalService
|
||||||
|
|
||||||
|
const Requirement = ({
|
||||||
|
config = {},
|
||||||
|
triggers,
|
||||||
|
emailAuth,
|
||||||
|
complianceServices,
|
||||||
|
customInfoRequests = []
|
||||||
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const {
|
const {
|
||||||
touched,
|
touched,
|
||||||
|
|
@ -557,27 +595,55 @@ const Requirement = ({ customInfoRequests, emailAuth }) => {
|
||||||
|
|
||||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||||
const isCustom = values?.requirement?.requirement === 'custom'
|
const isCustom = values?.requirement?.requirement === 'custom'
|
||||||
|
const isExternal = values?.requirement?.requirement === 'external'
|
||||||
|
|
||||||
|
const customRequirementsInUse = R.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
if (value.requirement.requirement === 'custom')
|
||||||
|
acc.push({
|
||||||
|
triggerType: value.triggerType,
|
||||||
|
id: value.requirement.customInfoRequestId
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
triggers
|
||||||
|
)
|
||||||
|
|
||||||
|
const availableCustomRequirements = R.filter(
|
||||||
|
it =>
|
||||||
|
!R.includes(
|
||||||
|
{ triggerType: config.triggerType, id: it.id },
|
||||||
|
customRequirementsInUse
|
||||||
|
),
|
||||||
|
customInfoRequests
|
||||||
|
)
|
||||||
|
|
||||||
const makeCustomReqOptions = () =>
|
const makeCustomReqOptions = () =>
|
||||||
customInfoRequests.map(it => ({
|
availableCustomRequirements.map(it => ({
|
||||||
value: it.id,
|
value: it.id,
|
||||||
display: it.customRequest.name
|
display: it.customRequest.name
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const enableCustomRequirement = customInfoRequests?.length > 0
|
const enableCustomRequirement = !R.isEmpty(availableCustomRequirements)
|
||||||
|
|
||||||
const customInfoOption = {
|
const customInfoOption = {
|
||||||
display: 'Custom information requirement',
|
display: 'Custom information requirement',
|
||||||
code: 'custom'
|
code: 'custom'
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemToRemove = emailAuth ? 'sms' : 'email'
|
const itemToRemove = emailAuth ? 'sms' : 'email'
|
||||||
const reqOptions = requirementOptions.filter(it => it.code !== itemToRemove)
|
const reqOptions = requirementOptions.filter(it => it.code !== itemToRemove)
|
||||||
const options = enableCustomRequirement
|
const options = R.clone(reqOptions)
|
||||||
? [...reqOptions, customInfoOption]
|
|
||||||
: [...reqOptions]
|
enableCustomRequirement && options.push(customInfoOption)
|
||||||
|
|
||||||
const titleClass = {
|
const titleClass = {
|
||||||
[classes.error]:
|
[classes.error]:
|
||||||
(!!errors.requirement && !isSuspend && !isCustom) ||
|
(!!errors.requirement && !isSuspend && !isCustom) ||
|
||||||
(isSuspend && hasRequirementError(errors, touched, values)) ||
|
(isSuspend && hasRequirementError(errors, touched, values)) ||
|
||||||
(isCustom && hasCustomRequirementError(errors, touched, values))
|
(isCustom && hasCustomRequirementError(errors, touched, values)) ||
|
||||||
|
(isExternal && hasExternalRequirementError(errors, touched, values))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -620,22 +686,50 @@ const Requirement = ({ customInfoRequests, emailAuth }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isExternal && (
|
||||||
|
<div className={classes.externalFields}>
|
||||||
|
<Field
|
||||||
|
className={classes.dropdownField}
|
||||||
|
component={Dropdown}
|
||||||
|
label="Service"
|
||||||
|
name="requirement.externalService"
|
||||||
|
options={complianceServices.map(it => ({
|
||||||
|
value: it.code,
|
||||||
|
display: it.display
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const requirements = (customInfoRequests, emailAuth) => ({
|
const requirements = (
|
||||||
|
config,
|
||||||
|
triggers,
|
||||||
|
customInfoRequests,
|
||||||
|
complianceServices,
|
||||||
|
emailAuth
|
||||||
|
) => ({
|
||||||
schema: requirementSchema,
|
schema: requirementSchema,
|
||||||
options: requirementOptions,
|
options: requirementOptions,
|
||||||
Component: Requirement,
|
Component: Requirement,
|
||||||
props: { customInfoRequests, emailAuth },
|
props: {
|
||||||
|
config,
|
||||||
|
triggers,
|
||||||
|
customInfoRequests,
|
||||||
|
emailAuth,
|
||||||
|
complianceServices
|
||||||
|
},
|
||||||
hasRequirementError: hasRequirementError,
|
hasRequirementError: hasRequirementError,
|
||||||
hasCustomRequirementError: hasCustomRequirementError,
|
hasCustomRequirementError: hasCustomRequirementError,
|
||||||
|
hasExternalRequirementError: hasExternalRequirementError,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
requirement: {
|
requirement: {
|
||||||
requirement: '',
|
requirement: '',
|
||||||
suspensionDays: '',
|
suspensionDays: '',
|
||||||
customInfoRequestId: ''
|
customInfoRequestId: '',
|
||||||
|
externalService: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -665,7 +759,7 @@ const customReqIdMatches = customReqId => it => {
|
||||||
return it.id === customReqId
|
return it.id === customReqId
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequirementInput = ({ customInfoRequests }) => {
|
const RequirementInput = ({ customInfoRequests = [] }) => {
|
||||||
const { values } = useFormikContext()
|
const { values } = useFormikContext()
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -700,7 +794,8 @@ const RequirementView = ({
|
||||||
requirement,
|
requirement,
|
||||||
suspensionDays,
|
suspensionDays,
|
||||||
customInfoRequestId,
|
customInfoRequestId,
|
||||||
customInfoRequests
|
externalService,
|
||||||
|
customInfoRequests = []
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const display =
|
const display =
|
||||||
|
|
@ -708,6 +803,8 @@ const RequirementView = ({
|
||||||
? R.path(['customRequest', 'name'])(
|
? R.path(['customRequest', 'name'])(
|
||||||
R.find(customReqIdMatches(customInfoRequestId))(customInfoRequests)
|
R.find(customReqIdMatches(customInfoRequestId))(customInfoRequests)
|
||||||
) ?? ''
|
) ?? ''
|
||||||
|
: requirement === 'external'
|
||||||
|
? `External Verification (${onlyFirstToUpper(externalService)})`
|
||||||
: getView(requirementOptions, 'display')(requirement)
|
: getView(requirementOptions, 'display')(requirement)
|
||||||
const isSuspend = requirement === 'suspend'
|
const isSuspend = requirement === 'suspend'
|
||||||
return (
|
return (
|
||||||
|
|
@ -840,7 +937,7 @@ const getElements = (currency, classes, customInfoRequests) => [
|
||||||
{
|
{
|
||||||
name: 'requirement',
|
name: 'requirement',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
width: 230,
|
width: 260,
|
||||||
bypassField: true,
|
bypassField: true,
|
||||||
input: () => <RequirementInput customInfoRequests={customInfoRequests} />,
|
input: () => <RequirementInput customInfoRequests={customInfoRequests} />,
|
||||||
view: it => (
|
view: it => (
|
||||||
|
|
@ -850,7 +947,7 @@ const getElements = (currency, classes, customInfoRequests) => [
|
||||||
{
|
{
|
||||||
name: 'threshold',
|
name: 'threshold',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
width: 284,
|
width: 254,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
input: () => <ThresholdInput currency={currency} />,
|
input: () => <ThresholdInput currency={currency} />,
|
||||||
view: (it, config) => <ThresholdView config={config} currency={currency} />
|
view: (it, config) => <ThresholdView config={config} currency={currency} />
|
||||||
|
|
@ -877,7 +974,7 @@ const sortBy = [
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
const fromServer = (triggers, customInfoRequests) => {
|
const fromServer = triggers => {
|
||||||
return R.map(
|
return R.map(
|
||||||
({
|
({
|
||||||
requirement,
|
requirement,
|
||||||
|
|
@ -885,12 +982,14 @@ const fromServer = (triggers, customInfoRequests) => {
|
||||||
threshold,
|
threshold,
|
||||||
thresholdDays,
|
thresholdDays,
|
||||||
customInfoRequestId,
|
customInfoRequestId,
|
||||||
|
externalService,
|
||||||
...rest
|
...rest
|
||||||
}) => ({
|
}) => ({
|
||||||
requirement: {
|
requirement: {
|
||||||
requirement,
|
requirement,
|
||||||
suspensionDays,
|
suspensionDays,
|
||||||
customInfoRequestId
|
customInfoRequestId,
|
||||||
|
externalService
|
||||||
},
|
},
|
||||||
threshold: {
|
threshold: {
|
||||||
threshold,
|
threshold,
|
||||||
|
|
@ -908,6 +1007,7 @@ const toServer = triggers =>
|
||||||
threshold: threshold.threshold,
|
threshold: threshold.threshold,
|
||||||
thresholdDays: threshold.thresholdDays,
|
thresholdDays: threshold.thresholdDays,
|
||||||
customInfoRequestId: requirement.customInfoRequestId,
|
customInfoRequestId: requirement.customInfoRequestId,
|
||||||
|
externalService: requirement.externalService,
|
||||||
...rest
|
...rest
|
||||||
}))(triggers)
|
}))(triggers)
|
||||||
|
|
||||||
|
|
|
||||||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -1342,7 +1342,7 @@
|
||||||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
|
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
|
||||||
},
|
},
|
||||||
"bitcoinjs-message": {
|
"bitcoinjs-message": {
|
||||||
"version": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.2",
|
"version": "npm:bitcoinjs-message@1.0.0-master.2",
|
||||||
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-message/-/bitcoinjs-message-1.0.0-master.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-message/-/bitcoinjs-message-1.0.0-master.2.tgz",
|
||||||
"integrity": "sha512-XSDGM3rA75vcDxeKqHPexika/TgWUFWdfKTv1lV8TZTb5XFHHD6ARckLdMOBiCf29eZSzbJQvF/OIWqNqMl/2A==",
|
"integrity": "sha512-XSDGM3rA75vcDxeKqHPexika/TgWUFWdfKTv1lV8TZTb5XFHHD6ARckLdMOBiCf29eZSzbJQvF/OIWqNqMl/2A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -4714,36 +4714,6 @@
|
||||||
"wif": "^2.0.1"
|
"wif": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bitcoinjs-message": {
|
|
||||||
"version": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-message/-/bitcoinjs-message-1.0.0-master.2.tgz",
|
|
||||||
"integrity": "sha512-XSDGM3rA75vcDxeKqHPexika/TgWUFWdfKTv1lV8TZTb5XFHHD6ARckLdMOBiCf29eZSzbJQvF/OIWqNqMl/2A==",
|
|
||||||
"requires": {
|
|
||||||
"bech32": "^1.1.3",
|
|
||||||
"bs58check": "^2.1.2",
|
|
||||||
"buffer-equals": "^1.0.3",
|
|
||||||
"create-hash": "^1.1.2",
|
|
||||||
"secp256k1": "5.0.0",
|
|
||||||
"varuint-bitcoin": "^1.0.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"bech32": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="
|
|
||||||
},
|
|
||||||
"secp256k1": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==",
|
|
||||||
"requires": {
|
|
||||||
"elliptic": "^6.5.4",
|
|
||||||
"node-addon-api": "^5.0.0",
|
|
||||||
"node-gyp-build": "^4.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bitcore-lib": {
|
"bitcore-lib": {
|
||||||
"version": "8.25.47",
|
"version": "8.25.47",
|
||||||
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.47.tgz",
|
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.47.tgz",
|
||||||
|
|
@ -8644,12 +8614,12 @@
|
||||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
|
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
|
||||||
},
|
},
|
||||||
"form-data": {
|
"form-data": {
|
||||||
"version": "2.5.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.6",
|
"combined-stream": "^1.0.8",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -12203,6 +12173,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
|
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
|
||||||
},
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.6",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"get-uri": {
|
"get-uri": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz",
|
||||||
|
|
@ -16423,6 +16403,16 @@
|
||||||
"readable-stream": "^2.3.5"
|
"readable-stream": "^2.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"form-data": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.6",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
"ethereumjs-wallet": "^0.6.3",
|
"ethereumjs-wallet": "^0.6.3",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"express-session": "^1.17.1",
|
"express-session": "^1.17.1",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
"futoin-hkdf": "^1.0.2",
|
"futoin-hkdf": "^1.0.2",
|
||||||
"got": "^7.1.0",
|
"got": "^7.1.0",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue