1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

@metamask/inpage-provider@^8.0.0 (#8640)

* @metamask/inpage-provider@^8.0.0
* Replace public config store with JSON-RPC notifications
* Encapsulate notification permissioning in permissions controller
* Update prefix of certain internal RPC methods and notifications
* Add accounts to getProviderState
* Send accounts with isUnlocked notification (#10007)
* Rename provider streams, notify provider of stream failures (#10006)
This commit is contained in:
Erik Marks 2020-12-08 11:48:47 -08:00 committed by GitHub
parent 55e5f5513c
commit 3bf94164ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 300 additions and 197 deletions

View File

@ -16,16 +16,13 @@ const inpageContent = fs.readFileSync(
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n` const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
const inpageBundle = inpageContent + inpageSuffix const inpageBundle = inpageContent + inpageSuffix
// Eventually this streaming injection could be replaced with: const CONTENT_SCRIPT = 'metamask-contentscript'
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction const INPAGE = 'metamask-inpage'
// const PROVIDER = 'metamask-provider'
// But for now that is only Firefox
// If we create a FireFox-only code path using that API,
// MetaMask will be much faster loading and performant on Firefox.
if (shouldInjectProvider()) { if (shouldInjectProvider()) {
injectScript(inpageBundle) injectScript(inpageBundle)
start() setupStreams()
} }
/** /**
@ -46,15 +43,6 @@ function injectScript(content) {
} }
} }
/**
* Sets up the stream communication and submits site metadata
*
*/
async function start() {
await setupStreams()
await domIsReady()
}
/** /**
* Sets up two-way communication streams between the * Sets up two-way communication streams between the
* browser extension and local per-page browser context. * browser extension and local per-page browser context.
@ -63,10 +51,10 @@ async function start() {
async function setupStreams() { async function setupStreams() {
// the transport-specific streams for communication between inpage and background // the transport-specific streams for communication between inpage and background
const pageStream = new LocalMessageDuplexStream({ const pageStream = new LocalMessageDuplexStream({
name: 'contentscript', name: CONTENT_SCRIPT,
target: 'inpage', target: INPAGE,
}) })
const extensionPort = extension.runtime.connect({ name: 'contentscript' }) const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT })
const extensionStream = new PortStream(extensionPort) const extensionStream = new PortStream(extensionPort)
// create and connect channel muxers // create and connect channel muxers
@ -79,26 +67,26 @@ async function setupStreams() {
pump(pageMux, pageStream, pageMux, (err) => pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err), logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
) )
pump(extensionMux, extensionStream, extensionMux, (err) => pump(extensionMux, extensionStream, extensionMux, (err) => {
logStreamDisconnectWarning('MetaMask Background Multiplex', err), logStreamDisconnectWarning('MetaMask Background Multiplex', err)
) notifyInpageOfStreamFailure()
})
// forward communication across inpage-background for these channels only // forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxers('provider', pageMux, extensionMux) forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux)
forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux)
// connect "phishing" channel to warning system // connect "phishing" channel to warning system
const phishingStream = extensionMux.createStream('phishing') const phishingStream = extensionMux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning) phishingStream.once('data', redirectToPhishingWarning)
} }
function forwardTrafficBetweenMuxers(channelName, muxA, muxB) { function forwardTrafficBetweenMuxes(channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName) const channelA = muxA.createStream(channelName)
const channelB = muxB.createStream(channelName) const channelB = muxB.createStream(channelName)
pump(channelA, channelB, channelA, (err) => pump(channelA, channelB, channelA, (error) =>
logStreamDisconnectWarning( console.debug(
`MetaMask muxed traffic for channel "${channelName}" failed.`, `MetaMask muxed traffic for channel "${channelName}" failed.`,
err, error,
), ),
) )
} }
@ -107,14 +95,35 @@ function forwardTrafficBetweenMuxers(channelName, muxA, muxB) {
* Error handler for page to extension stream disconnections * Error handler for page to extension stream disconnections
* *
* @param {string} remoteLabel - Remote stream name * @param {string} remoteLabel - Remote stream name
* @param {Error} err - Stream connection error * @param {Error} error - Stream connection error
*/ */
function logStreamDisconnectWarning(remoteLabel, err) { function logStreamDisconnectWarning(remoteLabel, error) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}` console.debug(
if (err) { `MetaMask Contentscript: Lost connection to "${remoteLabel}".`,
warningMsg += `\n${err.stack}` error,
} )
console.warn(warningMsg) }
/**
* This function must ONLY be called in pump destruction/close callbacks.
* Notifies the inpage context that streams have failed, via window.postMessage.
* Relies on obj-multiplex and post-message-stream implementation details.
*/
function notifyInpageOfStreamFailure() {
window.postMessage(
{
target: INPAGE, // the post-message-stream "target"
data: {
// this object gets passed to obj-multiplex
name: PROVIDER, // the obj-multiplex channel name
data: {
jsonrpc: '2.0',
method: 'METAMASK_STREAM_FAILURE',
},
},
},
window.location.origin,
)
} }
/** /**
@ -221,17 +230,3 @@ function redirectToPhishingWarning() {
href: window.location.href, href: window.location.href,
})}` })}`
} }
/**
* Returns a promise that resolves when the DOM is loaded (does not wait for images to load)
*/
async function domIsReady() {
// already loaded
if (['interactive', 'complete'].includes(document.readyState)) {
return undefined
}
// wait for load
return new Promise((resolve) =>
window.addEventListener('DOMContentLoaded', resolve, { once: true }),
)
}

View File

@ -19,10 +19,15 @@ export const CAVEAT_TYPES = {
} }
export const NOTIFICATION_NAMES = { export const NOTIFICATION_NAMES = {
accountsChanged: 'wallet_accountsChanged', accountsChanged: 'metamask_accountsChanged',
unlockStateChanged: 'metamask_unlockStateChanged',
chainChanged: 'metamask_chainChanged',
} }
export const LOG_IGNORE_METHODS = ['wallet_sendDomainMetadata'] export const LOG_IGNORE_METHODS = [
'wallet_registerOnboarding',
'wallet_watchAsset',
]
export const LOG_METHOD_TYPES = { export const LOG_METHOD_TYPES = {
restricted: 'restricted', restricted: 'restricted',
@ -82,8 +87,9 @@ export const SAFE_METHODS = [
'eth_submitWork', 'eth_submitWork',
'eth_syncing', 'eth_syncing',
'eth_uninstallFilter', 'eth_uninstallFilter',
'metamask_watchAsset',
'wallet_watchAsset',
'eth_getEncryptionPublicKey', 'eth_getEncryptionPublicKey',
'eth_decrypt', 'eth_decrypt',
'metamask_watchAsset',
'wallet_watchAsset',
'metamask_getProviderState',
] ]

View File

@ -28,6 +28,7 @@ export class PermissionsController {
getKeyringAccounts, getKeyringAccounts,
getRestrictedMethods, getRestrictedMethods,
getUnlockPromise, getUnlockPromise,
isUnlocked,
notifyDomain, notifyDomain,
notifyAllDomains, notifyAllDomains,
preferences, preferences,
@ -47,6 +48,7 @@ export class PermissionsController {
this._notifyDomain = notifyDomain this._notifyDomain = notifyDomain
this._notifyAllDomains = notifyAllDomains this._notifyAllDomains = notifyAllDomains
this._showPermissionRequest = showPermissionRequest this._showPermissionRequest = showPermissionRequest
this._isUnlocked = isUnlocked
this._restrictedMethods = getRestrictedMethods({ this._restrictedMethods = getRestrictedMethods({
getKeyringAccounts: this.getKeyringAccounts.bind(this), getKeyringAccounts: this.getKeyringAccounts.bind(this),
@ -463,21 +465,20 @@ export class PermissionsController {
throw new Error('Invalid accounts', newAccounts) throw new Error('Invalid accounts', newAccounts)
} }
this._notifyDomain(origin, { // We do not share accounts when the extension is locked.
method: NOTIFICATION_NAMES.accountsChanged, if (this._isUnlocked()) {
result: newAccounts, this._notifyDomain(origin, {
}) method: NOTIFICATION_NAMES.accountsChanged,
params: newAccounts,
// if the accounts changed from the perspective of the dapp, })
// update "last seen" time for the origin and account(s) this.permissionsLog.updateAccountsHistory(origin, newAccounts)
// exception: no accounts -> no times to update }
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
// NOTE: // NOTE:
// we don't check for accounts changing in the notifyAllDomains case, // We don't check for accounts changing in the notifyAllDomains case,
// because the log only records when accounts were last seen, // because the log only records when accounts were last seen, and the
// and the accounts only change for all domains at once when permissions // the accounts only change for all domains at once when permissions are
// are removed // removed.
} }
/** /**
@ -508,9 +509,11 @@ export class PermissionsController {
*/ */
clearPermissions() { clearPermissions() {
this.permissions.clearDomains() this.permissions.clearDomains()
// It's safe to notify that no accounts are available, regardless of
// extension lock state
this._notifyAllDomains({ this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged, method: NOTIFICATION_NAMES.accountsChanged,
result: [], params: [],
}) })
} }
@ -749,7 +752,3 @@ export class PermissionsController {
) )
} }
} }
export function addInternalMethodPrefix(method) {
return WALLET_PREFIX + method
}

View File

@ -58,6 +58,7 @@ export default class PermissionsLogController {
/** /**
* Updates the exposed account history for the given origin. * Updates the exposed account history for the given origin.
* Sets the 'last seen' time to Date.now() for the given accounts. * Sets the 'last seen' time to Date.now() for the given accounts.
* Returns if the accounts array is empty.
* *
* @param {string} origin - The origin that the accounts are exposed to. * @param {string} origin - The origin that the accounts are exposed to.
* @param {Array<string>} accounts - The accounts. * @param {Array<string>} accounts - The accounts.

View File

@ -73,7 +73,7 @@ export default function createPermissionsMethodMiddleware({
// custom method for getting metadata from the requesting domain, // custom method for getting metadata from the requesting domain,
// sent automatically by the inpage provider when it's initialized // sent automatically by the inpage provider when it's initialized
case 'wallet_sendDomainMetadata': { case 'metamask_sendDomainMetadata': {
if (typeof req.domainMetadata?.name === 'string') { if (typeof req.domainMetadata?.name === 'string') {
addDomainMetadata(req.origin, req.domainMetadata) addDomainMetadata(req.origin, req.domainMetadata)
} }

View File

@ -33,7 +33,7 @@ cleanContextForImports()
/* eslint-disable import/first */ /* eslint-disable import/first */
import log from 'loglevel' import log from 'loglevel'
import LocalMessageDuplexStream from 'post-message-stream' import LocalMessageDuplexStream from 'post-message-stream'
import { initProvider } from '@metamask/inpage-provider' import { initializeProvider } from '@metamask/inpage-provider'
restoreContextAfterImports() restoreContextAfterImports()
@ -45,10 +45,12 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
// setup background connection // setup background connection
const metamaskStream = new LocalMessageDuplexStream({ const metamaskStream = new LocalMessageDuplexStream({
name: 'inpage', name: 'metamask-inpage',
target: 'contentscript', target: 'metamask-contentscript',
}) })
initProvider({ initializeProvider({
connectionStream: metamaskStream, connectionStream: metamaskStream,
logger: log,
shouldShimWeb3: true,
}) })

View File

@ -23,6 +23,7 @@ const MESSAGE_TYPE = {
ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey', ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey',
ETH_SIGN: 'eth_sign', ETH_SIGN: 'eth_sign',
ETH_SIGN_TYPED_DATA: 'eth_signTypedData', ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
GET_PROVIDER_STATE: 'metamask_getProviderState',
LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage', LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage',
PERSONAL_SIGN: 'personal_sign', PERSONAL_SIGN: 'personal_sign',
WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET: 'wallet_watchAsset',

View File

@ -0,0 +1,46 @@
import { MESSAGE_TYPE } from '../../enums'
/**
* This RPC method gets background state relevant to the provider.
* The background sends RPC notifications on state changes, but the provider
* first requests state on initialization.
*/
const getProviderState = {
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
implementation: getProviderStateHandler,
}
export default getProviderState
/**
* @typedef {Object} ProviderStateHandlerResult
* @property {string} chainId - The current chain ID.
* @property {boolean} isUnlocked - Whether the extension is unlocked or not.
* @property {string} networkVersion - The current network ID.
*/
/**
* @typedef {Object} ProviderStateHandlerOptions
* @property {() => ProviderStateHandlerResult} getProviderState - A function that
* gets the current provider state.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<[]>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<ProviderStateHandlerResult>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {ProviderStateHandlerOptions} options
*/
async function getProviderStateHandler(
req,
res,
_next,
end,
{ getProviderState: _getProviderState },
) {
res.result = {
...(await _getProviderState(req.origin)),
}
return end()
}

View File

@ -1,5 +1,6 @@
import getProviderState from './get-provider-state'
import logWeb3ShimUsage from './log-web3-shim-usage' import logWeb3ShimUsage from './log-web3-shim-usage'
import watchAsset from './watch-asset' import watchAsset from './watch-asset'
const handlers = [logWeb3ShimUsage, watchAsset] const handlers = [getProviderState, logWeb3ShimUsage, watchAsset]
export default handlers export default handlers

View File

@ -1,9 +1,6 @@
import EventEmitter from 'events' import EventEmitter from 'events'
import pump from 'pump' import pump from 'pump'
import Dnode from 'dnode' import Dnode from 'dnode'
import ObservableStore from 'obs-store'
import asStream from 'obs-store/lib/asStream'
import { JsonRpcEngine } from 'json-rpc-engine' import { JsonRpcEngine } from 'json-rpc-engine'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import createEngineStream from 'json-rpc-middleware-stream/engineStream' import createEngineStream from 'json-rpc-middleware-stream/engineStream'
@ -53,6 +50,7 @@ import TokenRatesController from './controllers/token-rates'
import DetectTokensController from './controllers/detect-tokens' import DetectTokensController from './controllers/detect-tokens'
import SwapsController from './controllers/swaps' import SwapsController from './controllers/swaps'
import { PermissionsController } from './controllers/permissions' import { PermissionsController } from './controllers/permissions'
import { NOTIFICATION_NAMES } from './controllers/permissions/enums'
import getRestrictedMethods from './controllers/permissions/restrictedMethods' import getRestrictedMethods from './controllers/permissions/restrictedMethods'
import nodeify from './lib/nodeify' import nodeify from './lib/nodeify'
import accountImporter from './account-import-strategies' import accountImporter from './account-import-strategies'
@ -219,10 +217,11 @@ export default class MetamaskController extends EventEmitter {
initState: initState.KeyringController, initState: initState.KeyringController,
encryptor: opts.encryptor || undefined, encryptor: opts.encryptor || undefined,
}) })
this.keyringController.memStore.subscribe((s) => this.keyringController.memStore.subscribe((state) =>
this._onKeyringControllerUpdate(s), this._onKeyringControllerUpdate(state),
) )
this.keyringController.on('unlock', () => this.emit('unlock')) this.keyringController.on('unlock', () => this.emit('unlock'))
this.keyringController.on('lock', () => this._onLock())
this.permissionsController = new PermissionsController( this.permissionsController = new PermissionsController(
{ {
@ -233,6 +232,7 @@ export default class MetamaskController extends EventEmitter {
getUnlockPromise: this.appStateController.getUnlockPromise.bind( getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController, this.appStateController,
), ),
isUnlocked: this.isUnlocked.bind(this),
notifyDomain: this.notifyConnections.bind(this), notifyDomain: this.notifyConnections.bind(this),
notifyAllDomains: this.notifyAllConnections.bind(this), notifyAllDomains: this.notifyAllConnections.bind(this),
preferences: this.preferencesController.store, preferences: this.preferencesController.store,
@ -348,6 +348,9 @@ export default class MetamaskController extends EventEmitter {
tokenRatesStore: this.tokenRatesController.store, tokenRatesStore: this.tokenRatesController.store,
}) })
// ensure isClientOpenAndUnlocked is updated when memState updates
this.on('update', (memState) => this._onStateUpdate(memState))
this.store.updateStructure({ this.store.updateStructure({
AppStateController: this.appStateController.store, AppStateController: this.appStateController.store,
TransactionController: this.txController.store, TransactionController: this.txController.store,
@ -450,38 +453,37 @@ export default class MetamaskController extends EventEmitter {
} }
/** /**
* Constructor helper: initialize a public config store. * Gets relevant state for the provider of an external origin.
* This store is used to make some config info available to Dapps synchronously. *
* @param {string} origin - The origin to get the provider state for.
* @returns {Promise<{
* isUnlocked: boolean,
* networkVersion: string,
* chainId: string,
* accounts: string[],
* }>} An object with relevant state properties.
*/ */
createPublicConfigStore() { async getProviderState(origin) {
// subset of state for metamask inpage provider return {
const publicConfigStore = new ObservableStore() isUnlocked: this.isUnlocked(),
const { networkController } = this ...this.getProviderNetworkState(),
accounts: await this.permissionsController.getAccounts(origin),
// setup memStore subscription hooks
this.on('update', updatePublicConfigStore)
updatePublicConfigStore(this.getState())
publicConfigStore.destroy = () => {
this.removeEventListener &&
this.removeEventListener('update', updatePublicConfigStore)
} }
}
function updatePublicConfigStore(memState) { /**
const chainId = networkController.getCurrentChainId() * Gets network state relevant for external providers.
if (memState.network !== 'loading') { *
publicConfigStore.putState(selectPublicState(chainId, memState)) * @param {Object} [memState] - The MetaMask memState. If not provided,
} * this function will retrieve the most recent state.
* @returns {Object} An object with relevant network state properties.
*/
getProviderNetworkState(memState) {
const { network } = memState || this.getState()
return {
chainId: this.networkController.getCurrentChainId(),
networkVersion: network,
} }
function selectPublicState(chainId, { isUnlocked, network }) {
return {
isUnlocked,
chainId,
networkVersion: network,
}
}
return publicConfigStore
} }
//============================================================================= //=============================================================================
@ -1813,8 +1815,7 @@ export default class MetamaskController extends EventEmitter {
const mux = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
// messages between inpage and background // messages between inpage and background
this.setupProviderConnection(mux.createStream('provider'), sender) this.setupProviderConnection(mux.createStream('metamask-provider'), sender)
this.setupPublicConfig(mux.createStream('publicConfig'))
} }
/** /**
@ -1971,6 +1972,7 @@ export default class MetamaskController extends EventEmitter {
engine.push( engine.push(
createMethodMiddleware({ createMethodMiddleware({
origin, origin,
getProviderState: this.getProviderState.bind(this),
sendMetrics: this.metaMetricsController.trackEvent.bind( sendMetrics: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController, this.metaMetricsController,
), ),
@ -1993,29 +1995,6 @@ export default class MetamaskController extends EventEmitter {
return engine return engine
} }
/**
* A method for providing our public config info over a stream.
* This includes info we like to be synchronous if possible, like
* the current selected account, and network ID.
*
* Since synchronous methods have been deprecated in web3,
* this is a good candidate for deprecation.
*
* @param {*} outStream - The stream to provide public config over.
*/
setupPublicConfig(outStream) {
const configStore = this.createPublicConfigStore()
const configStream = asStream(configStore)
pump(configStream, outStream, (err) => {
configStore.destroy()
configStream.destroy()
if (err) {
log.error(err)
}
})
}
/** /**
* Adds a reference to a connection by origin. Ignores the 'metamask' origin. * Adds a reference to a connection by origin. Ignores the 'metamask' origin.
* Caller must ensure that the returned id is stored such that the reference * Caller must ensure that the returned id is stored such that the reference
@ -2066,37 +2045,51 @@ export default class MetamaskController extends EventEmitter {
/** /**
* Causes the RPC engines associated with the connections to the given origin * Causes the RPC engines associated with the connections to the given origin
* to emit a notification event with the given payload. * to emit a notification event with the given payload.
* Does nothing if the extension is locked or the origin is unknown. *
* The caller is responsible for ensuring that only permitted notifications
* are sent.
*
* Ignores unknown origins.
* *
* @param {string} origin - The connection's origin string. * @param {string} origin - The connection's origin string.
* @param {any} payload - The event payload. * @param {any} payload - The event payload.
*/ */
notifyConnections(origin, payload) { notifyConnections(origin, payload) {
const connections = this.connections[origin] const connections = this.connections[origin]
if (!this.isUnlocked() || !connections) {
return
}
Object.values(connections).forEach((conn) => { if (connections) {
conn.engine && conn.engine.emit('notification', payload) Object.values(connections).forEach((conn) => {
}) if (conn.engine) {
conn.engine.emit('notification', payload)
}
})
}
} }
/** /**
* Causes the RPC engines associated with all connections to emit a * Causes the RPC engines associated with all connections to emit a
* notification event with the given payload. * notification event with the given payload.
* Does nothing if the extension is locked.
* *
* @param {any} payload - The event payload. * If the "payload" parameter is a function, the payload for each connection
* will be the return value of that function called with the connection's
* origin.
*
* The caller is responsible for ensuring that only permitted notifications
* are sent.
*
* @param {any} payload - The event payload, or payload getter function.
*/ */
notifyAllConnections(payload) { notifyAllConnections(payload) {
if (!this.isUnlocked()) { const getPayload =
return typeof payload === 'function'
} ? (origin) => payload(origin)
: () => payload
Object.values(this.connections).forEach((origin) => { Object.values(this.connections).forEach((origin) => {
Object.values(origin).forEach((conn) => { Object.values(origin).forEach((conn) => {
conn.engine && conn.engine.emit('notification', payload) if (conn.engine) {
conn.engine.emit('notification', getPayload(origin))
}
}) })
}) })
} }
@ -2125,6 +2118,51 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker.syncWithAddresses(addresses) this.accountTracker.syncWithAddresses(addresses)
} }
/**
* Handle global unlock, triggered by KeyringController unlock.
* Notifies all connections that the extension is unlocked.
*/
_onUnlock() {
this.notifyAllConnections((origin) => {
return {
method: NOTIFICATION_NAMES.unlockStateChanged,
params: {
isUnlocked: true,
accounts: this.permissionsController.getAccounts(origin),
},
}
})
this.emit('unlock')
}
/**
* Handle global lock, triggered by KeyringController lock.
* Notifies all connections that the extension is locked.
*/
_onLock() {
this.notifyAllConnections({
method: NOTIFICATION_NAMES.unlockStateChanged,
params: {
isUnlocked: false,
},
})
this.emit('lock')
}
/**
* Handle memory state updates.
* - Ensure isClientOpenAndUnlocked is updated
* - Notifies all connections with the new provider network state
* - The external providers handle diffing the state
*/
_onStateUpdate(newState) {
this.isClientOpenAndUnlocked = newState.isUnlocked && this._isClientOpen
this.notifyAllConnections({
method: NOTIFICATION_NAMES.chainChanged,
params: this.getProviderNetworkState(newState),
})
}
// misc // misc
/** /**

View File

@ -81,7 +81,7 @@
"@metamask/eth-ledger-bridge-keyring": "^0.2.6", "@metamask/eth-ledger-bridge-keyring": "^0.2.6",
"@metamask/eth-token-tracker": "^3.0.1", "@metamask/eth-token-tracker": "^3.0.1",
"@metamask/etherscan-link": "^1.4.0", "@metamask/etherscan-link": "^1.4.0",
"@metamask/inpage-provider": "^6.1.0", "@metamask/inpage-provider": "^8.0.1",
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",
"@metamask/logo": "^2.5.0", "@metamask/logo": "^2.5.0",
"@popperjs/core": "^2.4.0", "@popperjs/core": "^2.4.0",

View File

@ -1022,7 +1022,7 @@ describe('MetaMaskController', function () {
} }
streamTest.write( streamTest.write(
{ {
name: 'provider', name: 'metamask-provider',
data: message, data: message,
}, },
null, null,
@ -1061,7 +1061,7 @@ describe('MetaMaskController', function () {
} }
streamTest.write( streamTest.write(
{ {
name: 'provider', name: 'metamask-provider',
data: message, data: message,
}, },
null, null,

View File

@ -32,8 +32,6 @@ const keyringAccounts = deepFreeze([
'0xcc74c7a59194e5d9268476955650d1e285be703c', '0xcc74c7a59194e5d9268476955650d1e285be703c',
]) ])
const getKeyringAccounts = async () => [...keyringAccounts]
const getIdentities = () => { const getIdentities = () => {
return keyringAccounts.reduce((identities, address, index) => { return keyringAccounts.reduce((identities, address, index) => {
identities[address] = { address, name: `Account ${index}` } identities[address] = { address, name: `Account ${index}` }
@ -62,8 +60,6 @@ const getRestrictedMethods = (permController) => {
} }
} }
const getUnlockPromise = () => Promise.resolve()
/** /**
* Gets default mock constructor options for a permissions controller. * Gets default mock constructor options for a permissions controller.
* *
@ -71,10 +67,10 @@ const getUnlockPromise = () => Promise.resolve()
*/ */
export function getPermControllerOpts() { export function getPermControllerOpts() {
return { return {
showPermissionRequest: noop, getKeyringAccounts: async () => [...keyringAccounts],
getKeyringAccounts, getUnlockPromise: () => Promise.resolve(),
getUnlockPromise,
getRestrictedMethods, getRestrictedMethods,
isUnlocked: () => true,
notifyDomain: noop, notifyDomain: noop,
notifyAllDomains: noop, notifyAllDomains: noop,
preferences: { preferences: {
@ -86,6 +82,7 @@ export function getPermControllerOpts() {
}, },
subscribe: noop, subscribe: noop,
}, },
showPermissionRequest: noop,
} }
} }
@ -467,7 +464,7 @@ export const getters = deepFreeze({
removedAccounts: () => { removedAccounts: () => {
return { return {
method: NOTIFICATION_NAMES.accountsChanged, method: NOTIFICATION_NAMES.accountsChanged,
result: [], params: [],
} }
}, },
@ -480,7 +477,7 @@ export const getters = deepFreeze({
newAccounts: (accounts) => { newAccounts: (accounts) => {
return { return {
method: NOTIFICATION_NAMES.accountsChanged, method: NOTIFICATION_NAMES.accountsChanged,
result: accounts, params: accounts,
} }
}, },
}, },
@ -586,17 +583,17 @@ export const getters = deepFreeze({
}, },
/** /**
* Gets a wallet_sendDomainMetadata RPC request object. * Gets a metamask_sendDomainMetadata RPC request object.
* *
* @param {string} origin - The origin of the request * @param {string} origin - The origin of the request
* @param {Object} name - The domainMetadata name * @param {Object} name - The domainMetadata name
* @param {Array<any>} [args] - Any other data for the request's domainMetadata * @param {Array<any>} [args] - Any other data for the request's domainMetadata
* @returns {Object} An RPC request object * @returns {Object} An RPC request object
*/ */
wallet_sendDomainMetadata: (origin, name, ...args) => { metamask_sendDomainMetadata: (origin, name, ...args) => {
return { return {
origin, origin,
method: 'wallet_sendDomainMetadata', method: 'metamask_sendDomainMetadata',
domainMetadata: { domainMetadata: {
...args, ...args,
name, name,

View File

@ -6,13 +6,9 @@ import sinon from 'sinon'
import { import {
METADATA_STORE_KEY, METADATA_STORE_KEY,
METADATA_CACHE_MAX_SIZE, METADATA_CACHE_MAX_SIZE,
WALLET_PREFIX,
} from '../../../../../app/scripts/controllers/permissions/enums' } from '../../../../../app/scripts/controllers/permissions/enums'
import { import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'
PermissionsController,
addInternalMethodPrefix,
} from '../../../../../app/scripts/controllers/permissions'
import { getRequestUserApprovalHelper, grantPermissions } from './helpers' import { getRequestUserApprovalHelper, grantPermissions } from './helpers'
@ -187,7 +183,7 @@ describe('permissions controller', function () {
assert.deepEqual( assert.deepEqual(
notifications[origin], notifications[origin],
[NOTIFICATIONS.removedAccounts()], [NOTIFICATIONS.removedAccounts()],
'origin should have single wallet_accountsChanged:[] notification', 'origin should have single metamask_accountsChanged:[] notification',
) )
}) })
@ -1325,11 +1321,18 @@ describe('permissions controller', function () {
}) })
it('notifyAccountsChanged records history and sends notification', async function () { it('notifyAccountsChanged records history and sends notification', async function () {
sinon.spy(permController, '_isUnlocked')
permController.notifyAccountsChanged( permController.notifyAccountsChanged(
DOMAINS.a.origin, DOMAINS.a.origin,
ACCOUNTS.a.permitted, ACCOUNTS.a.permitted,
) )
assert.ok(
permController._isUnlocked.calledOnce,
'_isUnlocked should have been called once',
)
assert.ok( assert.ok(
permController.permissionsLog.updateAccountsHistory.calledOnce, permController.permissionsLog.updateAccountsHistory.calledOnce,
'permissionsLog.updateAccountsHistory should have been called once', 'permissionsLog.updateAccountsHistory should have been called once',
@ -1342,6 +1345,25 @@ describe('permissions controller', function () {
) )
}) })
it('notifyAccountsChanged does nothing if _isUnlocked returns false', async function () {
permController._isUnlocked = sinon.fake.returns(false)
permController.notifyAccountsChanged(
DOMAINS.a.origin,
ACCOUNTS.a.permitted,
)
assert.ok(
permController._isUnlocked.calledOnce,
'_isUnlocked should have been called once',
)
assert.ok(
permController.permissionsLog.updateAccountsHistory.notCalled,
'permissionsLog.updateAccountsHistory should not have been called',
)
})
it('notifyAccountsChanged throws on invalid origin', async function () { it('notifyAccountsChanged throws on invalid origin', async function () {
assert.throws( assert.throws(
() => permController.notifyAccountsChanged(4, ACCOUNTS.a.permitted), () => permController.notifyAccountsChanged(4, ACCOUNTS.a.permitted),
@ -1604,11 +1626,5 @@ describe('permissions controller', function () {
'pending approval origins should have expected item', 'pending approval origins should have expected item',
) )
}) })
it('addInternalMethodPrefix', function () {
const str = 'foo'
const res = addInternalMethodPrefix(str)
assert.equal(res, WALLET_PREFIX + str, 'should prefix correctly')
})
}) })
}) })

View File

@ -286,7 +286,7 @@ describe('permissions log', function () {
assert.equal(log.length, 0, 'log should be empty') assert.equal(log.length, 0, 'log should be empty')
const res = { foo: 'bar' } const res = { foo: 'bar' }
const req1 = RPC_REQUESTS.wallet_sendDomainMetadata( const req1 = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin, DOMAINS.c.origin,
'foobar', 'foobar',
) )

View File

@ -808,7 +808,7 @@ describe('permissions middleware', function () {
}) })
}) })
describe('wallet_sendDomainMetadata', function () { describe('metamask_sendDomainMetadata', function () {
let permController, clock let permController, clock
beforeEach(function () { beforeEach(function () {
@ -828,7 +828,10 @@ describe('permissions middleware', function () {
DOMAINS.c.origin, DOMAINS.c.origin,
) )
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name) const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
)
const res = {} const res = {}
await assert.doesNotReject(cMiddleware(req, res), 'should not reject') await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
@ -861,7 +864,10 @@ describe('permissions middleware', function () {
extensionId, extensionId,
) )
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name) const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
)
const res = {} const res = {}
await assert.doesNotReject(cMiddleware(req, res), 'should not reject') await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
@ -885,7 +891,10 @@ describe('permissions middleware', function () {
DOMAINS.c.origin, DOMAINS.c.origin,
) )
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name) const req = RPC_REQUESTS.metamask_sendDomainMetadata(
DOMAINS.c.origin,
name,
)
const res = {} const res = {}
await assert.doesNotReject(cMiddleware(req, res), 'should not reject') await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
@ -907,7 +916,7 @@ describe('permissions middleware', function () {
DOMAINS.c.origin, DOMAINS.c.origin,
) )
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin) const req = RPC_REQUESTS.metamask_sendDomainMetadata(DOMAINS.c.origin)
delete req.domainMetadata delete req.domainMetadata
const res = {} const res = {}

View File

@ -2115,20 +2115,19 @@
resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.1.0.tgz#13829d8244bbf19ea658c0b20d21a77b67de0bdd" resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.1.0.tgz#13829d8244bbf19ea658c0b20d21a77b67de0bdd"
integrity sha512-Hggj4y0QIjDzKGTXzarhEPIQyFSB2bi2y6YLJNwaT4JmP30UB5Cj6gqoY0M4pj3QT57fzp0BUuGp7F/AUe28tw== integrity sha512-Hggj4y0QIjDzKGTXzarhEPIQyFSB2bi2y6YLJNwaT4JmP30UB5Cj6gqoY0M4pj3QT57fzp0BUuGp7F/AUe28tw==
"@metamask/inpage-provider@^6.1.0": "@metamask/inpage-provider@^8.0.1":
version "6.3.0" version "8.0.1"
resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-6.3.0.tgz#92d965e20912c24adbf973efbd07dbf339547741" resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-8.0.1.tgz#67b1f0733ae7c0e0e429dc5c067ba9d2dd6d66da"
integrity sha512-n7E06+8hWdYKmgJo84WFvgX6/BSqaOQEOMIrcbrP48LdkkZNEAChx6D8oUb2lYDQiWgahR+f20jsJoN4WmOjxw== integrity sha512-dN3IpiJtaHeiPzF01UXnrQ6TxXbXbkU54kiOHuIUe9e8s7vyPzgDgN2nj84xjmIkqxL0MKY90Wcp0obFKnNj+Q==
dependencies: dependencies:
eth-rpc-errors "^2.1.1" "@metamask/safe-event-emitter" "^2.0.0"
eth-rpc-errors "^4.0.2"
fast-deep-equal "^2.0.1" fast-deep-equal "^2.0.1"
is-stream "^2.0.0" is-stream "^2.0.0"
json-rpc-engine "^5.2.0" json-rpc-engine "^6.1.0"
json-rpc-middleware-stream "^2.1.1" json-rpc-middleware-stream "^2.1.1"
obj-multiplex "^1.0.0" obj-multiplex "^1.0.0"
obs-store "^4.0.3"
pump "^3.0.0" pump "^3.0.0"
safe-event-emitter "^1.0.1"
"@metamask/jazzicon@^2.0.0": "@metamask/jazzicon@^2.0.0":
version "2.0.0" version "2.0.0"
@ -9889,13 +9888,6 @@ eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2:
json-rpc-random-id "^1.0.0" json-rpc-random-id "^1.0.0"
xtend "^4.0.1" xtend "^4.0.1"
eth-rpc-errors@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-2.1.1.tgz#00a7d6c8a9c864a8ab7d0356be20964e5bee4b13"
integrity sha512-MY3zAa5ZF8hvgQu1HOF9agaK5GgigBRGpTJ8H0oVlE0NqMu13CW6syyjLXdeIDCGQTbUeHliU1z9dVmvMKx1Tg==
dependencies:
fast-safe-stringify "^2.0.6"
eth-rpc-errors@^3.0.0: eth-rpc-errors@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10"
@ -15221,7 +15213,7 @@ json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0:
promise-to-callback "^1.0.0" promise-to-callback "^1.0.0"
safe-event-emitter "^1.0.1" safe-event-emitter "^1.0.1"
json-rpc-engine@^5.2.0, json-rpc-engine@^5.3.0: json-rpc-engine@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.3.0.tgz#7dc7291766b28766ebda33eb6d3f4c6301c44ff4" resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.3.0.tgz#7dc7291766b28766ebda33eb6d3f4c6301c44ff4"
integrity sha512-+diJ9s8rxB+fbJhT7ZEf8r8spaLRignLd8jTgQ/h5JSGppAHGtNMZtCoabipCaleR1B3GTGxbXBOqhaJSGmPGQ== integrity sha512-+diJ9s8rxB+fbJhT7ZEf8r8spaLRignLd8jTgQ/h5JSGppAHGtNMZtCoabipCaleR1B3GTGxbXBOqhaJSGmPGQ==