mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-27 04:46:10 +01:00
8a8ce3a0c0
Adds the latest version of `@metamask/controllers`, and updates our usage of the `ApprovalController`, which has been migrated to `BaseControllerV2`. Of [the new `controllers` release](https://github.com/MetaMask/controllers/releases/tag/v15.0.0), only the `ApprovalController` migration should be breaking. This is the first time we use events on the `ControllerMessenger` to update the badge, so I turned the messenger into a property on the main `MetaMaskController` in order to subscribe to events on it in `background.js`. I confirmed that the badge does indeed update during local QA. As it turns out, [MetaMask/controllers#571](https://github.com/MetaMask/controllers/pull/571) was breaking for a single unit test case, which is now handled during setup and teardown for the related test suite (`metamask-controller.test.js`).
719 lines
22 KiB
JavaScript
719 lines
22 KiB
JavaScript
import nanoid from 'nanoid';
|
|
import { JsonRpcEngine } from 'json-rpc-engine';
|
|
import { ObservableStore } from '@metamask/obs-store';
|
|
import log from 'loglevel';
|
|
import { CapabilitiesController as RpcCap } from 'rpc-cap';
|
|
import { ethErrors } from 'eth-rpc-errors';
|
|
import { cloneDeep } from 'lodash';
|
|
|
|
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions';
|
|
import {
|
|
APPROVAL_TYPE,
|
|
SAFE_METHODS, // methods that do not require any permissions to use
|
|
WALLET_PREFIX,
|
|
METADATA_STORE_KEY,
|
|
METADATA_CACHE_MAX_SIZE,
|
|
LOG_STORE_KEY,
|
|
HISTORY_STORE_KEY,
|
|
NOTIFICATION_NAMES,
|
|
CAVEAT_TYPES,
|
|
} from './enums';
|
|
|
|
import createPermissionsMethodMiddleware from './permissionsMethodMiddleware';
|
|
import PermissionsLogController from './permissionsLog';
|
|
|
|
// instanbul ignore next
|
|
const noop = () => undefined;
|
|
|
|
export class PermissionsController {
|
|
constructor(
|
|
{
|
|
approvals,
|
|
getKeyringAccounts,
|
|
getRestrictedMethods,
|
|
getUnlockPromise,
|
|
isUnlocked,
|
|
notifyDomain,
|
|
notifyAllDomains,
|
|
preferences,
|
|
} = {},
|
|
restoredPermissions = {},
|
|
restoredState = {},
|
|
) {
|
|
// additional top-level store key set in _initializeMetadataStore
|
|
this.store = new ObservableStore({
|
|
[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._isUnlocked = isUnlocked;
|
|
|
|
this._restrictedMethods = getRestrictedMethods({
|
|
getKeyringAccounts: this.getKeyringAccounts.bind(this),
|
|
getIdentities: this._getIdentities.bind(this),
|
|
});
|
|
this.permissionsLog = new PermissionsLogController({
|
|
restrictedMethods: Object.keys(this._restrictedMethods),
|
|
store: this.store,
|
|
});
|
|
|
|
/**
|
|
* @type {import('@metamask/controllers').ApprovalController}
|
|
* @public
|
|
*/
|
|
this.approvals = approvals;
|
|
this._initializePermissions(restoredPermissions);
|
|
this._lastSelectedAddress = preferences.getState().selectedAddress;
|
|
this.preferences = preferences;
|
|
|
|
this._initializeMetadataStore(restoredState);
|
|
|
|
preferences.subscribe(async ({ selectedAddress }) => {
|
|
if (selectedAddress && selectedAddress !== this._lastSelectedAddress) {
|
|
this._lastSelectedAddress = selectedAddress;
|
|
await this._handleAccountSelected(selectedAddress);
|
|
}
|
|
});
|
|
}
|
|
|
|
createMiddleware({ origin, extensionId }) {
|
|
if (typeof origin !== 'string' || !origin.length) {
|
|
throw new Error('Must provide non-empty string origin.');
|
|
}
|
|
|
|
const metadataState = this.store.getState()[METADATA_STORE_KEY];
|
|
|
|
if (extensionId && metadataState[origin]?.extensionId !== extensionId) {
|
|
this.addDomainMetadata(origin, { extensionId });
|
|
}
|
|
|
|
const engine = new JsonRpcEngine();
|
|
|
|
engine.push(this.permissionsLog.createMiddleware());
|
|
|
|
engine.push(
|
|
createPermissionsMethodMiddleware({
|
|
addDomainMetadata: this.addDomainMetadata.bind(this),
|
|
getAccounts: this.getAccounts.bind(this, origin),
|
|
getUnlockPromise: () => this._getUnlockPromise(true),
|
|
hasPermission: this.hasPermission.bind(this, origin),
|
|
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
|
|
requestAccountsPermission: this._requestPermissions.bind(
|
|
this,
|
|
{ origin },
|
|
{ eth_accounts: {} },
|
|
),
|
|
}),
|
|
);
|
|
|
|
engine.push(
|
|
this.permissions.providerMiddlewareFunction.bind(this.permissions, {
|
|
origin,
|
|
}),
|
|
);
|
|
|
|
return engine.asMiddleware();
|
|
}
|
|
|
|
/**
|
|
* Request {@code eth_accounts} permissions
|
|
* @param {string} origin - The requesting origin
|
|
* @returns {Promise<string>} The permissions request ID
|
|
*/
|
|
async requestAccountsPermissionWithId(origin) {
|
|
const id = nanoid();
|
|
this._requestPermissions({ origin }, { eth_accounts: {} }, id).then(
|
|
async () => {
|
|
const permittedAccounts = await this.getAccounts(origin);
|
|
this.notifyAccountsChanged(origin, permittedAccounts);
|
|
},
|
|
);
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
noop,
|
|
_end,
|
|
);
|
|
|
|
function _end() {
|
|
if (res.error || !Array.isArray(res.result)) {
|
|
resolve([]);
|
|
} else {
|
|
resolve(res.result);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
|
|
/**
|
|
* Gets the identities from the preferences controller store
|
|
*
|
|
* @returns {Object} identities
|
|
*/
|
|
_getIdentities() {
|
|
return this.preferences.getState().identities;
|
|
}
|
|
|
|
/**
|
|
* Submits a permissions request to rpc-cap. Internal, background use only.
|
|
*
|
|
* @param {IOriginMetadata} domain - The external domain metadata.
|
|
* @param {IRequestedPermissions} permissions - The requested permissions.
|
|
* @param {string} [id] - The desired id of the permissions request, if any.
|
|
* @returns {Promise<IOcapLdCapability[]>} A Promise that resolves with the
|
|
* approved permissions, or rejects with an error.
|
|
*/
|
|
_requestPermissions(domain, permissions, id) {
|
|
return new Promise((resolve, reject) => {
|
|
// rpc-cap assigns an id to the request if there is none, as expected by
|
|
// requestUserApproval below
|
|
const req = {
|
|
id,
|
|
method: 'wallet_requestPermissions',
|
|
params: [permissions],
|
|
};
|
|
const res = {};
|
|
|
|
this.permissions.providerMiddlewareFunction(domain, req, res, noop, _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.
|
|
* Idempotent for a given request id.
|
|
*
|
|
* @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;
|
|
|
|
if (!this.approvals.has({ id })) {
|
|
log.debug(`Permissions request with id '${id}' not found.`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (Object.keys(approved.permissions).length === 0) {
|
|
this.approvals.reject(
|
|
id,
|
|
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,
|
|
);
|
|
this.approvals.accept(id, approved.permissions);
|
|
}
|
|
} catch (err) {
|
|
// if finalization fails, reject the request
|
|
this.approvals.reject(
|
|
id,
|
|
ethErrors.rpc.invalidRequest({
|
|
message: err.message,
|
|
data: err,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* User rejection callback. Rejects the Promise for the permissions request
|
|
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
|
|
* Idempotent for a given id.
|
|
*
|
|
* @param {string} id - The id of the request rejected by the user
|
|
*/
|
|
async rejectPermissionsRequest(id) {
|
|
if (!this.approvals.has({ id })) {
|
|
log.debug(`Permissions request with id '${id}' not found.`);
|
|
return;
|
|
}
|
|
|
|
this.approvals.reject(id, ethErrors.provider.userRejectedRequest());
|
|
}
|
|
|
|
/**
|
|
* Expose an account to the given origin. Changes the eth_accounts
|
|
* permissions and emits accountsChanged.
|
|
*
|
|
* Throws error if the origin or account is invalid, or if the update fails.
|
|
*
|
|
* @param {string} origin - The origin to expose the account to.
|
|
* @param {string} account - The new account to expose.
|
|
*/
|
|
async addPermittedAccount(origin, account) {
|
|
const domains = this.permissions.getDomains();
|
|
if (!domains[origin]) {
|
|
throw new Error('Unrecognized domain');
|
|
}
|
|
|
|
this.validatePermittedAccounts([account]);
|
|
|
|
const oldPermittedAccounts = this._getPermittedAccounts(origin);
|
|
if (oldPermittedAccounts.length === 0) {
|
|
throw new Error(`Origin does not have 'eth_accounts' permission`);
|
|
} else if (oldPermittedAccounts.includes(account)) {
|
|
throw new Error('Account is already permitted for origin');
|
|
}
|
|
|
|
this.permissions.updateCaveatFor(
|
|
origin,
|
|
'eth_accounts',
|
|
CAVEAT_NAMES.exposedAccounts,
|
|
[...oldPermittedAccounts, account],
|
|
);
|
|
|
|
const permittedAccounts = await this.getAccounts(origin);
|
|
|
|
this.notifyAccountsChanged(origin, permittedAccounts);
|
|
}
|
|
|
|
/**
|
|
* Removes an exposed account from the given origin. Changes the eth_accounts
|
|
* permission and emits accountsChanged.
|
|
* If origin only has a single permitted account, removes the eth_accounts
|
|
* permission from the origin.
|
|
*
|
|
* Throws error if the origin or account is invalid, or if the update fails.
|
|
*
|
|
* @param {string} origin - The origin to remove the account from.
|
|
* @param {string} account - The account to remove.
|
|
*/
|
|
async removePermittedAccount(origin, account) {
|
|
const domains = this.permissions.getDomains();
|
|
if (!domains[origin]) {
|
|
throw new Error('Unrecognized domain');
|
|
}
|
|
|
|
this.validatePermittedAccounts([account]);
|
|
|
|
const oldPermittedAccounts = this._getPermittedAccounts(origin);
|
|
if (oldPermittedAccounts.length === 0) {
|
|
throw new Error(`Origin does not have 'eth_accounts' permission`);
|
|
} else if (!oldPermittedAccounts.includes(account)) {
|
|
throw new Error('Account is not permitted for origin');
|
|
}
|
|
|
|
let newPermittedAccounts = oldPermittedAccounts.filter(
|
|
(acc) => acc !== account,
|
|
);
|
|
|
|
if (newPermittedAccounts.length === 0) {
|
|
this.removePermissionsFor({ [origin]: ['eth_accounts'] });
|
|
} else {
|
|
this.permissions.updateCaveatFor(
|
|
origin,
|
|
'eth_accounts',
|
|
CAVEAT_NAMES.exposedAccounts,
|
|
newPermittedAccounts,
|
|
);
|
|
|
|
newPermittedAccounts = await this.getAccounts(origin);
|
|
}
|
|
|
|
this.notifyAccountsChanged(origin, newPermittedAccounts);
|
|
}
|
|
|
|
/**
|
|
* Remove all permissions associated with a particular account. Any eth_accounts
|
|
* permissions left with no permitted accounts will be removed as well.
|
|
*
|
|
* Throws error if the account is invalid, or if the update fails.
|
|
*
|
|
* @param {string} account - The account to remove.
|
|
*/
|
|
async removeAllAccountPermissions(account) {
|
|
this.validatePermittedAccounts([account]);
|
|
|
|
const domains = this.permissions.getDomains();
|
|
const connectedOrigins = Object.keys(domains).filter((origin) =>
|
|
this._getPermittedAccounts(origin).includes(account),
|
|
);
|
|
|
|
await Promise.all(
|
|
connectedOrigins.map((origin) =>
|
|
this.removePermittedAccount(origin, account),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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 &&
|
|
c.name !== CAVEAT_NAMES.primaryAccountOnly,
|
|
);
|
|
|
|
ethAccounts.caveats.push({
|
|
type: CAVEAT_TYPES.limitResponseLength,
|
|
value: 1,
|
|
name: CAVEAT_NAMES.primaryAccountOnly,
|
|
});
|
|
|
|
ethAccounts.caveats.push({
|
|
type: CAVEAT_TYPES.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.
|
|
*/
|
|
validatePermittedAccounts(accounts) {
|
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
throw new Error('Must provide non-empty array of account(s).');
|
|
}
|
|
|
|
// assert accounts exist
|
|
const allIdentities = this._getIdentities();
|
|
accounts.forEach((acc) => {
|
|
if (!allIdentities[acc]) {
|
|
throw new Error(`Unknown account: ${acc}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify a domain that its permitted accounts have changed.
|
|
* Also updates the accounts history log.
|
|
*
|
|
* @param {string} origin - The origin of the domain to notify.
|
|
* @param {Array<string>} newAccounts - The currently permitted accounts.
|
|
*/
|
|
notifyAccountsChanged(origin, newAccounts) {
|
|
if (typeof origin !== 'string' || !origin) {
|
|
throw new Error(`Invalid origin: '${origin}'`);
|
|
}
|
|
|
|
if (!Array.isArray(newAccounts)) {
|
|
throw new Error('Invalid accounts', 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
|
|
// 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 - The map of domain origins to permissions to remove.
|
|
* e.g. { origin: [permissions] }
|
|
*/
|
|
removePermissionsFor(domains) {
|
|
Object.entries(domains).forEach(([origin, perms]) => {
|
|
this.permissions.removePermissionsFor(
|
|
origin,
|
|
perms.map((methodName) => {
|
|
if (methodName === 'eth_accounts') {
|
|
this.notifyAccountsChanged(origin, []);
|
|
}
|
|
|
|
return { parentCapability: methodName };
|
|
}),
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Removes all known domains and their related permissions.
|
|
*/
|
|
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,
|
|
params: [],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stores domain metadata for the given origin (domain).
|
|
* Deletes metadata for domains without permissions in a FIFO manner, once
|
|
* more than 100 distinct origins have been added since boot.
|
|
* Metadata is never deleted for domains with permissions, to prevent a
|
|
* degraded user experience, since metadata cannot yet be requested on demand.
|
|
*
|
|
* @param {string} origin - The origin whose domain metadata to store.
|
|
* @param {Object} metadata - The domain's metadata that will be stored.
|
|
*/
|
|
addDomainMetadata(origin, metadata) {
|
|
const oldMetadataState = this.store.getState()[METADATA_STORE_KEY];
|
|
const newMetadataState = { ...oldMetadataState };
|
|
|
|
// delete pending metadata origin from queue, and delete its metadata if
|
|
// it doesn't have any permissions
|
|
if (this._pendingSiteMetadata.size >= METADATA_CACHE_MAX_SIZE) {
|
|
const permissionsDomains = this.permissions.getDomains();
|
|
|
|
const oldOrigin = this._pendingSiteMetadata.values().next().value;
|
|
this._pendingSiteMetadata.delete(oldOrigin);
|
|
if (!permissionsDomains[oldOrigin]) {
|
|
delete newMetadataState[oldOrigin];
|
|
}
|
|
}
|
|
|
|
// add new metadata to store after popping
|
|
newMetadataState[origin] = {
|
|
...oldMetadataState[origin],
|
|
...metadata,
|
|
lastUpdated: Date.now(),
|
|
};
|
|
|
|
if (
|
|
!newMetadataState[origin].extensionId &&
|
|
!newMetadataState[origin].host
|
|
) {
|
|
newMetadataState[origin].host = new URL(origin).host;
|
|
}
|
|
|
|
this._pendingSiteMetadata.add(origin);
|
|
this._setDomainMetadata(newMetadataState);
|
|
}
|
|
|
|
/**
|
|
* Removes all domains without permissions from the restored metadata state,
|
|
* and rehydrates the metadata store.
|
|
*
|
|
* Requires PermissionsController._initializePermissions to have been called first.
|
|
*
|
|
* @param {Object} restoredState - The restored permissions controller state.
|
|
*/
|
|
_initializeMetadataStore(restoredState) {
|
|
const metadataState = restoredState[METADATA_STORE_KEY] || {};
|
|
const newMetadataState = this._trimDomainMetadata(metadataState);
|
|
|
|
this._pendingSiteMetadata = new Set();
|
|
this._setDomainMetadata(newMetadataState);
|
|
}
|
|
|
|
/**
|
|
* Trims the given metadataState object by removing metadata for all origins
|
|
* without permissions.
|
|
* Returns a new object; does not mutate the argument.
|
|
*
|
|
* @param {Object} metadataState - The metadata store state object to trim.
|
|
* @returns {Object} The new metadata state object.
|
|
*/
|
|
_trimDomainMetadata(metadataState) {
|
|
const newMetadataState = { ...metadataState };
|
|
const origins = Object.keys(metadataState);
|
|
const permissionsDomains = this.permissions.getDomains();
|
|
|
|
origins.forEach((origin) => {
|
|
if (!permissionsDomains[origin]) {
|
|
delete newMetadataState[origin];
|
|
}
|
|
});
|
|
|
|
return newMetadataState;
|
|
}
|
|
|
|
/**
|
|
* Replaces the existing domain metadata with the passed-in object.
|
|
* @param {Object} newMetadataState - The new metadata to set.
|
|
*/
|
|
_setDomainMetadata(newMetadataState) {
|
|
this.store.updateState({ [METADATA_STORE_KEY]: newMetadataState });
|
|
}
|
|
|
|
/**
|
|
* Get current set of permitted accounts for the given origin
|
|
*
|
|
* @param {string} origin - The origin to obtain permitted accounts for
|
|
* @returns {Array<string>} The list of permitted accounts
|
|
*/
|
|
_getPermittedAccounts(origin) {
|
|
const permittedAccounts = this.permissions
|
|
.getPermission(origin, 'eth_accounts')
|
|
?.caveats?.find((caveat) => caveat.name === CAVEAT_NAMES.exposedAccounts)
|
|
?.value;
|
|
|
|
return permittedAccounts || [];
|
|
}
|
|
|
|
/**
|
|
* When a new account is selected in the UI, emit accountsChanged to each origin
|
|
* where the selected account is exposed.
|
|
*
|
|
* Note: This will emit "false positive" accountsChanged events, but they are
|
|
* handled by the inpage provider.
|
|
*
|
|
* @param {string} account - The newly selected account's address.
|
|
*/
|
|
async _handleAccountSelected(account) {
|
|
if (typeof account !== 'string') {
|
|
throw new Error('Selected account should be a non-empty string.');
|
|
}
|
|
|
|
const domains = this.permissions.getDomains() || {};
|
|
const connectedDomains = Object.entries(domains)
|
|
.filter(([_, { permissions }]) => {
|
|
const ethAccounts = permissions.find(
|
|
(permission) => permission.parentCapability === 'eth_accounts',
|
|
);
|
|
const exposedAccounts = ethAccounts?.caveats.find(
|
|
(caveat) => caveat.name === 'exposedAccounts',
|
|
)?.value;
|
|
return exposedAccounts?.includes(account);
|
|
})
|
|
.map(([domain]) => domain);
|
|
|
|
await Promise.all(
|
|
connectedDomains.map((origin) =>
|
|
this._handleConnectedAccountSelected(origin),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* When a new account is selected in the UI, emit accountsChanged to 'origin'
|
|
*
|
|
* Note: This will emit "false positive" accountsChanged events, but they are
|
|
* handled by the inpage provider.
|
|
*
|
|
* @param {string} origin - The origin
|
|
*/
|
|
async _handleConnectedAccountSelected(origin) {
|
|
const permittedAccounts = await this.getAccounts(origin);
|
|
|
|
this.notifyAccountsChanged(origin, permittedAccounts);
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
metadata: { id, origin },
|
|
} = req;
|
|
|
|
return this.approvals.addAndShowApprovalRequest({
|
|
id,
|
|
origin,
|
|
type: APPROVAL_TYPE,
|
|
});
|
|
},
|
|
},
|
|
initState,
|
|
);
|
|
}
|
|
}
|