diff --git a/bin/lamassu-eth-sweep-to-new-wallet b/bin/lamassu-eth-sweep-to-new-wallet new file mode 100644 index 00000000..38b4c3c0 --- /dev/null +++ b/bin/lamassu-eth-sweep-to-new-wallet @@ -0,0 +1,130 @@ +#!/usr/bin/env node +const hdkey = require('ethereumjs-wallet/hdkey') +const hkdf = require('futoin-hkdf') +const crypto = require('crypto') +const path = require('path') +const pify = require('pify') +const fs = pify(require('fs')) +const _ = require('lodash/fp') +const { BigNumber } = require('bignumber.js') + +const mnemonicHelpers = require('../lib/mnemonic-helpers') +const options = require('../lib/options') +const settingsLoader = require('../lib/new-settings-loader') +const wallet = require('../lib/wallet') +const BN = require('../lib/bn') + +const defaultPrefixPath = "m/44'/60'/1'/0'" +const paymentPrefixPath = "m/44'/60'/0'/0'" + +function writeNewMnemonic (mnemonic) { + return fs.writeFile(options.mnemonicPath, mnemonic) +} + +function backupMnemonic () { + const folderPath = path.dirname(options.mnemonicPath) + const fileName = path.resolve(folderPath, `mnemonic-${Date.now()}.txt`) + return fs.copyFile(options.mnemonicPath, fileName) + .then(() => fileName) +} + +function computeSeed (seed) { + const masterSeed = mnemonicHelpers.toEntropyBuffer(seed) + return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' }) +} + +function generateRandomSeed () { + const seed = crypto + .randomBytes(32) + .toString('hex') + + return Buffer.from(seed, 'hex') +} + +function generateNewMnemonic (newSeed) { + return mnemonicHelpers.fromSeed(newSeed) +} + +function defaultWallet (seed) { + return defaultHdNode(seed).deriveChild(0).getWallet() +} + +function defaultAddress (seed) { + return defaultWallet(seed).getChecksumAddressString() +} + +function defaultHdNode (seed) { + const key = hdkey.fromMasterSeed(seed) + return key.derivePath(defaultPrefixPath) +} + +function getAllBalance () { + return settingsLoader.loadLatest() + .then(settings => wallet.balance(settings, 'ETH')) + .then(r => r.balance) +} + +function isInfuraRunning (settings) { + const isInfuraSelected = settings.config.wallets_ETH_wallet === 'infura' + const isInfuraConfigured = + !_.isNil(settings.accounts.infura) + && !_.isNil(settings.accounts.infura.apiKey) + && !_.isNil(settings.accounts.infura.apiSecret) + && !_.isNil(settings.accounts.infura.endpoint) + + return isInfuraSelected && isInfuraConfigured +} + +function isGethRunning (settings) { + return wallet.checkBlockchainStatus(settings, 'ETH') + .then(res => res === 'ready') +} + +const seed = generateRandomSeed() +const mnemonic = generateNewMnemonic(seed) +const mnemonicSeed = computeSeed(mnemonic) +const newAddress = defaultAddress(mnemonicSeed) + +settingsLoader.loadLatest() + .then(settings => Promise.all([isInfuraRunning(settings), isGethRunning(settings), settings])) + .then(([infuraIsRunning, gethIsRunning, settings]) => { + if (!infuraIsRunning && !gethIsRunning) { + console.log('Neither geth nor Infura are running, so the script cannot be executed.') + process.exit(2) + } + + return Promise.all([getAllBalance(), settings]) + }) + .then(([balance, settings]) => { + const tx = { + cryptoCode: 'ETH', + toAddress: newAddress, + cryptoAtoms: BN(balance.times(0.99999).toFixed(0, BigNumber.ROUND_DOWN)) + } + + const opts = { + chainId: 1, + nonce: 0, + includesFee: true + } + + return wallet.sendCoins(settings, tx, opts) + }) + .then(resTx => { + console.log('Successfully moved funds from the old wallet to the new one.') + console.log('Information about the transaction', resTx) + return backupMnemonic() + }) + .then(fileName => { + console.log(`Successfully backed up the old mnemonic, new location is ${fileName}`) + return writeNewMnemonic(mnemonic) + }) + .then(() => { + console.log('New mnemonic stored successfully! All your funds (minus the transaction fee) should be available in the next few minutes.') + console.log('Process finished successfully!') + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/lib/blockchain/install.js b/lib/blockchain/install.js index 1191f20e..d5789549 100644 --- a/lib/blockchain/install.js +++ b/lib/blockchain/install.js @@ -158,7 +158,8 @@ function run () { const validateAnswers = async (answers) => { if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false } 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)) 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 } diff --git a/lib/plugins/wallet/geth/base.js b/lib/plugins/wallet/geth/base.js index d37227a6..144d12a0 100644 --- a/lib/plugins/wallet/geth/base.js +++ b/lib/plugins/wallet/geth/base.js @@ -50,9 +50,10 @@ function isStrictAddress (cryptoCode, toAddress, settings, operatorId) { return cryptoCode === 'ETH' && util.isValidChecksumAddress(toAddress) } -function sendCoins (account, tx, settings, operatorId) { +function sendCoins (account, tx, settings, operatorId, feeMultiplier, _opts) { const { toAddress, cryptoAtoms, cryptoCode } = tx - return generateTx(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode) + const opts = { ..._opts, includesFee: _.defaultTo(false, _opts?.includesFee) } + return generateTx(toAddress, defaultWallet(account), cryptoAtoms, cryptoCode, opts) .then(pify(web3.eth.sendRawTransaction)) .then(txid => { return pify(web3.eth.getTransaction)(txid) @@ -95,7 +96,7 @@ function _balance (includePending, address, cryptoCode) { .then(balance => balance ? BN(balance) : BN(0)) } -function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) { +function generateTx (_toAddress, wallet, amount, cryptoCode, opts) { const fromAddress = '0x' + wallet.getAddress().toString('hex') const isErc20Token = coins.utils.isErc20Token(cryptoCode) @@ -125,18 +126,18 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) { .then(([gas, gasPrice, txCount]) => [ BN(gas), BN(gasPrice), - _.max([0, txCount, lastUsedNonces[fromAddress] + 1]) + _.max([1, txCount, lastUsedNonces[fromAddress] + 1]) ]) .then(([gas, gasPrice, txCount]) => { lastUsedNonces[fromAddress] = txCount - const toSend = includesFee + const toSend = opts.includesFee ? amount.minus(gasPrice.times(gas)) : amount const rawTx = { - chainId: 1, - nonce: txCount, + chainId: _.defaultTo(1, opts?.chainId), + nonce: _.defaultTo(txCount, opts?.nonce), gasPrice: hex(gasPrice), gasLimit: hex(gas), to: toAddress, @@ -172,8 +173,9 @@ function sweep (account, cryptoCode, hdIndex, settings, operatorId) { return confirmedBalance(fromAddress, cryptoCode) .then(r => { if (r.eq(0)) return + const opts = { includesFee: true } - return generateTx(defaultAddress(account), wallet, r, true, cryptoCode) + return generateTx(defaultAddress(account), wallet, r, cryptoCode, opts) .then(signedTx => pify(web3.eth.sendRawTransaction)(signedTx)) }) } @@ -237,4 +239,5 @@ function checkBlockchainStatus (cryptoCode) { .then(() => connect(`http://localhost:${coins.utils.getCryptoCurrency(cryptoCode).defaultPort}`)) .then(() => web3.eth.syncing) .then(res => res === false ? 'ready' : 'syncing') + .catch(() => 'disconnected') } diff --git a/lib/wallet.js b/lib/wallet.js index 1073a8a0..eddeb7f4 100644 --- a/lib/wallet.js +++ b/lib/wallet.js @@ -59,11 +59,11 @@ function _balance (settings, cryptoCode) { }) } -function sendCoins (settings, tx) { +function sendCoins (settings, tx, opts) { return fetchWallet(settings, tx.cryptoCode) .then(r => { const feeMultiplier = new BN(configManager.getWalletSettings(tx.cryptoCode, settings.config).feeMultiplier) - return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier) + return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier, opts) .then(res => { mem.clear(module.exports.balance) return res