feat: scorechain address analysis
This commit is contained in:
parent
5ae9b76c3b
commit
501da5f54a
15 changed files with 158 additions and 308 deletions
|
|
@ -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')
|
||||
|
||||
function rateWallet (account, cryptoCode, address) {
|
||||
function rateAddress (account, cryptoCode, address) {
|
||||
return new Promise((resolve, _) => {
|
||||
setTimeout(() => {
|
||||
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) {
|
||||
return new Promise((resolve, _) => {
|
||||
setTimeout(() => {
|
||||
|
|
@ -44,12 +20,9 @@ function isWalletScoringEnabled (account, cryptoCode) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
rateWallet,
|
||||
isValidWalletScore,
|
||||
getTransactionHash,
|
||||
getInputAddresses,
|
||||
rateAddress,
|
||||
rateTransaction:rateAddress,
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue