feat: backport remote BTC node support

This commit is contained in:
Sérgio Salgado 2023-02-01 15:16:35 +00:00
parent 24473de6d5
commit 1b6ba5e6dc
15 changed files with 272 additions and 55 deletions

View file

@ -1,54 +1,61 @@
const path = require('path')
const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
const common = require('./common')
const { isDevMode, isRemoteNode } = require('../environment-helper')
module.exports = { setup, updateCore }
const coinRec = coinUtils.getCryptoCurrency('BTC')
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp') : '/tmp'
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
function setup (dataDir) {
common.firewall([coinRec.defaultPort])
!isDevMode() && common.firewall([coinRec.defaultPort])
const config = buildConfig()
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
const cmd = `/usr/local/bin/${coinRec.daemon} -datadir=${dataDir}`
common.writeSupervisorConfig(coinRec, cmd)
const cmd = `${usrBinDir}/${coinRec.daemon} -datadir=${dataDir}`
!isDevMode() && common.writeSupervisorConfig(coinRec, cmd)
}
function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Bitcoin Core. This may take a minute...')
common.es(`sudo supervisorctl stop bitcoin`)
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...')
common.es(`cp /tmp/${coinRec.dir}/* /usr/local/bin/`)
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/bitcoin.tar.gz`)
common.es(`cp ${tmpDir}/${coinRec.dir}/* ${usrBinDir}/`)
common.es(`rm -r ${tmpDir}/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm ${tmpDir}/bitcoin.tar.gz`)
if (common.es(`grep "addresstype=p2sh-segwit" /mnt/blockchains/bitcoin/bitcoin.conf || true`)) {
if (common.es(`grep "addresstype=p2sh-segwit" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
common.logger.info(`Enabling bech32 receiving addresses in config file..`)
common.es(`sed -i 's/addresstype=p2sh-segwit/addresstype=bech32/g' /mnt/blockchains/bitcoin/bitcoin.conf`)
common.es(`sed -i 's/addresstype=p2sh-segwit/addresstype=bech32/g' ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
} else {
common.logger.info(`bech32 receiving addresses already defined, skipping...`)
}
if (common.es(`grep "changetype=" /mnt/blockchains/bitcoin/bitcoin.conf || true`)) {
if (common.es(`grep "changetype=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
common.logger.info(`changetype already defined, skipping...`)
} else {
common.logger.info(`Enabling bech32 change addresses in config file..`)
common.es(`echo "\nchangetype=bech32" >> /mnt/blockchains/bitcoin/bitcoin.conf`)
common.es(`echo "\nchangetype=bech32" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
}
if (common.es(`grep "listenonion=" /mnt/blockchains/bitcoin/bitcoin.conf || true`)) {
if (common.es(`grep "listenonion=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
common.logger.info(`listenonion already defined, skipping...`)
} else {
common.logger.info(`Setting 'listenonion=0' in config file...`)
common.es(`echo "\nlistenonion=0" >> /mnt/blockchains/bitcoin/bitcoin.conf`)
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
}
if (isCurrentlyRunning) {
if (isCurrentlyRunning && !isDevMode()) {
common.logger.info('Starting wallet...')
common.es(`sudo supervisorctl start bitcoin`)
}
@ -59,6 +66,7 @@ function updateCore (coinRec, isCurrentlyRunning) {
function buildConfig () {
return `rpcuser=lamassuserver
rpcpassword=${common.randomPass()}
${isDevMode() ? `regtest=1` : ``}
dbcache=500
server=1
connections=40
@ -68,8 +76,16 @@ daemon=0
addresstype=bech32
changetype=bech32
walletrbf=1
bind=0.0.0.0:8332
rpcport=8333
listenonion=0
fallbackfee=0.00005
rpcworkqueue=2000
${isDevMode()
? `[regtest]
rpcport=18333
bind=0.0.0.0:18332
${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}`
: `rpcport=8333
bind=0.0.0.0:8332
${isRemoteNode(coinRec) ? `connect=${process.env.BTC_NODE_HOST}:${process.env.BTC_NODE_PORT}` : ``}`}
`
}

View file

@ -3,11 +3,16 @@ const os = require('os')
const path = require('path')
const cp = require('child_process')
const fs = require('fs')
const makeDir = require('make-dir')
const _ = require('lodash/fp')
const logger = require('console-log-level')({level: 'info'})
const { isDevMode } = require('../environment-helper')
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
module.exports = {
es,
writeSupervisorConfig,
@ -106,6 +111,11 @@ function writeSupervisorConfig (coinRec, cmd, walletCmd = '') {
}
function isInstalledSoftware (coinRec) {
if (isDevMode()) {
return fs.existsSync(`${BLOCKCHAIN_DIR}/${coinRec.code}/${coinRec.configFile}`)
&& fs.existsSync(`${BLOCKCHAIN_DIR}/bin/${coinRec.daemon}`)
}
const nodeInstalled = fs.existsSync(`/etc/supervisor/conf.d/${coinRec.code}.conf`)
const walletInstalled = _.isNil(coinRec.wallet)
? true
@ -127,13 +137,19 @@ function fetchAndInstall (coinRec) {
es(`wget -q ${url}`)
es(`tar -xf ${downloadFile}`)
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
if (isDevMode()) {
makeDir.sync(usrBinDir)
}
if (_.isEmpty(binaries.files)) {
es(`sudo cp ${binDir}/* /usr/local/bin`)
es(`sudo cp ${binDir}/* ${usrBinDir}`)
return
}
_.forEach(([source, target]) => {
es(`sudo cp ${binDir}/${source} /usr/local/bin/${target}`)
es(`sudo cp ${binDir}/${source} ${usrBinDir}/${target}`)
}, binaries.files)
}

View file

@ -10,6 +10,7 @@ const _ = require('lodash/fp')
const { utils: coinUtils } = require('@lamassu/coins')
const settingsLoader = require('../new-settings-loader')
const wallet = require('../wallet')
const { isDevMode, isRemoteNode, isRemoteWallet } = require('../environment-helper')
const common = require('./common')
const doVolume = require('./do-volume')
@ -30,7 +31,10 @@ const PLUGINS = {
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
module.exports = {run}
module.exports = {
isEnvironmentValid,
run
}
function installedVolumeFilePath (crypto) {
return path.resolve(coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR), '.installed')
@ -52,43 +56,94 @@ function processCryptos (codes) {
logger.info('Thanks! Installing: %s. Will take a while...', _.join(', ', codes))
const goodVolume = doVolume.prepareVolume()
if (!goodVolume) {
logger.error('There was an error preparing the disk volume. Exiting.')
process.exit(1)
}
const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes)
_.forEach(setupCrypto, selectedCryptos)
common.es('sudo supervisorctl reread')
common.es('sudo supervisorctl update')
const blockchainDir = BLOCKCHAIN_DIR
const backupDir = path.resolve(os.homedir(), 'backups')
const rsyncCmd = `( \
(crontab -l 2>/dev/null || echo -n "") | grep -v "@daily rsync ".*"wallet.dat"; \
echo "@daily rsync -r --prune-empty-dirs --include='*/' \
--include='wallet.dat' \
--exclude='*' ${blockchainDir} ${backupDir} > /dev/null" \
) | crontab -`
common.es(rsyncCmd)
if (isDevMode()) {
_.forEach(setupCrypto, selectedCryptos)
} else {
const goodVolume = doVolume.prepareVolume()
_.forEach(c => {
updateCrypto(c)
common.es(`sudo supervisorctl start ${c.code}`)
}, selectedCryptos)
if (!goodVolume) {
logger.error('There was an error preparing the disk volume. Exiting.')
process.exit(1)
}
_.forEach(setupCrypto, selectedCryptos)
common.es('sudo supervisorctl reread')
common.es('sudo supervisorctl update')
const blockchainDir = BLOCKCHAIN_DIR
const backupDir = path.resolve(os.homedir(), 'backups')
const rsyncCmd = `( \
(crontab -l 2>/dev/null || echo -n "") | grep -v "@daily rsync ".*"wallet.dat"; \
echo "@daily rsync -r --prune-empty-dirs --include='*/' \
--include='wallet.dat' \
--exclude='*' ${blockchainDir} ${backupDir} > /dev/null" \
) | crontab -`
common.es(rsyncCmd)
_.forEach(c => {
updateCrypto(c)
common.es(`sudo supervisorctl start ${c.code}`)
}, selectedCryptos)
}
logger.info('Installation complete.')
}
function isEnvironmentValid (crypto) {
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_LOCATION`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_LOCATION is not set!`)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_WALLET_LOCATION`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_WALLET_LOCATION is not set!`)
if (isRemoteWallet(crypto) && !isRemoteNode(crypto))
throw new Error(`Invalid environment setup for ${crypto.display}: It's not possible to use a remote wallet without using a remote node!`)
if (isRemoteNode(crypto) && !isRemoteWallet(crypto)) {
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_HOST`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_HOST is not set!`)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_PORT`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_PORT is not set!`)
if (_.isEmpty(process.env.BLOCKCHAIN_DIR))
throw new Error(`The environment variable for BLOCKCHAIN_DIR is not set!`)
}
if (isRemoteWallet(crypto)) {
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_HOST`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_RPC_HOST is not set!`)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_RPC_PORT`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_RPC_PORT is not set!`)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_USER`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_USER is not set!`)
if (_.isEmpty(process.env[`${crypto.cryptoCode}_NODE_PASSWORD`]))
throw new Error(`The environment variable for ${crypto.cryptoCode}_NODE_PASSWORD is not set!`)
}
return true
}
function setupCrypto (crypto) {
logger.info(`Installing ${crypto.display}...`)
if (!isEnvironmentValid(crypto)) throw new Error(`Environment error for ${crypto.display}`)
if (isRemoteWallet(crypto)) {
logger.info(`Environment variable ${crypto.cryptoCode}_WALLET_LOCATION is set as 'remote', so there's no need to install a node in the system. Exiting...`)
return
}
const cryptoDir = coinUtils.cryptoDir(crypto, BLOCKCHAIN_DIR)
makeDir.sync(cryptoDir)
const cryptoPlugin = plugin(crypto)
const oldDir = process.cwd()
const tmpDir = '/tmp/blockchain-install'
const tmpDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'tmp', 'blockchain-install') : '/tmp/blockchain-install'
makeDir.sync(tmpDir)
process.chdir(tmpDir)
@ -97,7 +152,10 @@ function setupCrypto (crypto) {
cryptoPlugin.setup(cryptoDir)
common.writeFile(installedVolumeFilePath(crypto), '')
if (!isDevMode()) {
common.writeFile(installedVolumeFilePath(crypto), '')
}
process.chdir(oldDir)
}
@ -121,6 +179,8 @@ function plugin (crypto) {
function getBlockchainSyncStatus (cryptoList) {
return settingsLoader.loadLatest()
.then(settings => {
if (isDevMode()) return new Array(_.size(cryptoList)).fill('ready')
const blockchainStatuses = _.reduce((acc, value) => {
const processStatus = common.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`).trim()
return acc.then(a => {
@ -140,7 +200,9 @@ function getBlockchainSyncStatus (cryptoList) {
}
function isInstalled (crypto) {
return isInstalledSoftware(crypto) && isInstalledVolume(crypto)
return isDevMode()
? isInstalledSoftware(crypto)
: isInstalledSoftware(crypto) && isInstalledVolume(crypto)
}
function isDisabled (crypto) {

View file

@ -1,3 +1,21 @@
const path = require('path')
const isDevMode = () => process.env.NODE_ENV === 'development'
const isProdMode = () => process.env.NODE_ENV === 'production'
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
function isRemoteNode (crypto) {
return process.env[`${crypto.cryptoCode}_NODE_LOCATION`] === 'remote'
}
function isRemoteWallet (crypto) {
return process.env[`${crypto.cryptoCode}_WALLET_LOCATION`] === 'remote'
}
module.exports = {
isDevMode,
isProdMode,
isRemoteNode,
isRemoteWallet
}

View file

@ -7,6 +7,8 @@ const request = require('request-promise')
const { utils: coinUtils } = require('@lamassu/coins')
const logger = require('../../logger')
const { isRemoteNode, isRemoteWallet } = require('../../environment-helper')
const { isEnvironmentValid } = require('../../blockchain/install')
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
@ -28,7 +30,7 @@ function fetch (account = {}, method, params) {
if (_.isNil(account.port)) throw new Error('port attribute required for jsonRpc')
const url = _.defaultTo(`http://localhost:${account.port}`, account.url)
const url = _.defaultTo(`http://${account.host}:${account.port}`, account.url)
return axios({
method: 'post',
@ -109,15 +111,30 @@ function parseConf (confPath) {
function rpcConfig (cryptoRec) {
try {
if (isRemoteWallet(cryptoRec) && isEnvironmentValid(cryptoRec)) {
return {
username: process.env[`${cryptoRec.cryptoCode}_NODE_USER`],
password: process.env[`${cryptoRec.cryptoCode}_NODE_PASSWORD`],
host: process.env[`${cryptoRec.cryptoCode}_NODE_RPC_HOST`],
port: process.env[`${cryptoRec.cryptoCode}_NODE_RPC_PORT`]
}
}
const configPath = coinUtils.configPath(cryptoRec, BLOCKCHAIN_DIR)
const config = parseConf(configPath)
return {
username: config.rpcuser,
password: config.rpcpassword,
host: 'localhost',
port: config.rpcport || cryptoRec.defaultPort
}
} catch (err) {
logger.error('Wallet is currently not installed!')
if (!isEnvironmentValid(cryptoRec)) {
logger.error('Environment is not correctly setup for remote wallet usage!')
} else {
logger.error('Wallet is currently not installed!')
}
return {
port: cryptoRec.defaultPort
}

View file

@ -5,6 +5,7 @@ const BN = require('../../../bn')
const E = require('../../../error')
const logger = require('../../../logger')
const { utils: coinUtils } = require('@lamassu/coins')
const { isDevMode } = require('../../../environment-helper')
const cryptoRec = coinUtils.getCryptoCurrency('BTC')
const unitScale = cryptoRec.unitScale
@ -36,15 +37,15 @@ function checkCryptoCode (cryptoCode) {
function accountBalance (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getwalletinfo'))
.then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
.then(() => fetch('getbalances'))
.then(({ mine }) => new BN(mine.trusted).shiftedBy(unitScale).decimalPlaces(0))
.catch(errorHandle)
}
function accountUnconfirmedBalance (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getwalletinfo'))
.then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
.then(() => fetch('getbalances'))
.then(({ mine }) => new BN(mine.untrusted_pending).plus(mine.immature).shiftedBy(unitScale).decimalPlaces(0))
.catch(errorHandle)
}
@ -62,7 +63,7 @@ function estimateFee () {
function calculateFeeDiscount (feeMultiplier) {
// 0 makes bitcoind do automatic fee selection
const AUTOMATIC_FEE = 0
const AUTOMATIC_FEE = isDevMode() ? 0.01 : 0
if (!feeMultiplier || feeMultiplier.eq(1)) return AUTOMATIC_FEE
return estimateFee()
.then(estimatedFee => {

View file

@ -3,7 +3,6 @@ const BN = require('../../../bn')
const E = require('../../../error')
const _ = require('lodash/fp')
const ENV = process.env.NODE_ENV === undefined || process.env.NODE_ENV === 'development' ? 'development' : 'production'
const SUPPORTED_COINS = ['BTC']
const axios = require('axios').create({