Merge pull request #1689 from RafaelTaranto/chore/sumsub-rebase-simplified

chore: sumsub rebase simplified
This commit is contained in:
Rafael Taranto 2024-07-16 09:34:07 +01:00 committed by GitHub
commit 09c3fb8a70
29 changed files with 828 additions and 73 deletions

View 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
}

View file

@ -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
} }

View file

@ -109,6 +109,7 @@ type Trigger {
thresholdDays: Int thresholdDays: Int
customInfoRequestId: String customInfoRequestId: String
customInfoRequest: CustomInfoRequest customInfoRequest: CustomInfoRequest
externalService: String
} }
type TermsDetails { type TermsDetails {

View file

@ -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

View file

@ -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 {

View file

@ -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'
] ]
/* /*

View file

@ -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)

View file

@ -0,0 +1,6 @@
module.exports = {
PENDING: 'PENDING',
RETRY: 'RETRY',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED'
}

View 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
}

View 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

View 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
}

View 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
}

View file

@ -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 = []) {

View file

@ -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)

View 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()
}

View 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

View file

@ -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
} }

View file

@ -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>

View file

@ -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
} }
} }
` `

View file

@ -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 {

View file

@ -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) ?? []

View file

@ -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
} }

View 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

View file

@ -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) && (

View file

@ -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' && (

View file

@ -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))

View file

@ -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
View file

@ -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",

View file

@ -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",