diff --git a/bin/lamassu-eth-sweep-to-new-wallet b/bin/lamassu-eth-sweep-to-new-wallet new file mode 100644 index 00000000..8cb2ec6b --- /dev/null +++ b/bin/lamassu-eth-sweep-to-new-wallet @@ -0,0 +1,295 @@ +#!/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 coins = require('@lamassu/coins') +const Web3 = require('web3') +const web3 = new Web3() +const Tx = require('ethereumjs-tx') + +const mnemonicHelpers = require('../lib/mnemonic-helpers') +const options = require('../lib/options') +const settingsLoader = require('../lib/new-settings-loader') +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'" +let lastUsedNonces = {} + +const hex = bigNum => '0x' + bigNum.integerValue(BN.ROUND_DOWN).toString(16) + +function writeNewMnemonic (mnemonic) { + return fs.writeFile(`${options.mnemonicPath}-new-temp`, mnemonic) + .then(() => `${options.mnemonicPath}-new-temp`) +} + +function renameNewMnemonic () { + return fs.rename(`${options.mnemonicPath}-new-temp`, `${options.mnemonicPath}`) + .then(() => options.mnemonicPath) +} + +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 computeOperatorId (masterSeed) { + return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex') +} + +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 defaultWalletAcc (account) { + return defaultHdNodeAcc(account).deriveChild(0).getWallet() +} + +function defaultAddress (seed) { + return defaultWallet(seed).getChecksumAddressString() +} + +function defaultHdNode (seed) { + const key = hdkey.fromMasterSeed(seed) + return key.derivePath(defaultPrefixPath) +} + +function defaultHdNodeAcc (account) { + const key = hdkey.fromMasterSeed(account.seed) + return key.derivePath(defaultPrefixPath) +} + +function getAllBalance () { + return settingsLoader.loadLatest() + .then(settings => walletI.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 walletI.checkBlockchainStatus(settings, 'ETH') + .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 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) + } + + console.log(`Backing up old mnemonic...`) + return Promise.all([backupMnemonic(), infuraIsRunning, settings]) + }) + .then(([fileName, infuraIsRunning, settings]) => { + console.log(`Successfully backed up the old mnemonic, new location is ${fileName}`) + return Promise.all([writeNewMnemonic(mnemonic), infuraIsRunning, settings]) + }) + .then(([tempMnemonicFileName, infuraIsRunning, settings]) => { + console.log(`New mnemonic stored temporarily in ${tempMnemonicFileName}`) + console.log(`Starting funds transfer...`) + return Promise.all([infuraIsRunning, settings]) + }) + .then(([infuraIsRunning, 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, { account, operatorId }]) => { + const tx = { + cryptoCode: 'ETH', + toAddress: newAddress, + cryptoAtoms: BN(balance.times(0.99999).toFixed(0, BigNumber.ROUND_DOWN)) + } + + const opts = { + chainId: 3, + nonce: 0, + includesFee: true + } + + return sendCoins(account, tx, settings, operatorId, null, opts) + }) + .then(resTx => { + console.log('Successfully moved funds from the old wallet to the new one.') + console.log('Information about the transaction', resTx) + console.log('Moving the current mnemonic to the default file...') + return renameNewMnemonic() + }) + .then(() => { + 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') + return fs.mkdir(`${LOCKFILE_PATH}-finished`) + }) + .then(() => { + console.log('lamassu-eth-pending-sweep-finished lock file successfully created, this will automatically be deleted once the upgrade script finishes running') + console.log('Process finished successfully! You may now execute the upgrade script again') + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/lib/plugins/wallet/geth/base.js b/lib/plugins/wallet/geth/base.js index d37227a6..d9cee46c 100644 --- a/lib/plugins/wallet/geth/base.js +++ b/lib/plugins/wallet/geth/base.js @@ -50,7 +50,7 @@ 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) { const { toAddress, cryptoAtoms, cryptoCode } = tx return generateTx(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode) .then(pify(web3.eth.sendRawTransaction))