feat: ciphertrace cashout flow

This commit is contained in:
Sérgio Salgado 2022-01-10 15:06:27 +00:00
parent 8a4046ebbe
commit ae8c86a6a7
7 changed files with 153 additions and 7 deletions

View file

@ -8,7 +8,7 @@ const toObj = helper.toObj
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed', const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode', 'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
'receivedCryptoAtoms' ] 'receivedCryptoAtoms', 'walletScore' ]
module.exports = {upsert, update, insert} module.exports = {upsert, update, insert}

View file

@ -107,9 +107,50 @@ function processTxStatus (tx, settings) {
return pi.getStatus(tx) return pi.getStatus(tx)
.then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status })) .then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status }))
.then(_tx => getWalletScore(_tx, pi))
.then(_tx => selfPost(_tx, pi)) .then(_tx => selfPost(_tx, pi))
} }
function getWalletScore (tx, pi) {
const statuses = ['published', 'authorized', 'rejected', 'insufficientFunds']
if (_.includes(tx.status, statuses) && _.isNil(tx.walletScore)) {
// Transaction shows up on the blockchain, we can request the sender address
return pi.getTransactionHash(tx)
.then(txHashes => pi.getInputAddresses(tx, txHashes))
.then(addresses => {
const addressesPromise = []
_.forEach(it => addressesPromise.push(pi.rateWallet(tx.cryptoCode, it)), addresses)
return Promise.all(addressesPromise)
})
.then(scores => {
if (_.isNil(scores) || _.isEmpty(scores)) return tx
const highestScore = _.maxBy(it => it.score, scores)
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
return highestScore.isValid
? _.assign(tx, { walletScore: highestScore.score })
: _.assign(tx, {
walletScore: highestScore.score,
error: 'Ciphertrace score is above defined threshold',
errorCode: 'operatorCancel',
dispense: true
})
})
}
if (tx.status !== 'notSeen' && !_.isNil(tx.walletScore)) {
return pi.isValidWalletScore(tx.walletScore)
.then(isValid => isValid ? tx : _.assign(tx, {
error: 'Ciphertrace score is above defined threshold',
errorCode: 'operatorCancel',
dispense: true
}))
}
return tx
}
function monitorLiveIncoming (settings, applyFilter, coinFilter) { function monitorLiveIncoming (settings, applyFilter, coinFilter) {
const statuses = ['notSeen', 'published', 'insufficientFunds'] const statuses = ['notSeen', 'published', 'insufficientFunds']
const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE

View file

@ -836,6 +836,14 @@ function plugins (settings, deviceId) {
return walletScoring.isValidWalletScore(settings, 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)
}
return { return {
getRates, getRates,
buildRates, buildRates,
@ -865,7 +873,9 @@ function plugins (settings, deviceId) {
fetchCurrentConfigVersion, fetchCurrentConfigVersion,
pruneMachinesHeartbeat, pruneMachinesHeartbeat,
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }
} }

View file

@ -1,6 +1,8 @@
const axios = require('axios') const axios = require('axios')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const logger = require('../../../logger')
const NAME = 'CipherTrace' const NAME = 'CipherTrace'
const SUPPORTED_COINS = ['BTC', 'ETH', 'BCH', 'LTC', 'BNB', 'RSK'] const SUPPORTED_COINS = ['BTC', 'ETH', 'BCH', 'LTC', 'BNB', 'RSK']
@ -37,11 +39,53 @@ function isValidWalletScore(account, score) {
if (_.isNil(client)) return Promise.resolve(true) if (_.isNil(client)) return Promise.resolve(true)
const threshold = account.scoreThreshold const threshold = account.scoreThreshold
return Promise.resolve(score < threshold) return _.isNil(account) ? Promise.resolve(true) : Promise.resolve(score < threshold)
}
function getTransactionHash(account, cryptoCode, receivingAddress) {
const client = getClient(account)
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
const { apiVersion, authHeader } = client
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}`, {
headers: authHeader
})
.then(res => {
const data = res.data
if (_.size(data.txHistory) > 1) {
logger.warn('An address generated by this wallet was used in more than one transaction')
}
return _.join(', ', _.map(it => it.txHash, data.txHistory))
})
}
function getInputAddresses(account, cryptoCode, txHashes) {
const client = getClient(account)
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
const { apiVersion, authHeader } = client
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}tx?txhashes=${txHashes}`, {
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 = _.map(it => it.inputs, data.transactions)
const inputAddresses = _.map(it => it.address, _.flatten(transactionInputs))
return inputAddresses
})
} }
module.exports = { module.exports = {
NAME, NAME,
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }

View file

@ -6,7 +6,7 @@ function rateWallet (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)
return Promise.resolve(7) return Promise.resolve(2)
.then(score => resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD })) .then(score => resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD }))
}, 100) }, 100)
}) })
@ -20,8 +20,26 @@ function isValidWalletScore (account, score) {
}) })
} }
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)
})
}
module.exports = { module.exports = {
NAME, NAME,
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }

View file

@ -28,7 +28,27 @@ function isValidWalletScore (settings, score) {
}) })
} }
function getTransactionHash (settings, cryptoCode, receivingAddress) {
return Promise.resolve()
.then(() => {
const { plugin, account } = loadWalletScoring(settings)
return plugin.getTransactionHash(account, cryptoCode, receivingAddress)
})
}
function getInputAddresses (settings, cryptoCode, txHashes) {
return Promise.resolve()
.then(() => {
const { plugin, account } = loadWalletScoring(settings)
return plugin.getInputAddresses(account, cryptoCode, txHashes)
})
}
module.exports = { module.exports = {
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }

View file

@ -0,0 +1,13 @@
var db = require('./db')
exports.up = function (next) {
var sql = [
`ALTER TABLE cash_out_txs ADD COLUMN wallet_score SMALLINT`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}