#!/usr/bin/env node require('../lib/environment-helper') 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 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) const MNEMONIC_PATH = process.env.MNEMONIC_PATH function writeNewMnemonic (mnemonic) { return fs.writeFile(`${MNEMONIC_PATH}-new-temp`, mnemonic) .then(() => `${MNEMONIC_PATH}-new-temp`) } function renameNewMnemonic () { return fs.rename(`${MNEMONIC_PATH}-new-temp`, `${MNEMONIC_PATH}`) .then(() => MNEMONIC_PATH) } function backupMnemonic () { const folderPath = path.dirname(MNEMONIC_PATH) const fileName = path.resolve(folderPath, `mnemonic-${Date.now()}.txt`) return fs.copyFile(MNEMONIC_PATH, 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(MNEMONIC_PATH, '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: 1, 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) })