UNPKG

send-tokens

Version:

Simple CLI tool (and library) to send ERC20 tokens with a private key, mnemonic, or keystore.

313 lines (282 loc) 8.3 kB
'use strict' require('colors'); const _ = require('lodash'); const FlexContract = require('flex-contract'); const FlexEther = require('flex-ether'); const BigNumber = require('bignumber.js'); const ethjs = require('ethereumjs-util'); const ethwallet = require('ethereumjs-wallet'); const ethjshdwallet = require('ethereumjs-wallet/hdkey'); const bip39 = require('bip39'); const fs = require('mz/fs'); const readline = require('readline'); const process = require('process'); const prompt = require('prompt'); prompt.message = ''; prompt.delimiter = ':'; const ERC20_ABI = require('./erc20.abi.json'); module.exports = { sendTokens: sendTokens, toWallet: toWallet }; function toWallet(opts) { let from = null; let key = null; if (opts.mnemonic || opts.key || opts.keystore) { key = getPrivateKey(opts); if (key) from = keyToAddress(key); } return { address: from, key: key }; } async function sendTokens(token, to, amount, opts={}) { for (let addr of [token, to]) { if (!/^(\w+\.)*\w+\.(test|eth)$/.test(addr) && !ethjs.isValidAddress(addr)) throw new Error(`Invalid address: ${addr}`); } if (!_.isNumber(amount) && !/^\d+(\.\d+)?$/.test(amount)) throw new Error(`Invalid amount: ${amount}`); if (!_.isNil(opts.decimals) && !_.inRange(opts.decimals, 0, 256)) throw new Error(`Invalid decimals: ${opts.decimals}`); token = ethjs.isValidAddress(to) ? ethjs.toChecksumAddress(token) : token; to = ethjs.isValidAddress(to) ? ethjs.toChecksumAddress(to) : to; const confirmations = opts.confirmations || 0; const txOpts = await createTransferOpts(opts); const contract = createFlexContract(token, txOpts); const sender = await resolveSender(contract._eth, txOpts); const tokenDecimals = await resolveDecimals(contract); const tokenSymbol = await resolveSymbol(contract); const inputDecimals = _.isNumber(opts.decimals) ? opts.decimals : tokenDecimals; amount = toWei(amount, inputDecimals); if (!sender) throw new Error('Cannot determine sender.'); const logId = createLogId({ time: _.now(), token: token, to: to, amount: amount, from: sender }); const writeLog = opts.log ? createJSONLogger(logId, opts.log) : _.noop; const say = opts.quiet ? _.noop : console.log; say(`Token: ${tokenSymbol.bold} @ ${token.green.bold} (${tokenDecimals} decimal places)`); say(`${sender.blue.bold} -> ${toDecimal(amount, tokenDecimals).yellow.bold} ${tokenSymbol} -> ${to.blue.bold}`); if (opts.confirm) { if (!(await confirm())) return; } const {tx} = await transfer(contract, to, amount, txOpts); const txId = await tx.txId; if (_.isFunction(opts.onTxId)) opts.onTxId(txId); say(`Waiting for transaction ${txId.green.bold} to be mined...`); const receipt = await tx.confirmed(confirmations); const transferEvent = receipt.events.find(e => e.name === 'Transfer'); if (!transferEvent) { console.warning(`No ERC20 'Transfer' event raised! Verify transfer manually.`); } else { say(`Successfully transferred ${toDecimal(transferEvent.args[2], tokenDecimals).yellow.bold} ${tokenSymbol}!`); } writeLog({ from: sender, amount: amount, token: token, to: to, txId: txId, gas: receipt.gasUsed, block: receipt.blockNumber, }); return receipt; } function confirm() { return new Promise((accept, reject) => { prompt.get({ description: 'Proceed? [y/N]', name: 'answer', }, (err, {answer}) => { answer = (answer || 'n').toLowerCase(); accept(answer == 'y' || answer == 'yes'); }); }); } async function resolveSender(eth, opts) { const w = toWallet(opts); if (w && w.address) return w.address; return eth.getDefaultAccount(); } async function resolveDecimals(contract) { try { return _.toNumber(await contract.decimals()); } catch (err) { return 18; } } async function resolveSymbol(contract) { try { return await contract.symbol(); } catch (err) { return '???'; } } function toDecimal(amount, decimals) { return new BigNumber(amount).div(`1e${decimals}`).toString(10); } function toWei(amount, decimals) { return new BigNumber(amount).times(`1e${decimals}`).integerValue().toString(10); } async function createTransferOpts(opts) { const txOpts = {}; if (opts.key) { txOpts.key = ethjs.addHexPrefix(opts.key); if (!/^0x[a-f0-9]{64}$/i.test(txOpts.key)) throw new Error('Invalid private key.'); } else if (opts.keyFile) { txOpts.key = await fs.readFile(opts.keyFile, 'utf-8'); } else if (opts.keystoreFile) { txOpts.keystore = await fs.readFile(opts.keystoreFile, 'utf-8'); txOpts.password = opts.password; } else if (opts.keystore) { txOpts.keystore = opts.keystore; txOpts.password = opts.password; } else if (opts.mnemonic) { txOpts.mnemonicIndex = opts.mnemonicIndex || 0; txOpts.mnemonic = opts.mnemonic.trim(); } else if (opts.account) { txOpts.from = opts.account; } else if (opts.from) { txOpts.from = opts.from; } if (opts.provider) { if (_.isString(opts.provider)) { txOpts.providerURI = opts.provider; } else { txOpts.provider = opts.provider; } } else if (opts.providerURI) { txOpts.providerURI = opts.providerURI; } if (opts.network) { txOpts.network = opts.network; } if (opts.gasPrice) { txOpts.gasPrice = new BigNumber('1e9') .times(opts.gasPrice) .integerValue() .toString(10); } if (txOpts.keystore && !txOpts.password) txOpts.password = await promptForPassword(); return txOpts; } function promptForPassword() { return new Promise((accept, reject) => { prompt.get({ description: 'Enter password', name: 'pw', hidden: true, replace: '*' }, (err, {pw}) => { if (!pw) return reject(pw); accept(pw); }); }); } function createLogId(fields) { const s = { time: fields.time, token: ethjs.toChecksumAddress(fields.token), to: ethjs.toChecksumAddress(fields.to), from: ethjs.toChecksumAddress(fields.from), amount: ethjs.bufferToHex(ethjs.toUnsigned(new ethjs.BN(fields.amount))) }; return ethjs.bufferToHex( ethjs.keccak256(Buffer.from(JSON.stringify(s))).slice(0, 8)).slice(2); } function createJSONLogger(logId, file) { return (payload={}) => { const data = _.assign(payload, {time: Math.floor(_.now() / 1000), id: logId}); const line = JSON.stringify(data); fs.appendFileSync(file, `${line}\n`); }; } function createFlexContract(token, opts) { return new FlexContract( ERC20_ABI, token, { providerURI: opts.providerURI, provider: opts.provider, network: opts.network, infuraKey: opts.infuraKey, eth: opts.eth, net: require('net'), }, ); } async function transfer(contract, to, amount, opts={}) { let from = undefined; let key = undefined; if (!opts.mnemonic && !opts.key && !opts.keystore) { if (opts.from) from = opts.from; else from = await contract._eth.getDefaultAccount(); } else { const w = toWallet(opts, contract._eth); from = w.address; key = w.key; } if (!from) throw new Error('No account to send from.'); await verifyTokenBalance(contract, from, amount); return { tx: contract.transfer( to, amount, { from: key ? undefined : from, key: key, gasPrice: opts.gasPrice }, ), }; } async function verifyTokenBalance(contract, from, amount) { const balance = await contract.balanceOf(from); if (new BigNumber(balance).lt(amount)) throw new Error('Insufficient balance.'); } function keyToAddress(key) { return ethjs.toChecksumAddress(ethjs.bufferToHex( ethjs.privateToAddress(ethjs.toBuffer(key)))); } function getPrivateKey(opts) { if (opts.key) return ethjs.addHexPrefix(opts.key); if (opts.keystore) return fromKeystore(opts.keystore, opts.password); if (opts.mnemonic) return fromMnemonic(opts.mnemonic, opts.mnemonicIndex || 0); } function fromKeystore(keystore, pw) { if (!pw) throw new Error('Keystore requires password.'); if (_.isObject(keystore)) keystore = JSON.stringify(keystore); const wallet = ethwallet.fromV3(keystore, pw, true); return ethjs.bufferToHex(wallet.getPrivateKey()); } function fromMnemonic(mnemonic, idx=0) { const seed = bip39.mnemonicToSeedSync(mnemonic.trim()); const path = `m/44'/60'/0'/0/${idx}`; const node = ethjshdwallet.fromMasterSeed(seed).derivePath(path); const wallet = node.getWallet(); return ethjs.bufferToHex(wallet.getPrivateKey()); }