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:
parent
55e5f5513c
commit
3bf94164ac
@ -16,16 +16,13 @@ const inpageContent = fs.readFileSync(
|
||||
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
|
||||
const inpageBundle = inpageContent + inpageSuffix
|
||||
|
||||
// Eventually this streaming injection could be replaced with:
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
|
||||
//
|
||||
// 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.
|
||||
const CONTENT_SCRIPT = 'metamask-contentscript'
|
||||
const INPAGE = 'metamask-inpage'
|
||||
const PROVIDER = 'metamask-provider'
|
||||
|
||||
if (shouldInjectProvider()) {
|
||||
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
|
||||
* browser extension and local per-page browser context.
|
||||
@ -63,10 +51,10 @@ async function start() {
|
||||
async function setupStreams() {
|
||||
// the transport-specific streams for communication between inpage and background
|
||||
const pageStream = new LocalMessageDuplexStream({
|
||||
name: 'contentscript',
|
||||
target: 'inpage',
|
||||
name: CONTENT_SCRIPT,
|
||||
target: INPAGE,
|
||||
})
|
||||
const extensionPort = extension.runtime.connect({ name: 'contentscript' })
|
||||
const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT })
|
||||
const extensionStream = new PortStream(extensionPort)
|
||||
|
||||
// create and connect channel muxers
|
||||
@ -79,26 +67,26 @@ async function setupStreams() {
|
||||
pump(pageMux, pageStream, pageMux, (err) =>
|
||||
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
|
||||
)
|
||||
pump(extensionMux, extensionStream, extensionMux, (err) =>
|
||||
logStreamDisconnectWarning('MetaMask Background Multiplex', err),
|
||||
)
|
||||
pump(extensionMux, extensionStream, extensionMux, (err) => {
|
||||
logStreamDisconnectWarning('MetaMask Background Multiplex', err)
|
||||
notifyInpageOfStreamFailure()
|
||||
})
|
||||
|
||||
// forward communication across inpage-background for these channels only
|
||||
forwardTrafficBetweenMuxers('provider', pageMux, extensionMux)
|
||||
forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux)
|
||||
forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux)
|
||||
|
||||
// connect "phishing" channel to warning system
|
||||
const phishingStream = extensionMux.createStream('phishing')
|
||||
phishingStream.once('data', redirectToPhishingWarning)
|
||||
}
|
||||
|
||||
function forwardTrafficBetweenMuxers(channelName, muxA, muxB) {
|
||||
function forwardTrafficBetweenMuxes(channelName, muxA, muxB) {
|
||||
const channelA = muxA.createStream(channelName)
|
||||
const channelB = muxB.createStream(channelName)
|
||||
pump(channelA, channelB, channelA, (err) =>
|
||||
logStreamDisconnectWarning(
|
||||
pump(channelA, channelB, channelA, (error) =>
|
||||
console.debug(
|
||||
`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
|
||||
*
|
||||
* @param {string} remoteLabel - Remote stream name
|
||||
* @param {Error} err - Stream connection error
|
||||
* @param {Error} error - Stream connection error
|
||||
*/
|
||||
function logStreamDisconnectWarning(remoteLabel, err) {
|
||||
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
|
||||
if (err) {
|
||||
warningMsg += `\n${err.stack}`
|
||||
}
|
||||
console.warn(warningMsg)
|
||||
function logStreamDisconnectWarning(remoteLabel, error) {
|
||||
console.debug(
|
||||
`MetaMask Contentscript: Lost connection to "${remoteLabel}".`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
})}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }),
|
||||
)
|
||||
}
|
||||
|
@ -19,10 +19,15 @@ export const CAVEAT_TYPES = {
|
||||
}
|
||||
|
||||
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 = {
|
||||
restricted: 'restricted',
|
||||
@ -82,8 +87,9 @@ export const SAFE_METHODS = [
|
||||
'eth_submitWork',
|
||||
'eth_syncing',
|
||||
'eth_uninstallFilter',
|
||||
'metamask_watchAsset',
|
||||
'wallet_watchAsset',
|
||||
'eth_getEncryptionPublicKey',
|
||||
'eth_decrypt',
|
||||
'metamask_watchAsset',
|
||||
'wallet_watchAsset',
|
||||
'metamask_getProviderState',
|
||||
]
|
||||
|
@ -28,6 +28,7 @@ export class PermissionsController {
|
||||
getKeyringAccounts,
|
||||
getRestrictedMethods,
|
||||
getUnlockPromise,
|
||||
isUnlocked,
|
||||
notifyDomain,
|
||||
notifyAllDomains,
|
||||
preferences,
|
||||
@ -47,6 +48,7 @@ export class PermissionsController {
|
||||
this._notifyDomain = notifyDomain
|
||||
this._notifyAllDomains = notifyAllDomains
|
||||
this._showPermissionRequest = showPermissionRequest
|
||||
this._isUnlocked = isUnlocked
|
||||
|
||||
this._restrictedMethods = getRestrictedMethods({
|
||||
getKeyringAccounts: this.getKeyringAccounts.bind(this),
|
||||
@ -463,21 +465,20 @@ export class PermissionsController {
|
||||
throw new Error('Invalid accounts', newAccounts)
|
||||
}
|
||||
|
||||
this._notifyDomain(origin, {
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
result: newAccounts,
|
||||
})
|
||||
|
||||
// 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
|
||||
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
|
||||
// We do not share accounts when the extension is locked.
|
||||
if (this._isUnlocked()) {
|
||||
this._notifyDomain(origin, {
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
params: newAccounts,
|
||||
})
|
||||
this.permissionsLog.updateAccountsHistory(origin, newAccounts)
|
||||
}
|
||||
|
||||
// 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
|
||||
// We don't check for accounts changing in the notifyAllDomains case,
|
||||
// because the log only records when accounts were last seen, and the
|
||||
// the accounts only change for all domains at once when permissions are
|
||||
// removed.
|
||||
}
|
||||
|
||||
/**
|
||||
@ -508,9 +509,11 @@ export class PermissionsController {
|
||||
*/
|
||||
clearPermissions() {
|
||||
this.permissions.clearDomains()
|
||||
// It's safe to notify that no accounts are available, regardless of
|
||||
// extension lock state
|
||||
this._notifyAllDomains({
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
result: [],
|
||||
params: [],
|
||||
})
|
||||
}
|
||||
|
||||
@ -749,7 +752,3 @@ export class PermissionsController {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function addInternalMethodPrefix(method) {
|
||||
return WALLET_PREFIX + method
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ export default class PermissionsLogController {
|
||||
/**
|
||||
* Updates the exposed account history for the given origin.
|
||||
* 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 {Array<string>} accounts - The accounts.
|
||||
|
@ -73,7 +73,7 @@ export default function createPermissionsMethodMiddleware({
|
||||
|
||||
// custom method for getting metadata from the requesting domain,
|
||||
// sent automatically by the inpage provider when it's initialized
|
||||
case 'wallet_sendDomainMetadata': {
|
||||
case 'metamask_sendDomainMetadata': {
|
||||
if (typeof req.domainMetadata?.name === 'string') {
|
||||
addDomainMetadata(req.origin, req.domainMetadata)
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ cleanContextForImports()
|
||||
/* eslint-disable import/first */
|
||||
import log from 'loglevel'
|
||||
import LocalMessageDuplexStream from 'post-message-stream'
|
||||
import { initProvider } from '@metamask/inpage-provider'
|
||||
import { initializeProvider } from '@metamask/inpage-provider'
|
||||
|
||||
restoreContextAfterImports()
|
||||
|
||||
@ -45,10 +45,12 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
|
||||
|
||||
// setup background connection
|
||||
const metamaskStream = new LocalMessageDuplexStream({
|
||||
name: 'inpage',
|
||||
target: 'contentscript',
|
||||
name: 'metamask-inpage',
|
||||
target: 'metamask-contentscript',
|
||||
})
|
||||
|
||||
initProvider({
|
||||
initializeProvider({
|
||||
connectionStream: metamaskStream,
|
||||
logger: log,
|
||||
shouldShimWeb3: true,
|
||||
})
|
||||
|
@ -23,6 +23,7 @@ const MESSAGE_TYPE = {
|
||||
ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey',
|
||||
ETH_SIGN: 'eth_sign',
|
||||
ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
|
||||
GET_PROVIDER_STATE: 'metamask_getProviderState',
|
||||
LOG_WEB3_SHIM_USAGE: 'metamask_logWeb3ShimUsage',
|
||||
PERSONAL_SIGN: 'personal_sign',
|
||||
WATCH_ASSET: 'wallet_watchAsset',
|
||||
|
@ -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()
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import getProviderState from './get-provider-state'
|
||||
import logWeb3ShimUsage from './log-web3-shim-usage'
|
||||
import watchAsset from './watch-asset'
|
||||
|
||||
const handlers = [logWeb3ShimUsage, watchAsset]
|
||||
const handlers = [getProviderState, logWeb3ShimUsage, watchAsset]
|
||||
export default handlers
|
||||
|
@ -1,9 +1,6 @@
|
||||
import EventEmitter from 'events'
|
||||
|
||||
import pump from 'pump'
|
||||
import Dnode from 'dnode'
|
||||
import ObservableStore from 'obs-store'
|
||||
import asStream from 'obs-store/lib/asStream'
|
||||
import { JsonRpcEngine } from 'json-rpc-engine'
|
||||
import { debounce } from 'lodash'
|
||||
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 SwapsController from './controllers/swaps'
|
||||
import { PermissionsController } from './controllers/permissions'
|
||||
import { NOTIFICATION_NAMES } from './controllers/permissions/enums'
|
||||
import getRestrictedMethods from './controllers/permissions/restrictedMethods'
|
||||
import nodeify from './lib/nodeify'
|
||||
import accountImporter from './account-import-strategies'
|
||||
@ -219,10 +217,11 @@ export default class MetamaskController extends EventEmitter {
|
||||
initState: initState.KeyringController,
|
||||
encryptor: opts.encryptor || undefined,
|
||||
})
|
||||
this.keyringController.memStore.subscribe((s) =>
|
||||
this._onKeyringControllerUpdate(s),
|
||||
this.keyringController.memStore.subscribe((state) =>
|
||||
this._onKeyringControllerUpdate(state),
|
||||
)
|
||||
this.keyringController.on('unlock', () => this.emit('unlock'))
|
||||
this.keyringController.on('lock', () => this._onLock())
|
||||
|
||||
this.permissionsController = new PermissionsController(
|
||||
{
|
||||
@ -233,6 +232,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
|
||||
this.appStateController,
|
||||
),
|
||||
isUnlocked: this.isUnlocked.bind(this),
|
||||
notifyDomain: this.notifyConnections.bind(this),
|
||||
notifyAllDomains: this.notifyAllConnections.bind(this),
|
||||
preferences: this.preferencesController.store,
|
||||
@ -348,6 +348,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
tokenRatesStore: this.tokenRatesController.store,
|
||||
})
|
||||
|
||||
// ensure isClientOpenAndUnlocked is updated when memState updates
|
||||
this.on('update', (memState) => this._onStateUpdate(memState))
|
||||
|
||||
this.store.updateStructure({
|
||||
AppStateController: this.appStateController.store,
|
||||
TransactionController: this.txController.store,
|
||||
@ -450,38 +453,37 @@ export default class MetamaskController extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor helper: initialize a public config store.
|
||||
* This store is used to make some config info available to Dapps synchronously.
|
||||
* Gets relevant state for the provider of an external origin.
|
||||
*
|
||||
* @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() {
|
||||
// subset of state for metamask inpage provider
|
||||
const publicConfigStore = new ObservableStore()
|
||||
const { networkController } = this
|
||||
|
||||
// setup memStore subscription hooks
|
||||
this.on('update', updatePublicConfigStore)
|
||||
updatePublicConfigStore(this.getState())
|
||||
|
||||
publicConfigStore.destroy = () => {
|
||||
this.removeEventListener &&
|
||||
this.removeEventListener('update', updatePublicConfigStore)
|
||||
async getProviderState(origin) {
|
||||
return {
|
||||
isUnlocked: this.isUnlocked(),
|
||||
...this.getProviderNetworkState(),
|
||||
accounts: await this.permissionsController.getAccounts(origin),
|
||||
}
|
||||
}
|
||||
|
||||
function updatePublicConfigStore(memState) {
|
||||
const chainId = networkController.getCurrentChainId()
|
||||
if (memState.network !== 'loading') {
|
||||
publicConfigStore.putState(selectPublicState(chainId, memState))
|
||||
}
|
||||
/**
|
||||
* Gets network state relevant for external providers.
|
||||
*
|
||||
* @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)
|
||||
|
||||
// messages between inpage and background
|
||||
this.setupProviderConnection(mux.createStream('provider'), sender)
|
||||
this.setupPublicConfig(mux.createStream('publicConfig'))
|
||||
this.setupProviderConnection(mux.createStream('metamask-provider'), sender)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1971,6 +1972,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
engine.push(
|
||||
createMethodMiddleware({
|
||||
origin,
|
||||
getProviderState: this.getProviderState.bind(this),
|
||||
sendMetrics: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
),
|
||||
@ -1993,29 +1995,6 @@ export default class MetamaskController extends EventEmitter {
|
||||
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.
|
||||
* 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
|
||||
* 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 {any} payload - The event payload.
|
||||
*/
|
||||
notifyConnections(origin, payload) {
|
||||
const connections = this.connections[origin]
|
||||
if (!this.isUnlocked() || !connections) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.values(connections).forEach((conn) => {
|
||||
conn.engine && conn.engine.emit('notification', payload)
|
||||
})
|
||||
if (connections) {
|
||||
Object.values(connections).forEach((conn) => {
|
||||
if (conn.engine) {
|
||||
conn.engine.emit('notification', payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the RPC engines associated with all connections to emit a
|
||||
* 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) {
|
||||
if (!this.isUnlocked()) {
|
||||
return
|
||||
}
|
||||
const getPayload =
|
||||
typeof payload === 'function'
|
||||
? (origin) => payload(origin)
|
||||
: () => payload
|
||||
|
||||
Object.values(this.connections).forEach((origin) => {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
|
@ -81,7 +81,7 @@
|
||||
"@metamask/eth-ledger-bridge-keyring": "^0.2.6",
|
||||
"@metamask/eth-token-tracker": "^3.0.1",
|
||||
"@metamask/etherscan-link": "^1.4.0",
|
||||
"@metamask/inpage-provider": "^6.1.0",
|
||||
"@metamask/inpage-provider": "^8.0.1",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@metamask/logo": "^2.5.0",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
|
@ -1022,7 +1022,7 @@ describe('MetaMaskController', function () {
|
||||
}
|
||||
streamTest.write(
|
||||
{
|
||||
name: 'provider',
|
||||
name: 'metamask-provider',
|
||||
data: message,
|
||||
},
|
||||
null,
|
||||
@ -1061,7 +1061,7 @@ describe('MetaMaskController', function () {
|
||||
}
|
||||
streamTest.write(
|
||||
{
|
||||
name: 'provider',
|
||||
name: 'metamask-provider',
|
||||
data: message,
|
||||
},
|
||||
null,
|
||||
|
@ -32,8 +32,6 @@ const keyringAccounts = deepFreeze([
|
||||
'0xcc74c7a59194e5d9268476955650d1e285be703c',
|
||||
])
|
||||
|
||||
const getKeyringAccounts = async () => [...keyringAccounts]
|
||||
|
||||
const getIdentities = () => {
|
||||
return keyringAccounts.reduce((identities, address, 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.
|
||||
*
|
||||
@ -71,10 +67,10 @@ const getUnlockPromise = () => Promise.resolve()
|
||||
*/
|
||||
export function getPermControllerOpts() {
|
||||
return {
|
||||
showPermissionRequest: noop,
|
||||
getKeyringAccounts,
|
||||
getUnlockPromise,
|
||||
getKeyringAccounts: async () => [...keyringAccounts],
|
||||
getUnlockPromise: () => Promise.resolve(),
|
||||
getRestrictedMethods,
|
||||
isUnlocked: () => true,
|
||||
notifyDomain: noop,
|
||||
notifyAllDomains: noop,
|
||||
preferences: {
|
||||
@ -86,6 +82,7 @@ export function getPermControllerOpts() {
|
||||
},
|
||||
subscribe: noop,
|
||||
},
|
||||
showPermissionRequest: noop,
|
||||
}
|
||||
}
|
||||
|
||||
@ -467,7 +464,7 @@ export const getters = deepFreeze({
|
||||
removedAccounts: () => {
|
||||
return {
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
result: [],
|
||||
params: [],
|
||||
}
|
||||
},
|
||||
|
||||
@ -480,7 +477,7 @@ export const getters = deepFreeze({
|
||||
newAccounts: (accounts) => {
|
||||
return {
|
||||
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 {Object} name - The domainMetadata name
|
||||
* @param {Array<any>} [args] - Any other data for the request's domainMetadata
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
wallet_sendDomainMetadata: (origin, name, ...args) => {
|
||||
metamask_sendDomainMetadata: (origin, name, ...args) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'wallet_sendDomainMetadata',
|
||||
method: 'metamask_sendDomainMetadata',
|
||||
domainMetadata: {
|
||||
...args,
|
||||
name,
|
||||
|
@ -6,13 +6,9 @@ import sinon from 'sinon'
|
||||
import {
|
||||
METADATA_STORE_KEY,
|
||||
METADATA_CACHE_MAX_SIZE,
|
||||
WALLET_PREFIX,
|
||||
} from '../../../../../app/scripts/controllers/permissions/enums'
|
||||
|
||||
import {
|
||||
PermissionsController,
|
||||
addInternalMethodPrefix,
|
||||
} from '../../../../../app/scripts/controllers/permissions'
|
||||
import { PermissionsController } from '../../../../../app/scripts/controllers/permissions'
|
||||
|
||||
import { getRequestUserApprovalHelper, grantPermissions } from './helpers'
|
||||
|
||||
@ -187,7 +183,7 @@ describe('permissions controller', function () {
|
||||
assert.deepEqual(
|
||||
notifications[origin],
|
||||
[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 () {
|
||||
sinon.spy(permController, '_isUnlocked')
|
||||
|
||||
permController.notifyAccountsChanged(
|
||||
DOMAINS.a.origin,
|
||||
ACCOUNTS.a.permitted,
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
permController._isUnlocked.calledOnce,
|
||||
'_isUnlocked should have been called once',
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
permController.permissionsLog.updateAccountsHistory.calledOnce,
|
||||
'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 () {
|
||||
assert.throws(
|
||||
() => permController.notifyAccountsChanged(4, ACCOUNTS.a.permitted),
|
||||
@ -1604,11 +1626,5 @@ describe('permissions controller', function () {
|
||||
'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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -286,7 +286,7 @@ describe('permissions log', function () {
|
||||
assert.equal(log.length, 0, 'log should be empty')
|
||||
|
||||
const res = { foo: 'bar' }
|
||||
const req1 = RPC_REQUESTS.wallet_sendDomainMetadata(
|
||||
const req1 = RPC_REQUESTS.metamask_sendDomainMetadata(
|
||||
DOMAINS.c.origin,
|
||||
'foobar',
|
||||
)
|
||||
|
@ -808,7 +808,7 @@ describe('permissions middleware', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('wallet_sendDomainMetadata', function () {
|
||||
describe('metamask_sendDomainMetadata', function () {
|
||||
let permController, clock
|
||||
|
||||
beforeEach(function () {
|
||||
@ -828,7 +828,10 @@ describe('permissions middleware', function () {
|
||||
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 = {}
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
|
||||
@ -861,7 +864,10 @@ describe('permissions middleware', function () {
|
||||
extensionId,
|
||||
)
|
||||
|
||||
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin, name)
|
||||
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
|
||||
DOMAINS.c.origin,
|
||||
name,
|
||||
)
|
||||
const res = {}
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
|
||||
@ -885,7 +891,10 @@ describe('permissions middleware', function () {
|
||||
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 = {}
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject')
|
||||
@ -907,7 +916,7 @@ describe('permissions middleware', function () {
|
||||
DOMAINS.c.origin,
|
||||
)
|
||||
|
||||
const req = RPC_REQUESTS.wallet_sendDomainMetadata(DOMAINS.c.origin)
|
||||
const req = RPC_REQUESTS.metamask_sendDomainMetadata(DOMAINS.c.origin)
|
||||
delete req.domainMetadata
|
||||
const res = {}
|
||||
|
||||
|
24
yarn.lock
24
yarn.lock
@ -2115,20 +2115,19 @@
|
||||
resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.1.0.tgz#13829d8244bbf19ea658c0b20d21a77b67de0bdd"
|
||||
integrity sha512-Hggj4y0QIjDzKGTXzarhEPIQyFSB2bi2y6YLJNwaT4JmP30UB5Cj6gqoY0M4pj3QT57fzp0BUuGp7F/AUe28tw==
|
||||
|
||||
"@metamask/inpage-provider@^6.1.0":
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-6.3.0.tgz#92d965e20912c24adbf973efbd07dbf339547741"
|
||||
integrity sha512-n7E06+8hWdYKmgJo84WFvgX6/BSqaOQEOMIrcbrP48LdkkZNEAChx6D8oUb2lYDQiWgahR+f20jsJoN4WmOjxw==
|
||||
"@metamask/inpage-provider@^8.0.1":
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/inpage-provider/-/inpage-provider-8.0.1.tgz#67b1f0733ae7c0e0e429dc5c067ba9d2dd6d66da"
|
||||
integrity sha512-dN3IpiJtaHeiPzF01UXnrQ6TxXbXbkU54kiOHuIUe9e8s7vyPzgDgN2nj84xjmIkqxL0MKY90Wcp0obFKnNj+Q==
|
||||
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"
|
||||
is-stream "^2.0.0"
|
||||
json-rpc-engine "^5.2.0"
|
||||
json-rpc-engine "^6.1.0"
|
||||
json-rpc-middleware-stream "^2.1.1"
|
||||
obj-multiplex "^1.0.0"
|
||||
obs-store "^4.0.3"
|
||||
pump "^3.0.0"
|
||||
safe-event-emitter "^1.0.1"
|
||||
|
||||
"@metamask/jazzicon@^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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.3.0.tgz#7dc7291766b28766ebda33eb6d3f4c6301c44ff4"
|
||||
integrity sha512-+diJ9s8rxB+fbJhT7ZEf8r8spaLRignLd8jTgQ/h5JSGppAHGtNMZtCoabipCaleR1B3GTGxbXBOqhaJSGmPGQ==
|
||||
|
Loading…
Reference in New Issue
Block a user