Merge pull request #1778 from RafaelTaranto/refactor/sanctions-module

LAM-470 refactor: sanctions module
This commit is contained in:
Rafael Taranto 2024-12-20 15:10:46 +00:00 committed by GitHub
commit 51d7878ff7
9 changed files with 135 additions and 189 deletions

View file

@ -31,15 +31,6 @@ OPERATOR_DATA_DIR=
COIN_ATM_RADAR_URL=
## OFAC Sources variables
# These variables map to each other, similar to a zip HOF. Entries are separated by commas
# Example:
# OFAC_SOURCES_NAMES=name1,name2
# OFAC_SOURCES_URLS=url1,url2
OFAC_SOURCES_NAMES=
OFAC_SOURCES_URLS=
## Misc
HOSTNAME=

View file

@ -1,11 +0,0 @@
#!/usr/bin/env node
'use strict'
require('../lib/environment-helper')
const setEnvVariable = require('../tools/set-env-var')
if (!process.env.OFAC_SOURCES_NAMES && !process.env.OFAC_SOURCES_URLS) {
setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced')
setEnvVariable('OFAC_SOURCES_URLS', 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml')
}

View file

@ -27,8 +27,6 @@ services:
- FRONT_CAMERA_DIR=/lamassu-data/frontcamera
- OPERATOR_DATA_DIR=/lamassu-data/operatordata
- COIN_ATM_RADAR_URL=https://coinatmradar.info/api/lamassu/
- OFAC_SOURCES_NAMES=sdn_advanced,cons_advanced
- OFAC_SOURCES_URLS=https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml
- HOSTNAME=localhost
- LOG_LEVEL=info
@ -58,8 +56,6 @@ services:
- FRONT_CAMERA_DIR=/lamassu-data/frontcamera
- OPERATOR_DATA_DIR=/lamassu-data/operatordata
- COIN_ATM_RADAR_URL=https://coinatmradar.info/api/lamassu/
- OFAC_SOURCES_NAMES=sdn_advanced,cons_advanced
- OFAC_SOURCES_URLS=https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml
- HOSTNAME=172.29.0.3
- LOG_LEVEL=info
depends_on:

View file

@ -72,8 +72,8 @@ function validateOfac (deviceId, sanctionsActive, customer) {
function validationPatch (deviceId, sanctionsActive, customer) {
return validateOfac(deviceId, sanctionsActive, customer)
.then(sactions =>
_.isNil(customer.sanctions) || customer.sanctions !== sactions ?
.then(sanctions =>
_.isNil(customer.sanctions) || customer.sanctions !== sanctions ?
{ sanctions } :
{}
)

View file

@ -1,58 +1,50 @@
const parser = require('./parsing')
const https = require('https')
const URL = require('url')
const axios = require('axios')
const { createWriteStream } = require('fs')
const fs = require('fs/promises')
const { readFile, writeFile, rename, unlink } = fs
const { rename, writeFile, readFile, mkdir, copyFile, unlink } = require('fs/promises')
const path = require('path')
const _ = require('lodash/fp')
const logger = require('../logger')
const DOWNLOAD_DIR = path.resolve('/tmp')
const OFAC_DATA_DIR = process.env.OFAC_DATA_DIR
const OFAC_SOURCES_NAMES = process.env.OFAC_SOURCES_NAMES.split(',')
const OFAC_SOURCES_URLS = process.env.OFAC_SOURCES_URLS.split(',')
const OFAC_SOURCES_DIR = path.join(OFAC_DATA_DIR, 'sources')
const LAST_UPDATED_FILE = path.resolve(OFAC_DATA_DIR, 'last_updated.dat')
const ofacSources = _.map(
([name, url]) => ({ name, url }),
_.zip(OFAC_SOURCES_NAMES, OFAC_SOURCES_URLS)
)
const OFAC_SOURCES = [{
name: 'sdn_advanced',
url: 'https://sanctionslistservice.ofac.treas.gov/api/download/sdn_advanced.xml'
}, {
name: 'cons_advanced',
url: 'https://sanctionslistservice.ofac.treas.gov/api/download/cons_advanced.xml'
}]
const mkdir = path =>
fs.mkdir(path)
const _mkdir = path =>
mkdir(path)
.catch(err => err.code === 'EEXIST' ? Promise.resolve() : Promise.reject(err))
const promiseGetEtag = ({ url }) =>
new Promise((resolve, reject) => {
const parsed = URL.parse(url)
const requestOptions = {
hostname: parsed.hostname,
path: parsed.path,
method: 'HEAD'
}
const request = https.request(requestOptions, _.flow(
_.get(['headers', 'etag']),
resolve
))
request.on('error', reject)
request.end()
})
const download = (dstDir, { name, url }) => {
const dstFile = path.join(dstDir, name + '.xml')
const file = createWriteStream(dstFile)
const writer = createWriteStream(dstFile)
return new Promise((resolve, reject) => {
const request = https.get(url, response => {
response.pipe(file)
file.on('finish', () => file.close(() => resolve(dstFile)))
return axios({
method: 'get',
url: url,
responseType: 'stream',
}).then(response => {
return new Promise((resolve, reject) => {
response.data.pipe(writer)
let error = null
writer.on('error', err => {
error = err
writer.close()
reject(err)
})
writer.on('close', () => {
if (!error) {
resolve(dstFile)
}
})
})
request.on('error', reject)
})
}
@ -81,10 +73,21 @@ const parseToJson = srcFile => {
})
}
const moveToSourcesDir = (srcFile, ofacSourcesDir) => {
const moveToSourcesDir = async (srcFile, ofacSourcesDir) => {
const name = path.basename(srcFile)
const dstFile = path.join(ofacSourcesDir, name)
return rename(srcFile, dstFile)
try {
await rename(srcFile, dstFile)
} catch (err) {
if (err.code === 'EXDEV') {
// If rename fails due to cross-device link, fallback to copy + delete
await copyFile(srcFile, dstFile)
await unlink(srcFile)
} else {
throw err
}
}
return dstFile
}
function update () {
@ -92,67 +95,41 @@ function update () {
throw new Error('ofacDataDir must be defined in the environment')
}
if (!ofacSources) {
logger.error('ofacSources must be defined in the environment')
}
const OFAC_SOURCES_DIR = path.join(OFAC_DATA_DIR, 'sources')
const OFAC_ETAGS_FILE = path.join(OFAC_DATA_DIR, 'etags.json')
return mkdir(OFAC_DATA_DIR)
.then(() => mkdir(OFAC_SOURCES_DIR))
.then(() => writeFile(OFAC_ETAGS_FILE, '{}', {encoding: 'utf-8', flag: 'wx'}))
return _mkdir(OFAC_DATA_DIR)
.then(() => _mkdir(OFAC_SOURCES_DIR))
.catch(err => {
if (err.code === 'EEXIST') return
throw err
})
.then(() => {
const promiseOldEtags = readFile(OFAC_ETAGS_FILE, {encoding: 'utf-8'})
.then(json => JSON.parse(json))
.catch(_ => {
logger.error('Can\'t parse etags.json, getting new data...')
return {}
})
.then(() => readFile(LAST_UPDATED_FILE))
.then(data => {
const lastUpdate = new Date(data.toString())
const now = new Date()
const hoursSinceUpdate = (now - lastUpdate) / (1000 * 60 * 60)
const promiseNewEtags = Promise.resolve(ofacSources || [])
.then(sources => Promise.all(_.map(promiseGetEtag, sources))
.then(etags => _.map(
([source, etag]) => _.set('etag', etag, source),
_.zip(sources, etags)
))
)
return hoursSinceUpdate < 24
})
.catch(err => {
// If file doesn't exist, continue with update
if (err.code === 'ENOENT') return false
throw err
})
.then(skipUpdate => {
if (skipUpdate) return Promise.resolve()
return Promise.all([promiseOldEtags, promiseNewEtags])
.then(([oldEtags, newEtags]) => {
const hasNotChanged = ({name, etag}) => oldEtags[name] === etag
const downloads = _.flow(
_.map(file => download(DOWNLOAD_DIR, file).then(parseToJson))
)(OFAC_SOURCES)
const downloads = _.flow(
_.reject(hasNotChanged),
_.map(file => download(DOWNLOAD_DIR, file).then(parseToJson))
)(newEtags)
return Promise.all(downloads)
.then(parsed => {
const moves = _.map(src => moveToSourcesDir(src, OFAC_SOURCES_DIR), parsed)
const timestamp = new Date().toISOString()
const oldFileNames = _.keys(oldEtags)
const newFileNames = _.map(_.get('name'), newEtags)
const missingFileNames = _.difference(oldFileNames, newFileNames)
const resolve = name => path.join(OFAC_SOURCES_DIR, name + '.json')
const missing = _.map(resolve, missingFileNames)
const etagsJson = _.flow(
_.map(source => [source.name, source.etag]),
_.fromPairs,
obj => JSON.stringify(obj, null, 4)
)(newEtags)
return Promise.all(downloads)
.then(parsed => {
const moves = _.map(src => moveToSourcesDir(src, OFAC_SOURCES_DIR), parsed)
const deletions = _.map(unlink, missing)
const updateEtags = writeFile(OFAC_ETAGS_FILE, etagsJson)
return Promise.all([updateEtags, ...moves, ...deletions])
})
return Promise.all([...moves])
.then(() => writeFile(LAST_UPDATED_FILE, timestamp))
})
})
}
module.exports = {update}
module.exports = { update }

122
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "lamassu-server",
"version": "10.0.6",
"version": "10.0.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -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": {
@ -1230,6 +1198,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": {
@ -4289,11 +4291,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 +4329,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 +6168,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",

View file

@ -2,7 +2,7 @@
"name": "lamassu-server",
"description": "bitcoin atm client server protocol module",
"keywords": [],
"version": "10.0.6",
"version": "10.0.7",
"license": "./LICENSE",
"author": "Lamassu (https://lamassu.is)",
"dependencies": {
@ -114,7 +114,6 @@
"lamassu-update-to-mnemonic": "./bin/lamassu-update-to-mnemonic",
"lamassu-update-wallet-nodes": "./bin/lamassu-update-wallet-nodes",
"lamassu-configure-frontcamera": "./bin/lamassu-configure-frontcamera",
"lamassu-ofac-update-sources": "./bin/lamassu-ofac-update-sources",
"lamassu-devices": "./bin/lamassu-devices",
"lamassu-operator": "./bin/lamassu-operator",
"lamassu-coinatmradar": "./bin/lamassu-coinatmradar",

View file

@ -26,9 +26,6 @@ setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.HOME}/.lamassu/idphotocard`)
setEnvVariable('FRONT_CAMERA_DIR', `${process.env.HOME}/.lamassu/frontcamera`)
setEnvVariable('OPERATOR_DATA_DIR', `${process.env.HOME}/.lamassu/operatordata`)
setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced')
setEnvVariable('OFAC_SOURCES_URLS', 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml')
setEnvVariable('BTC_NODE_LOCATION', 'remote')
setEnvVariable('BTC_WALLET_LOCATION', 'local')

View file

@ -36,9 +36,6 @@ setEnvVariable('OPERATOR_DATA_DIR', `/opt/lamassu-server/operatordata`)
setEnvVariable('COIN_ATM_RADAR_URL', `https://coinatmradar.info/api/lamassu/`)
setEnvVariable('OFAC_SOURCES_NAMES', 'sdn_advanced,cons_advanced')
setEnvVariable('OFAC_SOURCES_URLS', 'https://www.treasury.gov/ofac/downloads/sanctions/1.0/sdn_advanced.xml,https://www.treasury.gov/ofac/downloads/sanctions/1.0/cons_advanced.xml')
setEnvVariable('BTC_NODE_LOCATION', 'local')
setEnvVariable('BTC_WALLET_LOCATION', 'local')
setEnvVariable('BCH_NODE_LOCATION', 'local')