diff --git a/.sample.env b/.sample.env index 09826711..db66efe7 100644 --- a/.sample.env +++ b/.sample.env @@ -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= diff --git a/bin/lamassu-btc-bumpfee b/bin/lamassu-btc-bumpfee new file mode 100644 index 00000000..bd17b3af --- /dev/null +++ b/bin/lamassu-btc-bumpfee @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +const inquirer = require('inquirer') + +const bitcoind = require('../lib/plugins/wallet/bitcoind/bitcoind') +const BN = require('../lib/bn') +const mempool = require('../lib/blockexplorers/mempool.space') + +const txId = process.argv[2] +if (!txId) { + console.error('Please provide a BTC transaction hash as input.') + process.exit(1) +} + +const bumpTransactionFee = async (txId) => { + const txData = await bitcoind.fetch('gettransaction', [txId, true, true]) + + const fee = new BN(txData.fee).abs().shiftedBy(8).decimalPlaces(0) + const size = txData.decoded.vsize + const satPerVb = fee.div(size) + + console.log(`Current fee: ${satPerVb.toFixed(2).toString()} sat/vB`) + + const recommendedFees = await mempool.getSatBEstimateFees() + + console.log('Recommended fees (sat/vB):', recommendedFees) + + const { selectedFee } = await inquirer.prompt([ + { + type: 'list', + name: 'selectedFee', + message: 'Select a fee higher than the current one:', + choices: Object.entries(recommendedFees) + .filter(([_, value]) => satPerVb.lt(value)) + .map(([key, value]) => ({name: `${key}: ${value} sat/vB`, value})), + }, + ]) + + const { txid } = await bitcoind.fetch('bumpfee', [txId, {fee_rate: selectedFee}]) + + console.log(` +Fee bumped to ${selectedFee.toFixed(2)} sat/vB +Transaction ID: ${txid} +`) +} + +bumpTransactionFee(txId) \ No newline at end of file diff --git a/bin/lamassu-ofac-update-sources b/bin/lamassu-ofac-update-sources deleted file mode 100755 index a79e6c0c..00000000 --- a/bin/lamassu-ofac-update-sources +++ /dev/null @@ -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') -} diff --git a/docker-compose.yaml b/docker-compose.yaml index bb8cb1ce..29ff0a05 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index 3e844f6c..97a08ab8 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -31,47 +31,47 @@ const BINARIES = { defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397', defaultDir: 'bitcoin-0.20.1/bin', - url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz', - UrlHash: 'c9840607d230d65f6938b81deaec0b98fe9cb14c3a41a5b13b2c05d044a48422', - dir: 'bitcoin-27.1/bin' + url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz', + dir: 'bitcoin-28.0/bin', + urlHash: '7fe294b02b25b51acb8e8e0a0eb5af6bbafa7cd0c5b0e5fcbb61263104a82fbc', }, ETH: { - url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz', - urlHash: 'fff507c90c180443456950e4fc0bf224d26ce5ea6896194ff864c3c3754c136b', - dir: 'geth-linux-amd64-1.14.8-a9523b64' + url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.12-293a300d.tar.gz', + dir: 'geth-linux-amd64-1.14.12-293a300d', + urlHash: 'e56216b9d179a66a8f71d3dee13ad554da5544d3d29dba33f64c9c0eda5a2237', }, ZEC: { - url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz', - urlHash: 'd385b9fbeeb145f60b0b339d256cabb342713ed3014cd634cf2d68078365abd2', - dir: 'zcash-5.9.0/bin' + url: 'https://download.z.cash/downloads/zcash-6.0.0-linux64-debian-bullseye.tar.gz', + dir: 'zcash-6.0.0/bin', + urlHash: '3cb82f490e9c8e88007a0216b5261b33ef0fda962b9258441b2def59cb272a4d', }, DASH: { defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219', defaultDir: 'dashcore-18.1.0/bin', - url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz', - urlHash: 'a7d0c1b04d53a9b1b3499eb82182c0fa57f4c8768c16163e5d05971bf45d7928', - dir: 'dashcore-21.1.0/bin' + url: 'https://github.com/dashpay/dash/releases/download/v21.1.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz', + dir: 'dashcore-21.1.1/bin' + urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5', }, LTC: { defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751', defaultDir: 'litecoin-0.18.1/bin', - url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz', - urlHash: 'ea231c630e2a243cb01affd4c2b95a2be71560f80b64b9f4bceaa13d736aa7cb', - dir: 'litecoin-0.21.3/bin' + url: 'https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz', + dir: 'litecoin-0.21.4/bin', + urlHash: '857fc41091f2bae65c3bf0fd4d388fca915fc93a03f16dd2578ac3cc92898390', }, BCH: { - url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz', - urlHash: '0dcc387cbaa3a039c97ddc8fb99c1fa7bff5dc6e4bd3a01d3c3095f595ad2dce', - dir: 'bitcoin-cash-node-27.1.0/bin', - files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']] + url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.0/bitcoin-cash-node-28.0.0-x86_64-linux-gnu.tar.gz', + dir: 'bitcoin-cash-node-28.0.0/bin', + files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']], + urlHash: 'ba735cd3b70fab35ac1496e38596cec1f8d34989924376de001d4a86198f7158', }, XMR: { - url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2', - urlHash: '47c7e6b4b88a57205800a2538065a7874174cd087eedc2526bee1ebcce0cc5e3', - dir: 'monero-x86_64-linux-gnu-v0.18.3.3', - files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']] + url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2', + dir: 'monero-x86_64-linux-gnu-v0.18.3.4', + files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']], + urlHash: '51ba03928d189c1c11b5379cab17dd9ae8d2230056dc05c872d0f8dba4a87f1d', } } diff --git a/lib/blockexplorers/mempool.space.js b/lib/blockexplorers/mempool.space.js index 389d6727..383c1c72 100644 --- a/lib/blockexplorers/mempool.space.js +++ b/lib/blockexplorers/mempool.space.js @@ -1,8 +1,16 @@ -const axios = require("axios"); - -const getSatBEstimateFee = () => { - return axios.get('https://mempool.space/api/v1/fees/recommended') - .then(r => r.data.hourFee) -} - -module.exports = { getSatBEstimateFee } \ No newline at end of file +const axios = require("axios"); + +const getSatBEstimateFee = () => { + return axios.get('https://mempool.space/api/v1/fees/recommended') + .then(r => r.data.hourFee) +} + +const getSatBEstimateFees = () => { + return axios.get('https://mempool.space/api/v1/fees/recommended') + .then(r => r.data) +} + +module.exports = { + getSatBEstimateFees, + getSatBEstimateFee +} \ No newline at end of file diff --git a/lib/compliance.js b/lib/compliance.js index 8ecd1770..becf9ee3 100644 --- a/lib/compliance.js +++ b/lib/compliance.js @@ -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 } : {} ) diff --git a/lib/ofac/update.js b/lib/ofac/update.js index d1408687..8c520a69 100644 --- a/lib/ofac/update.js +++ b/lib/ofac/update.js @@ -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 } diff --git a/lib/plugins/wallet/bitcoind/bitcoind.js b/lib/plugins/wallet/bitcoind/bitcoind.js index ad979a5c..ba5d17ae 100644 --- a/lib/plugins/wallet/bitcoind/bitcoind.js +++ b/lib/plugins/wallet/bitcoind/bitcoind.js @@ -219,5 +219,6 @@ module.exports = { sendCoinsBatch, checkBlockchainStatus, getTxHashesByAddress, + fetch, SUPPORTS_BATCHING } diff --git a/lib/plugins/wallet/geth/base.js b/lib/plugins/wallet/geth/base.js index bdf5bd73..8b9fa9dc 100644 --- a/lib/plugins/wallet/geth/base.js +++ b/lib/plugins/wallet/geth/base.js @@ -42,6 +42,10 @@ const SWEEP_QUEUE = new PQueue({ interval: 250, }) +const SEND_QUEUE = new PQueue({ + concurrency: 1, +}) + const infuraCalls = {} const pify = _function => { @@ -78,18 +82,20 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) { const { toAddress, cryptoAtoms, cryptoCode } = tx const isErc20Token = coins.utils.isErc20Token(cryptoCode) - return (isErc20Token ? generateErc20Tx : generateTx)(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode) - .then(pify(web3.eth.sendSignedTransaction)) - .then(txid => { - return pify(web3.eth.getTransaction)(txid) - .then(tx => { - if (!tx) return { txid } + return SEND_QUEUE.add(() => + (isErc20Token ? generateErc20Tx : generateTx)(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode) + .then(pify(web3.eth.sendSignedTransaction)) + .then(txid => { + return pify(web3.eth.getTransaction)(txid) + .then(tx => { + if (!tx) return { txid } - const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0) + const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0) - return { txid, fee } - }) - }) + return { txid, fee } + }) + }) + ) } function checkCryptoCode (cryptoCode) { diff --git a/package.json b/package.json index 3d381956..bbedb762 100644 --- a/package.json +++ b/package.json @@ -113,9 +113,9 @@ "lamassu-ofac-update": "./bin/lamassu-ofac-update", "lamassu-send-coins": "./bin/lamassu-send-coins", "lamassu-update-to-mnemonic": "./bin/lamassu-update-to-mnemonic", + "lamassu-btc-bumpfee": "./bin/lamassu-btc-bumpfee", "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", diff --git a/tools/build-dev-env.js b/tools/build-dev-env.js index eb72ec90..14c160cb 100644 --- a/tools/build-dev-env.js +++ b/tools/build-dev-env.js @@ -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') diff --git a/tools/build-prod-env.js b/tools/build-prod-env.js index 41e59aae..59a10ffd 100644 --- a/tools/build-prod-env.js +++ b/tools/build-prod-env.js @@ -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')