/* This file is part of web3.js. web3.js is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. web3.js is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ /** * @file accounts.js * @author Fabian Vogelsteller * @date 2017 */ 'use strict'; var core = require('web3-core'); var Method = require('web3-core-method'); var Account = require('eth-lib/lib/account'); var cryp = (typeof global === 'undefined') ? require('crypto-browserify') : require('crypto'); var scrypt = require('scrypt-js'); var uuid = require('uuid'); var utils = require('web3-utils'); var helpers = require('web3-core-helpers'); var { TransactionFactory } = require('@ethereumjs/tx'); var Common = require('@ethereumjs/common').default; var HardForks = require('@ethereumjs/common').Hardfork; var ethereumjsUtil = require('ethereumjs-util'); var isNot = function (value) { return (typeof value === 'undefined') || value === null; }; var isExist = function (value) { return (typeof value !== 'undefined') && value !== null; }; var Accounts = function Accounts() { var _this = this; // sets _requestmanager core.packageInit(this, arguments); // remove unecessary core functions delete this.BatchRequest; delete this.extend; var _ethereumCall = [ new Method({ name: 'getNetworkId', call: 'net_version', params: 0, outputFormatter: parseInt }), new Method({ name: 'getChainId', call: 'eth_chainId', params: 0, outputFormatter: utils.hexToNumber }), new Method({ name: 'getGasPrice', call: 'eth_gasPrice', params: 0 }), new Method({ name: 'getTransactionCount', call: 'eth_getTransactionCount', params: 2, inputFormatter: [function (address) { if (utils.isAddress(address)) { return address; } else { throw new Error('Address ' + address + ' is not a valid address to get the "transactionCount".'); } }, function () { return 'latest'; }] }), new Method({ name: 'getBlockByNumber', call: 'eth_getBlockByNumber', params: 2, inputFormatter: [function (blockNumber) { return blockNumber ? utils.toHex(blockNumber) : 'latest'; }, function () { return false; }] }), ]; // attach methods to this._ethereumCall this._ethereumCall = {}; _ethereumCall.forEach((method) => { method.attachToObject(_this._ethereumCall); method.setRequestManager(_this._requestManager); }); this.wallet = new Wallet(this); }; Accounts.prototype._addAccountFunctions = function (account) { var _this = this; // add sign functions account.signTransaction = function signTransaction(tx, callback) { return _this.signTransaction(tx, account.privateKey, callback); }; account.sign = function sign(data) { return _this.sign(data, account.privateKey); }; account.encrypt = function encrypt(password, options) { return _this.encrypt(account.privateKey, password, options); }; return account; }; Accounts.prototype.create = function create(entropy) { return this._addAccountFunctions(Account.create(entropy || utils.randomHex(32))); }; Accounts.prototype.privateKeyToAccount = function privateKeyToAccount(privateKey, ignoreLength) { if (!privateKey.startsWith('0x')) { privateKey = '0x' + privateKey; } // 64 hex characters + hex-prefix if (!ignoreLength && privateKey.length !== 66) { throw new Error("Private key must be 32 bytes long"); } return this._addAccountFunctions(Account.fromPrivate(privateKey)); }; Accounts.prototype.signTransaction = function signTransaction(tx, privateKey, callback) { var _this = this, error = false, transactionOptions = {}, hasTxSigningOptions = !!(tx && ((tx.chain && tx.hardfork) || tx.common)); callback = callback || function () { }; if (!tx) { error = new Error('No transaction object given!'); callback(error); return Promise.reject(error); } if (isExist(tx.common) && isNot(tx.common.customChain)) { error = new Error('If tx.common is provided it must have tx.common.customChain'); callback(error); return Promise.reject(error); } if (isExist(tx.common) && isNot(tx.common.customChain.chainId)) { error = new Error('If tx.common is provided it must have tx.common.customChain and tx.common.customChain.chainId'); callback(error); return Promise.reject(error); } if (isExist(tx.common) && isExist(tx.common.customChain.chainId) && isExist(tx.chainId) && tx.chainId !== tx.common.customChain.chainId) { error = new Error('Chain Id doesnt match in tx.chainId tx.common.customChain.chainId'); callback(error); return Promise.reject(error); } function signed(tx) { const error = _validateTransactionForSigning(tx); if (error) { callback(error); return Promise.reject(error); } try { var transaction = helpers.formatters.inputCallFormatter(Object.assign({}, tx)); transaction.data = transaction.data || '0x'; transaction.value = transaction.value || '0x'; transaction.gasLimit = transaction.gasLimit || transaction.gas; if (transaction.type === '0x1' && transaction.accessList === undefined) transaction.accessList = []; // Because tx has no @ethereumjs/tx signing options we use fetched vals. if (!hasTxSigningOptions) { transactionOptions.common = Common.forCustomChain('mainnet', { name: 'custom-network', networkId: transaction.networkId, chainId: transaction.chainId }, transaction.hardfork || HardForks.London); delete transaction.networkId; } else { if (transaction.common) { transactionOptions.common = Common.forCustomChain(transaction.common.baseChain || 'mainnet', { name: transaction.common.customChain.name || 'custom-network', networkId: transaction.common.customChain.networkId, chainId: transaction.common.customChain.chainId }, transaction.common.hardfork || HardForks.London); delete transaction.common; } if (transaction.chain) { transactionOptions.chain = transaction.chain; delete transaction.chain; } if (transaction.hardfork) { transactionOptions.hardfork = transaction.hardfork; delete transaction.hardfork; } } if (privateKey.startsWith('0x')) { privateKey = privateKey.substring(2); } var ethTx = TransactionFactory.fromTxData(transaction, transactionOptions); var signedTx = ethTx.sign(Buffer.from(privateKey, 'hex')); var validationErrors = signedTx.validate(true); if (validationErrors.length > 0) { let errorString = 'Signer Error: '; for (const validationError of validationErrors) { errorString += `${errorString} ${validationError}.`; } throw new Error(errorString); } var rlpEncoded = signedTx.serialize().toString('hex'); var rawTransaction = '0x' + rlpEncoded; var transactionHash = utils.keccak256(rawTransaction); var result = { messageHash: '0x' + Buffer.from(signedTx.getMessageToSign(true)).toString('hex'), v: '0x' + signedTx.v.toString('hex'), r: '0x' + signedTx.r.toString('hex'), s: '0x' + signedTx.s.toString('hex'), rawTransaction: rawTransaction, transactionHash: transactionHash }; callback(null, result); return result; } catch (e) { callback(e); return Promise.reject(e); } } tx.type = _handleTxType(tx); // Resolve immediately if nonce, chainId, price and signing options are provided if (tx.nonce !== undefined && tx.chainId !== undefined && (tx.gasPrice !== undefined || (tx.maxFeePerGas !== undefined && tx.maxPriorityFeePerGas !== undefined)) && hasTxSigningOptions) { return Promise.resolve(signed(tx)); } // Otherwise, get the missing info from the Ethereum Node return Promise.all([ ((isNot(tx.common) || isNot(tx.common.customChain.chainId)) ? //tx.common.customChain.chainId is not optional inside tx.common if tx.common is provided (isNot(tx.chainId) ? _this._ethereumCall.getChainId() : tx.chainId) : undefined), isNot(tx.nonce) ? _this._ethereumCall.getTransactionCount(_this.privateKeyToAccount(privateKey).address) : tx.nonce, isNot(hasTxSigningOptions) ? _this._ethereumCall.getNetworkId() : 1, _handleTxPricing(_this, tx) ]).then(function (args) { const [txchainId, txnonce, txnetworkId, txgasInfo] = args; if ((isNot(txchainId) && isNot(tx.common) && isNot(tx.common.customChain.chainId)) || isNot(txnonce) || isNot(txnetworkId) || isNot(txgasInfo)) { throw new Error('One of the values "chainId", "networkId", "gasPrice", or "nonce" couldn\'t be fetched: ' + JSON.stringify(args)); } return signed({ ...tx, ...((isNot(tx.common) || isNot(tx.common.customChain.chainId)) ? { chainId: txchainId } : {}), nonce: txnonce, networkId: txnetworkId, ...txgasInfo // Will either be gasPrice or maxFeePerGas and maxPriorityFeePerGas }); }); }; function _validateTransactionForSigning(tx) { if (tx.common && (tx.chain && tx.hardfork)) { return new Error('Please provide the @ethereumjs/common object or the chain and hardfork property but not all together.'); } if ((tx.chain && !tx.hardfork) || (tx.hardfork && !tx.chain)) { return new Error('When specifying chain and hardfork, both values must be defined. ' + 'Received "chain": ' + tx.chain + ', "hardfork": ' + tx.hardfork); } if ((!tx.gas && !tx.gasLimit) && (!tx.maxPriorityFeePerGas && !tx.maxFeePerGas)) { return new Error('"gas" is missing'); } if (tx.gas && tx.gasPrice) { if (tx.gas < 0 || tx.gasPrice < 0) { return new Error('Gas or gasPrice is lower than 0'); } } else { if (tx.maxPriorityFeePerGas < 0 || tx.maxFeePerGas < 0) { return new Error('maxPriorityFeePerGas or maxFeePerGas is lower than 0'); } } if (tx.nonce < 0 || tx.chainId < 0) { return new Error('Nonce or chainId is lower than 0'); } return; } function _handleTxType(tx) { // Taken from https://github.com/ethers-io/ethers.js/blob/2a7ce0e72a1e0c9469e10392b0329e75e341cf18/packages/abstract-signer/src.ts/index.ts#L215 const hasEip1559 = (tx.maxFeePerGas !== undefined || tx.maxPriorityFeePerGas !== undefined); let txType; if (tx.type !== undefined) { txType = utils.toHex(tx.type); } else if (tx.type === undefined && hasEip1559) { txType = '0x2'; } if (tx.gasPrice !== undefined && (txType === '0x2' || hasEip1559)) throw Error("eip-1559 transactions don't support gasPrice"); if ((txType === '0x1' || txType === '0x0') && hasEip1559) throw Error("pre-eip-1559 transaction don't support maxFeePerGas/maxPriorityFeePerGas"); if (hasEip1559 || ((tx.common && tx.common.hardfork && tx.common.hardfork.toLowerCase() === HardForks.London) || (tx.hardfork && tx.hardfork.toLowerCase() === HardForks.London))) { txType = '0x2'; } else if (tx.accessList || ((tx.common && tx.common.hardfork && tx.common.hardfork.toLowerCase() === HardForks.Berlin) || (tx.hardfork && tx.hardfork.toLowerCase() === HardForks.Berlin))) { txType = '0x1'; } return txType; } function _handleTxPricing(_this, tx) { return new Promise((resolve, reject) => { try { if ((tx.type === undefined || tx.type < '0x2') && tx.gasPrice !== undefined) { // Legacy transaction, return provided gasPrice resolve({ gasPrice: tx.gasPrice }); } else { Promise.all([ _this._ethereumCall.getBlockByNumber(), _this._ethereumCall.getGasPrice() ]).then(responses => { const [block, gasPrice] = responses; if ((tx.type === '0x2') && block && block.baseFeePerGas) { // The network supports EIP-1559 // Taken from https://github.com/ethers-io/ethers.js/blob/ba6854bdd5a912fe873d5da494cb5c62c190adde/packages/abstract-provider/src.ts/index.ts#L230 let maxPriorityFeePerGas, maxFeePerGas; if (tx.gasPrice) { // Using legacy gasPrice property on an eip-1559 network, // so use gasPrice as both fee properties maxPriorityFeePerGas = tx.gasPrice; maxFeePerGas = tx.gasPrice; delete tx.gasPrice; } else { maxPriorityFeePerGas = tx.maxPriorityFeePerGas || '0x9502F900'; // 2.5 Gwei maxFeePerGas = tx.maxFeePerGas || utils.toHex(utils.toBN(block.baseFeePerGas) .mul(utils.toBN(2)) .add(utils.toBN(maxPriorityFeePerGas))); } resolve({ maxFeePerGas, maxPriorityFeePerGas }); } else { if (tx.maxPriorityFeePerGas || tx.maxFeePerGas) throw Error("Network doesn't support eip-1559"); resolve({ gasPrice }); } }); } } catch (error) { reject(error); } }); } /* jshint ignore:start */ Accounts.prototype.recoverTransaction = function recoverTransaction(rawTx, txOptions = {}) { // Rely on EthereumJs/tx to determine the type of transaction const data = Buffer.from(rawTx.slice(2), "hex"); const tx = TransactionFactory.fromSerializedData(data); //update checksum return utils.toChecksumAddress(tx.getSenderAddress().toString("hex")); }; /* jshint ignore:end */ Accounts.prototype.hashMessage = function hashMessage(data) { var messageHex = utils.isHexStrict(data) ? data : utils.utf8ToHex(data); var messageBytes = utils.hexToBytes(messageHex); var messageBuffer = Buffer.from(messageBytes); var preamble = '\x19Ethereum Signed Message:\n' + messageBytes.length; var preambleBuffer = Buffer.from(preamble); var ethMessage = Buffer.concat([preambleBuffer, messageBuffer]); return ethereumjsUtil.bufferToHex(ethereumjsUtil.keccak256(ethMessage)); }; Accounts.prototype.sign = function sign(data, privateKey) { if (!privateKey.startsWith('0x')) { privateKey = '0x' + privateKey; } // 64 hex characters + hex-prefix if (privateKey.length !== 66) { throw new Error("Private key must be 32 bytes long"); } var hash = this.hashMessage(data); var signature = Account.sign(hash, privateKey); var vrs = Account.decodeSignature(signature); return { message: data, messageHash: hash, v: vrs[0], r: vrs[1], s: vrs[2], signature: signature }; }; Accounts.prototype.recover = function recover(message, signature, preFixed) { var args = [].slice.apply(arguments); if (!!message && typeof message === 'object') { return this.recover(message.messageHash, Account.encodeSignature([message.v, message.r, message.s]), true); } if (!preFixed) { message = this.hashMessage(message); } if (args.length >= 4) { preFixed = args.slice(-1)[0]; preFixed = typeof preFixed === 'boolean' ? !!preFixed : false; return this.recover(message, Account.encodeSignature(args.slice(1, 4)), preFixed); // v, r, s } return Account.recover(message, signature); }; // Taken from https://github.com/ethereumjs/ethereumjs-wallet Accounts.prototype.decrypt = function (v3Keystore, password, nonStrict) { /* jshint maxcomplexity: 10 */ if (!(typeof password === 'string')) { throw new Error('No password given.'); } var json = (!!v3Keystore && typeof v3Keystore === 'object') ? v3Keystore : JSON.parse(nonStrict ? v3Keystore.toLowerCase() : v3Keystore); if (json.version !== 3) { throw new Error('Not a valid V3 wallet'); } var derivedKey; var kdfparams; if (json.crypto.kdf === 'scrypt') { kdfparams = json.crypto.kdfparams; // FIXME: support progress reporting callback derivedKey = scrypt.syncScrypt(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen); } else if (json.crypto.kdf === 'pbkdf2') { kdfparams = json.crypto.kdfparams; if (kdfparams.prf !== 'hmac-sha256') { throw new Error('Unsupported parameters to PBKDF2'); } derivedKey = cryp.pbkdf2Sync(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.c, kdfparams.dklen, 'sha256'); } else { throw new Error('Unsupported key derivation scheme'); } var ciphertext = Buffer.from(json.crypto.ciphertext, 'hex'); var mac = utils.sha3(Buffer.from([...derivedKey.slice(16, 32), ...ciphertext])).replace('0x', ''); if (mac !== json.crypto.mac) { throw new Error('Key derivation failed - possibly wrong password'); } var decipher = cryp.createDecipheriv(json.crypto.cipher, derivedKey.slice(0, 16), Buffer.from(json.crypto.cipherparams.iv, 'hex')); var seed = '0x' + Buffer.from([...decipher.update(ciphertext), ...decipher.final()]).toString('hex'); return this.privateKeyToAccount(seed, true); }; Accounts.prototype.encrypt = function (privateKey, password, options) { /* jshint maxcomplexity: 20 */ var account = this.privateKeyToAccount(privateKey, true); options = options || {}; var salt = options.salt || cryp.randomBytes(32); var iv = options.iv || cryp.randomBytes(16); var derivedKey; var kdf = options.kdf || 'scrypt'; var kdfparams = { dklen: options.dklen || 32, salt: salt.toString('hex') }; if (kdf === 'pbkdf2') { kdfparams.c = options.c || 262144; kdfparams.prf = 'hmac-sha256'; derivedKey = cryp.pbkdf2Sync(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.c, kdfparams.dklen, 'sha256'); } else if (kdf === 'scrypt') { // FIXME: support progress reporting callback kdfparams.n = options.n || 8192; // 2048 4096 8192 16384 kdfparams.r = options.r || 8; kdfparams.p = options.p || 1; derivedKey = scrypt.syncScrypt(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen); } else { throw new Error('Unsupported kdf'); } var cipher = cryp.createCipheriv(options.cipher || 'aes-128-ctr', derivedKey.slice(0, 16), iv); if (!cipher) { throw new Error('Unsupported cipher'); } var ciphertext = Buffer.from([ ...cipher.update(Buffer.from(account.privateKey.replace('0x', ''), 'hex')), ...cipher.final() ]); var mac = utils.sha3(Buffer.from([...derivedKey.slice(16, 32), ...ciphertext])).replace('0x', ''); return { version: 3, id: uuid.v4({ random: options.uuid || cryp.randomBytes(16) }), address: account.address.toLowerCase().replace('0x', ''), crypto: { ciphertext: ciphertext.toString('hex'), cipherparams: { iv: iv.toString('hex') }, cipher: options.cipher || 'aes-128-ctr', kdf: kdf, kdfparams: kdfparams, mac: mac.toString('hex') } }; }; // Note: this is trying to follow closely the specs on // http://web3js.readthedocs.io/en/1.0/web3-eth-accounts.html function Wallet(accounts) { this._accounts = accounts; this.length = 0; this.defaultKeyName = 'web3js_wallet'; } Wallet.prototype._findSafeIndex = function (pointer) { pointer = pointer || 0; if (this.hasOwnProperty(pointer)) { return this._findSafeIndex(pointer + 1); } else { return pointer; } }; Wallet.prototype._currentIndexes = function () { var keys = Object.keys(this); var indexes = keys .map(function (key) { return parseInt(key); }) .filter(function (n) { return (n < 9e20); }); return indexes; }; Wallet.prototype.create = function (numberOfAccounts, entropy) { for (var i = 0; i < numberOfAccounts; ++i) { this.add(this._accounts.create(entropy).privateKey); } return this; }; Wallet.prototype.add = function (account) { if (typeof account === 'string') { account = this._accounts.privateKeyToAccount(account); } if (!this[account.address]) { account = this._accounts.privateKeyToAccount(account.privateKey); account.index = this._findSafeIndex(); this[account.index] = account; this[account.address] = account; this[account.address.toLowerCase()] = account; this.length++; return account; } else { return this[account.address]; } }; Wallet.prototype.remove = function (addressOrIndex) { var account = this[addressOrIndex]; if (account && account.address) { // address this[account.address].privateKey = null; delete this[account.address]; // address lowercase this[account.address.toLowerCase()].privateKey = null; delete this[account.address.toLowerCase()]; // index this[account.index].privateKey = null; delete this[account.index]; this.length--; return true; } else { return false; } }; Wallet.prototype.clear = function () { var _this = this; var indexes = this._currentIndexes(); indexes.forEach(function (index) { _this.remove(index); }); return this; }; Wallet.prototype.encrypt = function (password, options) { var _this = this; var indexes = this._currentIndexes(); var accounts = indexes.map(function (index) { return _this[index].encrypt(password, options); }); return accounts; }; Wallet.prototype.decrypt = function (encryptedWallet, password) { var _this = this; encryptedWallet.forEach(function (keystore) { var account = _this._accounts.decrypt(keystore, password); if (account) { _this.add(account); } else { throw new Error('Couldn\'t decrypt accounts. Password wrong?'); } }); return this; }; Wallet.prototype.save = function (password, keyName) { localStorage.setItem(keyName || this.defaultKeyName, JSON.stringify(this.encrypt(password))); return true; }; Wallet.prototype.load = function (password, keyName) { var keystore = localStorage.getItem(keyName || this.defaultKeyName); if (keystore) { try { keystore = JSON.parse(keystore); } catch (e) { } } return this.decrypt(keystore || [], password); }; if (!storageAvailable('localStorage')) { delete Wallet.prototype.save; delete Wallet.prototype.load; } /** * Checks whether a storage type is available or not * For more info on how this works, please refer to MDN documentation * https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#Feature-detecting_localStorage * * @method storageAvailable * @param {String} type the type of storage ('localStorage', 'sessionStorage') * @returns {Boolean} a boolean indicating whether the specified storage is available or not */ function storageAvailable(type) { var storage; try { storage = window[type]; var x = '__storage_test__'; storage.setItem(x, x); storage.removeItem(x); return true; } catch (e) { return e && ( // everything except Firefox e.code === 22 || // Firefox e.code === 1014 || // test name field too, because code might not be present // everything except Firefox e.name === 'QuotaExceededError' || // Firefox e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && // acknowledge QuotaExceededError only if there's something already stored (storage && storage.length !== 0); } } module.exports = Accounts;