feat: scorechain address analysis
This commit is contained in:
parent
5ae9b76c3b
commit
501da5f54a
15 changed files with 158 additions and 308 deletions
|
|
@ -36,8 +36,15 @@ function post (machineTx, pi) {
|
||||||
let addressReuse = false
|
let addressReuse = false
|
||||||
let walletScore = {}
|
let walletScore = {}
|
||||||
|
|
||||||
return Promise.all([settingsLoader.loadLatest(), checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi)])
|
const promises = [settingsLoader.loadLatest()]
|
||||||
.then(([{ config }, blacklistItems, isReusedAddress, fetchedWalletScore]) => {
|
|
||||||
|
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
||||||
|
if (isFirstPost) {
|
||||||
|
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
.then(([{ config }, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
|
||||||
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
|
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
|
||||||
|
|
||||||
walletScore = fetchedWalletScore
|
walletScore = fetchedWalletScore
|
||||||
|
|
@ -87,11 +94,7 @@ function logActionById (action, _rec, txId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForBlacklisted (tx) {
|
function checkForBlacklisted (tx) {
|
||||||
// Check only on addressScan and avoid testing for blacklist on every bill inserted
|
return blacklist.blocked(tx.toAddress, tx.cryptoCode)
|
||||||
if (!tx.fiat || tx.fiat.isZero()) {
|
|
||||||
return blacklist.blocked(tx.toAddress, tx.cryptoCode)
|
|
||||||
}
|
|
||||||
return Promise.resolve(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
|
|
@ -113,7 +116,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
walletScore: walletScore.score,
|
walletScore: walletScore.score,
|
||||||
operatorCompleted: true,
|
operatorCompleted: true,
|
||||||
error: 'Ciphertrace score is above defined threshold',
|
error: 'Chain analysis score is above defined threshold',
|
||||||
errorCode: 'scoreThresholdReached'
|
errorCode: 'scoreThresholdReached'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -167,32 +170,20 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function doesTxReuseAddress (tx) {
|
function doesTxReuseAddress (tx) {
|
||||||
if (!tx.fiat || tx.fiat.isZero()) {
|
const sql = `
|
||||||
const sql = `
|
SELECT EXISTS (
|
||||||
SELECT EXISTS (
|
SELECT DISTINCT to_address FROM (
|
||||||
SELECT DISTINCT to_address FROM (
|
SELECT to_address FROM cash_in_txs WHERE id != $1
|
||||||
SELECT to_address FROM cash_in_txs WHERE id != $1
|
) AS x WHERE to_address = $2
|
||||||
) AS x WHERE to_address = $2
|
)`
|
||||||
)`
|
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
||||||
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
|
||||||
}
|
|
||||||
return Promise.resolve(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletScore (tx, pi) {
|
function getWalletScore (tx, pi) {
|
||||||
return pi.isWalletScoringEnabled(tx)
|
return pi.isWalletScoringEnabled(tx)
|
||||||
.then(isEnabled => {
|
.then(isEnabled => {
|
||||||
if (!isEnabled) return null
|
if (!isEnabled) return null
|
||||||
if (!tx.fiat || tx.fiat.isZero()) {
|
return pi.rateAddress(tx.cryptoCode, tx.toAddress)
|
||||||
return pi.rateWallet(tx.cryptoCode, tx.toAddress)
|
|
||||||
}
|
|
||||||
// Passthrough the previous result
|
|
||||||
return pi.isValidWalletScore(tx.walletScore)
|
|
||||||
.then(isValid => ({
|
|
||||||
address: tx.toAddress,
|
|
||||||
score: tx.walletScore,
|
|
||||||
isValid
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,6 @@ function processTxStatus (tx, settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletScore (tx, pi) {
|
function getWalletScore (tx, pi) {
|
||||||
const rejectEmpty = message => x => _.isNil(x) || _.isEmpty(x) ? Promise.reject(new Error(message)) : x
|
|
||||||
const statuses = ['published', 'authorized', 'confirmed', 'insufficientFunds']
|
const statuses = ['published', 'authorized', 'confirmed', 'insufficientFunds']
|
||||||
|
|
||||||
if (!_.includes(tx.status, statuses) || !_.isNil(tx.walletScore)) {
|
if (!_.includes(tx.status, statuses) || !_.isNil(tx.walletScore)) {
|
||||||
|
|
@ -128,20 +127,13 @@ function getWalletScore (tx, pi) {
|
||||||
return pi.isWalletScoringEnabled(tx)
|
return pi.isWalletScoringEnabled(tx)
|
||||||
.then(isEnabled => {
|
.then(isEnabled => {
|
||||||
if (!isEnabled) return tx
|
if (!isEnabled) return tx
|
||||||
return pi.getTransactionHash(tx)
|
return pi.rateTransaction(tx)
|
||||||
.then(rejectEmpty('No transaction hashes'))
|
.then(res =>
|
||||||
.then(txHashes => pi.getInputAddresses(tx, txHashes))
|
res.isValid
|
||||||
.then(rejectEmpty('No input addresses'))
|
? _.assign(tx, { walletScore: res.score })
|
||||||
.then(addresses => Promise.all(_.map(it => pi.rateWallet(tx.cryptoCode, it), addresses)))
|
|
||||||
.then(rejectEmpty('No score ratings'))
|
|
||||||
.then(_.maxBy(_.get(['score'])))
|
|
||||||
.then(highestScore =>
|
|
||||||
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
|
|
||||||
highestScore.isValid
|
|
||||||
? _.assign(tx, { walletScore: highestScore.score })
|
|
||||||
: _.assign(tx, {
|
: _.assign(tx, {
|
||||||
walletScore: highestScore.score,
|
walletScore: res.score,
|
||||||
error: 'Ciphertrace score is above defined threshold',
|
error: 'Chain analysis score is above defined threshold',
|
||||||
errorCode: 'scoreThresholdReached',
|
errorCode: 'scoreThresholdReached',
|
||||||
dispense: true
|
dispense: true
|
||||||
})
|
})
|
||||||
|
|
@ -149,7 +141,7 @@ function getWalletScore (tx, pi) {
|
||||||
.catch(error => _.assign(tx, {
|
.catch(error => _.assign(tx, {
|
||||||
walletScore: 10,
|
walletScore: 10,
|
||||||
error: `Failure getting address score: ${error.message}`,
|
error: `Failure getting address score: ${error.message}`,
|
||||||
errorCode: 'ciphertraceError',
|
errorCode: 'walletScoringError',
|
||||||
dispense: true
|
dispense: true
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ 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 TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel', 'scoreThresholdReached', 'ciphertraceError']
|
const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel', 'scoreThresholdReached', 'walletScoringError']
|
||||||
|
|
||||||
const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR
|
const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR
|
||||||
const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR
|
const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
||||||
{ 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: 'ciphertrace', display: 'CipherTrace', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH] },
|
{ 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 }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -994,20 +994,8 @@ function plugins (settings, deviceId) {
|
||||||
.then(buildRates)
|
.then(buildRates)
|
||||||
}
|
}
|
||||||
|
|
||||||
function rateWallet (cryptoCode, address) {
|
function rateAddress (cryptoCode, address) {
|
||||||
return walletScoring.rateWallet(settings, cryptoCode, address)
|
return walletScoring.rateAddress(settings, cryptoCode, address)
|
||||||
}
|
|
||||||
|
|
||||||
function isValidWalletScore (score) {
|
|
||||||
return walletScoring.isValidWalletScore(settings, score)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTransactionHash (tx) {
|
|
||||||
return walletScoring.getTransactionHash(settings, tx.cryptoCode, tx.toAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInputAddresses (tx, txHashes) {
|
|
||||||
return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWalletScoringEnabled (tx) {
|
function isWalletScoringEnabled (tx) {
|
||||||
|
|
@ -1046,10 +1034,7 @@ function plugins (settings, deviceId) {
|
||||||
notifyOperator,
|
notifyOperator,
|
||||||
fetchCurrentConfigVersion,
|
fetchCurrentConfigVersion,
|
||||||
pruneMachinesHeartbeat,
|
pruneMachinesHeartbeat,
|
||||||
rateWallet,
|
rateAddress,
|
||||||
isValidWalletScore,
|
|
||||||
getTransactionHash,
|
|
||||||
getInputAddresses,
|
|
||||||
isWalletScoringEnabled,
|
isWalletScoringEnabled,
|
||||||
probeLN,
|
probeLN,
|
||||||
buildAvailableUnits
|
buildAvailableUnits
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
const coins = require('@lamassu/coins')
|
|
||||||
const axios = require('axios')
|
|
||||||
const _ = require('lodash/fp')
|
|
||||||
|
|
||||||
const logger = require('../../../logger')
|
|
||||||
|
|
||||||
const NAME = 'CipherTrace'
|
|
||||||
const SUPPORTED_COINS = ['BTC', 'ETH', 'BCH', 'LTC']
|
|
||||||
|
|
||||||
function getClient (account) {
|
|
||||||
if (_.isNil(account) || !account.enabled) return null
|
|
||||||
|
|
||||||
const [ctv1, username, secretKey] = account.authorizationValue.split(':')
|
|
||||||
if (_.isNil(ctv1) || _.isNil(username) || _.isNil(secretKey)) {
|
|
||||||
throw new Error('Invalid CipherTrace configuration')
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiVersion = ctv1.slice(-2)
|
|
||||||
const authHeader = {
|
|
||||||
'Authorization': account.authorizationValue
|
|
||||||
}
|
|
||||||
return { apiVersion, authHeader }
|
|
||||||
}
|
|
||||||
|
|
||||||
function rateWallet (account, cryptoCode, address) {
|
|
||||||
const client = getClient(account)
|
|
||||||
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
|
|
||||||
|
|
||||||
const { apiVersion, authHeader } = client
|
|
||||||
const threshold = account.scoreThreshold
|
|
||||||
|
|
||||||
logger.info(`** DEBUG ** rateWallet ENDPOINT: https://rest.ciphertrace.com/aml/${apiVersion}/${_.toLower(cryptoCode)}/risk?address=${address}`)
|
|
||||||
|
|
||||||
return axios.get(`https://rest.ciphertrace.com/aml/${apiVersion}/${_.toLower(cryptoCode)}/risk?address=${address}`, {
|
|
||||||
headers: authHeader
|
|
||||||
})
|
|
||||||
.then(res => ({ address, score: res.data.risk, isValid: res.data.risk < threshold }))
|
|
||||||
.then(result => {
|
|
||||||
logger.info(`** DEBUG ** rateWallet RETURN: ${result}`)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(`** DEBUG ** rateWallet ERROR: ${err.message}`)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidWalletScore (account, score) {
|
|
||||||
const client = getClient(account)
|
|
||||||
if (_.isNil(client)) return Promise.resolve(true)
|
|
||||||
|
|
||||||
const threshold = account.scoreThreshold
|
|
||||||
return _.isNil(account) ? Promise.resolve(true) : Promise.resolve(score < threshold)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAddressTransactionsHashes (receivingAddress, cryptoCode, client, wallet) {
|
|
||||||
const { apiVersion, authHeader } = client
|
|
||||||
|
|
||||||
logger.info(`** DEBUG ** getTransactionHash ENDPOINT: https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`)
|
|
||||||
|
|
||||||
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`, {
|
|
||||||
headers: authHeader
|
|
||||||
})
|
|
||||||
.then(_.flow(
|
|
||||||
_.get(['data', 'txHistory']),
|
|
||||||
_.map(_.get(['txHash']))
|
|
||||||
))
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(`** DEBUG ** getTransactionHash ERROR: ${err}`)
|
|
||||||
logger.error(`** DEBUG ** Fetching transactions hashes via wallet node...`)
|
|
||||||
return wallet.getTxHashesByAddress(cryptoCode, receivingAddress)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTransactionHash (account, cryptoCode, receivingAddress, wallet) {
|
|
||||||
const client = getClient(account)
|
|
||||||
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
|
|
||||||
return getAddressTransactionsHashes(receivingAddress, cryptoCode, client, wallet)
|
|
||||||
.then(txHashes => {
|
|
||||||
if (_.size(txHashes) > 1) {
|
|
||||||
logger.warn('An address generated by this wallet was used in more than one transaction')
|
|
||||||
}
|
|
||||||
logger.info('** DEBUG ** getTransactionHash RETURN: ', _.join(', ', txHashes))
|
|
||||||
return txHashes
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error('** DEBUG ** getTransactionHash from wallet node ERROR: ', err)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInputAddresses (account, cryptoCode, txHashes) {
|
|
||||||
const client = getClient(account)
|
|
||||||
if (_.isEmpty(txHashes) || !_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client))
|
|
||||||
return Promise.resolve([])
|
|
||||||
|
|
||||||
/* NOTE: The API accepts at most 10 hashes, and for us here more than 1 is already an exception, not the norm. */
|
|
||||||
if (_.size(txHashes) > 10)
|
|
||||||
return Promise.reject(new Error("Too many tx hashes -- shouldn't happen!"))
|
|
||||||
|
|
||||||
const { apiVersion, authHeader } = client
|
|
||||||
|
|
||||||
cryptoCode = _.toLower(cryptoCode)
|
|
||||||
const lastPathComp = cryptoCode !== 'btc' ? cryptoCode + '_tx' : 'tx'
|
|
||||||
|
|
||||||
txHashes = _(txHashes).take(10).join(',')
|
|
||||||
|
|
||||||
const url = `https://rest.ciphertrace.com/api/${apiVersion}/${lastPathComp}?txhashes=${txHashes}`
|
|
||||||
console.log('** DEBUG ** getInputAddresses ENDPOINT: ', url)
|
|
||||||
|
|
||||||
return axios.get(url, { headers: authHeader })
|
|
||||||
.then(res => {
|
|
||||||
const data = res.data
|
|
||||||
if (_.size(data.transactions) > 1) {
|
|
||||||
logger.warn('An address generated by this wallet was used in more than one transaction')
|
|
||||||
}
|
|
||||||
|
|
||||||
const transactionInputs = _.flatMap(it => it.inputs, data.transactions)
|
|
||||||
const inputAddresses = _.map(it => it.address, transactionInputs)
|
|
||||||
|
|
||||||
logger.info(`** DEBUG ** getInputAddresses RETURN: ${inputAddresses}`)
|
|
||||||
|
|
||||||
return inputAddresses
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error(`** DEBUG ** getInputAddresses ERROR: ${err.message}`)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWalletScoringEnabled (account, cryptoCode) {
|
|
||||||
const isAccountEnabled = !_.isNil(account) && account.enabled
|
|
||||||
|
|
||||||
if (!isAccountEnabled) return Promise.resolve(false)
|
|
||||||
|
|
||||||
if (!SUPPORTED_COINS.includes(cryptoCode) && !coins.utils.isErc20Token(cryptoCode)) {
|
|
||||||
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
NAME,
|
|
||||||
rateWallet,
|
|
||||||
isValidWalletScore,
|
|
||||||
getTransactionHash,
|
|
||||||
getInputAddresses,
|
|
||||||
isWalletScoringEnabled
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ const NAME = 'FakeScoring'
|
||||||
|
|
||||||
const { WALLET_SCORE_THRESHOLD } = require('../../../constants')
|
const { WALLET_SCORE_THRESHOLD } = require('../../../constants')
|
||||||
|
|
||||||
function rateWallet (account, cryptoCode, address) {
|
function rateAddress (account, cryptoCode, address) {
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve, _) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', address)
|
console.log('[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', address)
|
||||||
|
|
@ -12,30 +12,6 @@ function rateWallet (account, cryptoCode, address) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidWalletScore (account, score) {
|
|
||||||
return new Promise((resolve, _) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
return resolve(score < WALLET_SCORE_THRESHOLD)
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTransactionHash (account, cryptoCode, receivingAddress) {
|
|
||||||
return new Promise((resolve, _) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
return resolve('<Fake transaction hash>')
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInputAddresses (account, cryptoCode, txHashes) {
|
|
||||||
return new Promise((resolve, _) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
return resolve(['<Fake input address hash>'])
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWalletScoringEnabled (account, cryptoCode) {
|
function isWalletScoringEnabled (account, cryptoCode) {
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve, _) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -44,12 +20,9 @@ function isWalletScoringEnabled (account, cryptoCode) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NAME,
|
NAME,
|
||||||
rateWallet,
|
rateAddress,
|
||||||
isValidWalletScore,
|
rateTransaction:rateAddress,
|
||||||
getTransactionHash,
|
|
||||||
getInputAddresses,
|
|
||||||
isWalletScoringEnabled
|
isWalletScoringEnabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
lib/plugins/wallet-scoring/scorechain/scorechain.js
Normal file
74
lib/plugins/wallet-scoring/scorechain/scorechain.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
const axios = require('axios')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
|
||||||
|
const NAME = 'Scorechain'
|
||||||
|
const SUPPORTED_COINS = {
|
||||||
|
BTC: 'BITCOIN',
|
||||||
|
ETH: 'ETHEREUM',
|
||||||
|
USDT: 'ETHEREUM',
|
||||||
|
BCH: 'BITCOINCASH',
|
||||||
|
LTC: 'LITECOIN',
|
||||||
|
DASH: 'DASH',
|
||||||
|
TRX: 'TRON',
|
||||||
|
USDT_TRON: 'TRON'
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE = {
|
||||||
|
TRANSACTION: 'TRANSACTION',
|
||||||
|
ADDRESS: 'ADDRESS'
|
||||||
|
}
|
||||||
|
|
||||||
|
function rate (account, objectType, cryptoCode, objectId) {
|
||||||
|
if (_.isNil(account) || !account.enabled || !Object.keys(SUPPORTED_COINS).includes(cryptoCode)) return Promise.resolve(null)
|
||||||
|
|
||||||
|
const threshold = account.scoreThreshold
|
||||||
|
const payload = {
|
||||||
|
analysisType: 'ASSIGNED',
|
||||||
|
objectType,
|
||||||
|
objectId,
|
||||||
|
blockchain: SUPPORTED_COINS[cryptoCode],
|
||||||
|
coin: "ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post(`https://api.scorechain.com/v1/scoringAnalysis`, payload, { headers: { 'X-API-KEY': account.apiKey }
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
const resScore = res.data?.analysis?.assigned?.result?.score
|
||||||
|
if (!resScore) throw new Error('Failed to get score from Scorechain API')
|
||||||
|
|
||||||
|
// normalize score to 0-10 where 0 is the highest risk
|
||||||
|
// use 101 instead of 100 to avoid division by zero
|
||||||
|
return { score: (101 - resScore) / 10 - 0.1, isValid: resScore >= threshold }
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function rateTransaction (account, cryptoCode, transactionId) {
|
||||||
|
rate(account, TYPE.TRANSACTION, cryptoCode, transactionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rateAddress (account, cryptoCode, address) {
|
||||||
|
rate(account, TYPE.ADDRESS, cryptoCode, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWalletScoringEnabled (account, cryptoCode) {
|
||||||
|
const isAccountEnabled = !_.isNil(account) && account.enabled
|
||||||
|
|
||||||
|
if (!isAccountEnabled) return Promise.resolve(false)
|
||||||
|
|
||||||
|
if (!Object.keys(SUPPORTED_COINS).includes(cryptoCode)) {
|
||||||
|
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
NAME,
|
||||||
|
rateAddress,
|
||||||
|
rateTransaction,
|
||||||
|
isWalletScoringEnabled
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ function postTx (req, res, next) {
|
||||||
throw httpError(tx.error, 570)
|
throw httpError(tx.error, 570)
|
||||||
case 'scoreThresholdReached':
|
case 'scoreThresholdReached':
|
||||||
throw httpError(tx.error, 571)
|
throw httpError(tx.error, 571)
|
||||||
case 'ciphertraceError':
|
case 'walletScoringError':
|
||||||
throw httpError(tx.error, 572)
|
throw httpError(tx.error, 572)
|
||||||
default:
|
default:
|
||||||
throw httpError(tx.error, 500)
|
throw httpError(tx.error, 500)
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ function customerHistory (customerId, thresholdDays) {
|
||||||
FROM cash_out_txs txOut
|
FROM cash_out_txs txOut
|
||||||
WHERE txOut.customer_id = $1
|
WHERE txOut.customer_id = $1
|
||||||
AND txOut.created > now() - interval $2
|
AND txOut.created > now() - interval $2
|
||||||
AND (error_code IS NULL OR error_code NOT IN ('operatorCancel', 'scoreThresholdReached', 'ciphertraceError'))
|
AND (error_code IS NULL OR error_code NOT IN ('operatorCancel', 'scoreThresholdReached', 'walletScoringError', 'ciphertraceError'))
|
||||||
AND fiat > 0
|
AND fiat > 0
|
||||||
) ch WHERE NOT ch.expired ORDER BY ch.created`
|
) ch WHERE NOT ch.expired ORDER BY ch.created`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const argv = require('minimist')(process.argv.slice(2))
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
|
|
||||||
function loadWalletScoring (settings, cryptoCode) {
|
function loadWalletScoring (settings, cryptoCode) {
|
||||||
const pluginCode = argv.mockScoring ? 'mock-scoring' : 'ciphertrace'
|
const pluginCode = argv.mockScoring ? 'mock-scoring' : 'scorechain'
|
||||||
const wallet = cryptoCode ? ph.load(ph.WALLET, configManager.getWalletSettings(cryptoCode, settings.config).wallet) : null
|
const wallet = cryptoCode ? ph.load(ph.WALLET, configManager.getWalletSettings(cryptoCode, settings.config).wallet) : null
|
||||||
const plugin = ph.load(ph.WALLET_SCORING, pluginCode)
|
const plugin = ph.load(ph.WALLET_SCORING, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
@ -11,39 +11,21 @@ function loadWalletScoring (settings, cryptoCode) {
|
||||||
return { plugin, account, wallet }
|
return { plugin, account, wallet }
|
||||||
}
|
}
|
||||||
|
|
||||||
function rateWallet (settings, cryptoCode, address) {
|
function rateTransaction (settings, cryptoCode, address) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const { plugin, account } = loadWalletScoring(settings)
|
const { plugin, account } = loadWalletScoring(settings)
|
||||||
|
|
||||||
return plugin.rateWallet(account, cryptoCode, address)
|
return plugin.rateAddress(account, cryptoCode, address)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidWalletScore (settings, score) {
|
function rateAddress (settings, cryptoCode, address) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const { plugin, account } = loadWalletScoring(settings)
|
const { plugin, account } = loadWalletScoring(settings)
|
||||||
|
|
||||||
return plugin.isValidWalletScore(account, score)
|
return plugin.rateAddress(account, cryptoCode, address)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTransactionHash (settings, cryptoCode, receivingAddress) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
const { plugin, account, wallet } = loadWalletScoring(settings, cryptoCode)
|
|
||||||
|
|
||||||
return plugin.getTransactionHash(account, cryptoCode, receivingAddress, wallet)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInputAddresses (settings, cryptoCode, txHashes) {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
const { plugin, account } = loadWalletScoring(settings)
|
|
||||||
|
|
||||||
return plugin.getInputAddresses(account, cryptoCode, txHashes)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,9 +39,7 @@ function isWalletScoringEnabled (settings, cryptoCode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
rateWallet,
|
rateAddress,
|
||||||
isValidWalletScore,
|
rateTransaction,
|
||||||
getTransactionHash,
|
|
||||||
getInputAddresses,
|
|
||||||
isWalletScoringEnabled
|
isWalletScoringEnabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import bitgo from './bitgo'
|
||||||
import bitstamp from './bitstamp'
|
import bitstamp from './bitstamp'
|
||||||
import blockcypher from './blockcypher'
|
import blockcypher from './blockcypher'
|
||||||
import cex from './cex'
|
import cex from './cex'
|
||||||
import ciphertrace from './ciphertrace'
|
|
||||||
import galoy from './galoy'
|
import galoy from './galoy'
|
||||||
import infura from './infura'
|
import infura from './infura'
|
||||||
import itbit from './itbit'
|
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 telnyx from './telnyx'
|
import telnyx from './telnyx'
|
||||||
import trongrid from './trongrid'
|
import trongrid from './trongrid'
|
||||||
import twilio from './twilio'
|
import twilio from './twilio'
|
||||||
|
|
@ -30,7 +30,7 @@ export default {
|
||||||
[twilio.code]: twilio,
|
[twilio.code]: twilio,
|
||||||
[binanceus.code]: binanceus,
|
[binanceus.code]: binanceus,
|
||||||
[cex.code]: cex,
|
[cex.code]: cex,
|
||||||
[ciphertrace.code]: ciphertrace,
|
[scorechain.code]: scorechain,
|
||||||
[trongrid.code]: trongrid,
|
[trongrid.code]: trongrid,
|
||||||
[binance.code]: binance,
|
[binance.code]: binance,
|
||||||
[bitfinex.code]: bitfinex
|
[bitfinex.code]: bitfinex
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||||
import { secretTest, leadingZerosTest } from './helper'
|
import { secretTest, leadingZerosTest } from './helper'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
code: 'ciphertrace',
|
code: 'scorechain',
|
||||||
name: 'CipherTrace',
|
name: 'Scorechain',
|
||||||
title: 'CipherTrace (Scoring)',
|
title: 'Scorechain (Scoring)',
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
code: 'authorizationValue',
|
code: 'apikey',
|
||||||
display: 'Authorization value',
|
display: 'API Key',
|
||||||
component: SecretInputFormik
|
component: SecretInputFormik
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -29,8 +29,7 @@ export default {
|
||||||
settings: {
|
settings: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
disabledMessage: 'This plugin is disabled',
|
disabledMessage: 'This plugin is disabled',
|
||||||
label:
|
label: 'Enabled',
|
||||||
'Enabled (Supported coins: BTC, ETH, BCH, LTC and all active ERC-20 tokens)',
|
|
||||||
requirement: null,
|
requirement: null,
|
||||||
rightSideLabel: true
|
rightSideLabel: true
|
||||||
},
|
},
|
||||||
|
|
@ -39,13 +38,13 @@ export default {
|
||||||
],
|
],
|
||||||
getValidationSchema: account => {
|
getValidationSchema: account => {
|
||||||
return Yup.object().shape({
|
return Yup.object().shape({
|
||||||
authorizationValue: Yup.string('The authorization value must be a string')
|
apiKey: Yup.string('The API key must be a string')
|
||||||
.max(100, 'Too long')
|
.max(100, 'Too long')
|
||||||
.test(secretTest(account?.authorizationValue, 'authorization value')),
|
.test(secretTest(account?.apiKey, 'API key')),
|
||||||
scoreThreshold: Yup.number('The score threshold must be a number')
|
scoreThreshold: Yup.number('The score threshold must be a number')
|
||||||
.required('A score threshold is required')
|
.required('A score threshold is required')
|
||||||
.min(1, 'The score threshold must be between 1 and 10')
|
.min(1, 'The score threshold must be between 1 and 100')
|
||||||
.max(10, 'The score threshold must be between 1 and 10')
|
.max(100, 'The score threshold must be between 1 and 100')
|
||||||
.integer('The score threshold must be an integer')
|
.integer('The score threshold must be an integer')
|
||||||
.test(
|
.test(
|
||||||
'no-leading-zeros',
|
'no-leading-zeros',
|
||||||
|
|
@ -183,9 +183,13 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
FileSaver.saveAs(content, zipFilename)
|
FileSaver.saveAs(content, zipFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCiphertraceError = tx =>
|
const hasChainAnalysisError = tx =>
|
||||||
!R.isNil(tx.errorCode) &&
|
!R.isNil(tx.errorCode) &&
|
||||||
R.includes(tx.errorCode, ['scoreThresholdReached', 'ciphertraceError'])
|
R.includes(tx.errorCode, [
|
||||||
|
'scoreThresholdReached',
|
||||||
|
'walletScoringError',
|
||||||
|
'ciphertraceError'
|
||||||
|
])
|
||||||
|
|
||||||
const errorElements = (
|
const errorElements = (
|
||||||
<>
|
<>
|
||||||
|
|
@ -205,10 +209,10 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
r={3.5}
|
r={3.5}
|
||||||
fill={
|
fill={
|
||||||
it < tx.walletScore
|
it < tx.walletScore
|
||||||
? !hasCiphertraceError(tx)
|
? !hasChainAnalysisError(tx)
|
||||||
? primaryColor
|
? primaryColor
|
||||||
: errorColor
|
: errorColor
|
||||||
: !hasCiphertraceError(tx)
|
: !hasChainAnalysisError(tx)
|
||||||
? subheaderColor
|
? subheaderColor
|
||||||
: offErrorColor
|
: offErrorColor
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +225,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
noMargin
|
noMargin
|
||||||
className={classNames({
|
className={classNames({
|
||||||
[classes.bold]: true,
|
[classes.bold]: true,
|
||||||
[classes.error]: hasCiphertraceError(tx)
|
[classes.error]: hasChainAnalysisError(tx)
|
||||||
})}>
|
})}>
|
||||||
{tx.walletScore}
|
{tx.walletScore}
|
||||||
</P>
|
</P>
|
||||||
|
|
@ -359,7 +363,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
<Label>Address</Label>
|
<Label>Address</Label>
|
||||||
{!R.isNil(tx.walletScore) && (
|
{!R.isNil(tx.walletScore) && (
|
||||||
<HoverableTooltip parentElements={walletScoreEl}>
|
<HoverableTooltip parentElements={walletScoreEl}>
|
||||||
{`CipherTrace score: ${tx.walletScore}/10`}
|
{`Chain analysis score: ${tx.walletScore}/10`}
|
||||||
</HoverableTooltip>
|
</HoverableTooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -1148,6 +1148,25 @@
|
||||||
"fastpriorityqueue": "^0.7.1",
|
"fastpriorityqueue": "^0.7.1",
|
||||||
"typeforce": "^1.11.3",
|
"typeforce": "^1.11.3",
|
||||||
"varuint-bitcoin": "^1.1.2"
|
"varuint-bitcoin": "^1.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bitcoinjs-lib": {
|
||||||
|
"version": "npm:@bitgo-forks/bitcoinjs-lib@7.1.0-master.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-lib/-/bitcoinjs-lib-7.1.0-master.7.tgz",
|
||||||
|
"integrity": "sha512-FZle7954KnbbVXFCc5uYGtjq+0PFOnFxVchNwt3Kcv2nVusezTp29aeQwDi2Y+lM1dCoup2gJGXMkkREenY7KQ==",
|
||||||
|
"requires": {
|
||||||
|
"bech32": "^2.0.0",
|
||||||
|
"bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4",
|
||||||
|
"bs58check": "^2.1.2",
|
||||||
|
"create-hash": "^1.1.0",
|
||||||
|
"fastpriorityqueue": "^0.7.1",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"ripemd160": "^2.0.2",
|
||||||
|
"typeforce": "^1.11.3",
|
||||||
|
"varuint-bitcoin": "^1.1.2",
|
||||||
|
"wif": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@bitgo/utxo-ord": {
|
"@bitgo/utxo-ord": {
|
||||||
|
|
@ -4610,23 +4629,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz",
|
||||||
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="
|
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="
|
||||||
},
|
},
|
||||||
"bitcoinjs-lib": {
|
|
||||||
"version": "npm:@bitgo-forks/bitcoinjs-lib@7.1.0-master.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-lib/-/bitcoinjs-lib-7.1.0-master.7.tgz",
|
|
||||||
"integrity": "sha512-FZle7954KnbbVXFCc5uYGtjq+0PFOnFxVchNwt3Kcv2nVusezTp29aeQwDi2Y+lM1dCoup2gJGXMkkREenY7KQ==",
|
|
||||||
"requires": {
|
|
||||||
"bech32": "^2.0.0",
|
|
||||||
"bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4",
|
|
||||||
"bs58check": "^2.1.2",
|
|
||||||
"create-hash": "^1.1.0",
|
|
||||||
"fastpriorityqueue": "^0.7.1",
|
|
||||||
"json5": "^2.2.3",
|
|
||||||
"ripemd160": "^2.0.2",
|
|
||||||
"typeforce": "^1.11.3",
|
|
||||||
"varuint-bitcoin": "^1.1.2",
|
|
||||||
"wif": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bitcoinjs-message": {
|
"bitcoinjs-message": {
|
||||||
"version": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.2",
|
"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",
|
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-message/-/bitcoinjs-message-1.0.0-master.2.tgz",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue