Merge pull request #338 from poanetwork/vb-eip-1193-final

Refactoring #2 before EIP - 1193
This commit is contained in:
Victor Baranov 2020-03-27 21:46:41 +03:00 committed by GitHub
commit f9a91621b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1538 additions and 198 deletions

View File

@ -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<void>} Promises undefined
* @returns {Promise<void>} - 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<BN|void>} Promises a BN with a value equal to the balance of the current account, or undefined
* @returns {Promise<BN|void>} - 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<array>} Promises an array of transaction objects.
* @returns {Promise<array>} - 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
*

View File

@ -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,
})
}

View File

@ -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 })

View File

@ -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))
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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()
}
}

View File

@ -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,
}

View File

@ -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',
]

View File

@ -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<string>} 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
}

View File

@ -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()
})
}

View File

@ -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<Object>} The activity log.
*/
getActivityLog () {
return this.store.getState()[LOG_STORE_KEY] || []
}
/**
* Update the activity log.
*
* @param {Array<Object>} 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<string>} 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<string>} 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<string>} requestedMethods - The method names corresponding to the requested permissions.
* @param {string} origin - The origin of the permissions request.
* @param {Array<IOcapLdCapability>} 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<string>} 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<string>} 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<string>} 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 }), {},
)
}

View File

@ -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)
},
)
},
},
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
})

View File

@ -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'

View File

@ -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<string>} Promises a base 16 hex string that contains the user's "pending balance"
* @returns {Promise<string>} - 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

View File

@ -6,4 +6,4 @@ function createRandomId () {
return idCounter++
}
module.exports = createRandomId
export default createRandomId

View File

@ -1,4 +1,4 @@
const extractEthjsErrorMessage = require('./extractEthjsErrorMessage')
import extractEthjsErrorMessage from './extractEthjsErrorMessage'
module.exports = reportFailedTxToSentry

View File

@ -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'

View File

@ -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

View File

@ -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 = {

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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')

View File

@ -68,6 +68,7 @@ describe('MetaMaskController', function () {
},
},
initState: cloneDeep(firstTimeState),
platform: { showTransactionNotification: () => {} },
})
// disable diagnostics
metamaskController.diagnostics = null

View File

@ -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)
})
})

View File

@ -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 () {

View File

@ -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 () {

View File

@ -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)
})
})
})
})

View File

@ -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

View File

@ -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'

View File

@ -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,

View File

@ -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