feat: switch to a standalone script

This commit is contained in:
Sérgio Salgado 2022-06-13 16:57:19 +01:00
parent c16e0ea776
commit 6287464cea
4 changed files with 164 additions and 23 deletions

View file

@ -7,15 +7,24 @@ const pify = require('pify')
const fs = pify(require('fs')) const fs = pify(require('fs'))
const _ = require('lodash/fp') const _ = require('lodash/fp')
const { BigNumber } = require('bignumber.js') const { BigNumber } = require('bignumber.js')
const coins = require('@lamassu/coins')
const Web3 = require('web3')
const web3 = new Web3()
const Tx = require('ethereumjs-tx')
const mnemonicHelpers = require('../lib/mnemonic-helpers') const mnemonicHelpers = require('../lib/mnemonic-helpers')
const options = require('../lib/options') const options = require('../lib/options')
const settingsLoader = require('../lib/new-settings-loader') const settingsLoader = require('../lib/new-settings-loader')
const wallet = require('../lib/wallet')
const BN = require('../lib/bn') const BN = require('../lib/bn')
const ph = require('../lib/plugin-helper')
const configManager = require('../lib/new-config-manager')
const walletI = require('../lib/wallet')
const LOCKFILE_PATH = '/var/lock/lamassu-eth-pending-sweep'
const defaultPrefixPath = "m/44'/60'/1'/0'" const defaultPrefixPath = "m/44'/60'/1'/0'"
const paymentPrefixPath = "m/44'/60'/0'/0'" let lastUsedNonces = {}
const hex = bigNum => '0x' + bigNum.integerValue(BN.ROUND_DOWN).toString(16)
function writeNewMnemonic (mnemonic) { function writeNewMnemonic (mnemonic) {
return fs.writeFile(options.mnemonicPath, mnemonic) return fs.writeFile(options.mnemonicPath, mnemonic)
@ -33,6 +42,10 @@ function computeSeed (seed) {
return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' }) return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' })
} }
function computeOperatorId (masterSeed) {
return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex')
}
function generateRandomSeed () { function generateRandomSeed () {
const seed = crypto const seed = crypto
.randomBytes(32) .randomBytes(32)
@ -49,6 +62,10 @@ function defaultWallet (seed) {
return defaultHdNode(seed).deriveChild(0).getWallet() return defaultHdNode(seed).deriveChild(0).getWallet()
} }
function defaultWalletAcc (account) {
return defaultHdNodeAcc(account).deriveChild(0).getWallet()
}
function defaultAddress (seed) { function defaultAddress (seed) {
return defaultWallet(seed).getChecksumAddressString() return defaultWallet(seed).getChecksumAddressString()
} }
@ -58,9 +75,14 @@ function defaultHdNode (seed) {
return key.derivePath(defaultPrefixPath) return key.derivePath(defaultPrefixPath)
} }
function defaultHdNodeAcc (account) {
const key = hdkey.fromMasterSeed(account.seed)
return key.derivePath(defaultPrefixPath)
}
function getAllBalance () { function getAllBalance () {
return settingsLoader.loadLatest() return settingsLoader.loadLatest()
.then(settings => wallet.balance(settings, 'ETH')) .then(settings => walletI.balance(settings, 'ETH'))
.then(r => r.balance) .then(r => r.balance)
} }
@ -76,10 +98,120 @@ function isInfuraRunning (settings) {
} }
function isGethRunning (settings) { function isGethRunning (settings) {
return wallet.checkBlockchainStatus(settings, 'ETH') return walletI.checkBlockchainStatus(settings, 'ETH')
.then(res => res === 'ready') .then(res => res === 'ready')
.catch(() => false)
} }
function connect (url) {
if (!web3.isConnected()) {
web3.setProvider(new web3.providers.HttpProvider(url))
}
}
function sendCoins (account, tx, settings, operatorId, feeMultiplier, _opts) {
const { toAddress, cryptoAtoms, cryptoCode } = tx
const opts = { ..._opts, includesFee: _.defaultTo(false, _opts?.includesFee) }
return generateTx(toAddress, defaultWalletAcc(account), cryptoAtoms, cryptoCode, opts)
.then(pify(web3.eth.sendRawTransaction))
.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)
return { txid, fee }
})
})
}
function generateTx (_toAddress, wallet, amount, cryptoCode, opts) {
const fromAddress = '0x' + wallet.getAddress().toString('hex')
const isErc20Token = coins.utils.isErc20Token(cryptoCode)
const toAddress = isErc20Token ? coins.utils.getErc20Token(cryptoCode).contractAddress : _toAddress.toLowerCase()
let contract, contractData
if (isErc20Token) {
contract = web3.eth.contract(ABI.ERC20).at(toAddress)
contractData = isErc20Token && contract.transfer.getData(_toAddress.toLowerCase(), hex(toSend))
}
const txTemplate = {
from: fromAddress,
to: toAddress,
value: amount.toString()
}
if (isErc20Token) txTemplate.data = contractData
const promises = [
pify(web3.eth.estimateGas)(txTemplate),
pify(web3.eth.getGasPrice)(),
pify(web3.eth.getTransactionCount)(fromAddress)
]
return Promise.all(promises)
.then(([gas, gasPrice, txCount]) => [
BN(gas),
BN(gasPrice),
_.max([0, txCount, lastUsedNonces[fromAddress] + 1])
])
.then(([gas, gasPrice, txCount]) => {
lastUsedNonces[fromAddress] = txCount
const toSend = opts.includesFee
? amount.minus(gasPrice.times(gas))
: amount
const rawTx = {
chainId: _.defaultTo(1, opts?.chainId),
nonce: _.defaultTo(txCount, opts?.nonce),
gasPrice: hex(gasPrice),
gasLimit: hex(gas),
to: toAddress,
from: fromAddress,
value: isErc20Token ? hex(BN(0)) : hex(toSend)
}
if (isErc20Token) {
rawTx.data = contractData
}
const tx = new Tx(rawTx)
const privateKey = wallet.getPrivateKey()
tx.sign(privateKey)
return '0x' + tx.serialize().toString('hex')
})
}
function fetchWallet (settings, cryptoCode) {
return fs.readFile(options.mnemonicPath, 'utf8')
.then(mnemonic => {
const computeSeed = masterSeed =>
hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' })
const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic)
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet
const wallet = ph.load(ph.WALLET, plugin)
const rawAccount = settings.accounts[plugin]
const account = _.set('seed', computeSeed(masterSeed), rawAccount)
if (_.isFunction(wallet.run)) wallet.run(account)
const operatorId = computeOperatorId(masterSeed)
return { wallet, account, operatorId }
})
}
fs.exists(LOCKFILE_PATH, function(exists) {
if (!exists) {
console.log('Couldn\'t find the lamassu-eth-pending-sweep lock file, exiting...')
process.exit(1)
}
})
const seed = generateRandomSeed() const seed = generateRandomSeed()
const mnemonic = generateNewMnemonic(seed) const mnemonic = generateNewMnemonic(seed)
const mnemonicSeed = computeSeed(mnemonic) const mnemonicSeed = computeSeed(mnemonic)
@ -93,9 +225,18 @@ settingsLoader.loadLatest()
process.exit(2) process.exit(2)
} }
return Promise.all([getAllBalance(), settings]) if (infuraIsRunning) {
const endpoint = _.startsWith('https://')(settings.accounts.infura.endpoint)
? settings.accounts.infura.endpoint
: `https://${settings.accounts.infura.endpoint}`
connect(endpoint)
} else {
connect(`http://localhost:${coins.utils.getCryptoCurrency('ETH').defaultPort}`)
}
return Promise.all([getAllBalance(), settings, fetchWallet(settings, 'ETH')])
}) })
.then(([balance, settings]) => { .then(([balance, settings, { account, operatorId }]) => {
const tx = { const tx = {
cryptoCode: 'ETH', cryptoCode: 'ETH',
toAddress: newAddress, toAddress: newAddress,
@ -103,12 +244,12 @@ settingsLoader.loadLatest()
} }
const opts = { const opts = {
chainId: 1, chainId: 3,
nonce: 0, nonce: 0,
includesFee: true includesFee: true
} }
return wallet.sendCoins(settings, tx, opts) return sendCoins(account, tx, settings, operatorId, null, opts)
}) })
.then(resTx => { .then(resTx => {
console.log('Successfully moved funds from the old wallet to the new one.') console.log('Successfully moved funds from the old wallet to the new one.')
@ -121,6 +262,10 @@ settingsLoader.loadLatest()
}) })
.then(() => { .then(() => {
console.log('New mnemonic stored successfully! All your funds (minus the transaction fee) should be available in the next few minutes.') console.log('New mnemonic stored successfully! All your funds (minus the transaction fee) should be available in the next few minutes.')
return fs.rmdir(LOCKFILE_PATH)
})
.then(() => {
console.log('lamassu-eth-pending-sweep lock file successfully removed')
console.log('Process finished successfully!') console.log('Process finished successfully!')
process.exit(0) process.exit(0)
}) })

View file

@ -158,8 +158,7 @@ function run () {
const validateAnswers = async (answers) => { const validateAnswers = async (answers) => {
if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false } if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false }
return getBlockchainSyncStatus(cryptos) return getBlockchainSyncStatus(cryptos)
.then(_blockchainStatuses => { .then(blockchainStatuses => {
const blockchainStatuses = _.filter(it => it !== 'disconnected', _blockchainStatuses)
const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses)) const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses))
if (_.size(answers) + result.syncing > 2) { if (_.size(answers) + result.syncing > 2) {
return { message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`, isValid: false } return { message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`, isValid: false }

View file

@ -50,10 +50,9 @@ function isStrictAddress (cryptoCode, toAddress, settings, operatorId) {
return cryptoCode === 'ETH' && util.isValidChecksumAddress(toAddress) return cryptoCode === 'ETH' && util.isValidChecksumAddress(toAddress)
} }
function sendCoins (account, tx, settings, operatorId, feeMultiplier, _opts) { function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
const { toAddress, cryptoAtoms, cryptoCode } = tx const { toAddress, cryptoAtoms, cryptoCode } = tx
const opts = { ..._opts, includesFee: _.defaultTo(false, _opts?.includesFee) } return generateTx(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode)
return generateTx(toAddress, defaultWallet(account), cryptoAtoms, cryptoCode, opts)
.then(pify(web3.eth.sendRawTransaction)) .then(pify(web3.eth.sendRawTransaction))
.then(txid => { .then(txid => {
return pify(web3.eth.getTransaction)(txid) return pify(web3.eth.getTransaction)(txid)
@ -96,7 +95,7 @@ function _balance (includePending, address, cryptoCode) {
.then(balance => balance ? BN(balance) : BN(0)) .then(balance => balance ? BN(balance) : BN(0))
} }
function generateTx (_toAddress, wallet, amount, cryptoCode, opts) { function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
const fromAddress = '0x' + wallet.getAddress().toString('hex') const fromAddress = '0x' + wallet.getAddress().toString('hex')
const isErc20Token = coins.utils.isErc20Token(cryptoCode) const isErc20Token = coins.utils.isErc20Token(cryptoCode)
@ -126,18 +125,18 @@ function generateTx (_toAddress, wallet, amount, cryptoCode, opts) {
.then(([gas, gasPrice, txCount]) => [ .then(([gas, gasPrice, txCount]) => [
BN(gas), BN(gas),
BN(gasPrice), BN(gasPrice),
_.max([1, txCount, lastUsedNonces[fromAddress] + 1]) _.max([0, txCount, lastUsedNonces[fromAddress] + 1])
]) ])
.then(([gas, gasPrice, txCount]) => { .then(([gas, gasPrice, txCount]) => {
lastUsedNonces[fromAddress] = txCount lastUsedNonces[fromAddress] = txCount
const toSend = opts.includesFee const toSend = includesFee
? amount.minus(gasPrice.times(gas)) ? amount.minus(gasPrice.times(gas))
: amount : amount
const rawTx = { const rawTx = {
chainId: _.defaultTo(1, opts?.chainId), chainId: 1,
nonce: _.defaultTo(txCount, opts?.nonce), nonce: txCount,
gasPrice: hex(gasPrice), gasPrice: hex(gasPrice),
gasLimit: hex(gas), gasLimit: hex(gas),
to: toAddress, to: toAddress,
@ -173,9 +172,8 @@ function sweep (account, cryptoCode, hdIndex, settings, operatorId) {
return confirmedBalance(fromAddress, cryptoCode) return confirmedBalance(fromAddress, cryptoCode)
.then(r => { .then(r => {
if (r.eq(0)) return if (r.eq(0)) return
const opts = { includesFee: true }
return generateTx(defaultAddress(account), wallet, r, cryptoCode, opts) return generateTx(defaultAddress(account), wallet, r, true, cryptoCode)
.then(signedTx => pify(web3.eth.sendRawTransaction)(signedTx)) .then(signedTx => pify(web3.eth.sendRawTransaction)(signedTx))
}) })
} }
@ -239,5 +237,4 @@ function checkBlockchainStatus (cryptoCode) {
.then(() => connect(`http://localhost:${coins.utils.getCryptoCurrency(cryptoCode).defaultPort}`)) .then(() => connect(`http://localhost:${coins.utils.getCryptoCurrency(cryptoCode).defaultPort}`))
.then(() => web3.eth.syncing) .then(() => web3.eth.syncing)
.then(res => res === false ? 'ready' : 'syncing') .then(res => res === false ? 'ready' : 'syncing')
.catch(() => 'disconnected')
} }

View file

@ -59,11 +59,11 @@ function _balance (settings, cryptoCode) {
}) })
} }
function sendCoins (settings, tx, opts) { function sendCoins (settings, tx) {
return fetchWallet(settings, tx.cryptoCode) return fetchWallet(settings, tx.cryptoCode)
.then(r => { .then(r => {
const feeMultiplier = new BN(configManager.getWalletSettings(tx.cryptoCode, settings.config).feeMultiplier) const feeMultiplier = new BN(configManager.getWalletSettings(tx.cryptoCode, settings.config).feeMultiplier)
return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier, opts) return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier)
.then(res => { .then(res => {
mem.clear(module.exports.balance) mem.clear(module.exports.balance)
return res return res