diff --git a/bin/lamassu-eth-sweep-to-new-wallet b/bin/lamassu-eth-sweep-to-new-wallet index 38b4c3c0..8f72f438 100644 --- a/bin/lamassu-eth-sweep-to-new-wallet +++ b/bin/lamassu-eth-sweep-to-new-wallet @@ -7,15 +7,24 @@ 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 wallet = require('../lib/wallet') 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 paymentPrefixPath = "m/44'/60'/0'/0'" +let lastUsedNonces = {} + +const hex = bigNum => '0x' + bigNum.integerValue(BN.ROUND_DOWN).toString(16) function writeNewMnemonic (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' }) } +function computeOperatorId (masterSeed) { + return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex') +} + function generateRandomSeed () { const seed = crypto .randomBytes(32) @@ -49,6 +62,10 @@ 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() } @@ -58,9 +75,14 @@ function defaultHdNode (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 => wallet.balance(settings, 'ETH')) + .then(settings => walletI.balance(settings, 'ETH')) .then(r => r.balance) } @@ -76,10 +98,120 @@ function isInfuraRunning (settings) { } function isGethRunning (settings) { - return wallet.checkBlockchainStatus(settings, 'ETH') + 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) @@ -93,9 +225,18 @@ settingsLoader.loadLatest() 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 = { cryptoCode: 'ETH', toAddress: newAddress, @@ -103,12 +244,12 @@ settingsLoader.loadLatest() } const opts = { - chainId: 1, + chainId: 3, nonce: 0, includesFee: true } - return wallet.sendCoins(settings, tx, opts) + return sendCoins(account, tx, settings, operatorId, null, opts) }) .then(resTx => { console.log('Successfully moved funds from the old wallet to the new one.') @@ -121,6 +262,10 @@ settingsLoader.loadLatest() }) .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') console.log('Process finished successfully!') process.exit(0) }) diff --git a/lib/blockchain/install.js b/lib/blockchain/install.js index d5789549..1191f20e 100644 --- a/lib/blockchain/install.js +++ b/lib/blockchain/install.js @@ -158,8 +158,7 @@ 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 => { - const blockchainStatuses = _.filter(it => it !== 'disconnected', _blockchainStatuses) + .then(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 144d12a0..d9cee46c 100644 --- a/lib/plugins/wallet/geth/base.js +++ b/lib/plugins/wallet/geth/base.js @@ -50,10 +50,9 @@ function isStrictAddress (cryptoCode, toAddress, settings, operatorId) { 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 opts = { ..._opts, includesFee: _.defaultTo(false, _opts?.includesFee) } - return generateTx(toAddress, defaultWallet(account), cryptoAtoms, cryptoCode, opts) + return generateTx(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode) .then(pify(web3.eth.sendRawTransaction)) .then(txid => { return pify(web3.eth.getTransaction)(txid) @@ -96,7 +95,7 @@ function _balance (includePending, address, cryptoCode) { .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 isErc20Token = coins.utils.isErc20Token(cryptoCode) @@ -126,18 +125,18 @@ function generateTx (_toAddress, wallet, amount, cryptoCode, opts) { .then(([gas, gasPrice, txCount]) => [ BN(gas), BN(gasPrice), - _.max([1, txCount, lastUsedNonces[fromAddress] + 1]) + _.max([0, txCount, lastUsedNonces[fromAddress] + 1]) ]) .then(([gas, gasPrice, txCount]) => { lastUsedNonces[fromAddress] = txCount - const toSend = opts.includesFee + const toSend = includesFee ? amount.minus(gasPrice.times(gas)) : amount const rawTx = { - chainId: _.defaultTo(1, opts?.chainId), - nonce: _.defaultTo(txCount, opts?.nonce), + chainId: 1, + nonce: txCount, gasPrice: hex(gasPrice), gasLimit: hex(gas), to: toAddress, @@ -173,9 +172,8 @@ 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, cryptoCode, opts) + return generateTx(defaultAddress(account), wallet, r, true, cryptoCode) .then(signedTx => pify(web3.eth.sendRawTransaction)(signedTx)) }) } @@ -239,5 +237,4 @@ 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 eddeb7f4..1073a8a0 100644 --- a/lib/wallet.js +++ b/lib/wallet.js @@ -59,11 +59,11 @@ function _balance (settings, cryptoCode) { }) } -function sendCoins (settings, tx, opts) { +function sendCoins (settings, tx) { 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, opts) + return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier) .then(res => { mem.clear(module.exports.balance) return res