diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 465751e61..77312e502 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -1,6 +1,6 @@ -const ObservableStore = require('obs-store') -const PendingBalanceCalculator = require('../lib/pending-balance-calculator') -const BN = require('ethereumjs-util').BN +import ObservableStore from 'obs-store' +import PendingBalanceCalculator from '../lib/pending-balance-calculator' +import { BN } from 'ethereumjs-util' class BalanceController { @@ -8,7 +8,7 @@ class BalanceController { * Controller responsible for storing and updating an account's balance. * * @typedef {Object} BalanceController - * @param {Object} opts Initialize various properties of the class. + * @param {Object} opts - Initialize various properties of the class. * @property {string} address A base 16 hex string. The account address which has the balance managed by this * BalanceController. * @property {AccountTracker} accountTracker Stores and updates the users accounts @@ -47,7 +47,7 @@ class BalanceController { /** * Updates the ethBalance property to the current pending balance * - * @returns {Promise} Promises undefined + * @returns {Promise} - Promises undefined */ async updateBalance () { const balance = await this.balanceCalc.getBalance() @@ -68,7 +68,7 @@ class BalanceController { _registerUpdates () { const update = this.updateBalance.bind(this) - this.txController.on('tx:status-update', (txId, status) => { + this.txController.on('tx:status-update', (_, status) => { switch (status) { case 'submitted': case 'confirmed': @@ -87,7 +87,7 @@ class BalanceController { * Gets the balance, as a base 16 hex string, of the account at this BalanceController's current address. * If the current account has no balance, returns undefined. * - * @returns {Promise} Promises a BN with a value equal to the balance of the current account, or undefined + * @returns {Promise} - Promises a BN with a value equal to the balance of the current account, or undefined * if the current account has no balance * */ @@ -103,7 +103,7 @@ class BalanceController { * TransactionController passed to this BalanceController during construction. * * @private - * @returns {Promise} Promises an array of transaction objects. + * @returns {Promise} - Promises an array of transaction objects. * */ async _getPendingTransactions () { @@ -118,7 +118,7 @@ class BalanceController { /** * Validates that the passed options have all required properties. * - * @param {Object} opts The options object to validate + * @param {Object} opts - The options object to validate * @throws {string} Throw a custom error indicating that address, accountTracker, txController and blockTracker are * missing and at least one is required * diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 326bcb355..5ab71550d 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -1,21 +1,23 @@ -const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') -const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref') -const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty') -const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') -const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') -const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') -const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createInfuraMiddleware = require('eth-json-rpc-infura') -const BlockTracker = require('eth-block-tracker') +import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware' +import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware' +import createBlockReRefMiddleware from 'eth-json-rpc-middleware/block-ref' +import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty' +import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache' +import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache' +import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector' +import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware' +import createInfuraMiddleware from 'eth-json-rpc-infura' +import BlockTracker from 'eth-block-tracker' -module.exports = createInfuraClient +export default createInfuraClient function createInfuraClient ({ network }) { - const infuraMiddleware = createInfuraMiddleware({ network }) + const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) const infuraProvider = providerFromMiddleware(infuraMiddleware) const blockTracker = new BlockTracker({ provider: infuraProvider }) const networkMiddleware = mergeMiddleware([ + createNetworkAndChainIdMiddleware({ network }), createBlockCacheMiddleware({ blockTracker }), createInflightMiddleware(), createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }), @@ -25,3 +27,38 @@ function createInfuraClient ({ network }) { ]) return { networkMiddleware, blockTracker } } + +function createNetworkAndChainIdMiddleware ({ network }) { + let chainId + let netId + + switch (network) { + case 'mainnet': + netId = '1' + chainId = '0x01' + break + case 'ropsten': + netId = '3' + chainId = '0x03' + break + case 'rinkeby': + netId = '4' + chainId = '0x04' + break + case 'kovan': + netId = '42' + chainId = '0x2a' + break + case 'goerli': + netId = '5' + chainId = '0x05' + break + default: + throw new Error(`createInfuraClient - unknown network "${network}"`) + } + + return createScaffoldMiddleware({ + eth_chainId: chainId, + net_version: netId, + }) +} diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index a8cbf2aaf..a38477f8c 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -1,13 +1,13 @@ -const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') -const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') -const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') -const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') -const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') -const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') -const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const BlockTracker = require('eth-block-tracker') +import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware' +import createFetchMiddleware from 'eth-json-rpc-middleware/fetch' +import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite' +import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache' +import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache' +import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector' +import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware' +import BlockTracker from 'eth-block-tracker' -module.exports = createJsonRpcClient +export default createJsonRpcClient function createJsonRpcClient ({ rpcUrl }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }) diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js index 3a435e5d0..b4429f86d 100644 --- a/app/scripts/controllers/network/createLocalhostClient.js +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -1,14 +1,14 @@ -const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') -const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') -const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') -const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') -const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const BlockTracker = require('eth-block-tracker') +import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware' +import createFetchMiddleware from 'eth-json-rpc-middleware/fetch' +import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite' +import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector' +import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware' +import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware' +import BlockTracker from 'eth-block-tracker' const inTest = process.env.IN_TEST === 'true' -module.exports = createLocalhostClient +export default createLocalhostClient function createLocalhostClient () { const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) @@ -25,7 +25,7 @@ function createLocalhostClient () { } function delay (time) { - return new Promise(resolve => setTimeout(resolve, time)) + return new Promise((resolve) => setTimeout(resolve, time)) } diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index 9e6a45888..56874e6ff 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -1,9 +1,9 @@ -const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') -const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') -const createWalletSubprovider = require('eth-json-rpc-middleware/wallet') +import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware' +import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware' +import createWalletSubprovider from 'eth-json-rpc-middleware/wallet' +import { createPendingNonceMiddleware } from './middleware/pending' -module.exports = createMetamaskMiddleware +export default createMetamaskMiddleware function createMetamaskMiddleware ({ version, @@ -11,7 +11,11 @@ function createMetamaskMiddleware ({ processTransaction, processEthSignMessage, processTypedMessage, + processTypedMessageV3, + processTypedMessageV4, processPersonalMessage, + processDecryptMessage, + processEncryptionPublicKey, getPendingNonce, }) { const metamaskMiddleware = mergeMiddleware([ @@ -25,19 +29,13 @@ function createMetamaskMiddleware ({ processTransaction, processEthSignMessage, processTypedMessage, + processTypedMessageV3, + processTypedMessageV4, processPersonalMessage, + processDecryptMessage, + processEncryptionPublicKey, }), createPendingNonceMiddleware({ getPendingNonce }), ]) return metamaskMiddleware } - -function createPendingNonceMiddleware ({ getPendingNonce }) { - return createAsyncMiddleware(async (req, res, next) => { - if (req.method !== 'eth_getTransactionCount') return next() - const address = req.params[0] - const blockRef = req.params[1] - if (blockRef !== 'pending') return next() - res.result = await getPendingNonce(address) - }) -} diff --git a/app/scripts/controllers/network/middleware/pending.js b/app/scripts/controllers/network/middleware/pending.js new file mode 100644 index 000000000..ca0273658 --- /dev/null +++ b/app/scripts/controllers/network/middleware/pending.js @@ -0,0 +1,31 @@ +const { formatTxMetaForRpcResult } = require('../util') +import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware' + +export function createPendingNonceMiddleware ({ getPendingNonce }) { + return createAsyncMiddleware(async (req, res, next) => { + const { method, params } = req + if (method !== 'eth_getTransactionCount') { + return next() + } + const [param, blockRef] = params + if (blockRef !== 'pending') { + return next() + } + res.result = await getPendingNonce(param) + }) +} + +export function createPendingTxMiddleware ({ getPendingTransactionByHash }) { + return createAsyncMiddleware(async (req, res, next) => { + const { method, params } = req + if (method !== 'eth_getTransactionByHash') { + return next() + } + const [hash] = params + const txMeta = getPendingTransactionByHash(hash) + if (!txMeta) { + return next() + } + res.result = formatTxMetaForRpcResult(txMeta) + }) +} diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 64ea33b76..a42156287 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,20 +1,19 @@ -const assert = require('assert') -const EventEmitter = require('events') -const ObservableStore = require('obs-store') -const ComposedStore = require('obs-store/lib/composed') -const EthQuery = require('eth-query') -const JsonRpcEngine = require('json-rpc-engine') -const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine') -const log = require('loglevel') -const createMetamaskMiddleware = require('./createMetamaskMiddleware') -const createInfuraClient = require('./createInfuraClient') -const createJsonRpcClient = require('./createJsonRpcClient') -const createLocalhostClient = require('./createLocalhostClient') +import assert from 'assert' +import EventEmitter from 'events' +import ObservableStore from 'obs-store' +import ComposedStore from 'obs-store/lib/composed' +import EthQuery from 'eth-query' +import JsonRpcEngine from 'json-rpc-engine' +import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine' +import log from 'loglevel' +import createMetamaskMiddleware from './createMetamaskMiddleware' +import createInfuraClient from './createInfuraClient' +import createJsonRpcClient from './createJsonRpcClient' +import createLocalhostClient from './createLocalhostClient' const createPocketClient = require('./createPocketClient') const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') const ethNetProps = require('eth-net-props') -const parse = require('url-parse') -const extend = require('extend') +import parse from 'url-parse' const networks = { networkList: {} } const { isKnownProvider, getDPath } = require('../../../../old-ui/app/util') @@ -75,7 +74,7 @@ module.exports = class NetworkController extends EventEmitter { this.networkStore = new ObservableStore('loading') this.dProviderStore = new ObservableStore({dProvider: false}) this.networkConfig = new ObservableStore(defaultNetworkConfig) - this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, dProviderStore: this.dProviderStore }) + this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig, dProviderStore: this.dProviderStore }) this.on('networkDidChange', this.lookupNetwork) // provider and block tracker this._provider = null @@ -101,7 +100,9 @@ module.exports = class NetworkController extends EventEmitter { verifyNetwork () { // Check network when restoring connectivity: - if (this.isNetworkLoading()) this.lookupNetwork() + if (this.isNetworkLoading()) { + this.lookupNetwork() + } } getNetworkState () { @@ -253,10 +254,12 @@ module.exports = class NetworkController extends EventEmitter { _configureInfuraProvider ({ type }) { log.info('NetworkController - configureInfuraProvider', type) - const networkClient = createInfuraClient({ network: type }) + const networkClient = createInfuraClient({ + network: type, + }) this._setNetworkClient(networkClient) // setup networkConfig - var settings = { + const settings = { ticker: 'ETH', } this.networkConfig.putState(settings) @@ -285,10 +288,10 @@ module.exports = class NetworkController extends EventEmitter { nickname, } // setup networkConfig - var settings = { + let settings = { network: chainId, } - settings = extend(settings, networks.networkList['rpc']) + settings = Object.assign(settings, networks.networkList['rpc']) this.networkConfig.putState(settings) this._setNetworkClient(networkClient) } @@ -318,9 +321,4 @@ module.exports = class NetworkController extends EventEmitter { this._provider = provider this._blockTracker = blockTracker } - - _logBlock (block) { - log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) - this.verifyNetwork() - } } diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js index 7a4d573df..c5eeb0ff9 100644 --- a/app/scripts/controllers/network/util.js +++ b/app/scripts/controllers/network/util.js @@ -169,7 +169,27 @@ networks[RSK_TESTNET] = RSK_TESTNET_OBJ const getNetworkDisplayName = key => networks[key].displayName +function formatTxMetaForRpcResult (txMeta) { + return { + 'blockHash': txMeta.txReceipt ? txMeta.txReceipt.blockHash : null, + 'blockNumber': txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null, + 'from': txMeta.txParams.from, + 'gas': txMeta.txParams.gas, + 'gasPrice': txMeta.txParams.gasPrice, + 'hash': txMeta.hash, + 'input': txMeta.txParams.data || '0x', + 'nonce': txMeta.txParams.nonce, + 'to': txMeta.txParams.to, + 'transactionIndex': txMeta.txReceipt ? txMeta.txReceipt.transactionIndex : null, + 'value': txMeta.txParams.value || '0x0', + 'v': txMeta.v, + 'r': txMeta.r, + 's': txMeta.s, + } +} + module.exports = { networks, getNetworkDisplayName, + formatTxMetaForRpcResult, } diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js new file mode 100644 index 000000000..0e68232f4 --- /dev/null +++ b/app/scripts/controllers/permissions/enums.js @@ -0,0 +1,81 @@ + +export const WALLET_PREFIX = 'wallet_' + +export const HISTORY_STORE_KEY = 'permissionsHistory' + +export const LOG_STORE_KEY = 'permissionsLog' + +export const METADATA_STORE_KEY = 'domainMetadata' + +export const CAVEAT_NAMES = { + exposedAccounts: 'exposedAccounts', +} + +export const NOTIFICATION_NAMES = { + accountsChanged: 'wallet_accountsChanged', +} + +export const LOG_IGNORE_METHODS = [ + 'wallet_sendDomainMetadata', +] + +export const LOG_METHOD_TYPES = { + restricted: 'restricted', + internal: 'internal', +} + +export const LOG_LIMIT = 100 + +export const SAFE_METHODS = [ + 'web3_sha3', + 'net_listening', + 'net_peerCount', + 'net_version', + 'eth_blockNumber', + 'eth_call', + 'eth_chainId', + 'eth_coinbase', + 'eth_estimateGas', + 'eth_gasPrice', + 'eth_getBalance', + 'eth_getBlockByHash', + 'eth_getBlockByNumber', + 'eth_getBlockTransactionCountByHash', + 'eth_getBlockTransactionCountByNumber', + 'eth_getCode', + 'eth_getFilterChanges', + 'eth_getFilterLogs', + 'eth_getLogs', + 'eth_getStorageAt', + 'eth_getTransactionByBlockHashAndIndex', + 'eth_getTransactionByBlockNumberAndIndex', + 'eth_getTransactionByHash', + 'eth_getTransactionCount', + 'eth_getTransactionReceipt', + 'eth_getUncleByBlockHashAndIndex', + 'eth_getUncleByBlockNumberAndIndex', + 'eth_getUncleCountByBlockHash', + 'eth_getUncleCountByBlockNumber', + 'eth_getWork', + 'eth_hashrate', + 'eth_mining', + 'eth_newBlockFilter', + 'eth_newFilter', + 'eth_newPendingTransactionFilter', + 'eth_protocolVersion', + 'eth_sendRawTransaction', + 'eth_sendTransaction', + 'eth_sign', + 'personal_sign', + 'eth_signTypedData', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_submitHashrate', + 'eth_submitWork', + 'eth_syncing', + 'eth_uninstallFilter', + 'metamask_watchAsset', + 'wallet_watchAsset', + 'eth_getEncryptionPublicKey', + 'eth_decrypt', +] diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js new file mode 100644 index 000000000..6448360ec --- /dev/null +++ b/app/scripts/controllers/permissions/index.js @@ -0,0 +1,560 @@ +import JsonRpcEngine from 'json-rpc-engine' +import asMiddleware from 'json-rpc-engine/src/asMiddleware' +import ObservableStore from 'obs-store' +import log from 'loglevel' +import { CapabilitiesController as RpcCap } from 'rpc-cap' +import { ethErrors } from 'eth-json-rpc-errors' +import { cloneDeep } from 'lodash' + +import createMethodMiddleware from './methodMiddleware' +import PermissionsLogController from './permissionsLog' + +// Methods that do not require any permissions to use: +import { + SAFE_METHODS, // methods that do not require any permissions to use + WALLET_PREFIX, + METADATA_STORE_KEY, + LOG_STORE_KEY, + HISTORY_STORE_KEY, + CAVEAT_NAMES, + NOTIFICATION_NAMES, +} from './enums' + +export class PermissionsController { + + constructor ( + { + getKeyringAccounts, + getRestrictedMethods, + getUnlockPromise, + notifyDomain, + notifyAllDomains, + platform, + } = {}, + restoredPermissions = {}, + restoredState = {}) { + + this.store = new ObservableStore({ + [METADATA_STORE_KEY]: restoredState[METADATA_STORE_KEY] || {}, + [LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [], + [HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {}, + }) + + this.getKeyringAccounts = getKeyringAccounts + this.getUnlockPromise = getUnlockPromise + this._notifyDomain = notifyDomain + this.notifyAllDomains = notifyAllDomains + this._platform = platform + + this._restrictedMethods = getRestrictedMethods(this) + this.permissionsLog = new PermissionsLogController({ + restrictedMethods: Object.keys(this._restrictedMethods), + store: this.store, + }) + this.pendingApprovals = new Map() + this.pendingApprovalOrigins = new Set() + this._initializePermissions(restoredPermissions) + } + + createMiddleware ({ origin, extensionId }) { + + if (typeof origin !== 'string' || !origin.length) { + throw new Error('Must provide non-empty string origin.') + } + + if (extensionId) { + this.store.updateState({ + [METADATA_STORE_KEY]: { + ...this.store.getState()[METADATA_STORE_KEY], + [origin]: { extensionId }, + }, + }) + } + + const engine = new JsonRpcEngine() + + engine.push(this.permissionsLog.createMiddleware()) + + engine.push(createMethodMiddleware({ + store: this.store, + storeKey: METADATA_STORE_KEY, + getAccounts: this.getAccounts.bind(this, origin), + getUnlockPromise: this.getUnlockPromise, + hasPermission: this.hasPermission.bind(this, origin), + requestAccountsPermission: this._requestPermissions.bind( + this, origin, { eth_accounts: {} }, + ), + })) + + engine.push(this.permissions.providerMiddlewareFunction.bind( + this.permissions, { origin }, + )) + + return asMiddleware(engine) + } + + /** + * Returns the accounts that should be exposed for the given origin domain, + * if any. This method exists for when a trusted context needs to know + * which accounts are exposed to a given domain. + * + * @param {string} origin - The origin string. + */ + getAccounts (origin) { + // return new Promise((resolve, _) => { + + // const req = { method: 'eth_accounts' } + // const res = {} + // this.permissions.providerMiddlewareFunction( + // { origin }, req, res, () => {}, _end + // ) + + // function _end () { + // if (res.error || !Array.isArray(res.result)) { + // resolve([]) + // } else { + // resolve(res.result) + // } + // } + // }) + return this.getKeyringAccounts() + } + + /** + * Returns whether the given origin has the given permission. + * + * @param {string} origin - The origin to check. + * @param {string} permission - The permission to check for. + * @returns {boolean} Whether the origin has the permission. + */ + hasPermission (origin, permission) { + return Boolean(this.permissions.getPermission(origin, permission)) + } + + /** + * Submits a permissions request to rpc-cap. Internal, background use only. + * + * @param {string} origin - The origin string. + * @param {IRequestedPermissions} permissions - The requested permissions. + */ + _requestPermissions (origin, permissions) { + return new Promise((resolve, reject) => { + + // rpc-cap assigns an id to the request if there is none, as expected by + // requestUserApproval below + const req = { method: 'wallet_requestPermissions', params: [permissions] } + const res = {} + this.permissions.providerMiddlewareFunction( + { origin }, req, res, () => {}, _end, + ) + + function _end (_err) { + const err = _err || res.error + if (err) { + reject(err) + } else { + resolve(res.result) + } + } + }) + } + + /** + * User approval callback. Resolves the Promise for the permissions request + * waited upon by rpc-cap, see requestUserApproval in _initializePermissions. + * The request will be rejected if finalizePermissionsRequest fails. + * + * @param {Object} approved - The request object approved by the user + * @param {Array} accounts - The accounts to expose, if any + */ + async approvePermissionsRequest (approved, accounts) { + + const { id } = approved.metadata + const approval = this.pendingApprovals.get(id) + + if (!approval) { + log.error(`Permissions request with id '${id}' not found`) + return + } + + try { + + if (Object.keys(approved.permissions).length === 0) { + + approval.reject(ethErrors.rpc.invalidRequest({ + message: 'Must request at least one permission.', + })) + + } else { + + // attempt to finalize the request and resolve it, + // settings caveats as necessary + approved.permissions = await this.finalizePermissionsRequest( + approved.permissions, accounts, + ) + approval.resolve(approved.permissions) + } + } catch (err) { + + // if finalization fails, reject the request + approval.reject(ethErrors.rpc.invalidRequest({ + message: err.message, data: err, + })) + } + + this._removePendingApproval(id) + } + + /** + * User rejection callback. Rejects the Promise for the permissions request + * waited upon by rpc-cap, see requestUserApproval in _initializePermissions. + * + * @param {string} id - The id of the request rejected by the user + */ + async rejectPermissionsRequest (id) { + const approval = this.pendingApprovals.get(id) + + if (!approval) { + log.error(`Permissions request with id '${id}' not found`) + return + } + + approval.reject(ethErrors.provider.userRejectedRequest()) + this._removePendingApproval(id) + } + + /** + * @deprecated + * Grants the given origin the eth_accounts permission for the given account(s). + * This method should ONLY be called as a result of direct user action in the UI, + * with the intention of supporting legacy dapps that don't support EIP 1102. + * + * @param {string} origin - The origin to expose the account(s) to. + * @param {Array} accounts - The account(s) to expose. + */ + async legacyExposeAccounts (origin, accounts) { + + // accounts are validated by finalizePermissionsRequest + if (typeof origin !== 'string' || !origin.length) { + throw new Error('Must provide non-empty string origin.') + } + + const existingAccounts = await this.getAccounts(origin) + + if (existingAccounts.length > 0) { + throw new Error( + 'May not call legacyExposeAccounts on origin with exposed accounts.', + ) + } + + const permissions = await this.finalizePermissionsRequest( + { eth_accounts: {} }, accounts, + ) + + try { + + await new Promise((resolve, reject) => { + this.permissions.grantNewPermissions( + origin, permissions, {}, _end, + ) + + function _end (err) { + if (err) { + reject(err) + } else { + resolve() + } + } + }) + + this.notifyDomain(origin, { + method: NOTIFICATION_NAMES.accountsChanged, + result: accounts, + }) + this.permissionsLog.logAccountExposure(origin, accounts) + + } catch (error) { + + throw ethErrors.rpc.internal({ + message: `Failed to add 'eth_accounts' to '${origin}'.`, + data: { + originalError: error, + accounts, + }, + }) + } + } + + /** + * Update the accounts exposed to the given origin. Changes the eth_accounts + * permissions and emits accountsChanged. + * At least one account must be exposed. If no accounts are to be exposed, the + * eth_accounts permissions should be removed completely. + * + * Throws error if the update fails. + * + * @param {string} origin - The origin to change the exposed accounts for. + * @param {string[]} accounts - The new account(s) to expose. + */ + async updatePermittedAccounts (origin, accounts) { + + await this.validatePermittedAccounts(accounts) + + this.permissions.updateCaveatFor( + origin, 'eth_accounts', CAVEAT_NAMES.exposedAccounts, accounts, + ) + + this.notifyDomain(origin, { + method: NOTIFICATION_NAMES.accountsChanged, + result: accounts, + }) + } + + /** + * Finalizes a permissions request. Throws if request validation fails. + * Clones the passed-in parameters to prevent inadvertent modification. + * Sets (adds or replaces) caveats for the following permissions: + * - eth_accounts: the permitted accounts caveat + * + * @param {Object} requestedPermissions - The requested permissions. + * @param {string[]} requestedAccounts - The accounts to expose, if any. + * @returns {Object} The finalized permissions request object. + */ + async finalizePermissionsRequest (requestedPermissions, requestedAccounts) { + + const finalizedPermissions = cloneDeep(requestedPermissions) + const finalizedAccounts = cloneDeep(requestedAccounts) + + const { eth_accounts: ethAccounts } = finalizedPermissions + + if (ethAccounts) { + + await this.validatePermittedAccounts(finalizedAccounts) + + if (!ethAccounts.caveats) { + ethAccounts.caveats = [] + } + + // caveat names are unique, and we will only construct this caveat here + ethAccounts.caveats = ethAccounts.caveats.filter((c) => ( + c.name !== CAVEAT_NAMES.exposedAccounts + )) + + ethAccounts.caveats.push( + { + type: 'filterResponse', + value: finalizedAccounts, + name: CAVEAT_NAMES.exposedAccounts, + }, + ) + } + + return finalizedPermissions + } + + /** + * Validate an array of accounts representing accounts to be exposed + * to a domain. Throws error if validation fails. + * + * @param {string[]} accounts - An array of addresses. + */ + async validatePermittedAccounts (accounts) { + + if (!Array.isArray(accounts) || accounts.length === 0) { + throw new Error('Must provide non-empty array of account(s).') + } + + // assert accounts exist + const allAccounts = await this.getKeyringAccounts() + accounts.forEach((acc) => { + if (!allAccounts.includes(acc)) { + throw new Error(`Unknown account: ${acc}`) + } + }) + } + + notifyDomain (origin, payload) { + + // if the accounts changed from the perspective of the dapp, + // update "last seen" time for the origin and account(s) + // exception: no accounts -> no times to update + if ( + payload.method === NOTIFICATION_NAMES.accountsChanged && + Array.isArray(payload.result) + ) { + this.permissionsLog.updateAccountsHistory( + origin, payload.result, + ) + } + + this._notifyDomain(origin, payload) + + // NOTE: + // we don't check for accounts changing in the notifyAllDomains case, + // because the log only records when accounts were last seen, + // and the accounts only change for all domains at once when permissions + // are removed + } + + /** + * Removes the given permissions for the given domain. + * Should only be called after confirming that the permissions exist, to + * avoid sending unnecessary notifications. + * + * @param {Object} domains { origin: [permissions] } + */ + removePermissionsFor (domains) { + + Object.entries(domains).forEach(([origin, perms]) => { + + this.permissions.removePermissionsFor( + origin, + perms.map((methodName) => { + + if (methodName === 'eth_accounts') { + this.notifyDomain( + origin, + { method: NOTIFICATION_NAMES.accountsChanged, result: [] }, + ) + } + + return { parentCapability: methodName } + }), + ) + }) + } + + /** + * When a new account is selected in the UI for 'origin', emit accountsChanged + * to 'origin' if the selected account is permitted. + * + * Note: This will emit "false positive" accountsChanged events, but they are + * handled by the inpage provider. + * + * @param {string} origin - The origin. + * @param {string} account - The newly selected account's address. + */ + async handleNewAccountSelected (origin, account) { + + const permittedAccounts = await this.getAccounts(origin) + + if ( + typeof origin !== 'string' || !origin.length || + typeof account !== 'string' || !account.length + ) { + throw new Error('Should provide non-empty origin and account strings.') + } + + // do nothing if the account is not permitted for the origin, or + // if it's already first in the array of permitted accounts + if ( + !permittedAccounts.includes(account) || + permittedAccounts[0] === account + ) { + return + } + + const newPermittedAccounts = [account].concat( + permittedAccounts.filter((_account) => _account !== account), + ) + + // update permitted accounts to ensure that accounts are returned + // in the same order every time + await this.updatePermittedAccounts(origin, newPermittedAccounts) + } + + /** + * Removes all known domains and their related permissions. + */ + clearPermissions () { + this.permissions.clearDomains() + this.notifyAllDomains({ + method: NOTIFICATION_NAMES.accountsChanged, + result: [], + }) + } + + /** + * Adds a pending approval. + * @param {string} id - The id of the pending approval. + * @param {string} origin - The origin of the pending approval. + * @param {Function} resolve - The function resolving the pending approval Promise. + * @param {Function} reject - The function rejecting the pending approval Promise. + */ + _addPendingApproval (id, origin, resolve, reject) { + + if ( + this.pendingApprovalOrigins.has(origin) || + this.pendingApprovals.has(id) + ) { + throw new Error( + `Pending approval with id ${id} or origin ${origin} already exists.`, + ) + } + + this.pendingApprovals.set(id, { origin, resolve, reject }) + this.pendingApprovalOrigins.add(origin) + } + + /** + * Removes the pending approval with the given id. + * @param {string} id - The id of the pending approval to remove. + */ + _removePendingApproval (id) { + const { origin } = this.pendingApprovals.get(id) + this.pendingApprovalOrigins.delete(origin) + this.pendingApprovals.delete(id) + } + + /** + * A convenience method for retrieving a login object + * or creating a new one if needed. + * + * @param {string} origin = The origin string representing the domain. + */ + _initializePermissions (restoredState) { + + // these permission requests are almost certainly stale + const initState = { ...restoredState, permissionsRequests: [] } + + this.permissions = new RpcCap({ + + // Supports passthrough methods: + safeMethods: SAFE_METHODS, + + // optional prefix for internal methods + methodPrefix: WALLET_PREFIX, + + restrictedMethods: this._restrictedMethods, + + /** + * A promise-returning callback used to determine whether to approve + * permissions requests or not. + * + * Currently only returns a boolean, but eventually should return any + * specific parameters or amendments to the permissions. + * + * @param {string} req - The internal rpc-cap user request object. + */ + requestUserApproval: async (req) => { + const { origin, metadata: { id } } = req + + if (this.pendingApprovalOrigins.has(origin)) { + throw ethErrors.rpc.resourceUnavailable( + 'Permissions request already pending; please wait.', + ) + } + + this._platform.openExtensionInBrowser(`connect/${id}`) + + return new Promise((resolve, reject) => { + this._addPendingApproval(id, origin, resolve, reject) + }) + }, + }, initState) + } +} + +export function addInternalMethodPrefix (method) { + return WALLET_PREFIX + method +} diff --git a/app/scripts/controllers/permissions/methodMiddleware.js b/app/scripts/controllers/permissions/methodMiddleware.js new file mode 100644 index 000000000..c72a0611e --- /dev/null +++ b/app/scripts/controllers/permissions/methodMiddleware.js @@ -0,0 +1,109 @@ +import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware' +import { ethErrors } from 'eth-json-rpc-errors' + +/** + * Create middleware for handling certain methods and preprocessing permissions requests. + */ +export default function createMethodMiddleware ({ + getAccounts, + getUnlockPromise, + hasPermission, + requestAccountsPermission, + store, + storeKey, +}) { + + const isProcessingRequestAccounts = false + + return createAsyncMiddleware(async (req, res, next) => { + + switch (req.method) { + + // Intercepting eth_accounts requests for backwards compatibility: + // The getAccounts call below wraps the rpc-cap middleware, and returns + // an empty array in case of errors (such as 4100:unauthorized) + case 'eth_accounts': + + res.result = await getAccounts() + return + + case 'eth_requestAccounts': + + if (isProcessingRequestAccounts) { + res.error = ethErrors.rpc.resourceUnavailable( + 'Already processing eth_requestAccounts. Please wait.', + ) + return + } + + // if (hasPermission('eth_accounts')) { + // isProcessingRequestAccounts = true + // await getUnlockPromise() + // isProcessingRequestAccounts = false + // } + + // first, just try to get accounts + // let accounts = await getAccounts() + // if (accounts.length > 0) { + // res.result = accounts + // return + // } + + // if no accounts, request the accounts permission + // try { + // await requestAccountsPermission() + // } catch (err) { + // res.error = err + // return + // } + + // get the accounts again + const accounts = await getAccounts() + /* istanbul ignore else: too hard to induce, see below comment */ + if (accounts.length > 0) { + res.result = accounts + } else { + // this should never happen, because it should be caught in the + // above catch clause + res.error = ethErrors.rpc.internal( + 'Accounts unexpectedly unavailable. Please report this bug.', + ) + } + + return + + // custom method for getting metadata from the requesting domain, + // sent automatically by the inpage provider + case 'wallet_sendDomainMetadata': + + const storeState = store.getState()[storeKey] + const extensionId = storeState[req.origin] + ? storeState[req.origin].extensionId + : undefined + + if ( + req.domainMetadata && + typeof req.domainMetadata.name === 'string' + ) { + + store.updateState({ + [storeKey]: { + ...storeState, + [req.origin]: { + extensionId, + ...req.domainMetadata, + }, + }, + }) + } + + res.result = true + return + + default: + break + } + + next() + }) +} diff --git a/app/scripts/controllers/permissions/permissionsLog.js b/app/scripts/controllers/permissions/permissionsLog.js new file mode 100644 index 000000000..9dbbf70fe --- /dev/null +++ b/app/scripts/controllers/permissions/permissionsLog.js @@ -0,0 +1,410 @@ +import { cloneDeep } from 'lodash' +import { + CAVEAT_NAMES, + HISTORY_STORE_KEY, + LOG_IGNORE_METHODS, + LOG_LIMIT, + LOG_METHOD_TYPES, + LOG_STORE_KEY, + WALLET_PREFIX, +} from './enums' + +/** + * Controller with middleware for logging requests and responses to restricted + * and permissions-related methods. + */ +export default class PermissionsLogController { + + constructor ({ restrictedMethods, store }) { + this.restrictedMethods = restrictedMethods + this.store = store + } + + /** + * Get the activity log. + * + * @returns {Array} The activity log. + */ + getActivityLog () { + return this.store.getState()[LOG_STORE_KEY] || [] + } + + /** + * Update the activity log. + * + * @param {Array} logs - The new activity log array. + */ + updateActivityLog (logs) { + this.store.updateState({ [LOG_STORE_KEY]: logs }) + } + + /** + * Get the permissions history log. + * + * @returns {Object} The permissions history log. + */ + getHistory () { + return this.store.getState()[HISTORY_STORE_KEY] || {} + } + + /** + * Update the permissions history log. + * + * @param {Object} history - The new permissions history log object. + */ + updateHistory (history) { + this.store.updateState({ [HISTORY_STORE_KEY]: history }) + } + + /** + * Updates the exposed account history for the given origin. + * Sets the 'last seen' time to Date.now() for the given accounts. + * + * @param {string} origin - The origin that the accounts are exposed to. + * @param {Array} accounts - The accounts. + */ + updateAccountsHistory (origin, accounts) { + + if (accounts.length === 0) { + return + } + + const accountToTimeMap = getAccountToTimeMap(accounts, Date.now()) + + this.commitNewHistory(origin, { + eth_accounts: { + accounts: accountToTimeMap, + }, + }) + } + + /** + * Create a permissions log middleware. Records permissions activity and history: + * + * Activity: requests and responses for restricted and most wallet_ methods. + * + * History: for each origin, the last time a permission was granted, including + * which accounts were exposed, if any. + * + * @returns {JsonRpcEngineMiddleware} The permissions log middleware. + */ + createMiddleware () { + return (req, res, next, _end) => { + + let activityEntry, requestedMethods + const { origin, method } = req + const isInternal = method.startsWith(WALLET_PREFIX) + + // we only log certain methods + if ( + !LOG_IGNORE_METHODS.includes(method) && + (isInternal || this.restrictedMethods.includes(method)) + ) { + + activityEntry = this.logRequest(req, isInternal) + + if (method === `${WALLET_PREFIX}requestPermissions`) { + // get the corresponding methods from the requested permissions so + // that we can record permissions history + requestedMethods = this.getRequestedMethods(req) + } + } else if (method === 'eth_requestAccounts') { + + // eth_requestAccounts is a special case; we need to extract the accounts + // from it + activityEntry = this.logRequest(req, isInternal) + requestedMethods = [ 'eth_accounts' ] + } else { + // no-op + return next() + } + + // call next with a return handler for capturing the response + next((cb) => { + + const time = Date.now() + this.logResponse(activityEntry, res, time) + + if (requestedMethods && !res.error && res.result) { + // any permissions or accounts changes will be recorded on the response, + // so we only log permissions history here + this.logPermissionsHistory( + requestedMethods, origin, res.result, time, + method === 'eth_requestAccounts', + ) + } + cb() + }) + } + } + + /** + * Creates and commits an activity log entry, without response data. + * + * @param {Object} request - The request object. + * @param {boolean} isInternal - Whether the request is internal. + */ + logRequest (request, isInternal) { + const activityEntry = { + id: request.id, + method: request.method, + methodType: ( + isInternal ? LOG_METHOD_TYPES.internal : LOG_METHOD_TYPES.restricted + ), + origin: request.origin, + request: cloneDeep(request), + requestTime: Date.now(), + response: null, + responseTime: null, + success: null, + } + this.commitNewActivity(activityEntry) + return activityEntry + } + + /** + * Adds response data to an existing activity log entry. + * Entry assumed already committed (i.e., in the log). + * + * @param {Object} entry - The entry to add a response to. + * @param {Object} response - The response object. + * @param {number} time - Output from Date.now() + */ + logResponse (entry, response, time) { + + if (!entry || !response) { + return + } + + entry.response = cloneDeep(response) + entry.responseTime = time + entry.success = !response.error + } + + /** + * Commit a new entry to the activity log. + * Removes the oldest entry from the log if it exceeds the log limit. + * + * @param {Object} entry - The activity log entry. + */ + commitNewActivity (entry) { + + const logs = this.getActivityLog() + + // add new entry to end of log + logs.push(entry) + + // remove oldest log if exceeding size limit + if (logs.length > LOG_LIMIT) { + logs.shift() + } + + this.updateActivityLog(logs) + } + + /** + * Record account exposure and eth_accounts permissions history for the given + * origin. + * + * @param {string} origin - The origin accounts were exposed to. + * @param {Array} accounts - The accounts that were exposed. + */ + logAccountExposure (origin, accounts) { + + if ( + typeof origin !== 'string' || !origin.length || + !Array.isArray(accounts) || accounts.length === 0 + ) { + throw new Error( + 'Must provide non-empty string origin and array of accounts.', + ) + } + + this.logPermissionsHistory( + ['eth_accounts'], + origin, + accounts, + Date.now(), + true, + ) + } + + /** + * Create new permissions history log entries, if any, and commit them. + * + * @param {Array} requestedMethods - The method names corresponding to the requested permissions. + * @param {string} origin - The origin of the permissions request. + * @param {Array} result - The permissions request response.result. + * @param {string} time - The time of the request, i.e. Date.now(). + * @param {boolean} isEthRequestAccounts - Whether the permissions request was 'eth_requestAccounts'. + */ + logPermissionsHistory ( + requestedMethods, origin, result, + time, isEthRequestAccounts, + ) { + + let accounts, newEntries + + if (isEthRequestAccounts) { + + accounts = result + const accountToTimeMap = getAccountToTimeMap(accounts, time) + + newEntries = { + 'eth_accounts': { + accounts: accountToTimeMap, + lastApproved: time, + }, + } + } else { + + // Records new "lastApproved" times for the granted permissions, if any. + // Special handling for eth_accounts, in order to record the time the + // accounts were last seen or approved by the origin. + newEntries = result + .map((perm) => { + + if (perm.parentCapability === 'eth_accounts') { + accounts = this.getAccountsFromPermission(perm) + } + + return perm.parentCapability + }) + .reduce((acc, method) => { + + // all approved permissions will be included in the response, + // not just the newly requested ones + if (requestedMethods.includes(method)) { + + if (method === 'eth_accounts') { + + const accountToTimeMap = getAccountToTimeMap(accounts, time) + + acc[method] = { + lastApproved: time, + accounts: accountToTimeMap, + } + } else { + acc[method] = { lastApproved: time } + } + } + + return acc + }, {}) + } + + if (Object.keys(newEntries).length > 0) { + this.commitNewHistory(origin, newEntries) + } + } + + /** + * Commit new entries to the permissions history log. + * Merges the history for the given origin, overwriting existing entries + * with the same key (permission name). + * + * @param {string} origin - The requesting origin. + * @param {Object} newEntries - The new entries to commit. + */ + commitNewHistory (origin, newEntries) { + + // a simple merge updates most permissions + const history = this.getHistory() + const newOriginHistory = { + ...history[origin], + ...newEntries, + } + + // eth_accounts requires special handling, because of information + // we store about the accounts + const existingEthAccountsEntry = ( + history[origin] && history[origin]['eth_accounts'] + ) + const newEthAccountsEntry = newEntries['eth_accounts'] + + if (existingEthAccountsEntry && newEthAccountsEntry) { + + // we may intend to update just the accounts, not the permission + // itself + const lastApproved = ( + newEthAccountsEntry.lastApproved || + existingEthAccountsEntry.lastApproved + ) + + // merge old and new eth_accounts history entries + newOriginHistory['eth_accounts'] = { + lastApproved, + accounts: { + ...existingEthAccountsEntry.accounts, + ...newEthAccountsEntry.accounts, + }, + } + } + + history[origin] = newOriginHistory + + this.updateHistory(history) + } + + /** + * Get all requested methods from a permissions request. + * + * @param {Object} request - The request object. + * @returns {Array} The names of the requested permissions. + */ + getRequestedMethods (request) { + if ( + !request.params || + !request.params[0] || + typeof request.params[0] !== 'object' || + Array.isArray(request.params[0]) + ) { + return null + } + return Object.keys(request.params[0]) + } + + /** + * Get the permitted accounts from an eth_accounts permissions object. + * Returns an empty array if the permission is not eth_accounts. + * + * @param {Object} perm - The permissions object. + * @returns {Array} The permitted accounts. + */ + getAccountsFromPermission (perm) { + + if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) { + return [] + } + + const accounts = new Set() + for (const caveat of perm.caveats) { + + if ( + caveat.name === CAVEAT_NAMES.exposedAccounts && + Array.isArray(caveat.value) + ) { + + for (const value of caveat.value) { + accounts.add(value) + } + } + } + return [ ...accounts ] + } +} + +// helper functions + +/** + * Get a map from account addresses to the given time. + * + * @param {Array} accounts - An array of addresses. + * @param {number} time - A time, e.g. Date.now(). + * @returns {Object} A string:number map of addresses to time. + */ +function getAccountToTimeMap (accounts, time) { + return accounts.reduce( + (acc, account) => ({ ...acc, [account]: time }), {}, + ) +} diff --git a/app/scripts/controllers/permissions/restrictedMethods.js b/app/scripts/controllers/permissions/restrictedMethods.js new file mode 100644 index 000000000..7d9379250 --- /dev/null +++ b/app/scripts/controllers/permissions/restrictedMethods.js @@ -0,0 +1,21 @@ +export default function getRestrictedMethods (permissionsController) { + return { + + 'eth_accounts': { + description: `View the addresses of the user's chosen accounts.`, + method: (_, res, __, end) => { + permissionsController.getKeyringAccounts() + .then((accounts) => { + res.result = accounts + end() + }) + .catch( + (err) => { + res.error = err + end(err) + }, + ) + }, + }, + } +} diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 1d58e3768..325322c13 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -1,15 +1,21 @@ -const EventEmitter = require('events') -const ObservableStore = require('obs-store') -const ethUtil = require('ethereumjs-util') -const Transaction = require('ethereumjs-tx') -const EthQuery = require('ethjs-query') -const TransactionStateManager = require('./tx-state-manager') +import EventEmitter from 'safe-event-emitter' +import ObservableStore from 'obs-store' +import ethUtil from 'ethereumjs-util' +import Transaction from 'ethereumjs-tx' +import EthQuery from 'ethjs-query' + +import abi from 'human-standard-token-abi' +import abiDecoder from 'abi-decoder' + +abiDecoder.addABI(abi) + +import TransactionStateManager from './tx-state-manager' const TxGasUtil = require('./tx-gas-utils') const PendingTransactionTracker = require('./pending-tx-tracker') const NonceTracker = require('./nonce-tracker') -const txUtils = require('./lib/util') +import * as txUtils from './lib/util' const cleanErrorStack = require('../../lib/cleanErrorStack') -const log = require('loglevel') +import log from 'loglevel' const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker') const { TRANSACTION_TYPE_CANCEL, @@ -222,19 +228,28 @@ class TransactionController extends EventEmitter { @return {txMeta} */ - async retryTransaction (originalTxId) { - const originalTxMeta = this.txStateManager.getTx(originalTxId) - const lastGasPrice = originalTxMeta.txParams.gasPrice - const txMeta = this.txStateManager.generateTxMeta({ - txParams: originalTxMeta.txParams, - lastGasPrice, - loadingDefaults: false, - type: TRANSACTION_TYPE_RETRY, - }) - this.addTx(txMeta) - this.emit('newUnapprovedTx', txMeta) - return txMeta - } + async retryTransaction (originalTxId, gasPrice) { + const originalTxMeta = this.txStateManager.getTx(originalTxId) + const { txParams } = originalTxMeta + const lastGasPrice = gasPrice || originalTxMeta.txParams.gasPrice + const suggestedGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(this.getGasPrice()), 16) + const lastGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(lastGasPrice), 16) + // essentially lastGasPrice * 1.1 but + // dont trust decimals so a round about way of doing that + const lastGasPriceBNBumped = lastGasPriceBN.mul(new ethUtil.BN(110, 10)).div(new ethUtil.BN(100, 10)) + // transactions that are being retried require a >=%10 bump or the clients will throw an error + txParams.gasPrice = suggestedGasPriceBN.gt(lastGasPriceBNBumped) ? `0x${suggestedGasPriceBN.toString(16)}` : `0x${lastGasPriceBNBumped.toString(16)}` + + const txMeta = this.txStateManager.generateTxMeta({ + txParams: originalTxMeta.txParams, + lastGasPrice, + loadingDefaults: false, + type: TRANSACTION_TYPE_RETRY, + }) + this.addTx(txMeta) + this.emit('newUnapprovedTx', txMeta) + return txMeta +} /** * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The diff --git a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 4562568e9..ef085afd9 100644 --- a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -1,7 +1,8 @@ -const jsonDiffer = require('fast-json-patch') -const clone = require('clone') +import jsonDiffer from 'fast-json-patch' +import { cloneDeep } from 'lodash' + /** @module*/ -module.exports = { +export default { generateHistoryEntry, replayHistory, snapshotFromTxMeta, @@ -10,17 +11,19 @@ module.exports = { /** converts non-initial history entries into diffs - @param longHistory {array} + @param {array} longHistory @returns {array} */ function migrateFromSnapshotsToDiffs (longHistory) { return ( longHistory // convert non-initial history entries into diffs - .map((entry, index) => { - if (index === 0) return entry - return generateHistoryEntry(longHistory[index - 1], entry) - }) + .map((entry, index) => { + if (index === 0) { + return entry + } + return generateHistoryEntry(longHistory[index - 1], entry) + }) ) } @@ -31,16 +34,18 @@ function migrateFromSnapshotsToDiffs (longHistory) { path (the key and if a nested object then each key will be seperated with a `/`) value with the first entry having the note and a timestamp when the change took place - @param previousState {object} - the previous state of the object - @param newState {object} - the update object - @param note {string} - a optional note for the state change + @param {Object} previousState - the previous state of the object + @param {Object} newState - the update object + @param {string} [note] - a optional note for the state change @returns {array} */ function generateHistoryEntry (previousState, newState, note) { const entry = jsonDiffer.compare(previousState, newState) // Add a note to the first op, since it breaks if we append it to the entry if (entry[0]) { - if (note) entry[0].note = note + if (note) { + entry[0].note = note + } entry[0].timestamp = Date.now() } @@ -49,20 +54,20 @@ function generateHistoryEntry (previousState, newState, note) { /** Recovers previous txMeta state obj - @returns {object} + @returns {Object} */ function replayHistory (_shortHistory) { - const shortHistory = clone(_shortHistory) + const shortHistory = cloneDeep(_shortHistory) return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) } /** - @param txMeta {Object} - @returns {object} a clone object of the txMeta with out history + @param {Object} txMeta + @returns {Object} - a clone object of the txMeta with out history */ function snapshotFromTxMeta (txMeta) { // create txMeta snapshot for history - const snapshot = clone(txMeta) + const snapshot = cloneDeep(txMeta) // dont include previous history in this snapshot delete snapshot.history return snapshot diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 4d95b2c5b..aa795565d 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,11 +1,11 @@ -const extend = require('xtend') -const EventEmitter = require('events') -const ObservableStore = require('obs-store') -const ethUtil = require('ethereumjs-util') -const log = require('loglevel') -const txStateHistoryHelper = require('./lib/tx-state-history-helper') -const createId = require('../../lib/random-id') -const { getFinalStates } = require('./lib/util') +import ethUtil from 'ethereumjs-util' +import extend from 'extend' +import EventEmitter from 'safe-event-emitter' +import ObservableStore from 'obs-store' +import log from 'loglevel' +import txStateHistoryHelper from './lib/tx-state-history-helper' +import createId from '../../lib/random-id' +import { getFinalStates } from './lib/util' /** TransactionStateManager is responsible for the state of a transaction and storing the transaction diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js index 996c3477c..ddba7cbf5 100644 --- a/app/scripts/lib/createLoggerMiddleware.js +++ b/app/scripts/lib/createLoggerMiddleware.js @@ -1,6 +1,6 @@ -const log = require('loglevel') +import log from 'loglevel' -module.exports = createLoggerMiddleware +export default createLoggerMiddleware /** * Returns a middleware that logs RPC activity @@ -13,7 +13,9 @@ function createLoggerMiddleware (opts) { if (res.error) { log.error('Error in RPC response:\n', res) } - if (req.isMetamaskInternal) return + if (req.isMetamaskInternal) { + return + } log.info(`RPC (${opts.origin}):`, req, '->', res) cb() }) diff --git a/app/scripts/lib/extractEthjsErrorMessage.js b/app/scripts/lib/extractEthjsErrorMessage.js index 4891075c3..d727888ca 100644 --- a/app/scripts/lib/extractEthjsErrorMessage.js +++ b/app/scripts/lib/extractEthjsErrorMessage.js @@ -1,15 +1,15 @@ const ethJsRpcSlug = 'Error: [ethjs-rpc] rpc error with payload ' const errorLabelPrefix = 'Error: ' -module.exports = extractEthjsErrorMessage +export default extractEthjsErrorMessage /** * Extracts the important part of an ethjs-rpc error message. If the passed error is not an isEthjsRpcError, the error * is returned unchanged. * - * @param {string} errorMessage The error message to parse - * @returns {string} Returns an error message, either the same as was passed, or the ending message portion of an isEthjsRpcError + * @param {string} errorMessage - The error message to parse + * @returns {string} - Returns an error message, either the same as was passed, or the ending message portion of an isEthjsRpcError * * @example * // returns 'Transaction Failed: replacement transaction underpriced' diff --git a/app/scripts/lib/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js index 0f1dc19a9..6d1d4855b 100644 --- a/app/scripts/lib/pending-balance-calculator.js +++ b/app/scripts/lib/pending-balance-calculator.js @@ -1,5 +1,5 @@ -const BN = require('ethereumjs-util').BN -const normalize = require('eth-sig-util').normalize +import { BN } from 'ethereumjs-util' +import { normalize } from 'eth-sig-util' class PendingBalanceCalculator { @@ -8,8 +8,8 @@ class PendingBalanceCalculator { * pending transactions. * * @typedef {Object} PendingBalanceCalculator - * @param {Function} getBalance Returns a promise of a BN of the current balance in Wei - * @param {Function} getPendingTransactions Returns an array of TxMeta Objects, which have txParams properties, + * @param {Function} getBalance - Returns a promise of a BN of the current balance in Wei + * @param {Function} getPendingTransactions - Returns an array of TxMeta Objects, which have txParams properties, * which include value, gasPrice, and gas, all in a base=16 hex format. * */ @@ -22,7 +22,7 @@ class PendingBalanceCalculator { * Returns the users "pending balance": their current balance minus the total possible cost of all their * pending transactions. * - * @returns {Promise} Promises a base 16 hex string that contains the user's "pending balance" + * @returns {Promise} - Promises a base 16 hex string that contains the user's "pending balance" * */ async getBalance () { @@ -32,7 +32,9 @@ class PendingBalanceCalculator { ]) const [ balance, pending ] = results - if (!balance) return undefined + if (!balance) { + return undefined + } const pendingValue = pending.reduce((total, tx) => { return total.add(this.calculateMaxCost(tx)) @@ -44,11 +46,11 @@ class PendingBalanceCalculator { /** * Calculates the maximum possible cost of a single transaction, based on the value, gas price and gas limit. * - * @param {object} tx Contains all that data about a transaction. + * @param {Object} tx - Contains all that data about a transaction. * @property {object} tx.txParams Contains data needed to calculate the maximum cost of the transaction: gas, * gasLimit and value. * - * @returns {string} Returns a base 16 hex string that contains the maximum possible cost of the transaction. + * @returns {string} - Returns a base 16 hex string that contains the maximum possible cost of the transaction. */ calculateMaxCost (tx) { const txValue = tx.txParams.value @@ -66,8 +68,8 @@ class PendingBalanceCalculator { /** * Converts a hex string to a BN object * - * @param {string} hex A number represented as a hex string - * @returns {Object} A BN object + * @param {string} hex - A number represented as a hex string + * @returns {Object} - A BN object * */ hexToBn (hex) { @@ -76,4 +78,4 @@ class PendingBalanceCalculator { } -module.exports = PendingBalanceCalculator +export default PendingBalanceCalculator diff --git a/app/scripts/lib/random-id.js b/app/scripts/lib/random-id.js index 788f3370f..2fd50c1ff 100644 --- a/app/scripts/lib/random-id.js +++ b/app/scripts/lib/random-id.js @@ -6,4 +6,4 @@ function createRandomId () { return idCounter++ } -module.exports = createRandomId +export default createRandomId diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js index df5661e59..a7d5b3855 100644 --- a/app/scripts/lib/reportFailedTxToSentry.js +++ b/app/scripts/lib/reportFailedTxToSentry.js @@ -1,4 +1,4 @@ -const extractEthjsErrorMessage = require('./extractEthjsErrorMessage') +import extractEthjsErrorMessage from './extractEthjsErrorMessage' module.exports = reportFailedTxToSentry diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 058ce5541..3c2ed598d 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -1,6 +1,6 @@ const Raven = require('raven-js') const METAMASK_DEBUG = process.env.METAMASK_DEBUG -const extractEthjsErrorMessage = require('./extractEthjsErrorMessage') +import extractEthjsErrorMessage from './extractEthjsErrorMessage' const PROD = 'https://3bd485f8ed6047d882f3f010cbae46ca@sentry.io/1250701' const DEV = 'https://267dbd2f3447444faa637bc34bcc7317@sentry.io/1253429' diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index cd2f3564c..2b8e65076 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -19,7 +19,7 @@ const createEngineStream = require('json-rpc-middleware-stream/engineStream') const createFilterMiddleware = require('eth-json-rpc-filters') const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager') const createOriginMiddleware = require('./lib/createOriginMiddleware') -const createLoggerMiddleware = require('./lib/createLoggerMiddleware') +import createLoggerMiddleware from './lib/createLoggerMiddleware' import createTabIdMiddleware from './lib/createTabIdMiddleware' import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware' const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex @@ -42,6 +42,8 @@ const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') const TokenRatesController = require('./controllers/token-rates') const DetectTokensController = require('./controllers/detect-tokens') +// import { PermissionsController } from './controllers/permissions' +// import getRestrictedMethods from './controllers/permissions/restrictedMethods' const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') import { Mutex } from 'await-semaphore' @@ -206,6 +208,14 @@ module.exports = class MetamaskController extends EventEmitter { this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s)) this.keyringController.on('unlock', () => this.emit('unlock')) + // this.permissionsController = new PermissionsController({ + // getKeyringAccounts: this.keyringController.getAccounts.bind(this.keyringController), + // getRestrictedMethods, + // notifyDomain: this.notifyConnections.bind(this), + // notifyAllDomains: this.notifyAllConnections.bind(this), + // platform: opts.platform, + // }, initState.PermissionsController, initState.PermissionsMetadata) + // detect tokens controller this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, @@ -285,6 +295,8 @@ module.exports = class MetamaskController extends EventEmitter { NetworkController: this.networkController.store, InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, + // PermissionsController: this.permissionsController.permissions, + // PermissionsMetadata: this.permissionsController.store, }) this.memStore = new ComposableObservableStore(null, { @@ -307,6 +319,8 @@ module.exports = class MetamaskController extends EventEmitter { NoticeController: this.noticeController.memStore, ShapeshiftController: this.shapeshiftController.store, InfuraController: this.infuraController.store, + // PermissionsController: this.permissionsController.permissions, + // PermissionsMetadata: this.permissionsController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -341,6 +355,7 @@ module.exports = class MetamaskController extends EventEmitter { processDecryptMessage: this.newRequestDecryptMessage.bind(this), processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), getPendingNonce: this.getPendingNonce.bind(this), + getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0], } const providerProxy = this.networkController.initializeProvider(providerOpts) return providerProxy @@ -414,6 +429,7 @@ module.exports = class MetamaskController extends EventEmitter { const txController = this.txController const noticeController = this.noticeController const addressBookController = this.addressBookController + // const permissionsController = this.permissionsController return { // etc @@ -514,6 +530,16 @@ module.exports = class MetamaskController extends EventEmitter { // notices checkNotices: noticeController.updateNoticesList.bind(noticeController), markNoticeRead: noticeController.markNoticeRead.bind(noticeController), + + // // permissions + // approvePermissionsRequest: nodeify(permissionsController.approvePermissionsRequest, permissionsController), + // clearPermissions: permissionsController.clearPermissions.bind(permissionsController), + // getApprovedAccounts: nodeify(permissionsController.getAccounts.bind(permissionsController)), + // rejectPermissionsRequest: nodeify(permissionsController.rejectPermissionsRequest, permissionsController), + // removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController), + // updatePermittedAccounts: nodeify(permissionsController.updatePermittedAccounts, permissionsController), + // legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), + // handleNewAccountSelected: nodeify(this.handleNewAccountSelected, this), } } @@ -1710,6 +1736,8 @@ cancelEncryptionPublicKey (msgId, cb) { // filter and subscription polyfills engine.push(filterMiddleware) engine.push(subscriptionManager.middleware) + // permissions + // engine.push(this.permissionsController.createMiddleware({ origin, extensionId })) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) // sign typed data middleware diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js index ffbf24a4b..a22399695 100644 --- a/app/scripts/migrations/018.js +++ b/app/scripts/migrations/018.js @@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style */ const clone = require('clone') -const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper') +import txStateHistoryHelper from '../controllers/transactions/lib/tx-state-history-helper' module.exports = { diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index c7258117f..c2ebc3bf3 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -157,7 +157,7 @@ class ExtensionPlatform { } _subscribeToNotificationClicked () { - if (extension.notifications.onClicked.hasListener(this._viewOnExplorer)) { + if (!extension.notifications.onClicked.hasListener(this._viewOnExplorer)) { extension.notifications.onClicked.removeListener(this._viewOnExplorer) } extension.notifications.onClicked.addListener(this._viewOnExplorer) diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 4b72a3b2b..5f6c7708e 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -4,7 +4,7 @@ const startPopup = require('./popup-core') const PortStream = require('extension-port-stream') const { getEnvironmentType } = require('./lib/util') const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums') -const extension = require('extensionizer') +import extension from 'extensionizer' const ExtensionPlatform = require('./platforms/extension') const NotificationManager = require('./lib/notification-manager') const notificationManager = new NotificationManager() diff --git a/test/lib/createTxMeta.js b/test/lib/createTxMeta.js index 0e88e3cfb..ca318e6b1 100644 --- a/test/lib/createTxMeta.js +++ b/test/lib/createTxMeta.js @@ -1,4 +1,4 @@ -const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') +import txStateHistoryHelper from '../../app/scripts/controllers/transactions/lib/tx-state-history-helper' module.exports = createTxMeta diff --git a/test/stub/provider.js b/test/stub/provider.js index a1c70486d..d0aade056 100644 --- a/test/stub/provider.js +++ b/test/stub/provider.js @@ -1,4 +1,4 @@ -const JsonRpcEngine = require('json-rpc-engine') +import JsonRpcEngine from 'json-rpc-engine' const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold') const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware') const GanacheCore = require('ganache-core') diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 48edbeb3c..13e6546f8 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -68,6 +68,7 @@ describe('MetaMaskController', function () { }, }, initState: cloneDeep(firstTimeState), + platform: { showTransactionNotification: () => {} }, }) // disable diagnostics metamaskController.diagnostics = null diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index d32ec2d44..90197a21f 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -28,6 +28,9 @@ describe('Transaction Controller', function () { blockTrackerStub.getLatestBlock = noop txController = new TransactionController({ provider, + getGasPrice: function () { + return '0xee6b2800' + }, networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: blockTrackerStub, @@ -414,26 +417,28 @@ describe('Transaction Controller', function () { }) describe('#retryTransaction', function () { - it('should create a new txMeta with the same txParams as the original one', function (done) { + it('should create a new txMeta with the same txParams as the original one but with a higher gasPrice', function (done) { const txParams = { + gasPrice: '0xee6b2800', nonce: '0x00', from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', data: '0x0', } txController.txStateManager._saveTxList([ - { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [] }, + { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [{}] }, ]) txController.retryTransaction(1) - .then((txMeta) => { - assert.equal(txMeta.txParams.nonce, txParams.nonce, 'nonce should be the same') - assert.equal(txMeta.txParams.from, txParams.from, 'from should be the same') - assert.equal(txMeta.txParams.to, txParams.to, 'to should be the same') - assert.equal(txMeta.txParams.data, txParams.data, 'data should be the same') - assert.ok(('lastGasPrice' in txMeta), 'should have the key `lastGasPrice`') - assert.equal(txController.txStateManager.getTxList().length, 2) - done() - }).catch(done) + .then((txMeta) => { + assert.equal(txMeta.txParams.gasPrice, '0x10642ac00', 'gasPrice should have a %10 gasPrice bump') + assert.equal(txMeta.txParams.nonce, txParams.nonce, 'nonce should be the same') + assert.equal(txMeta.txParams.from, txParams.from, 'from should be the same') + assert.equal(txMeta.txParams.to, txParams.to, 'to should be the same') + assert.equal(txMeta.txParams.data, txParams.data, 'data should be the same') + assert.ok(('lastGasPrice' in txMeta), 'should have the key `lastGasPrice`') + assert.equal(txController.txStateManager.getTxList().length, 2) + done() + }).catch(done) }) }) diff --git a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js index fba0e7fda..e02547fad 100644 --- a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js +++ b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js @@ -1,5 +1,5 @@ const assert = require('assert') -const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') +import txStateHistoryHelper from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper' const testVault = require('../../../../data/v17-long-history.json') describe('Transaction state history helper', function () { diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js index 88bdaa60e..a593c2d06 100644 --- a/test/unit/app/controllers/transactions/tx-state-manager-test.js +++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js @@ -1,6 +1,6 @@ const assert = require('assert') const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager') -const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') +import txStateHistoryHelper from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper' const noop = () => true describe('TransactionStateManager', function () { diff --git a/test/unit/app/controllers/transactions/tx-utils-test.js b/test/unit/app/controllers/transactions/tx-utils-test.js index 029fab4d5..2b87ea93c 100644 --- a/test/unit/app/controllers/transactions/tx-utils-test.js +++ b/test/unit/app/controllers/transactions/tx-utils-test.js @@ -1,11 +1,11 @@ -const assert = require('assert') -const txUtils = require('../../../../../app/scripts/controllers/transactions/lib/util') +import assert from 'assert' +import * as txUtils from '../../../../../app/scripts/controllers/transactions/lib/util' describe('txUtils', function () { describe('#validateTxParams', function () { it('does not throw for positive values', function () { - var sample = { + const sample = { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', value: '0x01', } @@ -13,7 +13,7 @@ describe('txUtils', function () { }) it('returns error for negative values', function () { - var sample = { + const sample = { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', value: '-0x01', } @@ -25,8 +25,8 @@ describe('txUtils', function () { }) }) - describe('#normalizeTxParams', () => { - it('should normalize txParams', () => { + describe('#normalizeTxParams', function () { + it('should normalize txParams', function () { const txParams = { chainId: '0x1', from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402', @@ -50,7 +50,7 @@ describe('txUtils', function () { }) }) - describe('#validateRecipient', () => { + describe('#validateRecipient', function () { it('removes recipient for txParams with 0x when contract data is provided', function () { const zeroRecipientandDataTxParams = { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', @@ -66,33 +66,43 @@ describe('txUtils', function () { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', to: '0x', } - assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address') + assert.throws(() => { + txUtils.validateRecipient(zeroRecipientTxParams) + }, Error, 'Invalid recipient address') }) }) - describe('#validateFrom', () => { + describe('#validateFrom', function () { it('should error when from is not a hex string', function () { // where from is undefined const txParams = {} - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address ${txParams.from} not a string`) // where from is array txParams.from = [] - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address ${txParams.from} not a string`) // where from is a object txParams.from = {} - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address ${txParams.from} not a string`) // where from is a invalid address txParams.from = 'im going to fail' - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address`) // should run txParams.from = '0x1678a085c290ebd122dc42cba69373b5953b831d' txUtils.validateFrom(txParams) - }) + }) }) }) diff --git a/test/unit/app/pending-balance-test.js b/test/unit/app/pending-balance-test.js index 508635c46..0b2e17aaf 100644 --- a/test/unit/app/pending-balance-test.js +++ b/test/unit/app/pending-balance-test.js @@ -1,5 +1,5 @@ const assert = require('assert') -const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator') +import PendingBalanceCalculator from '../../../app/scripts/lib/pending-balance-calculator' const MockTxGen = require('../../lib/mock-tx-gen') const BN = require('ethereumjs-util').BN diff --git a/ui/app/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js new file mode 100644 index 000000000..e7e4c7fb2 --- /dev/null +++ b/ui/app/helpers/constants/transactions.js @@ -0,0 +1,27 @@ +export const UNAPPROVED_STATUS = 'unapproved' +export const REJECTED_STATUS = 'rejected' +export const APPROVED_STATUS = 'approved' +export const SIGNED_STATUS = 'signed' +export const SUBMITTED_STATUS = 'submitted' +export const CONFIRMED_STATUS = 'confirmed' +export const FAILED_STATUS = 'failed' +export const DROPPED_STATUS = 'dropped' +export const CANCELLED_STATUS = 'cancelled' + +export const TOKEN_METHOD_TRANSFER = 'transfer' +export const TOKEN_METHOD_APPROVE = 'approve' +export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom' + +export const SEND_ETHER_ACTION_KEY = 'sentEther' +export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment' +export const APPROVE_ACTION_KEY = 'approve' +export const SEND_TOKEN_ACTION_KEY = 'sentTokens' +export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' +export const SIGNATURE_REQUEST_KEY = 'signatureRequest' +export const DECRYPT_REQUEST_KEY = 'decryptRequest' +export const ENCRYPTION_PUBLIC_KEY_REQUEST_KEY = 'encryptionPublicKeyRequest' +export const CONTRACT_INTERACTION_KEY = 'contractInteraction' +export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt' +export const DEPOSIT_TRANSACTION_KEY = 'deposit' + +export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift' diff --git a/ui/app/select-app.js b/ui/app/select-app.js index 47d0dd75f..ddf4461e7 100644 --- a/ui/app/select-app.js +++ b/ui/app/select-app.js @@ -4,13 +4,11 @@ const connect = require('react-redux').connect const h = require('react-hyperscript') const { HashRouter } = require('react-router-dom') const OldApp = require('../../old-ui/app/app') -const { autoAddToBetaUI } = require('./selectors') const { setFeatureFlag } = require('./actions') const I18nProvider = require('./i18n-provider') function mapStateToProps (state) { return { - autoAdd: autoAddToBetaUI(state), isUnlocked: state.metamask.isUnlocked, isMascara: state.metamask.isMascara, firstTime: Object.keys(state.metamask.identities).length === 0, diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 1eec98a20..fb185b8bf 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -27,7 +27,6 @@ const selectors = { getSendAmount, getSelectedTokenToFiatRate, getSelectedTokenContract, - autoAddToBetaUI, getSendMaxModeState, getCurrentViewContext, getTotalUnapprovedCount, @@ -184,23 +183,6 @@ function getSelectedTokenContract (state) { : null } -function autoAddToBetaUI (state) { - const autoAddTransactionThreshold = 12 - const autoAddAccountsThreshold = 2 - const autoAddTokensThreshold = 1 - - const numberOfTransactions = state.metamask.selectedAddressTxList.length - const numberOfAccounts = Object.keys(getMetaMaskAccounts(state)).length - const numberOfTokensAdded = state.metamask.tokens.length - - const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && - (numberOfAccounts > autoAddAccountsThreshold) && - (numberOfTokensAdded > autoAddTokensThreshold) - const userIsNotInBeta = !state.metamask.featureFlags.betaUI - - return userIsNotInBeta && userPassesThreshold -} - function getCurrentViewContext (state) { const { currentView = {} } = state.appState return currentView.context