From 92de30de015dce1e3da7fd06d1c8ae703e687b9d Mon Sep 17 00:00:00 2001 From: Rafael Taranto Date: Mon, 19 Aug 2024 13:31:45 +0100 Subject: [PATCH] feat: elliptic wallet scoring --- lib/new-admin/config/accounts.js | 1 + .../wallet-scoring/elliptic/elliptic.js | 96 ++++++++++ .../src/pages/Services/schemas/elliptic.js | 64 +++++++ .../src/pages/Services/schemas/index.js | 2 + package-lock.json | 167 +++++++++++------- package.json | 3 +- tools/cert-gen.sh | 2 +- 7 files changed, 273 insertions(+), 62 deletions(-) create mode 100644 lib/plugins/wallet-scoring/elliptic/elliptic.js create mode 100644 new-lamassu-admin/src/pages/Services/schemas/elliptic.js diff --git a/lib/new-admin/config/accounts.js b/lib/new-admin/config/accounts.js index fdabccbd..8a2279d9 100644 --- a/lib/new-admin/config/accounts.js +++ b/lib/new-admin/config/accounts.js @@ -62,6 +62,7 @@ const ALL_ACCOUNTS = [ { 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: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDT_TRON, TRX] }, + { code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDT_TRON, TRX, ZEC] }, { 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 }, diff --git a/lib/plugins/wallet-scoring/elliptic/elliptic.js b/lib/plugins/wallet-scoring/elliptic/elliptic.js new file mode 100644 index 00000000..3bc3e3ff --- /dev/null +++ b/lib/plugins/wallet-scoring/elliptic/elliptic.js @@ -0,0 +1,96 @@ +const { AML } = require('elliptic-sdk') +const _ = require('lodash/fp') + +const NAME = 'Elliptic' + +const HOLLISTIC_COINS = { + BTC: 'BTC', + ETH: 'ETH', + USDT: 'USDT', + USDT_TRON: 'USDT', + LTC: 'LTC', + TRX: 'TRX' +} + +const SINGLE_ASSET_COINS = { + ZEC: { + asset: 'ZEC', + // TODO needs api to fetch blockchain name + blockchain: 'zcash' + }, + BCH: { + asset: 'BCH', + // TODO needs api to fetch blockchain name + blockchain: 'bitcoincash' + } +} + +const TYPE = { + TRANSACTION: 'transaction', + ADDRESS: 'address' +} + +const SUPPORTED_COINS = { ...HOLLISTIC_COINS, ...SINGLE_ASSET_COINS } + +function rate (account, objectType, cryptoCode, objectId) { + return isWalletScoringEnabled(account, cryptoCode).then(isEnabled => { + if (!isEnabled) return Promise.resolve(null) + + const aml = new AML({ + key: account.apiKey, + secret: account.apiSecret + }) + + const isHolistic = Object.keys(HOLLISTIC_COINS).includes(cryptoCode) + + const requestBody = { + subject: { + asset: isHolistic ? 'holistic' : SINGLE_ASSET_COINS[cryptoCode].asset, + blockchain: isHolistic ? 'holistic' : SINGLE_ASSET_COINS[cryptoCode].blockchain, + type: TYPE[objectType], + hash: objectId + }, + type: objectType === TYPE.ADDRESS ? 'wallet_exposure' : 'source_of_funds' + } + + const threshold = account.scoreThreshold + const endpoint = objectType === TYPE.ADDRESS ? '/v2/wallet/synchronous' : '/v2/analysis/synchronous' + + return aml.client + .post(endpoint, requestBody) + .then((res) => { + const resScore = res.data?.risk_score + + // normalize score to 0-10 where 0 is the lowest risk + // elliptic score can be null and contains decimals + return {score: Math.trunc((resScore || 0) / 10), isValid: (resScore || 0) < threshold} + }) + }) +} + +function rateTransaction (account, cryptoCode, transactionId) { + return rate(account, TYPE.TRANSACTION, cryptoCode, transactionId) +} + +function rateAddress (account, cryptoCode, address) { + return 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 +} diff --git a/new-lamassu-admin/src/pages/Services/schemas/elliptic.js b/new-lamassu-admin/src/pages/Services/schemas/elliptic.js new file mode 100644 index 00000000..287a8fa6 --- /dev/null +++ b/new-lamassu-admin/src/pages/Services/schemas/elliptic.js @@ -0,0 +1,64 @@ +import * as Yup from 'yup' + +import CheckboxFormik from 'src/components/inputs/formik/Checkbox' +import NumberInputFormik from 'src/components/inputs/formik/NumberInput' +import SecretInputFormik from 'src/components/inputs/formik/SecretInput' + +import { secretTest, leadingZerosTest } from './helper' + +export default { + code: 'elliptic', + name: 'Elliptic', + title: 'Elliptic (Scoring)', + elements: [ + { + code: 'apiKey', + display: 'API Key', + component: SecretInputFormik + }, + { + code: 'apiSecret', + display: 'API Secret', + component: SecretInputFormik + }, + { + code: 'scoreThreshold', + display: 'Score threshold', + component: NumberInputFormik, + face: true, + long: false + }, + { + code: 'enabled', + component: CheckboxFormik, + settings: { + enabled: true, + disabledMessage: 'This plugin is disabled', + label: 'Enabled', + requirement: null, + rightSideLabel: true + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'Too long') + .test(secretTest(account?.apiKey, 'API key')), + apiSecret: Yup.string('The API secret must be a string') + .max(100, 'Too long') + .test(secretTest(account?.apiKey, 'API key')), + scoreThreshold: Yup.number('The score threshold must be a number') + .required('A score threshold is required') + .min(1, 'The score threshold must be between 1 and 100') + .max(100, 'The score threshold must be between 1 and 100') + .integer('The score threshold must be an integer') + .test( + 'no-leading-zeros', + 'The score threshold must not have leading zeros', + leadingZerosTest + ) + }) + } +} diff --git a/new-lamassu-admin/src/pages/Services/schemas/index.js b/new-lamassu-admin/src/pages/Services/schemas/index.js index f1a1b53a..22368537 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/index.js +++ b/new-lamassu-admin/src/pages/Services/schemas/index.js @@ -5,6 +5,7 @@ import bitgo from './bitgo' import bitstamp from './bitstamp' import blockcypher from './blockcypher' import cex from './cex' +import elliptic from './elliptic' import galoy from './galoy' import inforu from './inforu' import infura from './infura' @@ -23,6 +24,7 @@ export default { [galoy.code]: galoy, [bitstamp.code]: bitstamp, [blockcypher.code]: blockcypher, + [elliptic.code]: elliptic, [inforu.code]: inforu, [infura.code]: infura, [itbit.code]: itbit, diff --git a/package-lock.json b/package-lock.json index a2fcb74d..06db5144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -990,38 +990,6 @@ "secp256k1": "^4.0.2", "secrets.js-grempe": "^1.1.0", "superagent": "3.8.3" - }, - "dependencies": { - "bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" - }, - "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": { - "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" - } - } - } - } } }, "@bitgo/sdk-coin-bch": { @@ -1137,6 +1105,11 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "buffer-equals": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", + "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==" } } }, @@ -1230,6 +1203,40 @@ "fastpriorityqueue": "^0.7.1", "typeforce": "^1.11.3", "varuint-bitcoin": "^1.1.2" + }, + "dependencies": { + "bip174": { + "version": "npm:@bitgo-forks/bip174@3.1.0-master.4", + "resolved": "https://registry.npmjs.org/@bitgo-forks/bip174/-/bip174-3.1.0-master.4.tgz", + "integrity": "sha512-WDRNzPSdJGDqQNqfN+L5KHNHFDmNOPYnUnT7NkEkfHWn5m1jSOfcf8Swaslt5P0xcSDiERdN2gZxFc6XtOqRYg==" + }, + "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" + } + }, + "ecpair": { + "version": "npm:@bitgo/ecpair@2.1.0-rc.0", + "resolved": "https://registry.npmjs.org/@bitgo/ecpair/-/ecpair-2.1.0-rc.0.tgz", + "integrity": "sha512-qPZetcEA1Lzzm9NsqsGF9NGorAGaXrv20eZjopLUjsdwftWcsYTE7lwzE/Xjdf4fcq6G4+vjrCudWAMGNfJqOQ==", + "requires": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + } + } } }, "@bitgo/utxo-ord": { @@ -1300,6 +1307,28 @@ "superagent": "^3.8.3", "tweetnacl": "^1.0.3", "uuid": "^8.3.2" + }, + "dependencies": { + "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==" + } + } + } } }, "@bitgo/sdk-lib-mpc": { @@ -4289,11 +4318,6 @@ "safe-buffer": "^5.2.1" } }, - "bip174": { - "version": "npm:@bitgo-forks/bip174@3.1.0-master.4", - "resolved": "https://registry.npmjs.org/@bitgo-forks/bip174/-/bip174-3.1.0-master.4.tgz", - "integrity": "sha512-WDRNzPSdJGDqQNqfN+L5KHNHFDmNOPYnUnT7NkEkfHWn5m1jSOfcf8Swaslt5P0xcSDiERdN2gZxFc6XtOqRYg==" - }, "bip32": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bip32/-/bip32-3.1.0.tgz", @@ -4332,21 +4356,34 @@ "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", "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==", + "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": "^2.0.0", - "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", + "bech32": "^1.1.3", "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" + "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": { @@ -6158,16 +6195,6 @@ "safe-buffer": "^5.0.1" } }, - "ecpair": { - "version": "npm:@bitgo/ecpair@2.1.0-rc.0", - "resolved": "https://registry.npmjs.org/@bitgo/ecpair/-/ecpair-2.1.0-rc.0.tgz", - "integrity": "sha512-qPZetcEA1Lzzm9NsqsGF9NGorAGaXrv20eZjopLUjsdwftWcsYTE7lwzE/Xjdf4fcq6G4+vjrCudWAMGNfJqOQ==", - "requires": { - "randombytes": "^2.1.0", - "typeforce": "^1.18.0", - "wif": "^2.0.6" - } - }, "ecurve": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz", @@ -6209,6 +6236,26 @@ } } }, + "elliptic-sdk": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/elliptic-sdk/-/elliptic-sdk-0.7.2.tgz", + "integrity": "sha512-TMhcMmBGyuGe7GcDHEd2AnTPjq4G9+aYn7D93U9/r3fwqiD/WRCQLg63gzWdXAmsq9KnuE4bbRiFmyF6tItbZw==", + "requires": { + "axios": "^1.3.4" + }, + "dependencies": { + "axios": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + } + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index 89b99d97..9e109bb2 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "@lamassu/coins": "v1.4.12", "@simplewebauthn/server": "^3.0.0", "@vonage/auth": "1.5.0", - "@vonage/sms": "1.7.0", "@vonage/server-client": "1.7.0", + "@vonage/sms": "1.7.0", "@vonage/vetch": "1.5.0", "apollo-server-express": "2.25.1", "argon2": "0.28.2", @@ -42,6 +42,7 @@ "date-fns-tz": "^1.1.6", "dateformat": "^3.0.3", "dotenv": "^16.0.0", + "elliptic-sdk": "^0.7.2", "ethereumjs-tx": "^1.3.3", "ethereumjs-util": "^5.2.0", "ethereumjs-wallet": "^0.6.3", diff --git a/tools/cert-gen.sh b/tools/cert-gen.sh index 9d3dcd4d..103728b7 100755 --- a/tools/cert-gen.sh +++ b/tools/cert-gen.sh @@ -90,6 +90,6 @@ rm /tmp/Lamassu_OP.csr.pem mkdir -p $OFAC_DATA_DIR/sources touch $OFAC_DATA_DIR/etags.json -node bin/scripts/build-dev-env.js +node tools/build-dev-env.js echo "Done."