mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Permission System 2.0 (#12243)
# Permission System 2.0 ## Background This PR migrates the extension permission system to [the new `PermissionController`](https://github.com/MetaMask/snaps-skunkworks/tree/main/packages/controllers/src/permissions). The original permission system, based on [`rpc-cap`](https://github.com/MetaMask/rpc-cap), introduced [`ZCAP-LD`](https://w3c-ccg.github.io/zcap-ld/)-like permissions to our JSON-RPC stack. We used it to [implement](https://github.com/MetaMask/metamask-extension/pull/7004) what we called "LoginPerSite" in [version 7.7.0](https://github.com/MetaMask/metamask-extension/releases/tag/v7.7.0) of the extension, which enabled the user to choose which accounts, if any, should be exposed to each dapp. While that was a worthwhile feature in and of itself, we wanted a permission _system_ in order to enable everything we are going to with Snaps. Unfortunately, the original permission system was difficult to use, and necessitated the creation of the original `PermissionsController` (note the "s"), which was more or less a wrapper for `rpc-cap`. With this PR, we shake off the yoke of the original permission system, in favor of the modular, self-contained, ergonomic, and more mature permission system 2.0. Note that [the `PermissionController` readme](https://github.com/MetaMask/snaps-skunkworks/tree/main/packages/controllers/src/permissions/README.md) explains how the new permission system works. The `PermissionController` and `SubjectMetadataController` are currently shipped via `@metamask/snap-controllers`. This is a temporary state of affairs, and we'll move them to `@metamask/controllers` once they've landed in prod. ## Changes in Detail First, the changes in this PR are not as big as they seem. Roughly half of the additions in this PR are fixtures in the test for the new migration (number 68), and a significant portion of the remaining ~2500 lines are due to find-and-replace changes in other test fixtures and UI files. - The extension `PermissionsController` has been deleted, and completely replaced with the new `PermissionController` from [`@metamask/snap-controllers`](https://www.npmjs.com/package/@metamask/snap-controllers). - The original `PermissionsController` "domain metadata" functionality is now managed by the new `SubjectMetadataController`, also from [`@metamask/snap-controllers`](https://www.npmjs.com/package/@metamask/snap-controllers). - The permission activity and history log controller has been renamed `PermissionLogController` and has its own top-level state key, but is otherwise functionally equivalent to the existing implementation. - Migration number 68 has been added to account for the new state changes. - The tests in `app/scripts/controllers/permissions` have been migrated from `mocha` to `jest`. Reviewers should focus their attention on the following files: - `app/scripts/` - `metamask-controller.js` - This is where most of the integration work for the new `PermissionController` occurs. Some functions that were internal to the original controller were moved here. - `controllers/permissions/` - `selectors.js` - These selectors are for `ControllerMessenger` selector subscriptions. The actual subscriptions occur in `metamask-controller.js`. See the `ControllerMessenger` implementation for details. - `specifications.js` - The caveat and permission specifications are required by the new `PermissionController`, and are used to specify the `eth_accounts` permission and its JSON-RPC method implementation. See the `PermissionController` readme for details. - `migrations/068.js` - The new state should be cross-referenced with the controllers that manage it. The accompanying tests should also be thoroughly reviewed. Some files may appear new but have just moved and/or been renamed: - `app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js` - This was previously implemented in `controllers/permissions/permissionsMethodMiddleware.js`. - `test/mocks/permissions.js` - A truncated version of `test/mocks/permission-controller.js`. Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
3054991c9b
commit
31cf7c10a4
@ -660,8 +660,8 @@ jobs:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: test:coverage
|
||||
command: yarn test:coverage
|
||||
name: test:coverage:mocha
|
||||
command: yarn test:coverage:mocha
|
||||
- run:
|
||||
name: test:coverage:jest
|
||||
command: yarn test:coverage:jest
|
||||
|
10
.eslintrc.js
10
.eslintrc.js
@ -22,6 +22,7 @@ module.exports = {
|
||||
|
||||
ignorePatterns: [
|
||||
'!.eslintrc.js',
|
||||
'!.mocharc.js',
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
'builds/**',
|
||||
@ -136,9 +137,10 @@ module.exports = {
|
||||
'ui/__mocks__/*.js',
|
||||
'shared/**/*.test.js',
|
||||
'development/**/*.test.js',
|
||||
'app/scripts/lib/**/*.test.js',
|
||||
'app/scripts/migrations/*.test.js',
|
||||
'app/scripts/platforms/*.test.js',
|
||||
'app/scripts/lib/**/*.test.js',
|
||||
'app/scripts/controllers/permissions/*.test.js',
|
||||
],
|
||||
extends: ['@metamask/eslint-config-mocha'],
|
||||
rules: {
|
||||
@ -161,9 +163,10 @@ module.exports = {
|
||||
'ui/__mocks__/*.js',
|
||||
'shared/**/*.test.js',
|
||||
'development/**/*.test.js',
|
||||
'app/scripts/lib/**/*.test.js',
|
||||
'app/scripts/migrations/*.test.js',
|
||||
'app/scripts/platforms/*.test.js',
|
||||
'app/scripts/lib/**/*.test.js',
|
||||
'app/scripts/controllers/permissions/*.test.js',
|
||||
],
|
||||
extends: ['@metamask/eslint-config-jest'],
|
||||
rules: {
|
||||
@ -186,7 +189,9 @@ module.exports = {
|
||||
{
|
||||
files: [
|
||||
'.eslintrc.js',
|
||||
'.mocharc.js',
|
||||
'babel.config.js',
|
||||
'jest.config.js',
|
||||
'nyc.config.js',
|
||||
'stylelint.config.js',
|
||||
'app/scripts/lockdown-run.js',
|
||||
@ -197,7 +202,6 @@ module.exports = {
|
||||
'test/setup.js',
|
||||
'test/helpers/protect-intrinsics-helpers.js',
|
||||
'test/lib/wait-until-called.js',
|
||||
'jest.config.js',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
|
@ -2,10 +2,11 @@ module.exports = {
|
||||
// TODO: Remove the `exit` setting, it can hide broken tests.
|
||||
exit: true,
|
||||
ignore: [
|
||||
'./app/scripts/lib/**/*.test.js',
|
||||
'./app/scripts/migrations/*.test.js',
|
||||
'./app/scripts/platforms/*.test.js',
|
||||
'./app/scripts/lib/**/*.test.js',
|
||||
'./app/scripts/controllers/permissions/*.test.js',
|
||||
],
|
||||
recursive: true,
|
||||
require: ['test/env.js', 'test/setup.js'],
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +0,0 @@
|
||||
const baseConfig = require('./.mocharc');
|
||||
|
||||
module.exports = Object.assign({}, baseConfig, {
|
||||
ignore: [...baseConfig.ignore, './app/scripts/controllers/permissions/*.test.js']
|
||||
});
|
@ -46,7 +46,7 @@ export const currentNetworkTxListSample = {
|
||||
]
|
||||
}
|
||||
|
||||
export const domainMetadata = {
|
||||
export const subjectMetadata = {
|
||||
"https://metamask.github.io": {
|
||||
"name": "E2E Test Dapp",
|
||||
"icon": "https://metamask.github.io/test-dapp/metamask-fox.svg",
|
||||
|
@ -1013,34 +1013,25 @@ const state = {
|
||||
goerli: null,
|
||||
mainnet: 10902989,
|
||||
},
|
||||
permissionsRequests: [],
|
||||
permissionsDescriptions: {},
|
||||
domains: {
|
||||
subjects: {
|
||||
'https://app.uniswap.org': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
permissions: {
|
||||
'eth_accounts': {
|
||||
invoker: 'https://app.uniswap.org',
|
||||
parentCapability: 'eth_accounts',
|
||||
id: 'a7342e4b-beae-4525-a36c-c0635fd03359',
|
||||
date: 1620710693178,
|
||||
caveats: [
|
||||
{
|
||||
type: 'limitResponseLength',
|
||||
value: 1,
|
||||
name: 'primaryAccountOnly',
|
||||
},
|
||||
{
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x64a845a5b02460acf8a3d84503b0d68d028b4bb4'],
|
||||
name: 'exposedAccounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
permissionsLog: [
|
||||
permissionActivityLog: [
|
||||
{
|
||||
id: 522690215,
|
||||
method: 'eth_accounts',
|
||||
@ -1171,7 +1162,7 @@ const state = {
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
permissionsHistory: {
|
||||
permissionHistory: {
|
||||
'https://metamask.github.io': {
|
||||
eth_accounts: {
|
||||
lastApproved: 1620710693213,
|
||||
@ -1181,7 +1172,7 @@ const state = {
|
||||
},
|
||||
},
|
||||
},
|
||||
domainMetadata: {
|
||||
subjectMetadata: {
|
||||
'https://metamask.github.io': {
|
||||
name: 'E2E Test Dapp',
|
||||
icon: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
|
||||
|
@ -533,13 +533,8 @@ function setupController(initState, initLangCode) {
|
||||
),
|
||||
);
|
||||
|
||||
// We're specifcally avoid using approvalController directly for better
|
||||
// Error support during rejection
|
||||
Object.keys(
|
||||
controller.permissionsController.approvals.state.pendingApprovals,
|
||||
).forEach((approvalId) =>
|
||||
controller.permissionsController.rejectPermissionsRequest(approvalId),
|
||||
);
|
||||
// Finally, reject all approvals managed by the ApprovalController
|
||||
controller.approvalController.clear();
|
||||
|
||||
updateBadge();
|
||||
}
|
||||
|
71
app/scripts/controllers/permissions/background-api.js
Normal file
71
app/scripts/controllers/permissions/background-api.js
Normal file
@ -0,0 +1,71 @@
|
||||
import {
|
||||
CaveatTypes,
|
||||
RestrictedMethods,
|
||||
} from '../../../../shared/constants/permissions';
|
||||
|
||||
export function getPermissionBackgroundApiMethods(permissionController) {
|
||||
return {
|
||||
addPermittedAccount: (origin, account) => {
|
||||
const existing = permissionController.getCaveat(
|
||||
origin,
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
if (existing.value.includes(account)) {
|
||||
throw new Error(
|
||||
`eth_accounts permission for origin "${origin}" already permits account "${account}".`,
|
||||
);
|
||||
}
|
||||
|
||||
permissionController.updateCaveat(
|
||||
origin,
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
[...existing.value, account],
|
||||
);
|
||||
},
|
||||
|
||||
removePermittedAccount: (origin, account) => {
|
||||
const existing = permissionController.getCaveat(
|
||||
origin,
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
if (!existing.value.includes(account)) {
|
||||
throw new Error(
|
||||
`eth_accounts permission for origin "${origin}" already does not permit account "${account}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const remainingAccounts = existing.value.filter(
|
||||
(existingAccount) => existingAccount !== account,
|
||||
);
|
||||
|
||||
if (remainingAccounts.length === 0) {
|
||||
permissionController.revokePermission(
|
||||
origin,
|
||||
RestrictedMethods.eth_accounts,
|
||||
);
|
||||
} else {
|
||||
permissionController.updateCaveat(
|
||||
origin,
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
remainingAccounts,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
requestAccountsPermissionWithId: async (origin) => {
|
||||
const [, { id }] = await permissionController.requestPermissions(
|
||||
{ origin },
|
||||
{
|
||||
eth_accounts: {},
|
||||
},
|
||||
);
|
||||
return id;
|
||||
},
|
||||
};
|
||||
}
|
181
app/scripts/controllers/permissions/background-api.test.js
Normal file
181
app/scripts/controllers/permissions/background-api.test.js
Normal file
@ -0,0 +1,181 @@
|
||||
import {
|
||||
CaveatTypes,
|
||||
RestrictedMethods,
|
||||
} from '../../../../shared/constants/permissions';
|
||||
import { getPermissionBackgroundApiMethods } from './background-api';
|
||||
|
||||
describe('permission background API methods', () => {
|
||||
describe('addPermittedAccount', () => {
|
||||
it('adds a permitted account', () => {
|
||||
const permissionController = {
|
||||
getCaveat: jest.fn().mockImplementationOnce(() => {
|
||||
return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] };
|
||||
}),
|
||||
updateCaveat: jest.fn(),
|
||||
};
|
||||
|
||||
getPermissionBackgroundApiMethods(
|
||||
permissionController,
|
||||
).addPermittedAccount('foo.com', '0x2');
|
||||
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.updateCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
['0x1', '0x2'],
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if the specified account is already permitted', () => {
|
||||
const permissionController = {
|
||||
getCaveat: jest.fn().mockImplementationOnce(() => {
|
||||
return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] };
|
||||
}),
|
||||
updateCaveat: jest.fn(),
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
getPermissionBackgroundApiMethods(
|
||||
permissionController,
|
||||
).addPermittedAccount('foo.com', '0x1'),
|
||||
).toThrow(
|
||||
`eth_accounts permission for origin "foo.com" already permits account "0x1".`,
|
||||
);
|
||||
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
expect(permissionController.updateCaveat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePermittedAccount', () => {
|
||||
it('removes a permitted account', () => {
|
||||
const permissionController = {
|
||||
getCaveat: jest.fn().mockImplementationOnce(() => {
|
||||
return {
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1', '0x2'],
|
||||
};
|
||||
}),
|
||||
revokePermission: jest.fn(),
|
||||
updateCaveat: jest.fn(),
|
||||
};
|
||||
|
||||
getPermissionBackgroundApiMethods(
|
||||
permissionController,
|
||||
).removePermittedAccount('foo.com', '0x2');
|
||||
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
expect(permissionController.revokePermission).not.toHaveBeenCalled();
|
||||
|
||||
expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.updateCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
['0x1'],
|
||||
);
|
||||
});
|
||||
|
||||
it('revokes the accounts permission if the removed account is the only permitted account', () => {
|
||||
const permissionController = {
|
||||
getCaveat: jest.fn().mockImplementationOnce(() => {
|
||||
return {
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1'],
|
||||
};
|
||||
}),
|
||||
revokePermission: jest.fn(),
|
||||
updateCaveat: jest.fn(),
|
||||
};
|
||||
|
||||
getPermissionBackgroundApiMethods(
|
||||
permissionController,
|
||||
).removePermittedAccount('foo.com', '0x1');
|
||||
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
expect(permissionController.revokePermission).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.revokePermission).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
);
|
||||
|
||||
expect(permissionController.updateCaveat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws if the specified account is not permitted', () => {
|
||||
const permissionController = {
|
||||
getCaveat: jest.fn().mockImplementationOnce(() => {
|
||||
return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] };
|
||||
}),
|
||||
revokePermission: jest.fn(),
|
||||
updateCaveat: jest.fn(),
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
getPermissionBackgroundApiMethods(
|
||||
permissionController,
|
||||
).removePermittedAccount('foo.com', '0x2'),
|
||||
).toThrow(
|
||||
`eth_accounts permission for origin "foo.com" already does not permit account "0x2".`,
|
||||
);
|
||||
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.getCaveat).toHaveBeenCalledWith(
|
||||
'foo.com',
|
||||
RestrictedMethods.eth_accounts,
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
expect(permissionController.revokePermission).not.toHaveBeenCalled();
|
||||
expect(permissionController.updateCaveat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestAccountsPermissionWithId', () => {
|
||||
it('request an accounts permission and returns the request id', async () => {
|
||||
const permissionController = {
|
||||
requestPermissions: jest.fn().mockImplementationOnce(async () => {
|
||||
return [null, { id: 'arbitraryId' }];
|
||||
}),
|
||||
};
|
||||
|
||||
const id = await getPermissionBackgroundApiMethods(
|
||||
permissionController,
|
||||
).requestAccountsPermissionWithId('foo.com');
|
||||
|
||||
expect(permissionController.requestPermissions).toHaveBeenCalledTimes(1);
|
||||
expect(permissionController.requestPermissions).toHaveBeenCalledWith(
|
||||
{ origin: 'foo.com' },
|
||||
{ eth_accounts: {} },
|
||||
);
|
||||
|
||||
expect(id).toStrictEqual('arbitraryId');
|
||||
});
|
||||
});
|
||||
});
|
39
app/scripts/controllers/permissions/caveat-mutators.js
Normal file
39
app/scripts/controllers/permissions/caveat-mutators.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { CaveatMutatorOperation } from '@metamask/snap-controllers';
|
||||
import { CaveatTypes } from '../../../../shared/constants/permissions';
|
||||
|
||||
/**
|
||||
* Factories that construct caveat mutator functions that are passed to
|
||||
* PermissionController.updatePermissionsByCaveat.
|
||||
*/
|
||||
export const CaveatMutatorFactories = {
|
||||
[CaveatTypes.restrictReturnedAccounts]: {
|
||||
removeAccount,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the target account from the value arrays of all
|
||||
* `restrictReturnedAccounts` caveats. No-ops if the target account is not in
|
||||
* the array, and revokes the parent permission if it's the only account in
|
||||
* the array.
|
||||
*
|
||||
* @param {string} targetAccount - The address of the account to remove from
|
||||
* all accounts permissions.
|
||||
* @param {string[]} existingAccounts - The account address array from the
|
||||
* account permissions.
|
||||
*/
|
||||
function removeAccount(targetAccount, existingAccounts) {
|
||||
const newAccounts = existingAccounts.filter(
|
||||
(address) => address !== targetAccount,
|
||||
);
|
||||
|
||||
if (newAccounts.length === existingAccounts.length) {
|
||||
return { operation: CaveatMutatorOperation.noop };
|
||||
} else if (newAccounts.length > 0) {
|
||||
return {
|
||||
operation: CaveatMutatorOperation.updateValue,
|
||||
value: newAccounts,
|
||||
};
|
||||
}
|
||||
return { operation: CaveatMutatorOperation.revokePermission };
|
||||
}
|
32
app/scripts/controllers/permissions/caveat-mutators.test.js
Normal file
32
app/scripts/controllers/permissions/caveat-mutators.test.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { CaveatMutatorOperation } from '@metamask/snap-controllers';
|
||||
import { CaveatTypes } from '../../../../shared/constants/permissions';
|
||||
import { CaveatMutatorFactories } from './caveat-mutators';
|
||||
|
||||
describe('caveat mutators', () => {
|
||||
describe('restrictReturnedAccounts', () => {
|
||||
const { removeAccount } = CaveatMutatorFactories[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
describe('removeAccount', () => {
|
||||
it('returns the no-op operation if the target account is not permitted', () => {
|
||||
expect(removeAccount('0x2', ['0x1'])).toStrictEqual({
|
||||
operation: CaveatMutatorOperation.noop,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the update operation and a new value if the target account is permitted', () => {
|
||||
expect(removeAccount('0x2', ['0x1', '0x2'])).toStrictEqual({
|
||||
operation: CaveatMutatorOperation.updateValue,
|
||||
value: ['0x1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the revoke permission operation the target account is the only permitted account', () => {
|
||||
expect(removeAccount('0x1', ['0x1'])).toStrictEqual({
|
||||
operation: CaveatMutatorOperation.revokePermission,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,20 +1,5 @@
|
||||
export const APPROVAL_TYPE = 'wallet_requestPermissions';
|
||||
|
||||
export const WALLET_PREFIX = 'wallet_';
|
||||
|
||||
export const HISTORY_STORE_KEY = 'permissionsHistory';
|
||||
|
||||
export const LOG_STORE_KEY = 'permissionsLog';
|
||||
|
||||
export const METADATA_STORE_KEY = 'domainMetadata';
|
||||
|
||||
export const METADATA_CACHE_MAX_SIZE = 100;
|
||||
|
||||
export const CAVEAT_TYPES = {
|
||||
limitResponseLength: 'limitResponseLength',
|
||||
filterResponse: 'filterResponse',
|
||||
};
|
||||
|
||||
export const NOTIFICATION_NAMES = {
|
||||
accountsChanged: 'metamask_accountsChanged',
|
||||
unlockStateChanged: 'metamask_unlockStateChanged',
|
||||
@ -31,64 +16,7 @@ export const LOG_METHOD_TYPES = {
|
||||
internal: 'internal',
|
||||
};
|
||||
|
||||
/**
|
||||
* The permission activity log size limit.
|
||||
*/
|
||||
export const LOG_LIMIT = 100;
|
||||
|
||||
export const SAFE_METHODS = [
|
||||
'eth_blockNumber',
|
||||
'eth_call',
|
||||
'eth_chainId',
|
||||
'eth_coinbase',
|
||||
'eth_decrypt',
|
||||
'eth_estimateGas',
|
||||
'eth_feeHistory',
|
||||
'eth_gasPrice',
|
||||
'eth_getBalance',
|
||||
'eth_getBlockByHash',
|
||||
'eth_getBlockByNumber',
|
||||
'eth_getBlockTransactionCountByHash',
|
||||
'eth_getBlockTransactionCountByNumber',
|
||||
'eth_getCode',
|
||||
'eth_getEncryptionPublicKey',
|
||||
'eth_getFilterChanges',
|
||||
'eth_getFilterLogs',
|
||||
'eth_getLogs',
|
||||
'eth_getProof',
|
||||
'eth_getStorageAt',
|
||||
'eth_getTransactionByBlockHashAndIndex',
|
||||
'eth_getTransactionByBlockNumberAndIndex',
|
||||
'eth_getTransactionByHash',
|
||||
'eth_getTransactionCount',
|
||||
'eth_getTransactionReceipt',
|
||||
'eth_getUncleByBlockHashAndIndex',
|
||||
'eth_getUncleByBlockNumberAndIndex',
|
||||
'eth_getUncleCountByBlockHash',
|
||||
'eth_getUncleCountByBlockNumber',
|
||||
'eth_getWork',
|
||||
'eth_hashrate',
|
||||
'eth_mining',
|
||||
'eth_newBlockFilter',
|
||||
'eth_newFilter',
|
||||
'eth_newPendingTransactionFilter',
|
||||
'eth_protocolVersion',
|
||||
'eth_sendRawTransaction',
|
||||
'eth_sendTransaction',
|
||||
'eth_sign',
|
||||
'eth_signTypedData',
|
||||
'eth_signTypedData_v1',
|
||||
'eth_signTypedData_v3',
|
||||
'eth_signTypedData_v4',
|
||||
'eth_submitHashrate',
|
||||
'eth_submitWork',
|
||||
'eth_syncing',
|
||||
'eth_uninstallFilter',
|
||||
'metamask_getProviderState',
|
||||
'metamask_watchAsset',
|
||||
'net_listening',
|
||||
'net_peerCount',
|
||||
'net_version',
|
||||
'personal_ecRecover',
|
||||
'personal_sign',
|
||||
'wallet_watchAsset',
|
||||
'web3_clientVersion',
|
||||
'web3_sha3',
|
||||
];
|
||||
|
@ -1,718 +1,4 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
export * from './caveat-mutators';
|
||||
export * from './background-api';
|
||||
export * from './specifications';
|
||||
export * from './selectors';
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import stringify from 'fast-safe-stringify';
|
||||
import { CAVEAT_NAMES } from '../../../../shared/constants/permissions';
|
||||
import { CaveatTypes } from '../../../../shared/constants/permissions';
|
||||
import {
|
||||
HISTORY_STORE_KEY,
|
||||
LOG_IGNORE_METHODS,
|
||||
LOG_LIMIT,
|
||||
LOG_METHOD_TYPES,
|
||||
LOG_STORE_KEY,
|
||||
WALLET_PREFIX,
|
||||
} from './enums';
|
||||
|
||||
@ -13,51 +12,59 @@ import {
|
||||
* Controller with middleware for logging requests and responses to restricted
|
||||
* and permissions-related methods.
|
||||
*/
|
||||
export default class PermissionsLogController {
|
||||
constructor({ restrictedMethods, store }) {
|
||||
export default class PermissionLogController {
|
||||
/**
|
||||
* @param {{ restrictedMethods: Set<string>, initState: Record<string, unknown> }} options - Options bag.
|
||||
*/
|
||||
constructor({ restrictedMethods, initState }) {
|
||||
this.restrictedMethods = restrictedMethods;
|
||||
this.store = store;
|
||||
this.store = new ObservableStore({
|
||||
permissionHistory: {},
|
||||
permissionActivityLog: [],
|
||||
...initState,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity log.
|
||||
* Get the restricted method activity log.
|
||||
*
|
||||
* @returns {Array<Object>} The activity log.
|
||||
*/
|
||||
getActivityLog() {
|
||||
return this.store.getState()[LOG_STORE_KEY] || [];
|
||||
return this.store.getState().permissionActivityLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the activity log.
|
||||
* Update the restricted method activity log.
|
||||
*
|
||||
* @param {Array<Object>} logs - The new activity log array.
|
||||
*/
|
||||
updateActivityLog(logs) {
|
||||
this.store.updateState({ [LOG_STORE_KEY]: logs });
|
||||
this.store.updateState({ permissionActivityLog: logs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions history log.
|
||||
* Get the permission history log.
|
||||
*
|
||||
* @returns {Object} The permissions history log.
|
||||
*/
|
||||
getHistory() {
|
||||
return this.store.getState()[HISTORY_STORE_KEY] || {};
|
||||
return this.store.getState().permissionHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the permissions history log.
|
||||
* Update the permission history log.
|
||||
*
|
||||
* @param {Object} history - The new permissions history log object.
|
||||
*/
|
||||
updateHistory(history) {
|
||||
this.store.updateState({ [HISTORY_STORE_KEY]: history });
|
||||
this.store.updateState({ permissionHistory: history });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the exposed account history for the given origin.
|
||||
* Sets the 'last seen' time to Date.now() for the given accounts.
|
||||
* Does **not** update the 'lastApproved' time for the permission itself.
|
||||
* Returns if the accounts array is empty.
|
||||
*
|
||||
* @param {string} origin - The origin that the accounts are exposed to.
|
||||
@ -96,7 +103,7 @@ export default class PermissionsLogController {
|
||||
// we only log certain methods
|
||||
if (
|
||||
!LOG_IGNORE_METHODS.includes(method) &&
|
||||
(isInternal || this.restrictedMethods.includes(method))
|
||||
(isInternal || this.restrictedMethods.has(method))
|
||||
) {
|
||||
activityEntry = this.logRequest(req, isInternal);
|
||||
|
||||
@ -341,7 +348,7 @@ export default class PermissionsLogController {
|
||||
const accounts = new Set();
|
||||
for (const caveat of perm.caveats) {
|
||||
if (
|
||||
caveat.name === CAVEAT_NAMES.exposedAccounts &&
|
||||
caveat.type === CaveatTypes.restrictReturnedAccounts &&
|
||||
Array.isArray(caveat.value)
|
||||
) {
|
||||
for (const value of caveat.value) {
|
@ -1,23 +1,15 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import nanoid from 'nanoid';
|
||||
import { useFakeTimers } from 'sinon';
|
||||
|
||||
import {
|
||||
constants,
|
||||
getters,
|
||||
noop,
|
||||
} from '../../../../test/mocks/permission-controller';
|
||||
import { validateActivityEntry } from '../../../../test/helpers/permission-controller-helpers';
|
||||
import PermissionsLogController from './permissionsLog';
|
||||
import stringify from 'fast-safe-stringify';
|
||||
import { constants, getters, noop } from '../../../../test/mocks/permissions';
|
||||
import PermissionLogController from './permission-log';
|
||||
import { LOG_LIMIT, LOG_METHOD_TYPES } from './enums';
|
||||
|
||||
const { PERMS, RPC_REQUESTS } = getters;
|
||||
|
||||
const {
|
||||
ACCOUNTS,
|
||||
EXPECTED_HISTORIES,
|
||||
DOMAINS,
|
||||
SUBJECTS,
|
||||
PERM_NAMES,
|
||||
REQUEST_IDS,
|
||||
RESTRICTED_METHODS,
|
||||
@ -25,10 +17,10 @@ const {
|
||||
|
||||
let clock;
|
||||
|
||||
const initPermLog = () => {
|
||||
return new PermissionsLogController({
|
||||
store: new ObservableStore(),
|
||||
const initPermLog = (initState = {}) => {
|
||||
return new PermissionLogController({
|
||||
restrictedMethods: RESTRICTED_METHODS,
|
||||
initState,
|
||||
});
|
||||
};
|
||||
|
||||
@ -59,21 +51,21 @@ const getSavedMockNext = (arr) => (handler) => {
|
||||
arr.push(handler);
|
||||
};
|
||||
|
||||
describe('permissions log', function () {
|
||||
describe('activity log', function () {
|
||||
describe('PermissionLogController', () => {
|
||||
describe('restricted method activity log', () => {
|
||||
let permLog, logMiddleware;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
permLog = initPermLog();
|
||||
logMiddleware = initMiddleware(permLog);
|
||||
});
|
||||
|
||||
it('records activity for restricted methods', function () {
|
||||
it('records activity for restricted methods', () => {
|
||||
let log, req, res;
|
||||
|
||||
// test_method, success
|
||||
|
||||
req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
|
||||
req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
|
||||
req.id = REQUEST_IDS.a;
|
||||
res = { foo: 'bar' };
|
||||
|
||||
@ -82,7 +74,7 @@ describe('permissions log', function () {
|
||||
log = permLog.getActivityLog();
|
||||
const entry1 = log[0];
|
||||
|
||||
assert.equal(log.length, 1, 'log should have single entry');
|
||||
expect(log).toHaveLength(1);
|
||||
validateActivityEntry(
|
||||
entry1,
|
||||
{ ...req },
|
||||
@ -93,7 +85,7 @@ describe('permissions log', function () {
|
||||
|
||||
// eth_accounts, failure
|
||||
|
||||
req = RPC_REQUESTS.eth_accounts(DOMAINS.b.origin);
|
||||
req = RPC_REQUESTS.eth_accounts(SUBJECTS.b.origin);
|
||||
req.id = REQUEST_IDS.b;
|
||||
res = { error: new Error('Unauthorized.') };
|
||||
|
||||
@ -102,7 +94,7 @@ describe('permissions log', function () {
|
||||
log = permLog.getActivityLog();
|
||||
const entry2 = log[1];
|
||||
|
||||
assert.equal(log.length, 2, 'log should have 2 entries');
|
||||
expect(log).toHaveLength(2);
|
||||
validateActivityEntry(
|
||||
entry2,
|
||||
{ ...req },
|
||||
@ -113,7 +105,7 @@ describe('permissions log', function () {
|
||||
|
||||
// eth_requestAccounts, success
|
||||
|
||||
req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin);
|
||||
req = RPC_REQUESTS.eth_requestAccounts(SUBJECTS.c.origin);
|
||||
req.id = REQUEST_IDS.c;
|
||||
res = { result: ACCOUNTS.c.permitted };
|
||||
|
||||
@ -122,7 +114,7 @@ describe('permissions log', function () {
|
||||
log = permLog.getActivityLog();
|
||||
const entry3 = log[2];
|
||||
|
||||
assert.equal(log.length, 3, 'log should have 3 entries');
|
||||
expect(log).toHaveLength(3);
|
||||
validateActivityEntry(
|
||||
entry3,
|
||||
{ ...req },
|
||||
@ -133,7 +125,7 @@ describe('permissions log', function () {
|
||||
|
||||
// test_method, no response
|
||||
|
||||
req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
|
||||
req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
|
||||
req.id = REQUEST_IDS.a;
|
||||
res = null;
|
||||
|
||||
@ -142,7 +134,7 @@ describe('permissions log', function () {
|
||||
log = permLog.getActivityLog();
|
||||
const entry4 = log[3];
|
||||
|
||||
assert.equal(log.length, 4, 'log should have 4 entries');
|
||||
expect(log).toHaveLength(4);
|
||||
validateActivityEntry(
|
||||
entry4,
|
||||
{ ...req },
|
||||
@ -152,14 +144,13 @@ describe('permissions log', function () {
|
||||
);
|
||||
|
||||
// validate final state
|
||||
|
||||
assert.equal(entry1, log[0], 'first log entry should remain');
|
||||
assert.equal(entry2, log[1], 'second log entry should remain');
|
||||
assert.equal(entry3, log[2], 'third log entry should remain');
|
||||
assert.equal(entry4, log[3], 'fourth log entry should remain');
|
||||
expect(entry1).toStrictEqual(log[0]);
|
||||
expect(entry2).toStrictEqual(log[1]);
|
||||
expect(entry3).toStrictEqual(log[2]);
|
||||
expect(entry4).toStrictEqual(log[3]);
|
||||
});
|
||||
|
||||
it('handles responses added out of order', function () {
|
||||
it('handles responses added out of order', () => {
|
||||
let log;
|
||||
|
||||
const handlerArray = [];
|
||||
@ -168,7 +159,7 @@ describe('permissions log', function () {
|
||||
const id2 = nanoid();
|
||||
const id3 = nanoid();
|
||||
|
||||
const req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
|
||||
const req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
|
||||
|
||||
// get make requests
|
||||
req.id = id1;
|
||||
@ -185,19 +176,15 @@ describe('permissions log', function () {
|
||||
|
||||
// verify log state
|
||||
log = permLog.getActivityLog();
|
||||
assert.equal(log.length, 3, 'log should have 3 entries');
|
||||
expect(log).toHaveLength(3);
|
||||
const entry1 = log[0];
|
||||
const entry2 = log[1];
|
||||
const entry3 = log[2];
|
||||
assert.ok(
|
||||
entry1.id === id1 &&
|
||||
entry1.response === null &&
|
||||
entry2.id === id2 &&
|
||||
entry2.response === null &&
|
||||
entry3.id === id3 &&
|
||||
entry3.response === null,
|
||||
'all entries should be in correct order and without responses',
|
||||
);
|
||||
|
||||
// all entries should be in correct order, without responses
|
||||
expect(entry1).toMatchObject({ id: id1, response: null });
|
||||
expect(entry2).toMatchObject({ id: id2, response: null });
|
||||
expect(entry3).toMatchObject({ id: id3, response: null });
|
||||
|
||||
// call response handlers
|
||||
for (const i of [1, 2, 0]) {
|
||||
@ -206,7 +193,7 @@ describe('permissions log', function () {
|
||||
|
||||
// verify log state again
|
||||
log = permLog.getActivityLog();
|
||||
assert.equal(log.length, 3, 'log should have 3 entries');
|
||||
expect(log).toHaveLength(3);
|
||||
|
||||
// verify all entries
|
||||
log = permLog.getActivityLog();
|
||||
@ -236,8 +223,8 @@ describe('permissions log', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('handles a lack of response', function () {
|
||||
let req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
|
||||
it('handles a lack of response', () => {
|
||||
let req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
|
||||
req.id = REQUEST_IDS.a;
|
||||
let res = { foo: 'bar' };
|
||||
|
||||
@ -247,7 +234,7 @@ describe('permissions log', function () {
|
||||
let log = permLog.getActivityLog();
|
||||
const entry1 = log[0];
|
||||
|
||||
assert.equal(log.length, 1, 'log should have single entry');
|
||||
expect(log).toHaveLength(1);
|
||||
validateActivityEntry(
|
||||
entry1,
|
||||
{ ...req },
|
||||
@ -257,7 +244,7 @@ describe('permissions log', function () {
|
||||
);
|
||||
|
||||
// next request should be handled as normal
|
||||
req = RPC_REQUESTS.eth_accounts(DOMAINS.b.origin);
|
||||
req = RPC_REQUESTS.eth_accounts(SUBJECTS.b.origin);
|
||||
req.id = REQUEST_IDS.b;
|
||||
res = { result: ACCOUNTS.b.permitted };
|
||||
|
||||
@ -265,7 +252,7 @@ describe('permissions log', function () {
|
||||
|
||||
log = permLog.getActivityLog();
|
||||
const entry2 = log[1];
|
||||
assert.equal(log.length, 2, 'log should have 2 entries');
|
||||
expect(log).toHaveLength(2);
|
||||
validateActivityEntry(
|
||||
entry2,
|
||||
{ ...req },
|
||||
@ -275,32 +262,32 @@ describe('permissions log', function () {
|
||||
);
|
||||
|
||||
// validate final state
|
||||
assert.equal(entry1, log[0], 'first log entry remains');
|
||||
assert.equal(entry2, log[1], 'second log entry remains');
|
||||
expect(entry1).toStrictEqual(log[0]);
|
||||
expect(entry2).toStrictEqual(log[1]);
|
||||
});
|
||||
|
||||
it('ignores expected methods', function () {
|
||||
it('ignores expected methods', () => {
|
||||
let log = permLog.getActivityLog();
|
||||
assert.equal(log.length, 0, 'log should be empty');
|
||||
expect(log).toHaveLength(0);
|
||||
|
||||
const res = { foo: 'bar' };
|
||||
const req1 = RPC_REQUESTS.metamask_sendDomainMetadata(
|
||||
DOMAINS.c.origin,
|
||||
SUBJECTS.c.origin,
|
||||
'foobar',
|
||||
);
|
||||
const req2 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'eth_getBlockNumber');
|
||||
const req3 = RPC_REQUESTS.custom(DOMAINS.b.origin, 'net_version');
|
||||
const req2 = RPC_REQUESTS.custom(SUBJECTS.b.origin, 'eth_getBlockNumber');
|
||||
const req3 = RPC_REQUESTS.custom(SUBJECTS.b.origin, 'net_version');
|
||||
|
||||
logMiddleware(req1, res);
|
||||
logMiddleware(req2, res);
|
||||
logMiddleware(req3, res);
|
||||
|
||||
log = permLog.getActivityLog();
|
||||
assert.equal(log.length, 0, 'log should still be empty');
|
||||
expect(log).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('enforces log limit', function () {
|
||||
const req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
|
||||
it('enforces log limit', () => {
|
||||
const req = RPC_REQUESTS.test_method(SUBJECTS.a.origin);
|
||||
const res = { foo: 'bar' };
|
||||
|
||||
// max out log
|
||||
@ -312,11 +299,7 @@ describe('permissions log', function () {
|
||||
|
||||
// check last entry valid
|
||||
let log = permLog.getActivityLog();
|
||||
assert.equal(
|
||||
log.length,
|
||||
LOG_LIMIT,
|
||||
'log should have LOG_LIMIT num entries',
|
||||
);
|
||||
expect(log).toHaveLength(LOG_LIMIT);
|
||||
|
||||
validateActivityEntry(
|
||||
log[LOG_LIMIT - 1],
|
||||
@ -335,11 +318,7 @@ describe('permissions log', function () {
|
||||
|
||||
// check log length
|
||||
log = permLog.getActivityLog();
|
||||
assert.equal(
|
||||
log.length,
|
||||
LOG_LIMIT,
|
||||
'log should have LOG_LIMIT num entries',
|
||||
);
|
||||
expect(log).toHaveLength(LOG_LIMIT);
|
||||
|
||||
// check first and last entries
|
||||
validateActivityEntry(
|
||||
@ -360,24 +339,22 @@ describe('permissions log', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('permissions history', function () {
|
||||
describe('permission history log', () => {
|
||||
let permLog, logMiddleware;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
permLog = initPermLog();
|
||||
logMiddleware = initMiddleware(permLog);
|
||||
initClock();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
afterEach(() => {
|
||||
tearDownClock();
|
||||
});
|
||||
|
||||
it('only updates history on responses', function () {
|
||||
let permHistory;
|
||||
|
||||
it('only updates history on responses', () => {
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
);
|
||||
const res = { result: [PERMS.granted.test_method()] };
|
||||
@ -385,27 +362,19 @@ describe('permissions log', function () {
|
||||
// noop => no response
|
||||
logMiddleware({ ...req }, { ...res }, noop);
|
||||
|
||||
permHistory = permLog.getHistory();
|
||||
assert.deepEqual(permHistory, {}, 'history should not have been updated');
|
||||
expect(permLog.getHistory()).toStrictEqual({});
|
||||
|
||||
// response => records granted permissions
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
permHistory = permLog.getHistory();
|
||||
assert.equal(
|
||||
Object.keys(permHistory).length,
|
||||
1,
|
||||
'history should have single origin',
|
||||
);
|
||||
assert.ok(
|
||||
Boolean(permHistory[DOMAINS.a.origin]),
|
||||
'history should have expected origin',
|
||||
);
|
||||
const permHistory = permLog.getHistory();
|
||||
expect(Object.keys(permHistory)).toHaveLength(1);
|
||||
expect(permHistory[SUBJECTS.a.origin]).toBeDefined();
|
||||
});
|
||||
|
||||
it('ignores malformed permissions requests', function () {
|
||||
it('ignores malformed permissions requests', () => {
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
);
|
||||
delete req.params;
|
||||
@ -414,18 +383,12 @@ describe('permissions log', function () {
|
||||
// no params => no response
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
assert.deepEqual(
|
||||
permLog.getHistory(),
|
||||
{},
|
||||
'history should not have been updated',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('records and updates account history as expected', async function () {
|
||||
let permHistory;
|
||||
|
||||
it('records and updates account history as expected', async () => {
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res = {
|
||||
@ -434,15 +397,7 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
// validate history
|
||||
|
||||
permHistory = permLog.getHistory();
|
||||
|
||||
assert.deepEqual(
|
||||
permHistory,
|
||||
EXPECTED_HISTORIES.case1[0],
|
||||
'should have correct history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[0]);
|
||||
|
||||
// mock permission requested again, with another approved account
|
||||
|
||||
@ -452,18 +407,12 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
permHistory = permLog.getHistory();
|
||||
|
||||
assert.deepEqual(
|
||||
permHistory,
|
||||
EXPECTED_HISTORIES.case1[1],
|
||||
'should have correct history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[1]);
|
||||
});
|
||||
|
||||
it('handles eth_accounts response without caveats', async function () {
|
||||
it('handles eth_accounts response without caveats', async () => {
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res = {
|
||||
@ -473,18 +422,12 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
// validate history
|
||||
|
||||
assert.deepEqual(
|
||||
permLog.getHistory(),
|
||||
EXPECTED_HISTORIES.case2[0],
|
||||
'should have expected history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case2[0]);
|
||||
});
|
||||
|
||||
it('handles extra caveats for eth_accounts', async function () {
|
||||
it('handles extra caveats for eth_accounts', async () => {
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res = {
|
||||
@ -494,20 +437,14 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
// validate history
|
||||
|
||||
assert.deepEqual(
|
||||
permLog.getHistory(),
|
||||
EXPECTED_HISTORIES.case1[0],
|
||||
'should have correct history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[0]);
|
||||
});
|
||||
|
||||
// wallet_requestPermissions returns all permissions approved for the
|
||||
// requesting origin, including old ones
|
||||
it('handles unrequested permissions on the response', async function () {
|
||||
it('handles unrequested permissions on the response', async () => {
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res = {
|
||||
@ -519,18 +456,12 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
// validate history
|
||||
|
||||
assert.deepEqual(
|
||||
permLog.getHistory(),
|
||||
EXPECTED_HISTORIES.case1[0],
|
||||
'should have correct history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case1[0]);
|
||||
});
|
||||
|
||||
it('does not update history if no new permissions are approved', async function () {
|
||||
it('does not update history if no new permissions are approved', async () => {
|
||||
let req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
);
|
||||
let res = {
|
||||
@ -539,20 +470,14 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
// validate history
|
||||
|
||||
assert.deepEqual(
|
||||
permLog.getHistory(),
|
||||
EXPECTED_HISTORIES.case4[0],
|
||||
'should have correct history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case4[0]);
|
||||
|
||||
// new permission requested, but not approved
|
||||
|
||||
clock.tick(1);
|
||||
|
||||
req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
res = {
|
||||
@ -561,18 +486,11 @@ describe('permissions log', function () {
|
||||
|
||||
logMiddleware({ ...req }, { ...res });
|
||||
|
||||
// validate history
|
||||
|
||||
assert.deepEqual(
|
||||
permLog.getHistory(),
|
||||
EXPECTED_HISTORIES.case4[0],
|
||||
'should have same history as before',
|
||||
);
|
||||
// history should be unmodified
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case4[0]);
|
||||
});
|
||||
|
||||
it('records and updates history for multiple origins, regardless of response order', async function () {
|
||||
let permHistory;
|
||||
|
||||
it('records and updates history for multiple origins, regardless of response order', async () => {
|
||||
// make first round of requests
|
||||
|
||||
const round1 = [];
|
||||
@ -581,7 +499,7 @@ describe('permissions log', function () {
|
||||
// first origin
|
||||
round1.push({
|
||||
req: RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
),
|
||||
res: {
|
||||
@ -592,7 +510,7 @@ describe('permissions log', function () {
|
||||
// second origin
|
||||
round1.push({
|
||||
req: RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.b.origin,
|
||||
SUBJECTS.b.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
),
|
||||
res: {
|
||||
@ -602,7 +520,7 @@ describe('permissions log', function () {
|
||||
|
||||
// third origin
|
||||
round1.push({
|
||||
req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, {
|
||||
req: RPC_REQUESTS.requestPermissions(SUBJECTS.c.origin, {
|
||||
[PERM_NAMES.test_method]: {},
|
||||
[PERM_NAMES.eth_accounts]: {},
|
||||
}),
|
||||
@ -623,14 +541,7 @@ describe('permissions log', function () {
|
||||
handlers1[i](noop);
|
||||
}
|
||||
|
||||
// validate history
|
||||
permHistory = permLog.getHistory();
|
||||
|
||||
assert.deepEqual(
|
||||
permHistory,
|
||||
EXPECTED_HISTORIES.case3[0],
|
||||
'should have expected history',
|
||||
);
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case3[0]);
|
||||
|
||||
// make next round of requests
|
||||
|
||||
@ -642,7 +553,7 @@ describe('permissions log', function () {
|
||||
// first origin
|
||||
round2.push({
|
||||
req: RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
SUBJECTS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
),
|
||||
res: {
|
||||
@ -654,7 +565,7 @@ describe('permissions log', function () {
|
||||
|
||||
// third origin
|
||||
round2.push({
|
||||
req: RPC_REQUESTS.requestPermissions(DOMAINS.c.origin, {
|
||||
req: RPC_REQUESTS.requestPermissions(SUBJECTS.c.origin, {
|
||||
[PERM_NAMES.eth_accounts]: {},
|
||||
}),
|
||||
res: {
|
||||
@ -667,14 +578,90 @@ describe('permissions log', function () {
|
||||
logMiddleware({ ...x.req }, { ...x.res });
|
||||
});
|
||||
|
||||
// validate history
|
||||
permHistory = permLog.getHistory();
|
||||
expect(permLog.getHistory()).toStrictEqual(EXPECTED_HISTORIES.case3[1]);
|
||||
});
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
permHistory,
|
||||
EXPECTED_HISTORIES.case3[1],
|
||||
'should have expected history',
|
||||
);
|
||||
describe('updateAccountsHistory', () => {
|
||||
beforeEach(() => {
|
||||
initClock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tearDownClock();
|
||||
});
|
||||
|
||||
it('does nothing if the list of accounts is empty', () => {
|
||||
const permLog = initPermLog();
|
||||
permLog.updateAccountsHistory('foo.com', []);
|
||||
|
||||
expect(permLog.getHistory()).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('updates the account history', () => {
|
||||
const permLog = initPermLog({
|
||||
permissionHistory: {
|
||||
'foo.com': {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
accounts: {
|
||||
'0x1': 1,
|
||||
},
|
||||
lastApproved: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
clock.tick(1);
|
||||
permLog.updateAccountsHistory('foo.com', ['0x1', '0x2']);
|
||||
|
||||
expect(permLog.getHistory()).toStrictEqual({
|
||||
'foo.com': {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
accounts: {
|
||||
'0x1': 2,
|
||||
'0x2': 2,
|
||||
},
|
||||
lastApproved: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates an activity log entry with respect to a request, response, and
|
||||
* relevant metadata.
|
||||
*
|
||||
* @param {Object} entry - The activity log entry to validate.
|
||||
* @param {Object} req - The request that generated the entry.
|
||||
* @param {Object} [res] - The response for the request, if any.
|
||||
* @param {'restricted'|'internal'} methodType - The method log controller method type of the request.
|
||||
* @param {boolean} success - Whether the request succeeded or not.
|
||||
*/
|
||||
function validateActivityEntry(entry, req, res, methodType, success) {
|
||||
expect(entry).toBeDefined();
|
||||
|
||||
expect(entry.id).toStrictEqual(req.id);
|
||||
expect(entry.method).toStrictEqual(req.method);
|
||||
expect(entry.origin).toStrictEqual(req.origin);
|
||||
expect(entry.methodType).toStrictEqual(methodType);
|
||||
expect(entry.request).toStrictEqual(stringify(req, null, 2));
|
||||
|
||||
expect(Number.isInteger(entry.requestTime)).toBe(true);
|
||||
if (res) {
|
||||
expect(Number.isInteger(entry.responseTime)).toBe(true);
|
||||
expect(entry.requestTime <= entry.responseTime).toBe(true);
|
||||
|
||||
expect(entry.success).toStrictEqual(success);
|
||||
expect(entry.response).toStrictEqual(stringify(res, null, 2));
|
||||
} else {
|
||||
expect(entry.requestTime > 0).toBe(true);
|
||||
expect(entry).toMatchObject({
|
||||
response: null,
|
||||
responseTime: null,
|
||||
success: null,
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,950 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
constants,
|
||||
getters,
|
||||
getPermControllerOpts,
|
||||
getPermissionsMiddleware,
|
||||
} from '../../../../test/mocks/permission-controller';
|
||||
import {
|
||||
getUserApprovalPromise,
|
||||
grantPermissions,
|
||||
} from '../../../../test/helpers/permission-controller-helpers';
|
||||
import { METADATA_STORE_KEY } from './enums';
|
||||
|
||||
import { PermissionsController } from '.';
|
||||
|
||||
const { CAVEATS, ERRORS, PERMS, RPC_REQUESTS } = getters;
|
||||
|
||||
const { ACCOUNTS, DOMAINS, PERM_NAMES } = constants;
|
||||
|
||||
const initPermController = () => {
|
||||
return new PermissionsController({
|
||||
...getPermControllerOpts(),
|
||||
});
|
||||
};
|
||||
|
||||
const createApprovalSpies = (permController) => {
|
||||
sinon.spy(permController.approvals, '_add');
|
||||
};
|
||||
|
||||
const getNextApprovalId = (permController) => {
|
||||
return permController.approvals._approvals.keys().next().value;
|
||||
};
|
||||
|
||||
const validatePermission = (perm, name, origin, caveats) => {
|
||||
assert.equal(
|
||||
name,
|
||||
perm.parentCapability,
|
||||
'should have expected permission name',
|
||||
);
|
||||
assert.equal(origin, perm.invoker, 'should have expected permission origin');
|
||||
if (caveats) {
|
||||
assert.deepEqual(
|
||||
caveats,
|
||||
perm.caveats,
|
||||
'should have expected permission caveats',
|
||||
);
|
||||
} else {
|
||||
assert.ok(!perm.caveats, 'should not have any caveats');
|
||||
}
|
||||
};
|
||||
|
||||
describe('permissions middleware', function () {
|
||||
describe('wallet_requestPermissions', function () {
|
||||
let permController;
|
||||
|
||||
beforeEach(function () {
|
||||
permController = initPermController();
|
||||
permController.notifyAccountsChanged = sinon.fake();
|
||||
});
|
||||
|
||||
it('grants permissions on user approval', async function () {
|
||||
createApprovalSpies(permController);
|
||||
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res = {};
|
||||
|
||||
const userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const pendingApproval = assert.doesNotReject(
|
||||
aMiddleware(req, res),
|
||||
'should not reject permissions request',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
assert.ok(
|
||||
permController.approvals._add.calledOnce,
|
||||
'should have added single approval request',
|
||||
);
|
||||
|
||||
const id = getNextApprovalId(permController);
|
||||
const approvedReq = PERMS.approvedRequest(
|
||||
id,
|
||||
PERMS.requests.eth_accounts(),
|
||||
);
|
||||
|
||||
await permController.approvePermissionsRequest(
|
||||
approvedReq,
|
||||
ACCOUNTS.a.permitted,
|
||||
);
|
||||
await pendingApproval;
|
||||
|
||||
assert.ok(
|
||||
res.result && !res.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
res.result.length,
|
||||
1,
|
||||
'origin should have single approved permission',
|
||||
);
|
||||
|
||||
validatePermission(
|
||||
res.result[0],
|
||||
PERM_NAMES.eth_accounts,
|
||||
DOMAINS.a.origin,
|
||||
CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
|
||||
);
|
||||
|
||||
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
|
||||
assert.deepEqual(
|
||||
aAccounts,
|
||||
[ACCOUNTS.a.primary],
|
||||
'origin should have correct accounts',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
permController.notifyAccountsChanged.calledOnceWith(
|
||||
DOMAINS.a.origin,
|
||||
aAccounts,
|
||||
),
|
||||
'expected notification call should have been made',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles serial approved requests that overwrite existing permissions', async function () {
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
// create first request
|
||||
|
||||
const req1 = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res1 = {};
|
||||
|
||||
// send, approve, and validate first request
|
||||
// note use of ACCOUNTS.a.permitted
|
||||
|
||||
let userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const pendingApproval1 = assert.doesNotReject(
|
||||
aMiddleware(req1, res1),
|
||||
'should not reject permissions request',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
const id1 = getNextApprovalId(permController);
|
||||
const approvedReq1 = PERMS.approvedRequest(
|
||||
id1,
|
||||
PERMS.requests.eth_accounts(),
|
||||
);
|
||||
|
||||
await permController.approvePermissionsRequest(
|
||||
approvedReq1,
|
||||
ACCOUNTS.a.permitted,
|
||||
);
|
||||
await pendingApproval1;
|
||||
|
||||
assert.ok(
|
||||
res1.result && !res1.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
res1.result.length,
|
||||
1,
|
||||
'origin should have single approved permission',
|
||||
);
|
||||
|
||||
validatePermission(
|
||||
res1.result[0],
|
||||
PERM_NAMES.eth_accounts,
|
||||
DOMAINS.a.origin,
|
||||
CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
|
||||
);
|
||||
|
||||
const accounts1 = await permController.getAccounts(DOMAINS.a.origin);
|
||||
assert.deepEqual(
|
||||
accounts1,
|
||||
[ACCOUNTS.a.primary],
|
||||
'origin should have correct accounts',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
permController.notifyAccountsChanged.calledOnceWith(
|
||||
DOMAINS.a.origin,
|
||||
accounts1,
|
||||
),
|
||||
'expected notification call should have been made',
|
||||
);
|
||||
|
||||
// create second request
|
||||
|
||||
const requestedPerms2 = {
|
||||
...PERMS.requests.eth_accounts(),
|
||||
...PERMS.requests.test_method(),
|
||||
};
|
||||
|
||||
const req2 = RPC_REQUESTS.requestPermissions(DOMAINS.a.origin, {
|
||||
...requestedPerms2,
|
||||
});
|
||||
const res2 = {};
|
||||
|
||||
// send, approve, and validate second request
|
||||
// note use of ACCOUNTS.b.permitted
|
||||
|
||||
userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const pendingApproval2 = assert.doesNotReject(
|
||||
aMiddleware(req2, res2),
|
||||
'should not reject permissions request',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
const id2 = getNextApprovalId(permController);
|
||||
const approvedReq2 = PERMS.approvedRequest(id2, { ...requestedPerms2 });
|
||||
|
||||
await permController.approvePermissionsRequest(
|
||||
approvedReq2,
|
||||
ACCOUNTS.b.permitted,
|
||||
);
|
||||
await pendingApproval2;
|
||||
|
||||
assert.ok(
|
||||
res2.result && !res2.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
res2.result.length,
|
||||
2,
|
||||
'origin should have single approved permission',
|
||||
);
|
||||
|
||||
validatePermission(
|
||||
res2.result[0],
|
||||
PERM_NAMES.eth_accounts,
|
||||
DOMAINS.a.origin,
|
||||
CAVEATS.eth_accounts(ACCOUNTS.b.permitted),
|
||||
);
|
||||
|
||||
validatePermission(
|
||||
res2.result[1],
|
||||
PERM_NAMES.test_method,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const accounts2 = await permController.getAccounts(DOMAINS.a.origin);
|
||||
assert.deepEqual(
|
||||
accounts2,
|
||||
[ACCOUNTS.b.primary],
|
||||
'origin should have correct accounts',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
permController.notifyAccountsChanged.callCount,
|
||||
2,
|
||||
'should have called notification method 2 times in total',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
permController.notifyAccountsChanged.lastCall.calledWith(
|
||||
DOMAINS.a.origin,
|
||||
accounts2,
|
||||
),
|
||||
'expected notification call should have been made',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects permissions on user rejection', async function () {
|
||||
createApprovalSpies(permController);
|
||||
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
PERM_NAMES.eth_accounts,
|
||||
);
|
||||
const res = {};
|
||||
|
||||
const expectedError = ERRORS.rejectPermissionsRequest.rejection();
|
||||
|
||||
const userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const requestRejection = assert.rejects(
|
||||
aMiddleware(req, res),
|
||||
expectedError,
|
||||
'request should be rejected with correct error',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
assert.ok(
|
||||
permController.approvals._add.calledOnce,
|
||||
'should have added single approval request',
|
||||
);
|
||||
|
||||
const id = getNextApprovalId(permController);
|
||||
|
||||
await permController.rejectPermissionsRequest(id);
|
||||
await requestRejection;
|
||||
|
||||
assert.ok(
|
||||
!res.result && res.error && res.error.message === expectedError.message,
|
||||
'response should have expected error and no result',
|
||||
);
|
||||
|
||||
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
|
||||
assert.deepEqual(
|
||||
aAccounts,
|
||||
[],
|
||||
'origin should have have correct accounts',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
permController.notifyAccountsChanged.notCalled,
|
||||
'should not have called notification method',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects requests with unknown permissions', async function () {
|
||||
createApprovalSpies(permController);
|
||||
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.requestPermissions(DOMAINS.a.origin, {
|
||||
...PERMS.requests.does_not_exist(),
|
||||
...PERMS.requests.test_method(),
|
||||
});
|
||||
const res = {};
|
||||
|
||||
const expectedError = ERRORS.rejectPermissionsRequest.methodNotFound(
|
||||
PERM_NAMES.does_not_exist,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
aMiddleware(req, res),
|
||||
expectedError,
|
||||
'request should be rejected with correct error',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
permController.approvals._add.notCalled,
|
||||
'no approval requests should have been added',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!res.result && res.error && res.error.message === expectedError.message,
|
||||
'response should have expected error and no result',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
permController.notifyAccountsChanged.notCalled,
|
||||
'should not have called notification method',
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts only a single pending permissions request per origin', async function () {
|
||||
createApprovalSpies(permController);
|
||||
|
||||
// two middlewares for two origins
|
||||
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
const bMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.b.origin,
|
||||
);
|
||||
|
||||
// create and start processing first request for first origin
|
||||
|
||||
const reqA1 = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
);
|
||||
const resA1 = {};
|
||||
|
||||
let userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const requestApproval1 = assert.doesNotReject(
|
||||
aMiddleware(reqA1, resA1),
|
||||
'should not reject permissions request',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
// create and start processing first request for second origin
|
||||
|
||||
const reqB1 = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.b.origin,
|
||||
PERM_NAMES.test_method,
|
||||
);
|
||||
const resB1 = {};
|
||||
|
||||
userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const requestApproval2 = assert.doesNotReject(
|
||||
bMiddleware(reqB1, resB1),
|
||||
'should not reject permissions request',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
assert.ok(
|
||||
permController.approvals._add.calledTwice,
|
||||
'should have added two approval requests',
|
||||
);
|
||||
|
||||
// create and start processing second request for first origin,
|
||||
// which should throw
|
||||
|
||||
const reqA2 = RPC_REQUESTS.requestPermission(
|
||||
DOMAINS.a.origin,
|
||||
PERM_NAMES.test_method,
|
||||
);
|
||||
const resA2 = {};
|
||||
|
||||
userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const expectedError = ERRORS.pendingApprovals.requestAlreadyPending(
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const requestApprovalFail = assert.rejects(
|
||||
aMiddleware(reqA2, resA2),
|
||||
expectedError,
|
||||
'request should be rejected with correct error',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
await requestApprovalFail;
|
||||
|
||||
assert.ok(
|
||||
!resA2.result &&
|
||||
resA2.error &&
|
||||
resA2.error.message === expectedError.message,
|
||||
'response should have expected error and no result',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
permController.approvals._add.callCount,
|
||||
3,
|
||||
'should have attempted to create three pending approvals',
|
||||
);
|
||||
assert.equal(
|
||||
permController.approvals._approvals.size,
|
||||
2,
|
||||
'should only have created two pending approvals',
|
||||
);
|
||||
|
||||
// now, remaining pending requests should be approved without issue
|
||||
|
||||
for (const id of permController.approvals._approvals.keys()) {
|
||||
await permController.approvePermissionsRequest(
|
||||
PERMS.approvedRequest(id, PERMS.requests.test_method()),
|
||||
);
|
||||
}
|
||||
await requestApproval1;
|
||||
await requestApproval2;
|
||||
|
||||
assert.ok(
|
||||
resA1.result && !resA1.error,
|
||||
'first response should have result and no error',
|
||||
);
|
||||
assert.equal(
|
||||
resA1.result.length,
|
||||
1,
|
||||
'first origin should have single approved permission',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
resB1.result && !resB1.error,
|
||||
'second response should have result and no error',
|
||||
);
|
||||
assert.equal(
|
||||
resB1.result.length,
|
||||
1,
|
||||
'second origin should have single approved permission',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restricted methods', function () {
|
||||
let permController;
|
||||
|
||||
beforeEach(function () {
|
||||
permController = initPermController();
|
||||
});
|
||||
|
||||
it('prevents restricted method access for unpermitted domain', async function () {
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.test_method(DOMAINS.a.origin);
|
||||
const res = {};
|
||||
|
||||
const expectedError = ERRORS.rpcCap.unauthorized();
|
||||
|
||||
await assert.rejects(
|
||||
aMiddleware(req, res),
|
||||
expectedError,
|
||||
'request should be rejected with correct error',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!res.result && res.error && res.error.code === expectedError.code,
|
||||
'response should have expected error and no result',
|
||||
);
|
||||
});
|
||||
|
||||
it('allows restricted method access for permitted domain', async function () {
|
||||
const bMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.b.origin,
|
||||
);
|
||||
|
||||
grantPermissions(
|
||||
permController,
|
||||
DOMAINS.b.origin,
|
||||
PERMS.finalizedRequests.test_method(),
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.test_method(DOMAINS.b.origin, true);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(bMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(
|
||||
res.result && res.result === 1,
|
||||
'response should have correct result',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eth_accounts', function () {
|
||||
let permController;
|
||||
|
||||
beforeEach(function () {
|
||||
permController = initPermController();
|
||||
});
|
||||
|
||||
it('returns empty array for non-permitted domain', async function () {
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(aMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(
|
||||
res.result && !res.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
assert.deepEqual(res.result, [], 'response should have correct result');
|
||||
});
|
||||
|
||||
it('returns correct accounts for permitted domain', async function () {
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
grantPermissions(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted),
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.eth_accounts(DOMAINS.a.origin);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(aMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(
|
||||
res.result && !res.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
assert.deepEqual(
|
||||
res.result,
|
||||
[ACCOUNTS.a.primary],
|
||||
'response should have correct result',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eth_requestAccounts', function () {
|
||||
let permController;
|
||||
|
||||
beforeEach(function () {
|
||||
permController = initPermController();
|
||||
});
|
||||
|
||||
it('requests accounts for unpermitted origin, and approves on user approval', async function () {
|
||||
createApprovalSpies(permController);
|
||||
|
||||
const userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin);
|
||||
const res = {};
|
||||
|
||||
const pendingApproval = assert.doesNotReject(
|
||||
aMiddleware(req, res),
|
||||
'should not reject permissions request',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
assert.ok(
|
||||
permController.approvals._add.calledOnce,
|
||||
'should have added single approval request',
|
||||
);
|
||||
|
||||
const id = getNextApprovalId(permController);
|
||||
const approvedReq = PERMS.approvedRequest(
|
||||
id,
|
||||
PERMS.requests.eth_accounts(),
|
||||
);
|
||||
|
||||
await permController.approvePermissionsRequest(
|
||||
approvedReq,
|
||||
ACCOUNTS.a.permitted,
|
||||
);
|
||||
|
||||
// wait for permission to be granted
|
||||
await pendingApproval;
|
||||
|
||||
const perms = permController.permissions.getPermissionsForDomain(
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
perms.length,
|
||||
1,
|
||||
'domain should have correct number of permissions',
|
||||
);
|
||||
|
||||
validatePermission(
|
||||
perms[0],
|
||||
PERM_NAMES.eth_accounts,
|
||||
DOMAINS.a.origin,
|
||||
CAVEATS.eth_accounts(ACCOUNTS.a.permitted),
|
||||
);
|
||||
|
||||
// we should also see the accounts on the response
|
||||
assert.ok(
|
||||
res.result && !res.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
res.result,
|
||||
[ACCOUNTS.a.primary],
|
||||
'result should have correct accounts',
|
||||
);
|
||||
|
||||
// we should also be able to get the accounts independently
|
||||
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
|
||||
assert.deepEqual(
|
||||
aAccounts,
|
||||
[ACCOUNTS.a.primary],
|
||||
'origin should have have correct accounts',
|
||||
);
|
||||
});
|
||||
|
||||
it('requests accounts for unpermitted origin, and rejects on user rejection', async function () {
|
||||
createApprovalSpies(permController);
|
||||
|
||||
const userApprovalPromise = getUserApprovalPromise(permController);
|
||||
|
||||
const aMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.a.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.a.origin);
|
||||
const res = {};
|
||||
|
||||
const expectedError = ERRORS.rejectPermissionsRequest.rejection();
|
||||
|
||||
const requestRejection = assert.rejects(
|
||||
aMiddleware(req, res),
|
||||
expectedError,
|
||||
'request should be rejected with correct error',
|
||||
);
|
||||
|
||||
await userApprovalPromise;
|
||||
|
||||
assert.ok(
|
||||
permController.approvals._add.calledOnce,
|
||||
'should have added single approval request',
|
||||
);
|
||||
|
||||
const id = getNextApprovalId(permController);
|
||||
|
||||
await permController.rejectPermissionsRequest(id);
|
||||
await requestRejection;
|
||||
|
||||
assert.ok(
|
||||
!res.result && res.error && res.error.message === expectedError.message,
|
||||
'response should have expected error and no result',
|
||||
);
|
||||
|
||||
const aAccounts = await permController.getAccounts(DOMAINS.a.origin);
|
||||
assert.deepEqual(
|
||||
aAccounts,
|
||||
[],
|
||||
'origin should have have correct accounts',
|
||||
);
|
||||
});
|
||||
|
||||
it('directly returns accounts for permitted domain', async function () {
|
||||
const cMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
);
|
||||
|
||||
grantPermissions(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(
|
||||
res.result && !res.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
assert.deepEqual(
|
||||
res.result,
|
||||
[ACCOUNTS.c.primary],
|
||||
'response should have correct result',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects new requests when request already pending', async function () {
|
||||
let unlock;
|
||||
const unlockPromise = new Promise((resolve) => {
|
||||
unlock = resolve;
|
||||
});
|
||||
|
||||
permController.getUnlockPromise = () => unlockPromise;
|
||||
|
||||
const cMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
);
|
||||
|
||||
grantPermissions(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.c.permitted),
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.eth_requestAccounts(DOMAINS.c.origin);
|
||||
const res = {};
|
||||
|
||||
// this will block until we resolve the unlock Promise
|
||||
const requestApproval = assert.doesNotReject(
|
||||
cMiddleware(req, res),
|
||||
'should not reject',
|
||||
);
|
||||
|
||||
// this will reject because of the already pending request
|
||||
await assert.rejects(
|
||||
cMiddleware({ ...req }, {}),
|
||||
ERRORS.eth_requestAccounts.requestAlreadyPending(DOMAINS.c.origin),
|
||||
);
|
||||
|
||||
// now unlock and let through the first request
|
||||
unlock();
|
||||
|
||||
await requestApproval;
|
||||
|
||||
assert.ok(
|
||||
res.result && !res.error,
|
||||
'response should have result and no error',
|
||||
);
|
||||
assert.deepEqual(
|
||||
res.result,
|
||||
[ACCOUNTS.c.primary],
|
||||
'response should have correct result',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metamask_sendDomainMetadata', function () {
|
||||
let permController, clock;
|
||||
|
||||
beforeEach(function () {
|
||||
permController = initPermController();
|
||||
clock = sinon.useFakeTimers(1);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('records domain metadata', async function () {
|
||||
const name = 'BAZ';
|
||||
|
||||
const cMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
|
||||
DOMAINS.c.origin,
|
||||
name,
|
||||
);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(res.result, 'result should be true');
|
||||
|
||||
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
|
||||
|
||||
assert.deepEqual(
|
||||
metadataStore,
|
||||
{
|
||||
[DOMAINS.c.origin]: {
|
||||
name,
|
||||
host: DOMAINS.c.host,
|
||||
lastUpdated: 1,
|
||||
},
|
||||
},
|
||||
'metadata should have been added to store',
|
||||
);
|
||||
});
|
||||
|
||||
it('records domain metadata and preserves extensionId', async function () {
|
||||
const extensionId = 'fooExtension';
|
||||
|
||||
const name = 'BAZ';
|
||||
|
||||
const cMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
extensionId,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
|
||||
DOMAINS.c.origin,
|
||||
name,
|
||||
);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(res.result, 'result should be true');
|
||||
|
||||
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
|
||||
|
||||
assert.deepEqual(
|
||||
metadataStore,
|
||||
{ [DOMAINS.c.origin]: { name, extensionId, lastUpdated: 1 } },
|
||||
'metadata should have been added to store',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not record domain metadata if no name', async function () {
|
||||
const name = null;
|
||||
|
||||
const cMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.metamask_sendDomainMetadata(
|
||||
DOMAINS.c.origin,
|
||||
name,
|
||||
);
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(res.result, 'result should be true');
|
||||
|
||||
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
|
||||
|
||||
assert.deepEqual(
|
||||
metadataStore,
|
||||
{},
|
||||
'metadata should not have been added to store',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not record domain metadata if no metadata', async function () {
|
||||
const cMiddleware = getPermissionsMiddleware(
|
||||
permController,
|
||||
DOMAINS.c.origin,
|
||||
);
|
||||
|
||||
const req = RPC_REQUESTS.metamask_sendDomainMetadata(DOMAINS.c.origin);
|
||||
delete req.domainMetadata;
|
||||
const res = {};
|
||||
|
||||
await assert.doesNotReject(cMiddleware(req, res), 'should not reject');
|
||||
|
||||
assert.ok(res.result, 'result should be true');
|
||||
|
||||
const metadataStore = permController.store.getState()[METADATA_STORE_KEY];
|
||||
|
||||
assert.deepEqual(
|
||||
metadataStore,
|
||||
{},
|
||||
'metadata should not have been added to store',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,112 +0,0 @@
|
||||
import { createAsyncMiddleware } from 'json-rpc-engine';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
|
||||
/**
|
||||
* Create middleware for handling certain methods and preprocessing permissions requests.
|
||||
*/
|
||||
export default function createPermissionsMethodMiddleware({
|
||||
addDomainMetadata,
|
||||
getAccounts,
|
||||
getUnlockPromise,
|
||||
hasPermission,
|
||||
notifyAccountsChanged,
|
||||
requestAccountsPermission,
|
||||
}) {
|
||||
let isProcessingRequestAccounts = false;
|
||||
|
||||
return createAsyncMiddleware(async (req, res, next) => {
|
||||
let responseHandler;
|
||||
|
||||
switch (req.method) {
|
||||
// Intercepting eth_accounts requests for backwards compatibility:
|
||||
// The getAccounts call below wraps the rpc-cap middleware, and returns
|
||||
// an empty array in case of errors (such as 4100:unauthorized)
|
||||
case 'eth_accounts': {
|
||||
res.result = await getAccounts();
|
||||
return;
|
||||
}
|
||||
|
||||
case 'eth_requestAccounts': {
|
||||
if (isProcessingRequestAccounts) {
|
||||
res.error = ethErrors.rpc.resourceUnavailable(
|
||||
'Already processing eth_requestAccounts. Please wait.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPermission('eth_accounts')) {
|
||||
isProcessingRequestAccounts = true;
|
||||
await getUnlockPromise();
|
||||
isProcessingRequestAccounts = false;
|
||||
}
|
||||
|
||||
// first, just try to get accounts
|
||||
let accounts = await getAccounts();
|
||||
if (accounts.length > 0) {
|
||||
res.result = accounts;
|
||||
return;
|
||||
}
|
||||
|
||||
// if no accounts, request the accounts permission
|
||||
try {
|
||||
await requestAccountsPermission();
|
||||
} catch (err) {
|
||||
res.error = err;
|
||||
return;
|
||||
}
|
||||
|
||||
// get the accounts again
|
||||
accounts = await getAccounts();
|
||||
/* istanbul ignore else: too hard to induce, see below comment */
|
||||
if (accounts.length > 0) {
|
||||
res.result = accounts;
|
||||
} else {
|
||||
// this should never happen, because it should be caught in the
|
||||
// above catch clause
|
||||
res.error = ethErrors.rpc.internal(
|
||||
'Accounts unexpectedly unavailable. Please report this bug.',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// custom method for getting metadata from the requesting domain,
|
||||
// sent automatically by the inpage provider when it's initialized
|
||||
case 'metamask_sendDomainMetadata': {
|
||||
if (typeof req.params?.name === 'string') {
|
||||
addDomainMetadata(req.origin, req.params);
|
||||
}
|
||||
res.result = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// register return handler to send accountsChanged notification
|
||||
case 'wallet_requestPermissions': {
|
||||
if ('eth_accounts' in req.params?.[0]) {
|
||||
responseHandler = async () => {
|
||||
if (Array.isArray(res.result)) {
|
||||
for (const permission of res.result) {
|
||||
if (permission.parentCapability === 'eth_accounts') {
|
||||
notifyAccountsChanged(await getAccounts());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// when this promise resolves, the response is on its way back
|
||||
// eslint-disable-next-line node/callback-return
|
||||
await next();
|
||||
|
||||
if (responseHandler) {
|
||||
responseHandler();
|
||||
}
|
||||
});
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import pify from 'pify';
|
||||
|
||||
import getRestrictedMethods from './restrictedMethods';
|
||||
|
||||
describe('restricted methods', function () {
|
||||
describe('eth_accounts', function () {
|
||||
it('should handle getKeyringAccounts error', async function () {
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getKeyringAccounts: async () => {
|
||||
throw new Error('foo');
|
||||
},
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
const fooError = new Error('foo');
|
||||
await assert.rejects(
|
||||
ethAccountsMethod(null, res, null),
|
||||
fooError,
|
||||
'Should reject with expected error',
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
res,
|
||||
{ error: fooError },
|
||||
'response should have expected error and no result',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing identity for first account when sorting', async function () {
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getIdentities: () => {
|
||||
return { '0x7e57e2': {} };
|
||||
},
|
||||
getKeyringAccounts: async () => ['0x7e57e2', '0x7e57e3'],
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
await assert.rejects(ethAccountsMethod(null, res, null));
|
||||
assert.ok(res.error instanceof Error, 'result should have error');
|
||||
assert.deepEqual(
|
||||
Object.keys(res),
|
||||
['error'],
|
||||
'result should only contain error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing identity for second account when sorting', async function () {
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getIdentities: () => {
|
||||
return { '0x7e57e3': {} };
|
||||
},
|
||||
getKeyringAccounts: async () => ['0x7e57e2', '0x7e57e3'],
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
await assert.rejects(ethAccountsMethod(null, res, null));
|
||||
assert.ok(res.error instanceof Error, 'result should have error');
|
||||
assert.deepEqual(
|
||||
Object.keys(res),
|
||||
['error'],
|
||||
'result should only contain error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return accounts in keyring order when none are selected', async function () {
|
||||
const keyringAccounts = ['0x7e57e2', '0x7e57e3', '0x7e57e4', '0x7e57e5'];
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getIdentities: () => {
|
||||
return keyringAccounts.reduce((identities, address) => {
|
||||
identities[address] = {};
|
||||
return identities;
|
||||
}, {});
|
||||
},
|
||||
getKeyringAccounts: async () => [...keyringAccounts],
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
await ethAccountsMethod(null, res, null);
|
||||
assert.deepEqual(
|
||||
res,
|
||||
{ result: keyringAccounts },
|
||||
'should return accounts in correct order',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return accounts in keyring order when all have same last selected time', async function () {
|
||||
const keyringAccounts = ['0x7e57e2', '0x7e57e3', '0x7e57e4', '0x7e57e5'];
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getIdentities: () => {
|
||||
return keyringAccounts.reduce((identities, address) => {
|
||||
identities[address] = { lastSelected: 1000 };
|
||||
return identities;
|
||||
}, {});
|
||||
},
|
||||
getKeyringAccounts: async () => [...keyringAccounts],
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
await ethAccountsMethod(null, res, null);
|
||||
assert.deepEqual(
|
||||
res,
|
||||
{ result: keyringAccounts },
|
||||
'should return accounts in correct order',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return accounts sorted by last selected (descending)', async function () {
|
||||
const keyringAccounts = ['0x7e57e2', '0x7e57e3', '0x7e57e4', '0x7e57e5'];
|
||||
const expectedResult = keyringAccounts.slice().reverse();
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getIdentities: () => {
|
||||
return keyringAccounts.reduce((identities, address, index) => {
|
||||
identities[address] = { lastSelected: index * 1000 };
|
||||
return identities;
|
||||
}, {});
|
||||
},
|
||||
getKeyringAccounts: async () => [...keyringAccounts],
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
await ethAccountsMethod(null, res, null);
|
||||
assert.deepEqual(
|
||||
res,
|
||||
{ result: expectedResult },
|
||||
'should return accounts in correct order',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return accounts sorted by last selected (descending) with unselected accounts last, in keyring order', async function () {
|
||||
const keyringAccounts = [
|
||||
'0x7e57e2',
|
||||
'0x7e57e3',
|
||||
'0x7e57e4',
|
||||
'0x7e57e5',
|
||||
'0x7e57e6',
|
||||
];
|
||||
const expectedResult = [
|
||||
'0x7e57e4',
|
||||
'0x7e57e2',
|
||||
'0x7e57e3',
|
||||
'0x7e57e5',
|
||||
'0x7e57e6',
|
||||
];
|
||||
const restrictedMethods = getRestrictedMethods({
|
||||
getIdentities: () => {
|
||||
return {
|
||||
'0x7e57e2': { lastSelected: 1000 },
|
||||
'0x7e57e3': {},
|
||||
'0x7e57e4': { lastSelected: 2000 },
|
||||
'0x7e57e5': {},
|
||||
'0x7e57e6': {},
|
||||
};
|
||||
},
|
||||
getKeyringAccounts: async () => [...keyringAccounts],
|
||||
});
|
||||
const ethAccountsMethod = pify(restrictedMethods.eth_accounts.method);
|
||||
|
||||
const res = {};
|
||||
await ethAccountsMethod(null, res, null);
|
||||
assert.deepEqual(
|
||||
res,
|
||||
{ result: expectedResult },
|
||||
'should return accounts in correct order',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,40 +0,0 @@
|
||||
export default function getRestrictedMethods({
|
||||
getIdentities,
|
||||
getKeyringAccounts,
|
||||
}) {
|
||||
return {
|
||||
eth_accounts: {
|
||||
method: async (_, res, __, end) => {
|
||||
try {
|
||||
const accounts = await getKeyringAccounts();
|
||||
const identities = getIdentities();
|
||||
res.result = accounts.sort((firstAddress, secondAddress) => {
|
||||
if (!identities[firstAddress]) {
|
||||
throw new Error(`Missing identity for address ${firstAddress}`);
|
||||
} else if (!identities[secondAddress]) {
|
||||
throw new Error(`Missing identity for address ${secondAddress}`);
|
||||
} else if (
|
||||
identities[firstAddress].lastSelected ===
|
||||
identities[secondAddress].lastSelected
|
||||
) {
|
||||
return 0;
|
||||
} else if (identities[firstAddress].lastSelected === undefined) {
|
||||
return 1;
|
||||
} else if (identities[secondAddress].lastSelected === undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (
|
||||
identities[secondAddress].lastSelected -
|
||||
identities[firstAddress].lastSelected
|
||||
);
|
||||
});
|
||||
end();
|
||||
} catch (err) {
|
||||
res.error = err;
|
||||
end(err);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
84
app/scripts/controllers/permissions/selectors.js
Normal file
84
app/scripts/controllers/permissions/selectors.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { CaveatTypes } from '../../../../shared/constants/permissions';
|
||||
|
||||
/**
|
||||
* This file contains selectors for PermissionController selector event
|
||||
* subscriptions, used to detect whenever a subject's accounts change so that
|
||||
* we can notify the subject via the `accountsChanged` provider event.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Record<string, Record<string, unknown>>} state - The
|
||||
* PermissionController state.
|
||||
* @returns {Record<string, unknown>} The PermissionController subjects.
|
||||
*/
|
||||
const getSubjects = (state) => state.subjects;
|
||||
|
||||
/**
|
||||
* Get the permitted accounts for each subject, keyed by origin.
|
||||
* The values of the returned map are immutable values from the
|
||||
* PermissionController state.
|
||||
*
|
||||
* @returns {Map<string, string[]>} The current origin:accounts[] map.
|
||||
*/
|
||||
export const getPermittedAccountsByOrigin = createSelector(
|
||||
getSubjects,
|
||||
(subjects) => {
|
||||
return Object.values(subjects).reduce((originToAccountsMap, subject) => {
|
||||
const caveat = subject.permissions?.eth_accounts?.caveats.find(
|
||||
({ type }) => type === CaveatTypes.restrictReturnedAccounts,
|
||||
);
|
||||
|
||||
if (caveat) {
|
||||
originToAccountsMap.set(subject.origin, caveat.value);
|
||||
}
|
||||
return originToAccountsMap;
|
||||
}, new Map());
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Given the current and previous exposed accounts for each PermissionController
|
||||
* subject, returns a new map containing all accounts that have changed.
|
||||
* The values of each map must be immutable values directly from the
|
||||
* PermissionController state, or an empty array instantiated in this
|
||||
* function.
|
||||
*
|
||||
* @param {Map<string, string[]>} newAccountsMap - The new origin:accounts[] map.
|
||||
* @param {Map<string, string[]>} [previousAccountsMap] - The previous origin:accounts[] map.
|
||||
* @returns {Map<string, string[]>} The origin:accounts[] map of changed accounts.
|
||||
*/
|
||||
export const getChangedAccounts = (newAccountsMap, previousAccountsMap) => {
|
||||
if (previousAccountsMap === undefined) {
|
||||
return newAccountsMap;
|
||||
}
|
||||
|
||||
const changedAccounts = new Map();
|
||||
if (newAccountsMap === previousAccountsMap) {
|
||||
return changedAccounts;
|
||||
}
|
||||
|
||||
const newOrigins = new Set([...newAccountsMap.keys()]);
|
||||
|
||||
for (const origin of previousAccountsMap.keys()) {
|
||||
const newAccounts = newAccountsMap.get(origin) ?? [];
|
||||
|
||||
// The values of these maps are references to immutable values, which is why
|
||||
// a strict equality check is enough for diffing. The values are either from
|
||||
// PermissionController state, or an empty array initialized in the previous
|
||||
// call to this function. `newAccountsMap` will never contain any empty
|
||||
// arrays.
|
||||
if (previousAccountsMap.get(origin) !== newAccounts) {
|
||||
changedAccounts.set(origin, newAccounts);
|
||||
}
|
||||
|
||||
newOrigins.delete(origin);
|
||||
}
|
||||
|
||||
// By now, newOrigins is either empty or contains some number of previously
|
||||
// unencountered origins, and all of their accounts have "changed".
|
||||
for (const origin of newOrigins.keys()) {
|
||||
changedAccounts.set(origin, newAccountsMap.get(origin));
|
||||
}
|
||||
return changedAccounts;
|
||||
};
|
116
app/scripts/controllers/permissions/selectors.test.js
Normal file
116
app/scripts/controllers/permissions/selectors.test.js
Normal file
@ -0,0 +1,116 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getChangedAccounts, getPermittedAccountsByOrigin } from './selectors';
|
||||
|
||||
describe('PermissionController selectors', () => {
|
||||
describe('getChangedAccounts', () => {
|
||||
it('returns the new value if the previous value is undefined', () => {
|
||||
const newAccounts = new Map([['foo.bar', ['0x1']]]);
|
||||
expect(getChangedAccounts(newAccounts)).toBe(newAccounts);
|
||||
});
|
||||
|
||||
it('returns an empty map if the new and previous values are the same', () => {
|
||||
const newAccounts = new Map([['foo.bar', ['0x1']]]);
|
||||
expect(getChangedAccounts(newAccounts, newAccounts)).toStrictEqual(
|
||||
new Map(),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a new map of the changed accounts if the new and previous values differ', () => {
|
||||
// We set this on the new and previous value under the key 'foo.bar' to
|
||||
// check that identical values are excluded.
|
||||
const identicalValue = ['0x1'];
|
||||
|
||||
const previousAccounts = new Map([
|
||||
['bar.baz', ['0x1']], // included: different accounts
|
||||
['fizz.buzz', ['0x1']], // included: removed in new value
|
||||
]);
|
||||
previousAccounts.set('foo.bar', identicalValue);
|
||||
|
||||
const newAccounts = new Map([
|
||||
['bar.baz', ['0x1', '0x2']], // included: different accounts
|
||||
['baz.fizz', ['0x3']], // included: brand new
|
||||
]);
|
||||
newAccounts.set('foo.bar', identicalValue);
|
||||
|
||||
expect(getChangedAccounts(newAccounts, previousAccounts)).toStrictEqual(
|
||||
new Map([
|
||||
['bar.baz', ['0x1', '0x2']],
|
||||
['fizz.buzz', []],
|
||||
['baz.fizz', ['0x3']],
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPermittedAccountsByOrigin', () => {
|
||||
it('memoizes and gets permitted accounts by origin', () => {
|
||||
const state1 = {
|
||||
subjects: {
|
||||
'foo.bar': {
|
||||
origin: 'foo.bar',
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [{ type: 'restrictReturnedAccounts', value: ['0x1'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
'bar.baz': {
|
||||
origin: 'bar.baz',
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [{ type: 'restrictReturnedAccounts', value: ['0x2'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
'baz.bizz': {
|
||||
origin: 'baz.fizz',
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{ type: 'restrictReturnedAccounts', value: ['0x1', '0x2'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'no.accounts': {
|
||||
// we shouldn't see this in the result
|
||||
permissions: {
|
||||
foobar: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expected1 = new Map([
|
||||
['foo.bar', ['0x1']],
|
||||
['bar.baz', ['0x2']],
|
||||
['baz.fizz', ['0x1', '0x2']],
|
||||
]);
|
||||
|
||||
const selected1 = getPermittedAccountsByOrigin(state1);
|
||||
|
||||
expect(selected1).toStrictEqual(expected1);
|
||||
// The selector should return the memoized value if state.subjects is
|
||||
// the same object
|
||||
expect(selected1).toBe(getPermittedAccountsByOrigin(state1));
|
||||
|
||||
// If we mutate the state, the selector return value should be different
|
||||
// from the first.
|
||||
const state2 = cloneDeep(state1);
|
||||
delete state2.subjects['foo.bar'];
|
||||
|
||||
const expected2 = new Map([
|
||||
['bar.baz', ['0x2']],
|
||||
['baz.fizz', ['0x1', '0x2']],
|
||||
]);
|
||||
|
||||
const selected2 = getPermittedAccountsByOrigin(state2);
|
||||
|
||||
expect(selected2).toStrictEqual(expected2);
|
||||
expect(selected2).not.toBe(selected1);
|
||||
// Since we didn't mutate the state at this point, the value should once
|
||||
// again be the memoized.
|
||||
expect(selected2).toBe(getPermittedAccountsByOrigin(state2));
|
||||
});
|
||||
});
|
||||
});
|
258
app/scripts/controllers/permissions/specifications.js
Normal file
258
app/scripts/controllers/permissions/specifications.js
Normal file
@ -0,0 +1,258 @@
|
||||
import { constructPermission } from '@metamask/snap-controllers';
|
||||
import {
|
||||
CaveatTypes,
|
||||
RestrictedMethods,
|
||||
} from '../../../../shared/constants/permissions';
|
||||
|
||||
/**
|
||||
* This file contains the specifications of the permissions and caveats
|
||||
* that are recognized by our permission system. See the PermissionController
|
||||
* README in @metamask/snap-controllers for details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The "keys" of all of permissions recognized by the PermissionController.
|
||||
* Permission keys and names have distinct meanings in the permission system.
|
||||
*/
|
||||
const PermissionKeys = Object.freeze({
|
||||
...RestrictedMethods,
|
||||
});
|
||||
|
||||
/**
|
||||
* Factory functions for all caveat types recognized by the
|
||||
* PermissionController.
|
||||
*/
|
||||
const CaveatFactories = Object.freeze({
|
||||
[CaveatTypes.restrictReturnedAccounts]: (accounts) => {
|
||||
return { type: CaveatTypes.restrictReturnedAccounts, value: accounts };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A PreferencesController identity object.
|
||||
*
|
||||
* @typedef {Object} Identity
|
||||
* @property {string} address - The address of the identity.
|
||||
* @property {string} name - The name of the identity.
|
||||
* @property {number} [lastSelected] - Unix timestamp of when the identity was
|
||||
* last selected in the UI.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the specifications for all caveats that will be recognized by the
|
||||
* PermissionController.
|
||||
*
|
||||
* @param {{
|
||||
* getIdentities: () => Record<string, Identity>,
|
||||
* }} options - Options bag.
|
||||
*/
|
||||
export const getCaveatSpecifications = ({ getIdentities }) => {
|
||||
return {
|
||||
[CaveatTypes.restrictReturnedAccounts]: {
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
|
||||
decorator: (method, caveat) => {
|
||||
return async (args) => {
|
||||
const result = await method(args);
|
||||
return result
|
||||
.filter((account) => caveat.value.includes(account))
|
||||
.slice(0, 1);
|
||||
};
|
||||
},
|
||||
|
||||
validator: (caveat, _origin, _target) =>
|
||||
validateCaveatAccounts(caveat.value, getIdentities),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the specifications for all permissions that will be recognized by the
|
||||
* PermissionController.
|
||||
*
|
||||
* @param {{
|
||||
* getAllAccounts: () => Promise<string[]>,
|
||||
* getIdentities: () => Record<string, Identity>,
|
||||
* }} options - Options bag.
|
||||
* @param options.getAllAccounts - A function that returns all Ethereum accounts
|
||||
* in the current MetaMask instance.
|
||||
* @param options.getIdentities - A function that returns the
|
||||
* `PreferencesController` identity objects for all Ethereum accounts in the
|
||||
* current MetaMask instance.
|
||||
*/
|
||||
export const getPermissionSpecifications = ({
|
||||
getAllAccounts,
|
||||
getIdentities,
|
||||
}) => {
|
||||
return {
|
||||
[PermissionKeys.eth_accounts]: {
|
||||
targetKey: PermissionKeys.eth_accounts,
|
||||
allowedCaveats: [CaveatTypes.restrictReturnedAccounts],
|
||||
|
||||
factory: (permissionOptions, requestData) => {
|
||||
if (Array.isArray(permissionOptions.caveats)) {
|
||||
throw new Error(
|
||||
`${PermissionKeys.eth_accounts} error: Received unexpected caveats. Any permitted caveats will be added automatically.`,
|
||||
);
|
||||
}
|
||||
|
||||
// This value will be further validated as part of the caveat.
|
||||
if (!requestData.approvedAccounts) {
|
||||
throw new Error(
|
||||
`${PermissionKeys.eth_accounts} error: No approved accounts specified.`,
|
||||
);
|
||||
}
|
||||
|
||||
return constructPermission({
|
||||
...permissionOptions,
|
||||
caveats: [
|
||||
CaveatFactories[CaveatTypes.restrictReturnedAccounts](
|
||||
requestData.approvedAccounts,
|
||||
),
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
methodImplementation: async (_args) => {
|
||||
const accounts = await getAllAccounts();
|
||||
const identities = getIdentities();
|
||||
|
||||
return accounts.sort((firstAddress, secondAddress) => {
|
||||
if (!identities[firstAddress]) {
|
||||
throw new Error(`Missing identity for address: "${firstAddress}".`);
|
||||
} else if (!identities[secondAddress]) {
|
||||
throw new Error(
|
||||
`Missing identity for address: "${secondAddress}".`,
|
||||
);
|
||||
} else if (
|
||||
identities[firstAddress].lastSelected ===
|
||||
identities[secondAddress].lastSelected
|
||||
) {
|
||||
return 0;
|
||||
} else if (identities[firstAddress].lastSelected === undefined) {
|
||||
return 1;
|
||||
} else if (identities[secondAddress].lastSelected === undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (
|
||||
identities[secondAddress].lastSelected -
|
||||
identities[firstAddress].lastSelected
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
validator: (permission, _origin, _target) => {
|
||||
const { caveats } = permission;
|
||||
if (
|
||||
!caveats ||
|
||||
caveats.length !== 1 ||
|
||||
caveats[0].type !== CaveatTypes.restrictReturnedAccounts
|
||||
) {
|
||||
throw new Error(
|
||||
`${PermissionKeys.eth_accounts} error: Invalid caveats. There must be a single caveat of type "${CaveatTypes.restrictReturnedAccounts}".`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the accounts associated with a caveat. In essence, ensures that
|
||||
* the accounts value is an array of non-empty strings, and that each string
|
||||
* corresponds to a PreferencesController identity.
|
||||
*
|
||||
* @param {string[]} accounts - The accounts associated with the caveat.
|
||||
* @param {() => Record<string, Identity>} getIdentities - Gets all
|
||||
* PreferencesController identities.
|
||||
*/
|
||||
function validateCaveatAccounts(accounts, getIdentities) {
|
||||
if (!Array.isArray(accounts) || accounts.length === 0) {
|
||||
throw new Error(
|
||||
`${PermissionKeys.eth_accounts} error: Expected non-empty array of Ethereum addresses.`,
|
||||
);
|
||||
}
|
||||
|
||||
const identities = getIdentities();
|
||||
accounts.forEach((address) => {
|
||||
if (!address || typeof address !== 'string') {
|
||||
throw new Error(
|
||||
`${PermissionKeys.eth_accounts} error: Expected an array of Ethereum addresses. Received: "${address}".`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!identities[address]) {
|
||||
throw new Error(
|
||||
`${PermissionKeys.eth_accounts} error: Received unrecognized address: "${address}".`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* All unrestricted methods recognized by the PermissionController.
|
||||
* Unrestricted methods are ignored by the permission system, but every
|
||||
* JSON-RPC request seen by the permission system must correspond to a
|
||||
* restricted or unrestricted method, or the request will be rejected with a
|
||||
* "method not found" error.
|
||||
*/
|
||||
export const unrestrictedMethods = Object.freeze([
|
||||
'eth_blockNumber',
|
||||
'eth_call',
|
||||
'eth_chainId',
|
||||
'eth_coinbase',
|
||||
'eth_decrypt',
|
||||
'eth_estimateGas',
|
||||
'eth_feeHistory',
|
||||
'eth_gasPrice',
|
||||
'eth_getBalance',
|
||||
'eth_getBlockByHash',
|
||||
'eth_getBlockByNumber',
|
||||
'eth_getBlockTransactionCountByHash',
|
||||
'eth_getBlockTransactionCountByNumber',
|
||||
'eth_getCode',
|
||||
'eth_getEncryptionPublicKey',
|
||||
'eth_getFilterChanges',
|
||||
'eth_getFilterLogs',
|
||||
'eth_getLogs',
|
||||
'eth_getProof',
|
||||
'eth_getStorageAt',
|
||||
'eth_getTransactionByBlockHashAndIndex',
|
||||
'eth_getTransactionByBlockNumberAndIndex',
|
||||
'eth_getTransactionByHash',
|
||||
'eth_getTransactionCount',
|
||||
'eth_getTransactionReceipt',
|
||||
'eth_getUncleByBlockHashAndIndex',
|
||||
'eth_getUncleByBlockNumberAndIndex',
|
||||
'eth_getUncleCountByBlockHash',
|
||||
'eth_getUncleCountByBlockNumber',
|
||||
'eth_getWork',
|
||||
'eth_hashrate',
|
||||
'eth_mining',
|
||||
'eth_newBlockFilter',
|
||||
'eth_newFilter',
|
||||
'eth_newPendingTransactionFilter',
|
||||
'eth_protocolVersion',
|
||||
'eth_sendRawTransaction',
|
||||
'eth_sendTransaction',
|
||||
'eth_sign',
|
||||
'eth_signTypedData',
|
||||
'eth_signTypedData_v1',
|
||||
'eth_signTypedData_v3',
|
||||
'eth_signTypedData_v4',
|
||||
'eth_submitHashrate',
|
||||
'eth_submitWork',
|
||||
'eth_syncing',
|
||||
'eth_uninstallFilter',
|
||||
'metamask_getProviderState',
|
||||
'metamask_watchAsset',
|
||||
'net_listening',
|
||||
'net_peerCount',
|
||||
'net_version',
|
||||
'personal_ecRecover',
|
||||
'personal_sign',
|
||||
'wallet_watchAsset',
|
||||
'web3_clientVersion',
|
||||
'web3_sha3',
|
||||
]);
|
340
app/scripts/controllers/permissions/specifications.test.js
Normal file
340
app/scripts/controllers/permissions/specifications.test.js
Normal file
@ -0,0 +1,340 @@
|
||||
import {
|
||||
CaveatTypes,
|
||||
RestrictedMethods,
|
||||
} from '../../../../shared/constants/permissions';
|
||||
import {
|
||||
getCaveatSpecifications,
|
||||
getPermissionSpecifications,
|
||||
unrestrictedMethods,
|
||||
} from './specifications';
|
||||
|
||||
// Note: This causes Date.now() to return the number 1.
|
||||
jest.useFakeTimers('modern').setSystemTime(1);
|
||||
|
||||
describe('PermissionController specifications', () => {
|
||||
describe('caveat specifications', () => {
|
||||
it('getCaveatSpecifications returns the expected specifications object', () => {
|
||||
const caveatSpecifications = getCaveatSpecifications({});
|
||||
expect(Object.keys(caveatSpecifications)).toHaveLength(1);
|
||||
expect(
|
||||
caveatSpecifications[CaveatTypes.restrictReturnedAccounts].type,
|
||||
).toStrictEqual(CaveatTypes.restrictReturnedAccounts);
|
||||
});
|
||||
|
||||
describe('restrictReturnedAccounts', () => {
|
||||
describe('decorator', () => {
|
||||
it('returns the first array member included in the caveat value', async () => {
|
||||
const getIdentities = jest.fn();
|
||||
const { decorator } = getCaveatSpecifications({ getIdentities })[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
const method = async () => ['0x1', '0x2', '0x3'];
|
||||
const caveat = {
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1', '0x2'],
|
||||
};
|
||||
const decorated = decorator(method, caveat);
|
||||
expect(await decorated()).toStrictEqual(['0x1']);
|
||||
});
|
||||
|
||||
it('returns an empty array if no array members are included in the caveat value', async () => {
|
||||
const getIdentities = jest.fn();
|
||||
const { decorator } = getCaveatSpecifications({ getIdentities })[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
const method = async () => ['0x1', '0x2', '0x3'];
|
||||
const caveat = {
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x5'],
|
||||
};
|
||||
const decorated = decorator(method, caveat);
|
||||
expect(await decorated()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('returns an empty array if the method result is an empty array', async () => {
|
||||
const getIdentities = jest.fn();
|
||||
const { decorator } = getCaveatSpecifications({ getIdentities })[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
const method = async () => [];
|
||||
const caveat = {
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1', '0x2'],
|
||||
};
|
||||
const decorated = decorator(method, caveat);
|
||||
expect(await decorated()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validator', () => {
|
||||
it('rejects invalid array values', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const { validator } = getCaveatSpecifications({ getIdentities })[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
[null, 'foo', {}, []].forEach((invalidValue) => {
|
||||
expect(() => validator({ value: invalidValue })).toThrow(
|
||||
/Expected non-empty array of Ethereum addresses\.$/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects falsy or non-string addresses', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const { validator } = getCaveatSpecifications({ getIdentities })[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
[[{}], [[]], [null], ['']].forEach((invalidValue) => {
|
||||
expect(() => validator({ value: invalidValue })).toThrow(
|
||||
/Expected an array of Ethereum addresses. Received:/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects addresses that have no corresponding identity', () => {
|
||||
const getIdentities = jest.fn().mockImplementationOnce(() => {
|
||||
return {
|
||||
'0x1': true,
|
||||
'0x3': true,
|
||||
};
|
||||
});
|
||||
|
||||
const { validator } = getCaveatSpecifications({ getIdentities })[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
];
|
||||
|
||||
expect(() => validator({ value: ['0x1', '0x2', '0x3'] })).toThrow(
|
||||
/Received unrecognized address:/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('permission specifications', () => {
|
||||
it('getPermissionSpecifications returns the expected specifications object', () => {
|
||||
const permissionSpecifications = getPermissionSpecifications({});
|
||||
expect(Object.keys(permissionSpecifications)).toHaveLength(1);
|
||||
expect(
|
||||
permissionSpecifications[RestrictedMethods.eth_accounts].targetKey,
|
||||
).toStrictEqual(RestrictedMethods.eth_accounts);
|
||||
});
|
||||
|
||||
describe('eth_accounts', () => {
|
||||
describe('factory', () => {
|
||||
it('constructs a valid eth_accounts permission', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const getAllAccounts = jest.fn();
|
||||
const { factory } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
expect(
|
||||
factory(
|
||||
{ invoker: 'foo.bar', target: 'eth_accounts' },
|
||||
{ approvedAccounts: ['0x1'] },
|
||||
),
|
||||
).toStrictEqual({
|
||||
caveats: [
|
||||
{
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1'],
|
||||
},
|
||||
],
|
||||
date: 1,
|
||||
id: expect.any(String),
|
||||
invoker: 'foo.bar',
|
||||
parentCapability: 'eth_accounts',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if no approvedAccounts are specified', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const getAllAccounts = jest.fn();
|
||||
const { factory } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
expect(() =>
|
||||
factory(
|
||||
{ invoker: 'foo.bar', target: 'eth_accounts' },
|
||||
{}, // no approvedAccounts
|
||||
),
|
||||
).toThrow(/No approved accounts specified\.$/u);
|
||||
});
|
||||
|
||||
it('throws an error if any caveats are specified directly', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const getAllAccounts = jest.fn();
|
||||
const { factory } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
expect(() =>
|
||||
factory(
|
||||
{
|
||||
caveats: [
|
||||
{
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1', '0x2'],
|
||||
},
|
||||
],
|
||||
invoker: 'foo.bar',
|
||||
target: 'eth_accounts',
|
||||
},
|
||||
{ approvedAccounts: ['0x1'] },
|
||||
),
|
||||
).toThrow(/Received unexpected caveats./u);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methodImplementation', () => {
|
||||
it('returns the keyring accounts in lastSelected order', async () => {
|
||||
const getIdentities = jest.fn().mockImplementationOnce(() => {
|
||||
return {
|
||||
'0x1': {
|
||||
lastSelected: 1,
|
||||
},
|
||||
'0x2': {},
|
||||
'0x3': {
|
||||
lastSelected: 3,
|
||||
},
|
||||
'0x4': {
|
||||
lastSelected: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
const getAllAccounts = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ['0x1', '0x2', '0x3', '0x4']);
|
||||
|
||||
const { methodImplementation } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
expect(await methodImplementation()).toStrictEqual([
|
||||
'0x3',
|
||||
'0x4',
|
||||
'0x1',
|
||||
'0x2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws if a keyring account is missing an address (case 1)', async () => {
|
||||
const getIdentities = jest.fn().mockImplementationOnce(() => {
|
||||
return {
|
||||
'0x2': {
|
||||
lastSelected: 3,
|
||||
},
|
||||
'0x3': {
|
||||
lastSelected: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
const getAllAccounts = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ['0x1', '0x2', '0x3']);
|
||||
|
||||
const { methodImplementation } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
await expect(() => methodImplementation()).rejects.toThrow(
|
||||
'Missing identity for address: "0x1".',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if a keyring account is missing an address (case 2)', async () => {
|
||||
const getIdentities = jest.fn().mockImplementationOnce(() => {
|
||||
return {
|
||||
'0x1': {
|
||||
lastSelected: 1,
|
||||
},
|
||||
'0x3': {
|
||||
lastSelected: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
const getAllAccounts = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ['0x1', '0x2', '0x3']);
|
||||
|
||||
const { methodImplementation } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
await expect(() => methodImplementation()).rejects.toThrow(
|
||||
'Missing identity for address: "0x2".',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validator', () => {
|
||||
it('accepts valid permissions', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const getAllAccounts = jest.fn();
|
||||
const { validator } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
expect(() =>
|
||||
validator({
|
||||
caveats: [
|
||||
{
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: ['0x1', '0x2'],
|
||||
},
|
||||
],
|
||||
date: 1,
|
||||
id: expect.any(String),
|
||||
invoker: 'foo.bar',
|
||||
parentCapability: 'eth_accounts',
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects invalid caveats', () => {
|
||||
const getIdentities = jest.fn();
|
||||
const getAllAccounts = jest.fn();
|
||||
const { validator } = getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts,
|
||||
})[RestrictedMethods.eth_accounts];
|
||||
|
||||
[null, [], [1, 2], [{ type: 'foobar' }]].forEach(
|
||||
(invalidCaveatsValue) => {
|
||||
expect(() =>
|
||||
validator({
|
||||
caveats: invalidCaveatsValue,
|
||||
date: 1,
|
||||
id: expect.any(String),
|
||||
invoker: 'foo.bar',
|
||||
parentCapability: 'eth_accounts',
|
||||
}),
|
||||
).toThrow(/Invalid caveats./u);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unrestricted methods', () => {
|
||||
it('defines the unrestricted methods', () => {
|
||||
expect(Array.isArray(unrestrictedMethods)).toBe(true);
|
||||
expect(Object.isFrozen(unrestrictedMethods)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -10,11 +10,12 @@ const callbackNoop = function (err) {
|
||||
* A generator that returns a function which, when passed a promise, can treat that promise as a node style callback.
|
||||
* The prime advantage being that callbacks are better for error handling.
|
||||
*
|
||||
* @param {Function} fn - The function to handle as a callback
|
||||
* @param {Object} context - The context in which the fn is to be called, most often a this reference
|
||||
* @param {Function} fn - The function to handle as a callback.
|
||||
* @param {Object} context - The context in which the function is to be called,
|
||||
* most often a `this` reference.
|
||||
*
|
||||
*/
|
||||
export default function nodeify(fn, context) {
|
||||
export function nodeify(fn, context) {
|
||||
return function (...args) {
|
||||
const lastArg = args[args.length - 1];
|
||||
const lastArgIsCallback = typeof lastArg === 'function';
|
||||
@ -36,3 +37,19 @@ export default function nodeify(fn, context) {
|
||||
promiseToCallback(result)(callback);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new object where every function property is nodeified, and every
|
||||
* non-function property is unmodified.
|
||||
*
|
||||
* @param {Record<string, unknown>} obj - The object whose function values to
|
||||
* `nodeify`.
|
||||
* @param {Object} context - The context in which the function is to be called.
|
||||
*/
|
||||
export function nodeifyObject(obj, context) {
|
||||
return Object.entries(obj).reduce((nodeified, [key, value]) => {
|
||||
nodeified[key] =
|
||||
typeof value === 'function' ? nodeify(value, context) : value;
|
||||
return nodeified;
|
||||
}, {});
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import nodeify from './nodeify';
|
||||
import { nodeify, nodeifyObject } from './nodeify';
|
||||
|
||||
describe('nodeify', () => {
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
promiseFunc(a) {
|
||||
const solution = this.foo + a;
|
||||
return Promise.resolve(solution);
|
||||
},
|
||||
const getObject = () => {
|
||||
return {
|
||||
foo: 'bar',
|
||||
promiseFunc(a) {
|
||||
const solution = this.foo + a;
|
||||
return Promise.resolve(solution);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('should retain original context', () => {
|
||||
const obj = getObject();
|
||||
const nodified = nodeify(obj.promiseFunc, obj);
|
||||
nodified('baz', (_, res) => {
|
||||
expect(res).toStrictEqual('barbaz');
|
||||
@ -17,6 +20,7 @@ describe('nodeify', () => {
|
||||
});
|
||||
|
||||
it('no callback - should allow the last argument to not be a function', async () => {
|
||||
const obj = getObject();
|
||||
const nodified = nodeify(obj.promiseFunc, obj);
|
||||
await expect(() => {
|
||||
nodified('baz');
|
||||
@ -38,4 +42,44 @@ describe('nodeify', () => {
|
||||
expect(err.message).toStrictEqual('boom!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodeifyObject', () => {
|
||||
it('nodeifies every function of an object', async () => {
|
||||
const obj = {
|
||||
notFunction: 'bar',
|
||||
syncFunction: () => 'hello',
|
||||
asyncFunction: async () => 'goodbye',
|
||||
};
|
||||
|
||||
const nodeified = nodeifyObject(obj, null);
|
||||
expect(nodeified.notFunction).toStrictEqual(obj.notFunction);
|
||||
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
nodeified.syncFunction((err, result) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new Error(`should not have thrown any error: ${err.message}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
expect(result).toStrictEqual('hello');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
nodeified.asyncFunction((err, result) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new Error(`should not have thrown any error: ${err.message}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
expect(result).toStrictEqual('goodbye');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { permissionRpcMethods } from '@metamask/snap-controllers';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network';
|
||||
import handlers from './handlers';
|
||||
import localHandlers from './handlers';
|
||||
|
||||
const handlerMap = handlers.reduce((map, handler) => {
|
||||
const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers];
|
||||
|
||||
const handlerMap = allHandlers.reduce((map, handler) => {
|
||||
for (const methodName of handler.methodNames) {
|
||||
map.set(methodName, handler);
|
||||
}
|
||||
@ -10,23 +13,17 @@ const handlerMap = handlers.reduce((map, handler) => {
|
||||
}, new Map());
|
||||
|
||||
/**
|
||||
* Returns a middleware that implements the RPC methods defined in the handlers
|
||||
* directory.
|
||||
*
|
||||
* The purpose of this middleware is to create portable RPC method
|
||||
* implementations that are decoupled from the rest of our background
|
||||
* architecture.
|
||||
* Creates a json-rpc-engine middleware of RPC method implementations.
|
||||
*
|
||||
* Handlers consume functions that hook into the background, and only depend
|
||||
* on their signatures, not e.g. controller internals.
|
||||
*
|
||||
* Eventually, we'll want to extract this middleware into its own package.
|
||||
*
|
||||
* @param {Object} opts - The middleware options
|
||||
* @param {Record<string, unknown>} hooks - Required "hooks" into our
|
||||
* controllers.
|
||||
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
|
||||
*/
|
||||
export default function createMethodMiddleware(opts) {
|
||||
return function methodMiddleware(req, res, next, end) {
|
||||
export default function createMethodMiddleware(hooks) {
|
||||
return async function methodMiddleware(req, res, next, end) {
|
||||
// Reject unsupported methods.
|
||||
if (UNSUPPORTED_RPC_METHODS.has(req.method)) {
|
||||
return end(ethErrors.rpc.methodNotSupported());
|
||||
@ -35,7 +32,18 @@ export default function createMethodMiddleware(opts) {
|
||||
const handler = handlerMap.get(req.method);
|
||||
if (handler) {
|
||||
const { implementation, hookNames } = handler;
|
||||
return implementation(req, res, next, end, selectHooks(opts, hookNames));
|
||||
try {
|
||||
// Implementations may or may not be async, so we must await them.
|
||||
return await implementation(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
end,
|
||||
selectHooks(hooks, hookNames),
|
||||
);
|
||||
} catch (error) {
|
||||
return end(error);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
|
@ -0,0 +1,33 @@
|
||||
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||
|
||||
/**
|
||||
* A wrapper for `eth_accounts` that returns an empty array when permission is denied.
|
||||
*/
|
||||
|
||||
const requestEthereumAccounts = {
|
||||
methodNames: [MESSAGE_TYPE.ETH_ACCOUNTS],
|
||||
implementation: ethAccountsHandler,
|
||||
hookNames: {
|
||||
getAccounts: true,
|
||||
},
|
||||
};
|
||||
export default requestEthereumAccounts;
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, Function>} EthAccountsOptions
|
||||
* @property {Function} getAccounts - Gets the accounts for the requesting
|
||||
* origin.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object.
|
||||
* @param {import('json-rpc-engine').JsonRpcResponse<true>} 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 {EthAccountsOptions} options - The RPC method hooks.
|
||||
*/
|
||||
async function ethAccountsHandler(_req, res, _next, end, { getAccounts }) {
|
||||
res.result = await getAccounts();
|
||||
return end();
|
||||
}
|
@ -1,14 +1,20 @@
|
||||
import addEthereumChain from './add-ethereum-chain';
|
||||
import switchEthereumChain from './switch-ethereum-chain';
|
||||
import ethAccounts from './eth-accounts';
|
||||
import getProviderState from './get-provider-state';
|
||||
import logWeb3ShimUsage from './log-web3-shim-usage';
|
||||
import requestAccounts from './request-accounts';
|
||||
import sendMetadata from './send-metadata';
|
||||
import switchEthereumChain from './switch-ethereum-chain';
|
||||
import watchAsset from './watch-asset';
|
||||
|
||||
const handlers = [
|
||||
addEthereumChain,
|
||||
switchEthereumChain,
|
||||
ethAccounts,
|
||||
getProviderState,
|
||||
logWeb3ShimUsage,
|
||||
requestAccounts,
|
||||
sendMetadata,
|
||||
switchEthereumChain,
|
||||
watchAsset,
|
||||
];
|
||||
export default handlers;
|
||||
|
@ -0,0 +1,108 @@
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||
|
||||
/**
|
||||
* This method attempts to retrieve the Ethereum accounts available to the
|
||||
* requester, or initiate a request for account access if none are currently
|
||||
* available. It is essentially a wrapper of wallet_requestPermissions that
|
||||
* only errors if the user rejects the request. We maintain the method for
|
||||
* backwards compatibility reasons.
|
||||
*/
|
||||
|
||||
const requestEthereumAccounts = {
|
||||
methodNames: [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS],
|
||||
implementation: requestEthereumAccountsHandler,
|
||||
hookNames: {
|
||||
origin: true,
|
||||
getAccounts: true,
|
||||
getUnlockPromise: true,
|
||||
hasPermission: true,
|
||||
requestAccountsPermission: true,
|
||||
},
|
||||
};
|
||||
export default requestEthereumAccounts;
|
||||
|
||||
// Used to rate-limit pending requests to one per origin
|
||||
const locks = new Set();
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, string | Function>} RequestEthereumAccountsOptions
|
||||
* @property {string} origin - The requesting origin.
|
||||
* @property {Function} getAccounts - Gets the accounts for the requesting
|
||||
* origin.
|
||||
* @property {Function} getUnlockPromise - Gets a promise that resolves when
|
||||
* the extension unlocks.
|
||||
* @property {Function} hasPermission - Returns whether the requesting origin
|
||||
* has the specified permission.
|
||||
* @property {Function} requestAccountsPermission - Requests the `eth_accounts`
|
||||
* permission for the requesting origin.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} _req - The JSON-RPC request object.
|
||||
* @param {import('json-rpc-engine').JsonRpcResponse<true>} 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 {RequestEthereumAccountsOptions} options - The RPC method hooks.
|
||||
*/
|
||||
async function requestEthereumAccountsHandler(
|
||||
_req,
|
||||
res,
|
||||
_next,
|
||||
end,
|
||||
{
|
||||
origin,
|
||||
getAccounts,
|
||||
getUnlockPromise,
|
||||
hasPermission,
|
||||
requestAccountsPermission,
|
||||
},
|
||||
) {
|
||||
if (locks.has(origin)) {
|
||||
res.error = ethErrors.rpc.resourceUnavailable(
|
||||
`Already processing ${MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS}. Please wait.`,
|
||||
);
|
||||
return end();
|
||||
}
|
||||
|
||||
if (hasPermission(MESSAGE_TYPE.ETH_ACCOUNTS)) {
|
||||
// We wait for the extension to unlock in this case only, because permission
|
||||
// requests are handled when the extension is unlocked, regardless of the
|
||||
// lock state when they were received.
|
||||
try {
|
||||
locks.add(origin);
|
||||
await getUnlockPromise();
|
||||
res.result = await getAccounts();
|
||||
end();
|
||||
} catch (error) {
|
||||
end(error);
|
||||
} finally {
|
||||
locks.delete(origin);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If no accounts, request the accounts permission
|
||||
try {
|
||||
await requestAccountsPermission();
|
||||
} catch (err) {
|
||||
res.error = err;
|
||||
return end();
|
||||
}
|
||||
|
||||
// Get the approved accounts
|
||||
const accounts = await getAccounts();
|
||||
/* istanbul ignore else: too hard to induce, see below comment */
|
||||
if (accounts.length > 0) {
|
||||
res.result = accounts;
|
||||
} else {
|
||||
// This should never happen, because it should be caught in the
|
||||
// above catch clause
|
||||
res.error = ethErrors.rpc.internal(
|
||||
'Accounts unexpectedly unavailable. Please report this bug.',
|
||||
);
|
||||
}
|
||||
|
||||
return end();
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||
|
||||
/**
|
||||
* This internal method is used by our external provider to send metadata about
|
||||
* permission subjects so that we can e.g. display a proper name and icon in
|
||||
* our UI.
|
||||
*/
|
||||
|
||||
const sendMetadata = {
|
||||
methodNames: [MESSAGE_TYPE.SEND_METADATA],
|
||||
implementation: sendMetadataHandler,
|
||||
hookNames: {
|
||||
addSubjectMetadata: true,
|
||||
},
|
||||
};
|
||||
export default sendMetadata;
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, Function>} SendMetadataOptions
|
||||
* @property {Function} addSubjectMetadata - A function that records subject
|
||||
* metadata, bound to the requesting origin.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object.
|
||||
* @param {import('json-rpc-engine').JsonRpcResponse<true>} 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 {SendMetadataOptions} options
|
||||
*/
|
||||
function sendMetadataHandler(req, res, _next, end, { addSubjectMetadata }) {
|
||||
const { params } = req;
|
||||
if (params && typeof params === 'object' && !Array.isArray(params)) {
|
||||
const { icon = null, name = null, ...remainingParams } = params;
|
||||
addSubjectMetadata({ ...remainingParams, iconUrl: icon, name });
|
||||
} else {
|
||||
return end(ethErrors.rpc.invalidParams({ data: params }));
|
||||
}
|
||||
|
||||
res.result = true;
|
||||
return end();
|
||||
}
|
@ -9,6 +9,7 @@ import createFilterMiddleware from 'eth-json-rpc-filters';
|
||||
import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager';
|
||||
import { providerAsMiddleware } from 'eth-json-rpc-middleware';
|
||||
import KeyringController from 'eth-keyring-controller';
|
||||
import { errorCodes as rpcErrorCodes, ethErrors } from 'eth-rpc-errors';
|
||||
import { Mutex } from 'await-semaphore';
|
||||
import { stripHexPrefix } from 'ethereumjs-util';
|
||||
import log from 'loglevel';
|
||||
@ -18,7 +19,6 @@ import LatticeKeyring from 'eth-lattice-keyring';
|
||||
import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring';
|
||||
import EthQuery from 'eth-query';
|
||||
import nanoid from 'nanoid';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { captureException } from '@sentry/browser';
|
||||
import {
|
||||
AddressBookController,
|
||||
@ -35,6 +35,11 @@ import {
|
||||
AssetsContractController,
|
||||
CollectibleDetectionController,
|
||||
} from '@metamask/controllers';
|
||||
import {
|
||||
PermissionController,
|
||||
SubjectMetadataController,
|
||||
} from '@metamask/snap-controllers';
|
||||
|
||||
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
|
||||
import {
|
||||
GAS_API_BASE_URL,
|
||||
@ -46,6 +51,10 @@ import {
|
||||
DEVICE_NAMES,
|
||||
KEYRING_TYPES,
|
||||
} from '../../shared/constants/hardware-wallets';
|
||||
import {
|
||||
CaveatTypes,
|
||||
RestrictedMethods,
|
||||
} from '../../shared/constants/permissions';
|
||||
import { UI_NOTIFICATIONS } from '../../shared/notifications';
|
||||
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
|
||||
import { MILLISECOND } from '../../shared/constants/time';
|
||||
@ -76,16 +85,24 @@ import PersonalMessageManager from './lib/personal-message-manager';
|
||||
import TypedMessageManager from './lib/typed-message-manager';
|
||||
import TransactionController from './controllers/transactions';
|
||||
import DetectTokensController from './controllers/detect-tokens';
|
||||
import PermissionLogController from './controllers/permissions/permission-log';
|
||||
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 { nodeify, nodeifyObject } from './lib/nodeify';
|
||||
import accountImporter from './account-import-strategies';
|
||||
import seedPhraseVerifier from './lib/seed-phrase-verifier';
|
||||
import MetaMetricsController from './controllers/metametrics';
|
||||
import { segment } from './lib/segment';
|
||||
import createMetaRPCHandler from './lib/createMetaRPCHandler';
|
||||
import {
|
||||
CaveatMutatorFactories,
|
||||
getCaveatSpecifications,
|
||||
getChangedAccounts,
|
||||
getPermissionBackgroundApiMethods,
|
||||
getPermissionSpecifications,
|
||||
getPermittedAccountsByOrigin,
|
||||
unrestrictedMethods,
|
||||
} from './controllers/permissions';
|
||||
|
||||
export const METAMASK_CONTROLLER_EVENTS = {
|
||||
// Fired after state changes that impact the extension badge (unapproved msg count)
|
||||
@ -454,24 +471,43 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.keyringController.on('unlock', () => this._onUnlock());
|
||||
this.keyringController.on('lock', () => this._onLock());
|
||||
|
||||
this.permissionsController = new PermissionsController(
|
||||
{
|
||||
approvals: this.approvalController,
|
||||
getKeyringAccounts: this.keyringController.getAccounts.bind(
|
||||
const getIdentities = () =>
|
||||
this.preferencesController.store.getState().identities;
|
||||
|
||||
this.permissionController = new PermissionController({
|
||||
messenger: this.controllerMessenger.getRestricted({
|
||||
name: 'PermissionController',
|
||||
allowedActions: [
|
||||
`${this.approvalController.name}:addRequest`,
|
||||
`${this.approvalController.name}:hasRequest`,
|
||||
`${this.approvalController.name}:acceptRequest`,
|
||||
`${this.approvalController.name}:rejectRequest`,
|
||||
],
|
||||
}),
|
||||
state: initState.PermissionController,
|
||||
caveatSpecifications: getCaveatSpecifications({ getIdentities }),
|
||||
permissionSpecifications: getPermissionSpecifications({
|
||||
getIdentities,
|
||||
getAllAccounts: this.keyringController.getAccounts.bind(
|
||||
this.keyringController,
|
||||
),
|
||||
getRestrictedMethods,
|
||||
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,
|
||||
},
|
||||
initState.PermissionsController,
|
||||
initState.PermissionsMetadata,
|
||||
);
|
||||
}),
|
||||
unrestrictedMethods,
|
||||
});
|
||||
|
||||
this.permissionLogController = new PermissionLogController({
|
||||
restrictedMethods: new Set(Object.keys(RestrictedMethods)),
|
||||
initState: initState.PermissionLogController,
|
||||
});
|
||||
|
||||
this.subjectMetadataController = new SubjectMetadataController({
|
||||
messenger: this.controllerMessenger.getRestricted({
|
||||
name: 'SubjectMetadataController',
|
||||
allowedActions: [`${this.permissionController.name}:hasPermissions`],
|
||||
}),
|
||||
state: initState.SubjectMetadataController,
|
||||
subjectCacheLimit: 100,
|
||||
});
|
||||
|
||||
this.detectTokensController = new DetectTokensController({
|
||||
preferences: this.preferencesController,
|
||||
@ -508,9 +544,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.txController = new TransactionController({
|
||||
initState:
|
||||
initState.TransactionController || initState.TransactionManager,
|
||||
getPermittedAccounts: this.permissionsController.getAccounts.bind(
|
||||
this.permissionsController,
|
||||
),
|
||||
getPermittedAccounts: this.getPermittedAccounts.bind(this),
|
||||
getProviderConfig: this.networkController.getProviderConfig.bind(
|
||||
this.networkController,
|
||||
),
|
||||
@ -670,8 +704,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
AlertController: this.alertController.store,
|
||||
OnboardingController: this.onboardingController.store,
|
||||
IncomingTransactionsController: this.incomingTransactionsController.store,
|
||||
PermissionsController: this.permissionsController.permissions,
|
||||
PermissionsMetadata: this.permissionsController.store,
|
||||
PermissionController: this.permissionController,
|
||||
PermissionLogController: this.permissionLogController.store,
|
||||
SubjectMetadataController: this.subjectMetadataController,
|
||||
ThreeBoxController: this.threeBoxController.store,
|
||||
NotificationController: this.notificationController,
|
||||
GasFeeController: this.gasFeeController,
|
||||
@ -702,8 +737,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
OnboardingController: this.onboardingController.store,
|
||||
IncomingTransactionsController: this.incomingTransactionsController
|
||||
.store,
|
||||
PermissionsController: this.permissionsController.permissions,
|
||||
PermissionsMetadata: this.permissionsController.store,
|
||||
PermissionController: this.permissionController,
|
||||
PermissionLogController: this.permissionLogController.store,
|
||||
SubjectMetadataController: this.subjectMetadataController,
|
||||
ThreeBoxController: this.threeBoxController.store,
|
||||
SwapsController: this.swapsController.store,
|
||||
EnsController: this.ensController.store,
|
||||
@ -738,10 +774,77 @@ export default class MetamaskController extends EventEmitter {
|
||||
);
|
||||
});
|
||||
|
||||
this.setupControllerEventSubscriptions();
|
||||
|
||||
// TODO:LegacyProvider: Delete
|
||||
this.publicConfigStore = this.createPublicConfigStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up BaseController V2 event subscriptions. Currently, this includes
|
||||
* the subscriptions necessary to notify permission subjects of account
|
||||
* changes.
|
||||
*
|
||||
* Some of the subscriptions in this method are ControllerMessenger selector
|
||||
* event subscriptions. See the relevant @metamask/controllers documentation
|
||||
* for more information.
|
||||
*
|
||||
* Note that account-related notifications emitted when the extension
|
||||
* becomes unlocked are handled in MetaMaskController._onUnlock.
|
||||
*/
|
||||
setupControllerEventSubscriptions() {
|
||||
const handleAccountsChange = async (origin, newAccounts) => {
|
||||
if (this.isUnlocked()) {
|
||||
this.notifyConnections(origin, {
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
// This should be the same as the return value of `eth_accounts`,
|
||||
// namely an array of the current / most recently selected Ethereum
|
||||
// account.
|
||||
params:
|
||||
newAccounts.length < 2
|
||||
? // If the length is 1 or 0, the accounts are sorted by definition.
|
||||
newAccounts
|
||||
: // If the length is 2 or greater, we have to execute
|
||||
// `eth_accounts` vi this method.
|
||||
await this.getPermittedAccounts(origin),
|
||||
});
|
||||
}
|
||||
|
||||
this.permissionLogController.updateAccountsHistory(origin, newAccounts);
|
||||
};
|
||||
|
||||
// This handles account changes whenever the selected address changes.
|
||||
let lastSelectedAddress;
|
||||
this.preferencesController.store.subscribe(async ({ selectedAddress }) => {
|
||||
if (selectedAddress && selectedAddress !== lastSelectedAddress) {
|
||||
lastSelectedAddress = selectedAddress;
|
||||
const permittedAccountsMap = getPermittedAccountsByOrigin(
|
||||
this.permissionController.state,
|
||||
);
|
||||
|
||||
for (const [origin, accounts] of permittedAccountsMap.entries()) {
|
||||
if (accounts.includes(selectedAddress)) {
|
||||
handleAccountsChange(origin, accounts);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This handles account changes every time relevant permission state
|
||||
// changes, for any reason.
|
||||
this.controllerMessenger.subscribe(
|
||||
`${this.permissionController.name}:stateChange`,
|
||||
async (currentValue, previousValue) => {
|
||||
const changedAccounts = getChangedAccounts(currentValue, previousValue);
|
||||
|
||||
for (const [origin, accounts] of changedAccounts.entries()) {
|
||||
handleAccountsChange(origin, accounts);
|
||||
}
|
||||
},
|
||||
getPermittedAccountsByOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor helper: initialize a provider.
|
||||
*/
|
||||
@ -759,7 +862,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
const selectedAddress = this.preferencesController.getSelectedAddress();
|
||||
return selectedAddress ? [selectedAddress] : [];
|
||||
} else if (this.isUnlocked()) {
|
||||
return await this.permissionsController.getAccounts(origin);
|
||||
return await this.getPermittedAccounts(origin);
|
||||
}
|
||||
return []; // changing this is a breaking change
|
||||
},
|
||||
@ -835,7 +938,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
return {
|
||||
isUnlocked: this.isUnlocked(),
|
||||
...this.getProviderNetworkState(),
|
||||
accounts: await this.permissionsController.getAccounts(origin),
|
||||
accounts: await this.getPermittedAccounts(origin),
|
||||
};
|
||||
}
|
||||
|
||||
@ -888,7 +991,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
metaMetricsController,
|
||||
networkController,
|
||||
onboardingController,
|
||||
permissionsController,
|
||||
permissionController,
|
||||
preferencesController,
|
||||
swapsController,
|
||||
threeBoxController,
|
||||
@ -1208,29 +1311,18 @@ export default class MetamaskController extends EventEmitter {
|
||||
initializeThreeBox: nodeify(this.initializeThreeBox, this),
|
||||
|
||||
// permissions
|
||||
removePermissionsFor: permissionController.revokePermissions.bind(
|
||||
permissionController,
|
||||
),
|
||||
approvePermissionsRequest: nodeify(
|
||||
permissionsController.approvePermissionsRequest,
|
||||
permissionsController,
|
||||
permissionController.acceptPermissionsRequest,
|
||||
permissionController,
|
||||
),
|
||||
rejectPermissionsRequest: nodeify(
|
||||
permissionsController.rejectPermissionsRequest,
|
||||
permissionsController,
|
||||
),
|
||||
removePermissionsFor: permissionsController.removePermissionsFor.bind(
|
||||
permissionsController,
|
||||
),
|
||||
addPermittedAccount: nodeify(
|
||||
permissionsController.addPermittedAccount,
|
||||
permissionsController,
|
||||
),
|
||||
removePermittedAccount: nodeify(
|
||||
permissionsController.removePermittedAccount,
|
||||
permissionsController,
|
||||
),
|
||||
requestAccountsPermissionWithId: nodeify(
|
||||
permissionsController.requestAccountsPermissionWithId,
|
||||
permissionsController,
|
||||
permissionController.rejectPermissionsRequest,
|
||||
permissionController,
|
||||
),
|
||||
...nodeifyObject(getPermissionBackgroundApiMethods(permissionController)),
|
||||
|
||||
// swaps
|
||||
fetchAndSetQuotes: nodeify(
|
||||
@ -1432,7 +1524,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.preferencesController.setAddresses([]);
|
||||
|
||||
// clear permissions
|
||||
this.permissionsController.clearPermissions();
|
||||
this.permissionController.clearState();
|
||||
|
||||
// clear accounts in accountTracker
|
||||
this.accountTracker.clearAccounts();
|
||||
@ -1925,6 +2017,48 @@ export default class MetamaskController extends EventEmitter {
|
||||
return selectedAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the permitted accounts for the specified origin. Returns an empty
|
||||
* array if no accounts are permitted.
|
||||
*
|
||||
* @param {string} origin - The origin whose exposed accounts to retrieve.
|
||||
* @returns {Promise<string[]>} The origin's permitted accounts, or an empty
|
||||
* array.
|
||||
*/
|
||||
async getPermittedAccounts(origin) {
|
||||
try {
|
||||
return await this.permissionController.executeRestrictedMethod(
|
||||
origin,
|
||||
RestrictedMethods.eth_accounts,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code === rpcErrorCodes.provider.unauthorized) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops exposing the account with the specified address to all third parties.
|
||||
* Exposed accounts are stored in caveats of the eth_accounts permission. This
|
||||
* method uses `PermissionController.updatePermissionsByCaveat` to
|
||||
* remove the specified address from every eth_accounts permission. If a
|
||||
* permission only included this address, the permission is revoked entirely.
|
||||
*
|
||||
* @param {string} targetAccount - The address of the account to stop exposing
|
||||
* to third parties.
|
||||
*/
|
||||
removeAllAccountPermissions(targetAccount) {
|
||||
this.permissionController.updatePermissionsByCaveat(
|
||||
CaveatTypes.restrictReturnedAccounts,
|
||||
(existingAccounts) =>
|
||||
CaveatMutatorFactories[
|
||||
CaveatTypes.restrictReturnedAccounts
|
||||
].removeAccount(targetAccount, existingAccounts),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an account from state / storage.
|
||||
*
|
||||
@ -1933,7 +2067,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
*/
|
||||
async removeAccount(address) {
|
||||
// Remove all associated permissions
|
||||
await this.permissionsController.removeAllAccountPermissions(address);
|
||||
this.removeAllAccountPermissions(address);
|
||||
// Remove account from the preferences controller
|
||||
this.preferencesController.removeAddress(address);
|
||||
// Remove account from the account tracker controller
|
||||
@ -2607,10 +2741,12 @@ export default class MetamaskController extends EventEmitter {
|
||||
*/
|
||||
setupProviderConnection(outStream, sender, isInternal) {
|
||||
const origin = isInternal ? 'metamask' : new URL(sender.url).origin;
|
||||
let extensionId;
|
||||
if (sender.id !== this.extension.runtime.id) {
|
||||
extensionId = sender.id;
|
||||
this.subjectMetadataController.addSubjectMetadata(origin, {
|
||||
extensionId: sender.id,
|
||||
});
|
||||
}
|
||||
|
||||
let tabId;
|
||||
if (sender.tab && sender.tab.id) {
|
||||
tabId = sender.tab.id;
|
||||
@ -2619,7 +2755,6 @@ export default class MetamaskController extends EventEmitter {
|
||||
const engine = this.setupProviderEngine({
|
||||
origin,
|
||||
location: sender.url,
|
||||
extensionId,
|
||||
tabId,
|
||||
isInternal,
|
||||
});
|
||||
@ -2648,17 +2783,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
* @param {Object} options - Provider engine options
|
||||
* @param {string} options.origin - The origin of the sender
|
||||
* @param {string} options.location - The full URL of the sender
|
||||
* @param {extensionId} [options.extensionId] - The extension ID of the sender, if the sender is an external extension
|
||||
* @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab
|
||||
* @param {boolean} [options.isInternal] - True if called for a connection to an internal process
|
||||
**/
|
||||
setupProviderEngine({
|
||||
origin,
|
||||
location,
|
||||
extensionId,
|
||||
tabId,
|
||||
isInternal = false,
|
||||
}) {
|
||||
setupProviderEngine({ origin, location, tabId, isInternal = false }) {
|
||||
// setup json rpc engine stack
|
||||
const engine = new JsonRpcEngine();
|
||||
const { provider, blockTracker } = this;
|
||||
@ -2689,40 +2817,47 @@ export default class MetamaskController extends EventEmitter {
|
||||
registerOnboarding: this.onboardingController.registerOnboarding,
|
||||
}),
|
||||
);
|
||||
engine.push(this.permissionLogController.createMiddleware());
|
||||
engine.push(
|
||||
createMethodMiddleware({
|
||||
origin,
|
||||
|
||||
// Miscellaneous
|
||||
addSubjectMetadata: this.subjectMetadataController.addSubjectMetadata.bind(
|
||||
this.subjectMetadataController,
|
||||
origin,
|
||||
),
|
||||
getProviderState: this.getProviderState.bind(this),
|
||||
sendMetrics: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
|
||||
this.appStateController,
|
||||
),
|
||||
handleWatchAssetRequest: this.tokensController.watchAsset.bind(
|
||||
this.tokensController,
|
||||
),
|
||||
getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind(
|
||||
this.alertController,
|
||||
),
|
||||
setWeb3ShimUsageRecorded: this.alertController.setWeb3ShimUsageRecorded.bind(
|
||||
this.alertController,
|
||||
),
|
||||
findCustomRpcBy: this.findCustomRpcBy.bind(this),
|
||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||
this.networkController,
|
||||
),
|
||||
requestUserApproval: this.approvalController.addAndShowApprovalRequest.bind(
|
||||
this.approvalController,
|
||||
),
|
||||
updateRpcTarget: ({ rpcUrl, chainId, ticker, nickname }) => {
|
||||
this.networkController.setRpcTarget(
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
nickname,
|
||||
);
|
||||
},
|
||||
setProviderType: this.networkController.setProviderType.bind(
|
||||
this.networkController,
|
||||
sendMetrics: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
),
|
||||
|
||||
// Permission-related
|
||||
getAccounts: this.getPermittedAccounts.bind(this, origin),
|
||||
getPermissionsForOrigin: this.permissionController.getPermissions.bind(
|
||||
this.permissionController,
|
||||
origin,
|
||||
),
|
||||
hasPermission: this.permissionController.hasPermission.bind(
|
||||
this.permissionController,
|
||||
origin,
|
||||
),
|
||||
requestAccountsPermission: this.permissionController.requestPermissions.bind(
|
||||
this.permissionController,
|
||||
{ origin },
|
||||
{ eth_accounts: {} },
|
||||
),
|
||||
|
||||
// Custom RPC-related
|
||||
addCustomRpc: async ({
|
||||
chainId,
|
||||
blockExplorerUrl,
|
||||
@ -2740,6 +2875,29 @@ export default class MetamaskController extends EventEmitter {
|
||||
},
|
||||
);
|
||||
},
|
||||
findCustomRpcBy: this.findCustomRpcBy.bind(this),
|
||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||
this.networkController,
|
||||
),
|
||||
setProviderType: this.networkController.setProviderType.bind(
|
||||
this.networkController,
|
||||
),
|
||||
updateRpcTarget: ({ rpcUrl, chainId, ticker, nickname }) => {
|
||||
this.networkController.setRpcTarget(
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
nickname,
|
||||
);
|
||||
},
|
||||
|
||||
// Web3 shim-related
|
||||
getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind(
|
||||
this.alertController,
|
||||
),
|
||||
setWeb3ShimUsageRecorded: this.alertController.setWeb3ShimUsageRecorded.bind(
|
||||
this.alertController,
|
||||
),
|
||||
}),
|
||||
);
|
||||
// filter and subscription polyfills
|
||||
@ -2748,7 +2906,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
if (!isInternal) {
|
||||
// permissions
|
||||
engine.push(
|
||||
this.permissionsController.createMiddleware({ origin, extensionId }),
|
||||
this.permissionController.createPermissionMiddleware({
|
||||
origin,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// forward to metamask primary provider
|
||||
@ -2835,7 +2995,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
* Ignores unknown origins.
|
||||
*
|
||||
* @param {string} origin - The connection's origin string.
|
||||
* @param {any} payload - The event payload.
|
||||
* @param {unknown} payload - The event payload.
|
||||
*/
|
||||
notifyConnections(origin, payload) {
|
||||
const connections = this.connections[origin];
|
||||
@ -2860,7 +3020,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
* The caller is responsible for ensuring that only permitted notifications
|
||||
* are sent.
|
||||
*
|
||||
* @param {any} payload - The event payload, or payload getter function.
|
||||
* @param {unknown} payload - The event payload, or payload getter function.
|
||||
*/
|
||||
notifyAllConnections(payload) {
|
||||
const getPayload =
|
||||
@ -2902,8 +3062,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle global unlock, triggered by KeyringController unlock.
|
||||
* Notifies all connections that the extension is unlocked.
|
||||
* Handle global application unlock.
|
||||
* Notifies all connections that the extension is unlocked, and which
|
||||
* account(s) are currently accessible, if any.
|
||||
*/
|
||||
_onUnlock() {
|
||||
this.notifyAllConnections(async (origin) => {
|
||||
@ -2911,15 +3072,19 @@ export default class MetamaskController extends EventEmitter {
|
||||
method: NOTIFICATION_NAMES.unlockStateChanged,
|
||||
params: {
|
||||
isUnlocked: true,
|
||||
accounts: await this.permissionsController.getAccounts(origin),
|
||||
accounts: await this.getPermittedAccounts(origin),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// In the current implementation, this handler is triggered by a
|
||||
// KeyringController event. Other controllers subscribe to the 'unlock'
|
||||
// event of the MetaMaskController itself.
|
||||
this.emit('unlock');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle global lock, triggered by KeyringController lock.
|
||||
* Handle global application lock.
|
||||
* Notifies all connections that the extension is locked.
|
||||
*/
|
||||
_onLock() {
|
||||
@ -2929,6 +3094,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
isUnlocked: false,
|
||||
},
|
||||
});
|
||||
|
||||
// In the current implementation, this handler is triggered by a
|
||||
// KeyringController event. Other controllers subscribe to the 'lock'
|
||||
// event of the MetaMaskController itself.
|
||||
this.emit('lock');
|
||||
}
|
||||
|
||||
|
@ -768,10 +768,7 @@ describe('MetaMaskController', function () {
|
||||
sinon.stub(metamaskController.preferencesController, 'removeAddress');
|
||||
sinon.stub(metamaskController.accountTracker, 'removeAccount');
|
||||
sinon.stub(metamaskController.keyringController, 'removeAccount');
|
||||
sinon.stub(
|
||||
metamaskController.permissionsController,
|
||||
'removeAllAccountPermissions',
|
||||
);
|
||||
sinon.stub(metamaskController, 'removeAllAccountPermissions');
|
||||
|
||||
ret = await metamaskController.removeAccount(addressToRemove);
|
||||
});
|
||||
@ -780,7 +777,7 @@ describe('MetaMaskController', function () {
|
||||
metamaskController.keyringController.removeAccount.restore();
|
||||
metamaskController.accountTracker.removeAccount.restore();
|
||||
metamaskController.preferencesController.removeAddress.restore();
|
||||
metamaskController.permissionsController.removeAllAccountPermissions.restore();
|
||||
metamaskController.removeAllAccountPermissions.restore();
|
||||
});
|
||||
|
||||
it('should call preferencesController.removeAddress', async function () {
|
||||
@ -804,9 +801,9 @@ describe('MetaMaskController', function () {
|
||||
),
|
||||
);
|
||||
});
|
||||
it('should call permissionsController.removeAllAccountPermissions', async function () {
|
||||
it('should call metamaskController.removeAllAccountPermissions', async function () {
|
||||
assert(
|
||||
metamaskController.permissionsController.removeAllAccountPermissions.calledWith(
|
||||
metamaskController.removeAllAccountPermissions.calledWith(
|
||||
addressToRemove,
|
||||
),
|
||||
);
|
||||
|
161
app/scripts/migrations/068.js
Normal file
161
app/scripts/migrations/068.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
const version = 68;
|
||||
|
||||
/**
|
||||
* Transforms the PermissionsController and PermissionsMetadata substates
|
||||
* to match the new permission system.
|
||||
*/
|
||||
export default {
|
||||
version,
|
||||
async migrate(originalVersionedData) {
|
||||
const versionedData = cloneDeep(originalVersionedData);
|
||||
versionedData.meta.version = version;
|
||||
const state = versionedData.data;
|
||||
const newState = transformState(state);
|
||||
versionedData.data = newState;
|
||||
return versionedData;
|
||||
},
|
||||
};
|
||||
|
||||
function transformState(state) {
|
||||
const {
|
||||
PermissionsController = {},
|
||||
PermissionsMetadata = {},
|
||||
...remainingState
|
||||
} = state;
|
||||
|
||||
const {
|
||||
domainMetadata = {},
|
||||
permissionsHistory = {},
|
||||
permissionsLog = [],
|
||||
} = PermissionsMetadata;
|
||||
|
||||
return {
|
||||
...remainingState,
|
||||
PermissionController: getPermissionControllerState(PermissionsController),
|
||||
PermissionLogController: {
|
||||
permissionActivityLog: permissionsLog,
|
||||
permissionHistory: permissionsHistory,
|
||||
},
|
||||
SubjectMetadataController: getSubjectMetadataControllerState(
|
||||
domainMetadata,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function getPermissionControllerState(PermissionsController) {
|
||||
const { domains = {} } = PermissionsController;
|
||||
|
||||
/**
|
||||
* Example existing domain entry. Every existing domain will have a single
|
||||
* eth_accounts permission, which simplifies the transform.
|
||||
*
|
||||
* 'https://metamask.github.io': {
|
||||
* permissions: [
|
||||
* {
|
||||
* '@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
* 'caveats': [
|
||||
* {
|
||||
* name: 'primaryAccountOnly',
|
||||
* type: 'limitResponseLength',
|
||||
* value: 1,
|
||||
* },
|
||||
* {
|
||||
* name: 'exposedAccounts',
|
||||
* type: 'filterResponse',
|
||||
* value: ['0x0c97a5c81e50a02ff8be73cc3f0a0569e61f4ed8'],
|
||||
* },
|
||||
* ],
|
||||
* 'date': 1616006369498,
|
||||
* 'id': '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
|
||||
* 'invoker': 'https://metamask.github.io',
|
||||
* 'parentCapability': 'eth_accounts',
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
*/
|
||||
|
||||
const ETH_ACCOUNTS = 'eth_accounts';
|
||||
const NEW_CAVEAT_TYPE = 'restrictReturnedAccounts';
|
||||
const OLD_CAVEAT_NAME = 'exposedAccounts';
|
||||
|
||||
const subjects = Object.entries(domains).reduce(
|
||||
(transformed, [origin, domainEntry]) => {
|
||||
const {
|
||||
permissions: [ethAccountsPermission],
|
||||
} = domainEntry;
|
||||
|
||||
// There are two caveats for each eth_accounts permission, but we only
|
||||
// need the value of one of them in the new permission system.
|
||||
const oldCaveat = ethAccountsPermission.caveats.find(
|
||||
(caveat) => caveat.name === OLD_CAVEAT_NAME,
|
||||
);
|
||||
|
||||
const newPermission = {
|
||||
...ethAccountsPermission,
|
||||
caveats: [{ type: NEW_CAVEAT_TYPE, value: oldCaveat.value }],
|
||||
};
|
||||
|
||||
// We never used this, and just omit it in the new system.
|
||||
delete newPermission['@context'];
|
||||
|
||||
transformed[origin] = {
|
||||
origin,
|
||||
permissions: {
|
||||
[ETH_ACCOUNTS]: newPermission,
|
||||
},
|
||||
};
|
||||
return transformed;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
subjects,
|
||||
};
|
||||
}
|
||||
|
||||
function getSubjectMetadataControllerState(domainMetadata) {
|
||||
/**
|
||||
* Example existing domainMetadata entry.
|
||||
*
|
||||
* "https://www.youtube.com": {
|
||||
* "host": "www.youtube.com",
|
||||
* "icon": null,
|
||||
* "lastUpdated": 1637697914908,
|
||||
* "name": "YouTube"
|
||||
* }
|
||||
*/
|
||||
|
||||
const subjectMetadata = Object.entries(domainMetadata).reduce(
|
||||
(transformed, [origin, metadata]) => {
|
||||
const {
|
||||
name = null,
|
||||
icon = null,
|
||||
extensionId = null,
|
||||
...other
|
||||
} = metadata;
|
||||
|
||||
// We're getting rid of these.
|
||||
delete other.lastUpdated;
|
||||
delete other.host;
|
||||
|
||||
if (origin) {
|
||||
transformed[origin] = {
|
||||
name,
|
||||
iconUrl: icon,
|
||||
extensionId,
|
||||
...other,
|
||||
origin,
|
||||
};
|
||||
}
|
||||
return transformed;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
subjectMetadata,
|
||||
};
|
||||
}
|
450
app/scripts/migrations/068.test.js
Normal file
450
app/scripts/migrations/068.test.js
Normal file
@ -0,0 +1,450 @@
|
||||
import migration68 from './068';
|
||||
|
||||
describe('migration #68', () => {
|
||||
it('should update the version metadata', async () => {
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 67,
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
const newStorage = await migration68.migrate(oldStorage);
|
||||
expect(newStorage.meta).toStrictEqual({
|
||||
version: 68,
|
||||
});
|
||||
});
|
||||
|
||||
it('should migrate all data', async () => {
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 67,
|
||||
},
|
||||
data: getOldState(),
|
||||
};
|
||||
|
||||
const newStorage = await migration68.migrate(oldStorage);
|
||||
expect(newStorage).toMatchObject({
|
||||
meta: {
|
||||
version: 68,
|
||||
},
|
||||
data: {
|
||||
FooController: { a: 'b' },
|
||||
PermissionController: { subjects: expect.any(Object) },
|
||||
PermissionLogController: {
|
||||
permissionActivityLog: expect.any(Object),
|
||||
permissionHistory: expect.any(Object),
|
||||
},
|
||||
SubjectMetadataController: { subjectMetadata: expect.any(Object) },
|
||||
},
|
||||
});
|
||||
expect(newStorage.PermissionsController).toBeUndefined();
|
||||
expect(newStorage.PermissionsMetadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should migrate the PermissionsController state', async () => {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
PermissionsController: getOldState().PermissionsController,
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration68.migrate(oldStorage);
|
||||
const { PermissionController } = newStorage.data;
|
||||
|
||||
expect(PermissionController).toStrictEqual({
|
||||
subjects: {
|
||||
'https://faucet.metamask.io': {
|
||||
origin: 'https://faucet.metamask.io',
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0xc42edfcc21ed14dda456aa0756c153f7985d8813'],
|
||||
},
|
||||
],
|
||||
date: 1597334833084,
|
||||
id: 'e01bada4-ddc7-47b6-be67-d4603733e0e9',
|
||||
invoker: 'https://faucet.metamask.io',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
},
|
||||
},
|
||||
'https://metamask.github.io': {
|
||||
origin: 'https://metamask.github.io',
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
|
||||
},
|
||||
],
|
||||
date: 1616006369498,
|
||||
id: '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
|
||||
invoker: 'https://metamask.github.io',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
},
|
||||
},
|
||||
'https://xdai.io': {
|
||||
origin: 'https://xdai.io',
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
|
||||
},
|
||||
],
|
||||
date: 1605908022382,
|
||||
id: '88c5de24-11a9-4f1e-9651-b072f4c11928',
|
||||
invoker: 'https://xdai.io',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should migrate the PermissionsMetadata state', async () => {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
PermissionsMetadata: getOldState().PermissionsMetadata,
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration68.migrate(oldStorage);
|
||||
const {
|
||||
PermissionLogController,
|
||||
SubjectMetadataController,
|
||||
} = newStorage.data;
|
||||
const expected = getOldState().PermissionsMetadata;
|
||||
|
||||
expect(PermissionLogController.permissionHistory).toStrictEqual(
|
||||
expected.permissionsHistory,
|
||||
);
|
||||
expect(PermissionLogController.permissionActivityLog).toStrictEqual(
|
||||
expected.permissionsLog,
|
||||
);
|
||||
|
||||
expect(SubjectMetadataController).toStrictEqual({
|
||||
subjectMetadata: {
|
||||
'https://1inch.exchange': {
|
||||
iconUrl: 'https://1inch.exchange/assets/favicon/favicon-32x32.png',
|
||||
name: 'DEX Aggregator - 1inch.exchange',
|
||||
origin: 'https://1inch.exchange',
|
||||
extensionId: null,
|
||||
},
|
||||
'https://ascii-tree-generator.com': {
|
||||
iconUrl: 'https://ascii-tree-generator.com/favicon.ico',
|
||||
name: 'ASCII Tree Generator',
|
||||
origin: 'https://ascii-tree-generator.com',
|
||||
extensionId: null,
|
||||
},
|
||||
'https://caniuse.com': {
|
||||
iconUrl: 'https://caniuse.com/img/favicon-128.png',
|
||||
name: 'Can I use... Support tables for HTML5, CSS3, etc',
|
||||
origin: 'https://caniuse.com',
|
||||
extensionId: null,
|
||||
},
|
||||
'https://core-geth.org': {
|
||||
iconUrl: 'https://core-geth.org/icons/icon-48x48.png',
|
||||
name: 'core-geth.org',
|
||||
origin: 'https://core-geth.org',
|
||||
extensionId: null,
|
||||
},
|
||||
'https://docs.npmjs.com': {
|
||||
iconUrl: 'https://docs.npmjs.com/favicon-32x32.png',
|
||||
name: 'package-locks | npm Docs',
|
||||
origin: 'https://docs.npmjs.com',
|
||||
extensionId: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle domain metadata edge cases', async () => {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
PermissionsMetadata: {
|
||||
domainMetadata: {
|
||||
'foo.bar': {
|
||||
// no name
|
||||
icon: 'fooIcon',
|
||||
extensionId: 'fooExtension', // non-null
|
||||
origin: null, // should get overwritten
|
||||
extraProperty: 'bar', // should be preserved
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration68.migrate(oldStorage);
|
||||
expect(
|
||||
newStorage.data.SubjectMetadataController.subjectMetadata,
|
||||
).toStrictEqual({
|
||||
'foo.bar': {
|
||||
name: null, // replaced with null
|
||||
iconUrl: 'fooIcon', // preserved value, changed name
|
||||
extensionId: 'fooExtension', // preserved
|
||||
origin: 'foo.bar', // overwritten with correct origin
|
||||
extraProperty: 'bar', // preserved
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getOldState() {
|
||||
return {
|
||||
FooController: { a: 'b' }, // just to ensure it's not touched
|
||||
PermissionsController: {
|
||||
domains: {
|
||||
'https://faucet.metamask.io': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
{
|
||||
name: 'primaryAccountOnly',
|
||||
type: 'limitResponseLength',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
value: ['0xc42edfcc21ed14dda456aa0756c153f7985d8813'],
|
||||
},
|
||||
],
|
||||
'date': 1597334833084,
|
||||
'id': 'e01bada4-ddc7-47b6-be67-d4603733e0e9',
|
||||
'invoker': 'https://faucet.metamask.io',
|
||||
'parentCapability': 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
'https://metamask.github.io': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
{
|
||||
name: 'primaryAccountOnly',
|
||||
type: 'limitResponseLength',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
|
||||
},
|
||||
],
|
||||
'date': 1616006369498,
|
||||
'id': '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
|
||||
'invoker': 'https://metamask.github.io',
|
||||
'parentCapability': 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
'https://xdai.io': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
{
|
||||
name: 'primaryAccountOnly',
|
||||
type: 'limitResponseLength',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
|
||||
},
|
||||
],
|
||||
'date': 1605908022382,
|
||||
'id': '88c5de24-11a9-4f1e-9651-b072f4c11928',
|
||||
'invoker': 'https://xdai.io',
|
||||
'parentCapability': 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
permissionsDescriptions: {},
|
||||
permissionsRequests: [],
|
||||
},
|
||||
PermissionsMetadata: {
|
||||
domainMetadata: {
|
||||
'https://1inch.exchange': {
|
||||
host: '1inch.exchange',
|
||||
icon: 'https://1inch.exchange/assets/favicon/favicon-32x32.png',
|
||||
lastUpdated: 1605489265143,
|
||||
name: 'DEX Aggregator - 1inch.exchange',
|
||||
},
|
||||
'https://ascii-tree-generator.com': {
|
||||
host: 'ascii-tree-generator.com',
|
||||
icon: 'https://ascii-tree-generator.com/favicon.ico',
|
||||
lastUpdated: 1637721988618,
|
||||
name: 'ASCII Tree Generator',
|
||||
},
|
||||
'https://caniuse.com': {
|
||||
host: 'caniuse.com',
|
||||
icon: 'https://caniuse.com/img/favicon-128.png',
|
||||
lastUpdated: 1637692936599,
|
||||
name: 'Can I use... Support tables for HTML5, CSS3, etc',
|
||||
},
|
||||
'https://core-geth.org': {
|
||||
host: 'core-geth.org',
|
||||
icon: 'https://core-geth.org/icons/icon-48x48.png',
|
||||
lastUpdated: 1637692093173,
|
||||
name: 'core-geth.org',
|
||||
},
|
||||
'https://docs.npmjs.com': {
|
||||
host: 'docs.npmjs.com',
|
||||
icon: 'https://docs.npmjs.com/favicon-32x32.png',
|
||||
lastUpdated: 1637721451476,
|
||||
name: 'package-locks | npm Docs',
|
||||
},
|
||||
},
|
||||
permissionsHistory: {
|
||||
'https://opensea.io': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
'0xc42edfcc21ed14dda456aa0756c153f7985d8813': 1617399873696,
|
||||
},
|
||||
lastApproved: 1617399873696,
|
||||
},
|
||||
},
|
||||
'https://faucet.metamask.io': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1620369333736,
|
||||
},
|
||||
lastApproved: 1610405614031,
|
||||
},
|
||||
},
|
||||
'https://metamask.github.io': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1620759882723,
|
||||
'0xf9eab18b7db3adf8cd6bd5f4aed9e1d5e0e7f926': 1616005950557,
|
||||
},
|
||||
lastApproved: 1620759882723,
|
||||
},
|
||||
},
|
||||
'https://xdai.io': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': 1620369333736,
|
||||
},
|
||||
lastApproved: 1605908022384,
|
||||
},
|
||||
},
|
||||
},
|
||||
permissionsLog: [
|
||||
{
|
||||
id: 3642448888,
|
||||
method: 'eth_accounts',
|
||||
methodType: 'restricted',
|
||||
origin: 'https://metamask.github.io',
|
||||
request: {
|
||||
id: 3642448888,
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_accounts',
|
||||
origin: 'https://metamask.github.io',
|
||||
tabId: 489,
|
||||
},
|
||||
requestTime: 1615325885561,
|
||||
response: {
|
||||
id: 3642448888,
|
||||
jsonrpc: '2.0',
|
||||
result: [],
|
||||
},
|
||||
responseTime: 1615325885561,
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
id: 2960964763,
|
||||
method: 'wallet_getPermissions',
|
||||
methodType: 'internal',
|
||||
origin: 'https://metamask.github.io',
|
||||
request: {
|
||||
id: 2960964763,
|
||||
jsonrpc: '2.0',
|
||||
method: 'wallet_getPermissions',
|
||||
origin: 'https://metamask.github.io',
|
||||
tabId: 145,
|
||||
},
|
||||
requestTime: 1620759866273,
|
||||
response: {
|
||||
id: 2960964763,
|
||||
jsonrpc: '2.0',
|
||||
result: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
{
|
||||
name: 'primaryAccountOnly',
|
||||
type: 'limitResponseLength',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
|
||||
},
|
||||
],
|
||||
'date': 1616006369498,
|
||||
'id': '3d0bdc27-e8e4-4fb0-a24b-340d61f6a3fa',
|
||||
'invoker': 'https://metamask.github.io',
|
||||
'parentCapability': 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
responseTime: 1620759866273,
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
id: 2960964764,
|
||||
method: 'eth_accounts',
|
||||
methodType: 'restricted',
|
||||
origin: 'https://metamask.github.io',
|
||||
request: {
|
||||
id: 2960964764,
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_accounts',
|
||||
origin: 'https://metamask.github.io',
|
||||
tabId: 145,
|
||||
},
|
||||
requestTime: 1620759866280,
|
||||
response: {
|
||||
id: 2960964764,
|
||||
jsonrpc: '2.0',
|
||||
result: [],
|
||||
},
|
||||
responseTime: 1620759866280,
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
id: 519616456,
|
||||
method: 'eth_accounts',
|
||||
methodType: 'restricted',
|
||||
origin: 'http://localhost:9011',
|
||||
request:
|
||||
'{\n "method": "eth_accounts",\n "jsonrpc": "2.0",\n "id": 519616456,\n "origin": "http://localhost:9011",\n "tabId": 1020\n}',
|
||||
requestTime: 1636479612050,
|
||||
response:
|
||||
'{\n "id": 519616456,\n "jsonrpc": "2.0",\n "result": []\n}',
|
||||
responseTime: 1636479612051,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
@ -71,6 +71,7 @@ import m064 from './064';
|
||||
import m065 from './065';
|
||||
import m066 from './066';
|
||||
import m067 from './067';
|
||||
import m068 from './068';
|
||||
|
||||
const migrations = [
|
||||
m002,
|
||||
@ -139,6 +140,7 @@ const migrations = [
|
||||
m065,
|
||||
m066,
|
||||
m067,
|
||||
m068,
|
||||
];
|
||||
|
||||
export default migrations;
|
||||
|
@ -1,15 +1,25 @@
|
||||
module.exports = {
|
||||
collectCoverageFrom: ['<rootDir>/ui/**/*.js', '<rootDir>/shared/**/*.js'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/app/scripts/controllers/permissions/*.js',
|
||||
'<rootDir>/shared/**/*.js',
|
||||
'<rootDir>/ui/**/*.js',
|
||||
],
|
||||
coverageDirectory: './jest-coverage/main',
|
||||
coveragePathIgnorePatterns: ['.stories.js', '.snap'],
|
||||
coverageReporters: ['html', 'text-summary'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
'global': {
|
||||
branches: 35,
|
||||
functions: 37,
|
||||
lines: 43,
|
||||
statements: 43,
|
||||
},
|
||||
'./app/scripts/controllers/permissions/*.js': {
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
statements: 100,
|
||||
},
|
||||
},
|
||||
// TODO: enable resetMocks
|
||||
// resetMocks: true,
|
||||
@ -19,9 +29,10 @@ module.exports = {
|
||||
testMatch: [
|
||||
'<rootDir>/ui/**/*.test.js',
|
||||
'<rootDir>/shared/**/*.test.js',
|
||||
'<rootDir>/app/scripts/lib/**/*.test.js',
|
||||
'<rootDir>/app/scripts/migrations/*.test.js',
|
||||
'<rootDir>/app/scripts/platforms/*.test.js',
|
||||
'<rootDir>/app/scripts/lib/**/*.test.js',
|
||||
'<rootDir>/app/scripts/controllers/permissions/*.test.js',
|
||||
],
|
||||
testTimeout: 2500,
|
||||
transform: {
|
||||
|
@ -548,7 +548,6 @@
|
||||
"@metamask/contract-metadata": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"await-semaphore": true,
|
||||
"buffer": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-json-rpc-infura": true,
|
||||
@ -558,11 +557,9 @@
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-tx": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-wallet": true,
|
||||
"ethers": true,
|
||||
"ethjs-query": true,
|
||||
"ethjs-unit": true,
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
@ -642,6 +639,16 @@
|
||||
"gl-vec3": true
|
||||
}
|
||||
},
|
||||
"@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"end-of-stream": true,
|
||||
"once": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/obs-store": {
|
||||
"globals": {
|
||||
"localStorage": true
|
||||
@ -652,6 +659,18 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/safe-event-emitter": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
@ -672,6 +691,34 @@
|
||||
"sha.js": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers": {
|
||||
"globals": {
|
||||
"URL": true,
|
||||
"Worker": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/object-multiplex": true,
|
||||
"@metamask/obs-store": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"@metamask/snap-workers": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eth-rpc-errors": true,
|
||||
"fast-deep-equal": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"json-rpc-middleware-stream": true,
|
||||
"nanoid": true,
|
||||
"pump": true
|
||||
}
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"globals": {
|
||||
"Element": true,
|
||||
@ -1769,13 +1816,6 @@
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-tx": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"ethereum-common": true,
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-util": {
|
||||
"packages": {
|
||||
"assert": true,
|
||||
@ -2677,7 +2717,11 @@
|
||||
}
|
||||
},
|
||||
"json-rpc-middleware-stream": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
@ -4291,15 +4335,6 @@
|
||||
"console": true
|
||||
}
|
||||
},
|
||||
"rpc-cap": {
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"eth-rpc-errors": true,
|
||||
"is-subset": true,
|
||||
"json-rpc-engine": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"packages": {
|
||||
"buffer": true
|
||||
|
@ -548,7 +548,6 @@
|
||||
"@metamask/contract-metadata": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"await-semaphore": true,
|
||||
"buffer": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-json-rpc-infura": true,
|
||||
@ -558,11 +557,9 @@
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-tx": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-wallet": true,
|
||||
"ethers": true,
|
||||
"ethjs-query": true,
|
||||
"ethjs-unit": true,
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
@ -642,6 +639,16 @@
|
||||
"gl-vec3": true
|
||||
}
|
||||
},
|
||||
"@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"end-of-stream": true,
|
||||
"once": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/obs-store": {
|
||||
"globals": {
|
||||
"localStorage": true
|
||||
@ -652,6 +659,18 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/safe-event-emitter": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
@ -672,6 +691,34 @@
|
||||
"sha.js": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers": {
|
||||
"globals": {
|
||||
"URL": true,
|
||||
"Worker": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/object-multiplex": true,
|
||||
"@metamask/obs-store": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"@metamask/snap-workers": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eth-rpc-errors": true,
|
||||
"fast-deep-equal": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"json-rpc-middleware-stream": true,
|
||||
"nanoid": true,
|
||||
"pump": true
|
||||
}
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"globals": {
|
||||
"Element": true,
|
||||
@ -1769,13 +1816,6 @@
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-tx": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"ethereum-common": true,
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-util": {
|
||||
"packages": {
|
||||
"assert": true,
|
||||
@ -2677,7 +2717,11 @@
|
||||
}
|
||||
},
|
||||
"json-rpc-middleware-stream": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
@ -4291,15 +4335,6 @@
|
||||
"console": true
|
||||
}
|
||||
},
|
||||
"rpc-cap": {
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"eth-rpc-errors": true,
|
||||
"is-subset": true,
|
||||
"json-rpc-engine": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"packages": {
|
||||
"buffer": true
|
||||
|
@ -548,7 +548,6 @@
|
||||
"@metamask/contract-metadata": true,
|
||||
"abort-controller": true,
|
||||
"async-mutex": true,
|
||||
"await-semaphore": true,
|
||||
"buffer": true,
|
||||
"eth-ens-namehash": true,
|
||||
"eth-json-rpc-infura": true,
|
||||
@ -558,11 +557,9 @@
|
||||
"eth-query": true,
|
||||
"eth-rpc-errors": true,
|
||||
"eth-sig-util": true,
|
||||
"ethereumjs-tx": true,
|
||||
"ethereumjs-util": true,
|
||||
"ethereumjs-wallet": true,
|
||||
"ethers": true,
|
||||
"ethjs-query": true,
|
||||
"ethjs-unit": true,
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
@ -642,6 +639,16 @@
|
||||
"gl-vec3": true
|
||||
}
|
||||
},
|
||||
"@metamask/object-multiplex": {
|
||||
"globals": {
|
||||
"console.warn": true
|
||||
},
|
||||
"packages": {
|
||||
"end-of-stream": true,
|
||||
"once": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/obs-store": {
|
||||
"globals": {
|
||||
"localStorage": true
|
||||
@ -652,6 +659,18 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"@metamask/post-message-stream": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"location.origin": true,
|
||||
"onmessage": "write",
|
||||
"postMessage": true,
|
||||
"removeEventListener": true
|
||||
},
|
||||
"packages": {
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"@metamask/safe-event-emitter": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
@ -672,6 +691,34 @@
|
||||
"sha.js": true
|
||||
}
|
||||
},
|
||||
"@metamask/snap-controllers": {
|
||||
"globals": {
|
||||
"URL": true,
|
||||
"Worker": true,
|
||||
"clearTimeout": true,
|
||||
"console.error": true,
|
||||
"console.log": true,
|
||||
"console.warn": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"@metamask/object-multiplex": true,
|
||||
"@metamask/obs-store": true,
|
||||
"@metamask/post-message-stream": true,
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"@metamask/snap-workers": true,
|
||||
"deep-freeze-strict": true,
|
||||
"eth-rpc-errors": true,
|
||||
"fast-deep-equal": true,
|
||||
"immer": true,
|
||||
"json-rpc-engine": true,
|
||||
"json-rpc-middleware-stream": true,
|
||||
"nanoid": true,
|
||||
"pump": true
|
||||
}
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"globals": {
|
||||
"Element": true,
|
||||
@ -1769,13 +1816,6 @@
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-tx": {
|
||||
"packages": {
|
||||
"buffer": true,
|
||||
"ethereum-common": true,
|
||||
"ethereumjs-util": true
|
||||
}
|
||||
},
|
||||
"ethereumjs-util": {
|
||||
"packages": {
|
||||
"assert": true,
|
||||
@ -2677,7 +2717,11 @@
|
||||
}
|
||||
},
|
||||
"json-rpc-middleware-stream": {
|
||||
"globals": {
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
@ -4291,15 +4335,6 @@
|
||||
"console": true
|
||||
}
|
||||
},
|
||||
"rpc-cap": {
|
||||
"packages": {
|
||||
"@metamask/controllers": true,
|
||||
"eth-rpc-errors": true,
|
||||
"is-subset": true,
|
||||
"json-rpc-engine": true,
|
||||
"uuid": true
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"packages": {
|
||||
"buffer": true
|
||||
|
@ -1045,6 +1045,16 @@
|
||||
"buffer-equal": true
|
||||
}
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"util.inherits": true
|
||||
},
|
||||
"packages": {
|
||||
"delegates": true,
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"arr-diff": {
|
||||
"packages": {
|
||||
"arr-flatten": true,
|
||||
@ -1453,6 +1463,7 @@
|
||||
"anymatch": true,
|
||||
"async-each": true,
|
||||
"braces": true,
|
||||
"fsevents": true,
|
||||
"glob-parent": true,
|
||||
"inherits": true,
|
||||
"is-binary-path": true,
|
||||
@ -1719,6 +1730,16 @@
|
||||
"through2": true
|
||||
}
|
||||
},
|
||||
"detect-libc": {
|
||||
"builtin": {
|
||||
"child_process.spawnSync": true,
|
||||
"fs.readdirSync": true,
|
||||
"os.platform": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env": true
|
||||
}
|
||||
},
|
||||
"detective": {
|
||||
"packages": {
|
||||
"acorn-node": true,
|
||||
@ -2409,6 +2430,45 @@
|
||||
"process.version": true
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"fs.stat": true,
|
||||
"path.join": true,
|
||||
"util.inherits": true
|
||||
},
|
||||
"globals": {
|
||||
"__dirname": true,
|
||||
"process.nextTick": true,
|
||||
"process.platform": true,
|
||||
"setImmediate": true
|
||||
},
|
||||
"native": true,
|
||||
"packages": {
|
||||
"node-pre-gyp": true
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"builtin": {
|
||||
"util.format": true
|
||||
},
|
||||
"globals": {
|
||||
"clearInterval": true,
|
||||
"process": true,
|
||||
"setImmediate": true,
|
||||
"setInterval": true
|
||||
},
|
||||
"packages": {
|
||||
"aproba": true,
|
||||
"console-control-strings": true,
|
||||
"has-unicode": true,
|
||||
"object-assign": true,
|
||||
"signal-exit": true,
|
||||
"string-width": true,
|
||||
"strip-ansi": true,
|
||||
"wide-align": true
|
||||
}
|
||||
},
|
||||
"get-assigned-identifiers": {
|
||||
"builtin": {
|
||||
"assert.equal": true
|
||||
@ -2782,6 +2842,16 @@
|
||||
"process.argv": true
|
||||
}
|
||||
},
|
||||
"has-unicode": {
|
||||
"builtin": {
|
||||
"os.type": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env.LANG": true,
|
||||
"process.env.LC_ALL": true,
|
||||
"process.env.LC_CTYPE": true
|
||||
}
|
||||
},
|
||||
"has-value": {
|
||||
"packages": {
|
||||
"get-value": true,
|
||||
@ -2953,6 +3023,11 @@
|
||||
"is-plain-object": true
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"packages": {
|
||||
"number-is-nan": true
|
||||
}
|
||||
},
|
||||
"is-glob": {
|
||||
"packages": {
|
||||
"is-extglob": true
|
||||
@ -3478,6 +3553,56 @@
|
||||
"setTimeout": true
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"fs.existsSync": true,
|
||||
"fs.readFileSync": true,
|
||||
"fs.renameSync": true,
|
||||
"path.dirname": true,
|
||||
"path.existsSync": true,
|
||||
"path.join": true,
|
||||
"path.resolve": true,
|
||||
"url.parse": true,
|
||||
"url.resolve": true,
|
||||
"util.inherits": true
|
||||
},
|
||||
"globals": {
|
||||
"__dirname": true,
|
||||
"console.log": true,
|
||||
"process.arch": true,
|
||||
"process.cwd": true,
|
||||
"process.env": true,
|
||||
"process.platform": true,
|
||||
"process.version.substr": true,
|
||||
"process.versions": true
|
||||
},
|
||||
"packages": {
|
||||
"detect-libc": true,
|
||||
"nopt": true,
|
||||
"npmlog": true,
|
||||
"rimraf": true,
|
||||
"semver": true
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"builtin": {
|
||||
"path": true,
|
||||
"stream.Stream": true,
|
||||
"url": true
|
||||
},
|
||||
"globals": {
|
||||
"console": true,
|
||||
"process.argv": true,
|
||||
"process.env.DEBUG_NOPT": true,
|
||||
"process.env.NOPT_DEBUG": true,
|
||||
"process.platform": true
|
||||
},
|
||||
"packages": {
|
||||
"abbrev": true,
|
||||
"osenv": true
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"builtin": {
|
||||
"url.parse": true,
|
||||
@ -3505,6 +3630,22 @@
|
||||
"once": true
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
"builtin": {
|
||||
"events.EventEmitter": true,
|
||||
"util": true
|
||||
},
|
||||
"globals": {
|
||||
"process.nextTick": true,
|
||||
"process.stderr": true
|
||||
},
|
||||
"packages": {
|
||||
"are-we-there-yet": true,
|
||||
"console-control-strings": true,
|
||||
"gauge": true,
|
||||
"set-blocking": true
|
||||
}
|
||||
},
|
||||
"object-copy": {
|
||||
"packages": {
|
||||
"copy-descriptor": true,
|
||||
@ -3586,6 +3727,54 @@
|
||||
"readable-stream": true
|
||||
}
|
||||
},
|
||||
"os-homedir": {
|
||||
"builtin": {
|
||||
"os.homedir": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env": true,
|
||||
"process.getuid": true,
|
||||
"process.platform": true
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"globals": {
|
||||
"process.env.SystemRoot": true,
|
||||
"process.env.TEMP": true,
|
||||
"process.env.TMP": true,
|
||||
"process.env.TMPDIR": true,
|
||||
"process.env.windir": true,
|
||||
"process.platform": true
|
||||
}
|
||||
},
|
||||
"osenv": {
|
||||
"builtin": {
|
||||
"child_process.exec": true,
|
||||
"path": true
|
||||
},
|
||||
"globals": {
|
||||
"process.env.COMPUTERNAME": true,
|
||||
"process.env.ComSpec": true,
|
||||
"process.env.EDITOR": true,
|
||||
"process.env.HOSTNAME": true,
|
||||
"process.env.PATH": true,
|
||||
"process.env.PROMPT": true,
|
||||
"process.env.PS1": true,
|
||||
"process.env.Path": true,
|
||||
"process.env.SHELL": true,
|
||||
"process.env.USER": true,
|
||||
"process.env.USERDOMAIN": true,
|
||||
"process.env.USERNAME": true,
|
||||
"process.env.VISUAL": true,
|
||||
"process.env.path": true,
|
||||
"process.nextTick": true,
|
||||
"process.platform": true
|
||||
},
|
||||
"packages": {
|
||||
"os-homedir": true,
|
||||
"os-tmpdir": true
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"packages": {
|
||||
"p-try": true
|
||||
@ -4290,6 +4479,12 @@
|
||||
"lru-cache": true
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"globals": {
|
||||
"process.stderr": true,
|
||||
"process.stdout": true
|
||||
}
|
||||
},
|
||||
"set-value": {
|
||||
"packages": {
|
||||
"extend-shallow": true,
|
||||
@ -4549,6 +4744,7 @@
|
||||
},
|
||||
"string-width": {
|
||||
"packages": {
|
||||
"code-point-at": true,
|
||||
"emoji-regex": true,
|
||||
"is-fullwidth-code-point": true,
|
||||
"strip-ansi": true
|
||||
@ -5201,6 +5397,11 @@
|
||||
"isexe": true
|
||||
}
|
||||
},
|
||||
"wide-align": {
|
||||
"packages": {
|
||||
"string-width": true
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"builtin": {
|
||||
"fs.createWriteStream": true,
|
||||
|
10
package.json
10
package.json
@ -27,18 +27,14 @@
|
||||
"test:unit": "./test/test-unit-combined.sh",
|
||||
"test:unit:jest": "./test/test-unit-jest.sh",
|
||||
"test:unit:global": "mocha test/unit-global/*.test.js",
|
||||
"test:unit:mocha": "mocha --config '.mocharc.js' './app/**/*.test.js'",
|
||||
"test:unit:lax": "mocha --config '.mocharc.lax.js' './app/**/*.test.js'",
|
||||
"test:unit:strict": "mocha './app/scripts/controllers/permissions/*.test.js'",
|
||||
"test:unit:mocha": "mocha './app/**/*.test.js'",
|
||||
"test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js",
|
||||
"test:e2e:chrome:metrics": "SELENIUM_BROWSER=chrome node test/e2e/run-e2e-test.js test/e2e/metrics.spec.js",
|
||||
"test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js",
|
||||
"test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox node test/e2e/run-e2e-test.js test/e2e/metrics.spec.js",
|
||||
"test:e2e:single": "node test/e2e/run-e2e-test.js",
|
||||
"test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html",
|
||||
"test:coverage:mocha": "nyc --reporter=text --reporter=html yarn test:unit:mocha",
|
||||
"test:coverage:jest": "yarn test:unit:jest --coverage --maxWorkers=2",
|
||||
"test:coverage:strict": "nyc --check-coverage yarn test:unit:strict",
|
||||
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
||||
"ganache:start": "./development/run-ganache.sh",
|
||||
"sentry:publish": "node ./development/sentry-publish.js",
|
||||
"lint:prettier": "prettier '**/*.json'",
|
||||
@ -120,6 +116,7 @@
|
||||
"@metamask/obs-store": "^5.0.0",
|
||||
"@metamask/post-message-stream": "^4.0.0",
|
||||
"@metamask/providers": "^8.1.1",
|
||||
"@metamask/snap-controllers": "^0.4.0",
|
||||
"@ngraveio/bc-ur": "^1.1.6",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@reduxjs/toolkit": "^1.6.2",
|
||||
@ -207,7 +204,6 @@
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^3.0.1",
|
||||
"rpc-cap": "^3.2.1",
|
||||
"safe-event-emitter": "^1.0.1",
|
||||
"ses": "^0.12.4",
|
||||
"single-call-balance-checker-abi": "^1.0.0",
|
||||
|
@ -29,17 +29,20 @@ export const PLATFORM_FIREFOX = 'Firefox';
|
||||
export const PLATFORM_OPERA = 'Opera';
|
||||
|
||||
export const MESSAGE_TYPE = {
|
||||
ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain',
|
||||
ETH_ACCOUNTS: 'eth_accounts',
|
||||
ETH_DECRYPT: 'eth_decrypt',
|
||||
ETH_GET_ENCRYPTION_PUBLIC_KEY: 'eth_getEncryptionPublicKey',
|
||||
ETH_REQUEST_ACCOUNTS: 'eth_requestAccounts',
|
||||
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',
|
||||
SEND_METADATA: 'metamask_sendDomainMetadata',
|
||||
SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain',
|
||||
WATCH_ASSET: 'wallet_watchAsset',
|
||||
WATCH_ASSET_LEGACY: 'metamask_watchAsset',
|
||||
ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain',
|
||||
SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain',
|
||||
};
|
||||
|
||||
export const POLLING_TOKEN_ENVIRONMENT_TYPES = {
|
||||
|
@ -1,4 +1,7 @@
|
||||
export const CAVEAT_NAMES = {
|
||||
exposedAccounts: 'exposedAccounts',
|
||||
primaryAccountOnly: 'primaryAccountOnly',
|
||||
};
|
||||
export const CaveatTypes = Object.freeze({
|
||||
restrictReturnedAccounts: 'restrictReturnedAccounts',
|
||||
});
|
||||
|
||||
export const RestrictedMethods = Object.freeze({
|
||||
eth_accounts: 'eth_accounts',
|
||||
});
|
||||
|
@ -62,11 +62,11 @@ describe('Permissions', function () {
|
||||
tag: 'h2',
|
||||
});
|
||||
await driver.waitForSelector({
|
||||
css: '.connected-sites-list__domain-name',
|
||||
css: '.connected-sites-list__subject-name',
|
||||
text: '127.0.0.1:8080',
|
||||
});
|
||||
const domains = await driver.findClickableElements(
|
||||
'.connected-sites-list__domain-name',
|
||||
'.connected-sites-list__subject-name',
|
||||
);
|
||||
assert.equal(domains.length, 1);
|
||||
|
||||
|
@ -28,7 +28,6 @@ describe('Personal sign', function () {
|
||||
await driver.clickElement('#personalSign');
|
||||
|
||||
await driver.waitUntilXWindowHandles(3);
|
||||
|
||||
const windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle(
|
||||
'MetaMask Notification',
|
||||
|
@ -1,123 +0,0 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import stringify from 'fast-safe-stringify';
|
||||
|
||||
import { noop } from '../mocks/permission-controller';
|
||||
|
||||
/**
|
||||
* Grants the given permissions to the given origin, using the given permissions
|
||||
* controller.
|
||||
*
|
||||
* Just a wrapper for an rpc-cap middleware function.
|
||||
*
|
||||
* @param {PermissionsController} permController - The permissions controller.
|
||||
* @param {string} origin - The origin to grant permissions to.
|
||||
* @param {Object} permissions - The permissions to grant.
|
||||
*/
|
||||
export function grantPermissions(permController, origin, permissions) {
|
||||
permController.permissions.grantNewPermissions(origin, permissions, {}, noop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapper for the given permissions controller's requestUserApproval
|
||||
* function, so we don't have to worry about its internals.
|
||||
*
|
||||
* @param {PermissionsController} permController - The permissions controller.
|
||||
* @return {Function} A convenient wrapper for the requestUserApproval function.
|
||||
*/
|
||||
export function getRequestUserApprovalHelper(permController) {
|
||||
/**
|
||||
* Returns a request object that can be passed to requestUserApproval.
|
||||
*
|
||||
* @param {string} id - The internal permissions request ID (not the RPC request ID).
|
||||
* @param {string} [origin] - The origin of the request, if necessary.
|
||||
* @returns {Object} The corresponding request object.
|
||||
*/
|
||||
return (id, origin = 'defaultOrigin') => {
|
||||
return permController.permissions.requestUserApproval({
|
||||
metadata: { id, origin, type: 'NO_TYPE' },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise that resolves once a pending user approval has been set.
|
||||
* Calls the underlying requestUserApproval function as normal, and restores it
|
||||
* once the Promise is resolved.
|
||||
*
|
||||
* This function must be called on the permissions controller for each request.
|
||||
*
|
||||
* @param {PermissionsController} permController - A permissions controller.
|
||||
* @returns {Promise<void>} A Promise that resolves once a pending approval
|
||||
* has been set.
|
||||
*/
|
||||
export function getUserApprovalPromise(permController) {
|
||||
const originalFunction = permController.permissions.requestUserApproval;
|
||||
return new Promise((resolveHelperPromise) => {
|
||||
permController.permissions.requestUserApproval = (req) => {
|
||||
const userApprovalPromise = originalFunction(req);
|
||||
permController.permissions.requestUserApproval = originalFunction;
|
||||
resolveHelperPromise();
|
||||
return userApprovalPromise;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an activity log entry with respect to a request, response, and
|
||||
* relevant metadata.
|
||||
*
|
||||
* @param {Object} entry - The activity log entry to validate.
|
||||
* @param {Object} req - The request that generated the entry.
|
||||
* @param {Object} [res] - The response for the request, if any.
|
||||
* @param {'restricted'|'internal'} methodType - The method log controller method type of the request.
|
||||
* @param {boolean} success - Whether the request succeeded or not.
|
||||
*/
|
||||
export function validateActivityEntry(entry, req, res, methodType, success) {
|
||||
assert.doesNotThrow(() => {
|
||||
_validateActivityEntry(entry, req, res, methodType, success);
|
||||
}, 'should have expected activity entry');
|
||||
}
|
||||
|
||||
function _validateActivityEntry(entry, req, res, methodType, success) {
|
||||
assert.ok(entry, 'entry should exist');
|
||||
|
||||
assert.equal(entry.id, req.id);
|
||||
assert.equal(entry.method, req.method);
|
||||
assert.equal(entry.origin, req.origin);
|
||||
assert.equal(entry.methodType, methodType);
|
||||
assert.equal(
|
||||
entry.request,
|
||||
stringify(req, null, 2),
|
||||
'entry.request should equal the request',
|
||||
);
|
||||
|
||||
if (res) {
|
||||
assert.ok(
|
||||
Number.isInteger(entry.requestTime) &&
|
||||
Number.isInteger(entry.responseTime),
|
||||
'request and response times should be numbers',
|
||||
);
|
||||
assert.ok(
|
||||
entry.requestTime <= entry.responseTime,
|
||||
'request time should be less than response time',
|
||||
);
|
||||
|
||||
assert.equal(entry.success, success);
|
||||
assert.deepEqual(
|
||||
entry.response,
|
||||
stringify(res, null, 2),
|
||||
'entry.response should equal the response',
|
||||
);
|
||||
} else {
|
||||
assert.ok(
|
||||
Number.isInteger(entry.requestTime) && entry.requestTime > 0,
|
||||
'entry should have non-zero request time',
|
||||
);
|
||||
assert.ok(
|
||||
entry.success === null &&
|
||||
entry.responseTime === null &&
|
||||
entry.response === null,
|
||||
'entry response values should be null',
|
||||
);
|
||||
}
|
||||
}
|
@ -1,736 +0,0 @@
|
||||
import { ethErrors, errorCodes } from 'eth-rpc-errors';
|
||||
import deepFreeze from 'deep-freeze-strict';
|
||||
|
||||
import { ApprovalController, ControllerMessenger } from '@metamask/controllers';
|
||||
|
||||
import _getRestrictedMethods from '../../app/scripts/controllers/permissions/restrictedMethods';
|
||||
|
||||
import { CAVEAT_NAMES } from '../../shared/constants/permissions';
|
||||
import {
|
||||
CAVEAT_TYPES,
|
||||
NOTIFICATION_NAMES,
|
||||
} from '../../app/scripts/controllers/permissions/enums';
|
||||
|
||||
/**
|
||||
* README
|
||||
* This file contains three primary kinds of mocks:
|
||||
* - Mocks for initializing a permissions controller and getting a permissions
|
||||
* middleware
|
||||
* - Functions for getting various mock objects consumed or produced by
|
||||
* permissions controller methods
|
||||
* - Immutable mock values like Ethereum accounts and expected states
|
||||
*/
|
||||
|
||||
export const noop = () => undefined;
|
||||
|
||||
/**
|
||||
* Mock Permissions Controller and Middleware
|
||||
*/
|
||||
|
||||
const keyringAccounts = deepFreeze([
|
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
|
||||
'0xc42edfcc21ed14dda456aa0756c153f7985d8813',
|
||||
'0x7ae1cdd37bcbdb0e1f491974da8022bfdbf9c2bf',
|
||||
'0xcc74c7a59194e5d9268476955650d1e285be703c',
|
||||
]);
|
||||
|
||||
const getIdentities = () => {
|
||||
return keyringAccounts.reduce((identities, address, index) => {
|
||||
identities[address] = { address, name: `Account ${index}` };
|
||||
return identities;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// perm controller initialization helper
|
||||
const getRestrictedMethods = (permController) => {
|
||||
return {
|
||||
// the actual, production restricted methods
|
||||
..._getRestrictedMethods(permController),
|
||||
|
||||
// our own dummy method for testing
|
||||
test_method: {
|
||||
description: `This method is only for testing.`,
|
||||
method: (req, res, __, end) => {
|
||||
if (req.params[0]) {
|
||||
res.result = 1;
|
||||
} else {
|
||||
res.result = 0;
|
||||
}
|
||||
end();
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets default mock constructor options for a permissions controller.
|
||||
*
|
||||
* @returns {Object} A PermissionsController constructor options object.
|
||||
*/
|
||||
export function getPermControllerOpts() {
|
||||
return {
|
||||
approvals: new ApprovalController({
|
||||
messenger: new ControllerMessenger(),
|
||||
showApprovalRequest: noop,
|
||||
}),
|
||||
getKeyringAccounts: async () => [...keyringAccounts],
|
||||
getUnlockPromise: () => Promise.resolve(),
|
||||
getRestrictedMethods,
|
||||
isUnlocked: () => true,
|
||||
notifyDomain: noop,
|
||||
notifyAllDomains: noop,
|
||||
preferences: {
|
||||
getState: () => {
|
||||
return {
|
||||
identities: getIdentities(),
|
||||
selectedAddress: keyringAccounts[0],
|
||||
};
|
||||
},
|
||||
subscribe: noop,
|
||||
},
|
||||
showPermissionRequest: noop,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Promise-wrapped permissions controller middleware function.
|
||||
*
|
||||
* @param {PermissionsController} permController - The permissions controller to get a
|
||||
* middleware for.
|
||||
* @param {string} origin - The origin for the middleware.
|
||||
* @param {string} extensionId - The extension id for the middleware.
|
||||
* @returns {Function} A Promise-wrapped middleware function with convenient default args.
|
||||
*/
|
||||
export function getPermissionsMiddleware(permController, origin, extensionId) {
|
||||
const middleware = permController.createMiddleware({ origin, extensionId });
|
||||
return (req, res = {}, next = noop, end) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
end = end || _end;
|
||||
|
||||
middleware(req, res, next, end);
|
||||
|
||||
// emulates json-rpc-engine error handling
|
||||
function _end(err) {
|
||||
if (err || res.error) {
|
||||
reject(err || res.error);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} notifications - An object that will store notifications produced
|
||||
* by the permissions controller.
|
||||
* @returns {Function} A function passed to the permissions controller at initialization,
|
||||
* for recording notifications.
|
||||
*/
|
||||
export const getNotifyDomain = (notifications = {}) => (
|
||||
origin,
|
||||
notification,
|
||||
) => {
|
||||
notifications[origin].push(notification);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} notifications - An object that will store notifications produced
|
||||
* by the permissions controller.
|
||||
* @returns {Function} A function passed to the permissions controller at initialization,
|
||||
* for recording notifications.
|
||||
*/
|
||||
export const getNotifyAllDomains = (notifications = {}) => (notification) => {
|
||||
Object.keys(notifications).forEach((origin) => {
|
||||
notifications[origin].push(notification);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Constants and Mock Objects
|
||||
* - e.g. permissions, caveats, and permission requests
|
||||
*/
|
||||
|
||||
const DOMAINS = {
|
||||
a: { origin: 'https://foo.xyz', host: 'foo.xyz' },
|
||||
b: { origin: 'https://bar.abc', host: 'bar.abc' },
|
||||
c: { origin: 'https://baz.def', host: 'baz.def' },
|
||||
};
|
||||
|
||||
const PERM_NAMES = {
|
||||
eth_accounts: 'eth_accounts',
|
||||
test_method: 'test_method',
|
||||
does_not_exist: 'does_not_exist',
|
||||
};
|
||||
|
||||
const ACCOUNTS = {
|
||||
a: {
|
||||
permitted: keyringAccounts.slice(0, 3),
|
||||
primary: keyringAccounts[0],
|
||||
},
|
||||
b: {
|
||||
permitted: [keyringAccounts[0]],
|
||||
primary: keyringAccounts[0],
|
||||
},
|
||||
c: {
|
||||
permitted: [keyringAccounts[1]],
|
||||
primary: keyringAccounts[1],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Helpers for getting mock caveats.
|
||||
*/
|
||||
const CAVEATS = {
|
||||
/**
|
||||
* Gets a correctly formatted eth_accounts exposedAccounts caveat.
|
||||
*
|
||||
* @param {Array<string>} accounts - The accounts for the caveat
|
||||
* @returns {Object} An eth_accounts exposedAccounts caveats
|
||||
*/
|
||||
eth_accounts: (accounts) => {
|
||||
return [
|
||||
{
|
||||
type: CAVEAT_TYPES.limitResponseLength,
|
||||
value: 1,
|
||||
name: CAVEAT_NAMES.primaryAccountOnly,
|
||||
},
|
||||
{
|
||||
type: CAVEAT_TYPES.filterResponse,
|
||||
value: accounts,
|
||||
name: CAVEAT_NAMES.exposedAccounts,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Each function here corresponds to what would be a type or interface consumed
|
||||
* by permissions controller functions if we used TypeScript.
|
||||
*/
|
||||
const PERMS = {
|
||||
/**
|
||||
* The argument to approvePermissionsRequest
|
||||
* @param {string} id - The rpc-cap permissions request id.
|
||||
* @param {Object} permissions - The approved permissions, request-formatted.
|
||||
*/
|
||||
approvedRequest: (id, permissions = {}) => {
|
||||
return {
|
||||
permissions: { ...permissions },
|
||||
metadata: { id },
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Requested permissions objects, as passed to wallet_requestPermissions.
|
||||
*/
|
||||
requests: {
|
||||
/**
|
||||
* @returns {Object} A permissions request object with eth_accounts
|
||||
*/
|
||||
eth_accounts: () => {
|
||||
return { eth_accounts: {} };
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A permissions request object with test_method
|
||||
*/
|
||||
test_method: () => {
|
||||
return { test_method: {} };
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A permissions request object with does_not_exist
|
||||
*/
|
||||
does_not_exist: () => {
|
||||
return { does_not_exist: {} };
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Finalized permission requests, as returned by finalizePermissionsRequest
|
||||
*/
|
||||
finalizedRequests: {
|
||||
/**
|
||||
* @param {Array<string>} accounts - The accounts for the eth_accounts permission caveat
|
||||
* @returns {Object} A finalized permissions request object with eth_accounts and its caveat
|
||||
*/
|
||||
eth_accounts: (accounts) => {
|
||||
return {
|
||||
eth_accounts: {
|
||||
caveats: CAVEATS.eth_accounts(accounts),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A finalized permissions request object with test_method
|
||||
*/
|
||||
test_method: () => {
|
||||
return {
|
||||
test_method: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Partial members of res.result for successful:
|
||||
* - wallet_requestPermissions
|
||||
* - wallet_getPermissions
|
||||
*/
|
||||
granted: {
|
||||
/**
|
||||
* @param {Array<string>} accounts - The accounts for the eth_accounts permission caveat
|
||||
* @returns {Object} A granted permissions object with eth_accounts and its caveat
|
||||
*/
|
||||
eth_accounts: (accounts) => {
|
||||
return {
|
||||
parentCapability: PERM_NAMES.eth_accounts,
|
||||
caveats: CAVEATS.eth_accounts(accounts),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A granted permissions object with test_method
|
||||
*/
|
||||
test_method: () => {
|
||||
return {
|
||||
parentCapability: PERM_NAMES.test_method,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Objects with function values for getting correctly formatted permissions,
|
||||
* caveats, errors, permissions requests etc.
|
||||
*/
|
||||
export const getters = deepFreeze({
|
||||
CAVEATS,
|
||||
|
||||
PERMS,
|
||||
|
||||
/**
|
||||
* Getters for errors by the method or workflow that throws them.
|
||||
*/
|
||||
ERRORS: {
|
||||
validatePermittedAccounts: {
|
||||
invalidParam: () => {
|
||||
return {
|
||||
name: 'Error',
|
||||
message: 'Must provide non-empty array of account(s).',
|
||||
};
|
||||
},
|
||||
|
||||
nonKeyringAccount: (account) => {
|
||||
return {
|
||||
name: 'Error',
|
||||
message: `Unknown account: ${account}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
finalizePermissionsRequest: {
|
||||
grantEthAcountsFailure: (origin) => {
|
||||
return {
|
||||
// name: 'EthereumRpcError',
|
||||
message: `Failed to add 'eth_accounts' to '${origin}'.`,
|
||||
code: errorCodes.rpc.internal,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
addPermittedAccount: {
|
||||
alreadyPermitted: () => {
|
||||
return {
|
||||
message: 'Account is already permitted for origin',
|
||||
};
|
||||
},
|
||||
invalidOrigin: () => {
|
||||
return {
|
||||
message: 'Unrecognized domain',
|
||||
};
|
||||
},
|
||||
noEthAccountsPermission: () => {
|
||||
return {
|
||||
message: `Origin does not have 'eth_accounts' permission`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
removePermittedAccount: {
|
||||
notPermitted: () => {
|
||||
return {
|
||||
message: 'Account is not permitted for origin',
|
||||
};
|
||||
},
|
||||
invalidOrigin: () => {
|
||||
return {
|
||||
message: 'Unrecognized domain',
|
||||
};
|
||||
},
|
||||
noEthAccountsPermission: () => {
|
||||
return {
|
||||
message: `Origin does not have 'eth_accounts' permission`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
_handleAccountSelected: {
|
||||
invalidParams: () => {
|
||||
return {
|
||||
name: 'Error',
|
||||
message: 'Selected account should be a non-empty string.',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
approvePermissionsRequest: {
|
||||
noPermsRequested: () => {
|
||||
return {
|
||||
message: 'Must request at least one permission.',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rejectPermissionsRequest: {
|
||||
rejection: () => {
|
||||
return {
|
||||
message: ethErrors.provider.userRejectedRequest().message,
|
||||
};
|
||||
},
|
||||
methodNotFound: (methodName) => {
|
||||
return {
|
||||
message: `The method '${methodName}' does not exist / is not available.`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
createMiddleware: {
|
||||
badOrigin: () => {
|
||||
return {
|
||||
message: 'Must provide non-empty string origin.',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rpcCap: {
|
||||
unauthorized: () => {
|
||||
return {
|
||||
code: 4100,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
pendingApprovals: {
|
||||
duplicateOriginOrId: (id, origin) => {
|
||||
return {
|
||||
message: `Pending approval with id '${id}' or origin '${origin}' already exists.`,
|
||||
};
|
||||
},
|
||||
requestAlreadyPending: (origin) => {
|
||||
return {
|
||||
message: `Request of type 'wallet_requestPermissions' already pending for origin ${origin}. Please wait.`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
eth_requestAccounts: {
|
||||
requestAlreadyPending: () => {
|
||||
return {
|
||||
message: 'Already processing eth_requestAccounts. Please wait.',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
notifyAccountsChanged: {
|
||||
invalidOrigin: (origin) => {
|
||||
return {
|
||||
message: `Invalid origin: '${origin}'`,
|
||||
};
|
||||
},
|
||||
invalidAccounts: () => {
|
||||
return {
|
||||
message: 'Invalid accounts',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Getters for notifications produced by the permissions controller.
|
||||
*/
|
||||
NOTIFICATIONS: {
|
||||
/**
|
||||
* Gets a removed accounts notification.
|
||||
*
|
||||
* @returns {Object} An accountsChanged notification with an empty array as its result
|
||||
*/
|
||||
removedAccounts: () => {
|
||||
return {
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a new accounts notification.
|
||||
*
|
||||
* @param {Array<string>} accounts - The accounts added to the notification.
|
||||
* @returns {Object} An accountsChanged notification with the given accounts as its result
|
||||
*/
|
||||
newAccounts: (accounts) => {
|
||||
return {
|
||||
method: NOTIFICATION_NAMES.accountsChanged,
|
||||
params: accounts,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Getters for mock RPC request objects.
|
||||
*/
|
||||
RPC_REQUESTS: {
|
||||
/**
|
||||
* Gets an arbitrary RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {string} method - The request method
|
||||
* @param {Array<any>} params - The request parameters
|
||||
* @param {string} [id] - The request id
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
custom: (origin, method, params = [], id) => {
|
||||
const req = {
|
||||
origin,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
if (id !== undefined) {
|
||||
req.id = id;
|
||||
}
|
||||
return req;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an eth_accounts RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
eth_accounts: (origin) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'eth_accounts',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a test_method RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {boolean} param - The request param
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
test_method: (origin, param = false) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'test_method',
|
||||
params: [param],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an eth_requestAccounts RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
eth_requestAccounts: (origin) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'eth_requestAccounts',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a wallet_requestPermissions RPC request object,
|
||||
* for a single permission.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {string} permissionName - The name of the permission to request
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
requestPermission: (origin, permissionName) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'wallet_requestPermissions',
|
||||
params: [PERMS.requests[permissionName]()],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a wallet_requestPermissions RPC request object,
|
||||
* for multiple permissions.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {Object} permissions - A permission request object
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
requestPermissions: (origin, permissions = {}) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'wallet_requestPermissions',
|
||||
params: [permissions],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
metamask_sendDomainMetadata: (origin, name, ...args) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'metamask_sendDomainMetadata',
|
||||
params: {
|
||||
...args,
|
||||
name,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Objects with immutable mock values.
|
||||
*/
|
||||
export const constants = deepFreeze({
|
||||
ALL_ACCOUNTS: keyringAccounts,
|
||||
|
||||
DUMMY_ACCOUNT: '0xabc',
|
||||
|
||||
EXTRA_ACCOUNT: keyringAccounts[3],
|
||||
|
||||
REQUEST_IDS: {
|
||||
a: '1',
|
||||
b: '2',
|
||||
c: '3',
|
||||
},
|
||||
|
||||
DOMAINS: { ...DOMAINS },
|
||||
|
||||
ACCOUNTS: { ...ACCOUNTS },
|
||||
|
||||
PERM_NAMES: { ...PERM_NAMES },
|
||||
|
||||
RESTRICTED_METHODS: ['eth_accounts', 'test_method'],
|
||||
|
||||
/**
|
||||
* Mock permissions history objects.
|
||||
*/
|
||||
EXPECTED_HISTORIES: {
|
||||
case1: [
|
||||
{
|
||||
[DOMAINS.a.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.a.permitted[0]]: 1,
|
||||
[ACCOUNTS.a.permitted[1]]: 1,
|
||||
[ACCOUNTS.a.permitted[2]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[DOMAINS.a.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 2,
|
||||
accounts: {
|
||||
[ACCOUNTS.a.permitted[0]]: 2,
|
||||
[ACCOUNTS.a.permitted[1]]: 1,
|
||||
[ACCOUNTS.a.permitted[2]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
case2: [
|
||||
{
|
||||
[DOMAINS.a.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
case3: [
|
||||
{
|
||||
[DOMAINS.a.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||
},
|
||||
[DOMAINS.b.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.b.permitted[0]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOMAINS.c.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.c.permitted[0]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[DOMAINS.a.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 2 },
|
||||
},
|
||||
[DOMAINS.b.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.b.permitted[0]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
[DOMAINS.c.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 2,
|
||||
accounts: {
|
||||
[ACCOUNTS.c.permitted[0]]: 1,
|
||||
[ACCOUNTS.b.permitted[0]]: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
case4: [
|
||||
{
|
||||
[DOMAINS.a.origin]: {
|
||||
[PERM_NAMES.test_method]: {
|
||||
lastApproved: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
369
test/mocks/permissions.js
Normal file
369
test/mocks/permissions.js
Normal file
@ -0,0 +1,369 @@
|
||||
import deepFreeze from 'deep-freeze-strict';
|
||||
import { CaveatTypes } from '../../shared/constants/permissions';
|
||||
|
||||
/**
|
||||
* This file contains mocks for the PermissionLogController tests.
|
||||
*/
|
||||
|
||||
export const noop = () => undefined;
|
||||
|
||||
const keyringAccounts = deepFreeze([
|
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
|
||||
'0xc42edfcc21ed14dda456aa0756c153f7985d8813',
|
||||
'0x7ae1cdd37bcbdb0e1f491974da8022bfdbf9c2bf',
|
||||
'0xcc74c7a59194e5d9268476955650d1e285be703c',
|
||||
]);
|
||||
|
||||
const SUBJECTS = {
|
||||
a: { origin: 'https://foo.xyz' },
|
||||
b: { origin: 'https://bar.abc' },
|
||||
c: { origin: 'https://baz.def' },
|
||||
};
|
||||
|
||||
const PERM_NAMES = {
|
||||
eth_accounts: 'eth_accounts',
|
||||
test_method: 'test_method',
|
||||
does_not_exist: 'does_not_exist',
|
||||
};
|
||||
|
||||
const ACCOUNTS = {
|
||||
a: {
|
||||
permitted: keyringAccounts.slice(0, 3),
|
||||
primary: keyringAccounts[0],
|
||||
},
|
||||
b: {
|
||||
permitted: [keyringAccounts[0]],
|
||||
primary: keyringAccounts[0],
|
||||
},
|
||||
c: {
|
||||
permitted: [keyringAccounts[1]],
|
||||
primary: keyringAccounts[1],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Helpers for getting mock caveats.
|
||||
*/
|
||||
const CAVEATS = {
|
||||
/**
|
||||
* Gets a correctly formatted eth_accounts restrictReturnedAccounts caveat.
|
||||
*
|
||||
* @param {Array<string>} accounts - The accounts for the caveat
|
||||
* @returns {Object} An eth_accounts restrictReturnedAccounts caveats
|
||||
*/
|
||||
eth_accounts: (accounts) => {
|
||||
return [
|
||||
{
|
||||
type: CaveatTypes.restrictReturnedAccounts,
|
||||
value: accounts,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Each function here corresponds to what would be a type or interface consumed
|
||||
* by permissions controller functions if we used TypeScript.
|
||||
*/
|
||||
const PERMS = {
|
||||
/**
|
||||
* Requested permissions objects, as passed to wallet_requestPermissions.
|
||||
*/
|
||||
requests: {
|
||||
/**
|
||||
* @returns {Object} A permissions request object with eth_accounts
|
||||
*/
|
||||
eth_accounts: () => {
|
||||
return { eth_accounts: {} };
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A permissions request object with test_method
|
||||
*/
|
||||
test_method: () => {
|
||||
return { test_method: {} };
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A permissions request object with does_not_exist
|
||||
*/
|
||||
does_not_exist: () => {
|
||||
return { does_not_exist: {} };
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Partial members of res.result for successful:
|
||||
* - wallet_requestPermissions
|
||||
* - wallet_getPermissions
|
||||
*/
|
||||
granted: {
|
||||
/**
|
||||
* @param {Array<string>} accounts - The accounts for the eth_accounts permission caveat
|
||||
* @returns {Object} A granted permissions object with eth_accounts and its caveat
|
||||
*/
|
||||
eth_accounts: (accounts) => {
|
||||
return {
|
||||
parentCapability: PERM_NAMES.eth_accounts,
|
||||
caveats: CAVEATS.eth_accounts(accounts),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} A granted permissions object with test_method
|
||||
*/
|
||||
test_method: () => {
|
||||
return {
|
||||
parentCapability: PERM_NAMES.test_method,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Objects with function values for getting correctly formatted permissions,
|
||||
* caveats, errors, permissions requests etc.
|
||||
*/
|
||||
export const getters = deepFreeze({
|
||||
PERMS,
|
||||
|
||||
/**
|
||||
* Getters for mock RPC request objects.
|
||||
*/
|
||||
RPC_REQUESTS: {
|
||||
/**
|
||||
* Gets an arbitrary RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {string} method - The request method
|
||||
* @param {Array<any>} params - The request parameters
|
||||
* @param {string} [id] - The request id
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
custom: (origin, method, params = [], id) => {
|
||||
const req = {
|
||||
origin,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
if (id !== undefined) {
|
||||
req.id = id;
|
||||
}
|
||||
return req;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an eth_accounts RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
eth_accounts: (origin) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'eth_accounts',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a test_method RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {boolean} param - The request param
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
test_method: (origin, param = false) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'test_method',
|
||||
params: [param],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an eth_requestAccounts RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
eth_requestAccounts: (origin) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'eth_requestAccounts',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a wallet_requestPermissions RPC request object,
|
||||
* for a single permission.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {string} permissionName - The name of the permission to request
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
requestPermission: (origin, permissionName) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'wallet_requestPermissions',
|
||||
params: [PERMS.requests[permissionName]()],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a wallet_requestPermissions RPC request object,
|
||||
* for multiple permissions.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {Object} permissions - A permission request object
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
requestPermissions: (origin, permissions = {}) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'wallet_requestPermissions',
|
||||
params: [permissions],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a metamask_sendDomainMetadata RPC request object.
|
||||
*
|
||||
* @param {string} origin - The origin of the request
|
||||
* @param {Object} name - The subjectMetadata name
|
||||
* @param {Array<any>} [args] - Any other data for the request's subjectMetadata
|
||||
* @returns {Object} An RPC request object
|
||||
*/
|
||||
metamask_sendDomainMetadata: (origin, name, ...args) => {
|
||||
return {
|
||||
origin,
|
||||
method: 'metamask_sendDomainMetadata',
|
||||
params: {
|
||||
...args,
|
||||
name,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Objects with immutable mock values.
|
||||
*/
|
||||
export const constants = deepFreeze({
|
||||
REQUEST_IDS: {
|
||||
a: '1',
|
||||
b: '2',
|
||||
c: '3',
|
||||
},
|
||||
|
||||
SUBJECTS: { ...SUBJECTS },
|
||||
|
||||
ACCOUNTS: { ...ACCOUNTS },
|
||||
|
||||
PERM_NAMES: { ...PERM_NAMES },
|
||||
|
||||
RESTRICTED_METHODS: new Set(['eth_accounts', 'test_method']),
|
||||
|
||||
/**
|
||||
* Mock permissions history objects.
|
||||
*/
|
||||
EXPECTED_HISTORIES: {
|
||||
case1: [
|
||||
{
|
||||
[SUBJECTS.a.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.a.permitted[0]]: 1,
|
||||
[ACCOUNTS.a.permitted[1]]: 1,
|
||||
[ACCOUNTS.a.permitted[2]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[SUBJECTS.a.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 2,
|
||||
accounts: {
|
||||
[ACCOUNTS.a.permitted[0]]: 2,
|
||||
[ACCOUNTS.a.permitted[1]]: 1,
|
||||
[ACCOUNTS.a.permitted[2]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
case2: [
|
||||
{
|
||||
[SUBJECTS.a.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
case3: [
|
||||
{
|
||||
[SUBJECTS.a.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||
},
|
||||
[SUBJECTS.b.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.b.permitted[0]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
[SUBJECTS.c.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.c.permitted[0]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[SUBJECTS.a.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 2 },
|
||||
},
|
||||
[SUBJECTS.b.origin]: {
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 1,
|
||||
accounts: {
|
||||
[ACCOUNTS.b.permitted[0]]: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
[SUBJECTS.c.origin]: {
|
||||
[PERM_NAMES.test_method]: { lastApproved: 1 },
|
||||
[PERM_NAMES.eth_accounts]: {
|
||||
lastApproved: 2,
|
||||
accounts: {
|
||||
[ACCOUNTS.c.permitted[0]]: 1,
|
||||
[ACCOUNTS.b.permitted[0]]: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
case4: [
|
||||
{
|
||||
[SUBJECTS.a.origin]: {
|
||||
[PERM_NAMES.test_method]: {
|
||||
lastApproved: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
@ -70,7 +70,7 @@ export default class AccountMenu extends Component {
|
||||
selectedAddress: PropTypes.string,
|
||||
showAccountDetail: PropTypes.func,
|
||||
toggleAccountMenu: PropTypes.func,
|
||||
addressConnectedDomainMap: PropTypes.object,
|
||||
addressConnectedSubjectMap: PropTypes.object,
|
||||
originOfCurrentTab: PropTypes.string,
|
||||
};
|
||||
|
||||
@ -147,7 +147,7 @@ export default class AccountMenu extends Component {
|
||||
selectedAddress,
|
||||
keyrings,
|
||||
showAccountDetail,
|
||||
addressConnectedDomainMap,
|
||||
addressConnectedSubjectMap,
|
||||
originOfCurrentTab,
|
||||
} = this.props;
|
||||
const { searchQuery } = this.state;
|
||||
@ -177,8 +177,9 @@ export default class AccountMenu extends Component {
|
||||
kr.accounts.includes(identity.address)
|
||||
);
|
||||
});
|
||||
const addressDomains = addressConnectedDomainMap[identity.address] || {};
|
||||
const iconAndNameForOpenDomain = addressDomains[originOfCurrentTab];
|
||||
const addressSubjects =
|
||||
addressConnectedSubjectMap[identity.address] || {};
|
||||
const iconAndNameForOpenSubject = addressSubjects[originOfCurrentTab];
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -210,11 +211,11 @@ export default class AccountMenu extends Component {
|
||||
/>
|
||||
</div>
|
||||
{this.renderKeyringType(keyring)}
|
||||
{iconAndNameForOpenDomain ? (
|
||||
{iconAndNameForOpenSubject ? (
|
||||
<div className="account-menu__icon-list">
|
||||
<SiteIcon
|
||||
icon={iconAndNameForOpenDomain.icon}
|
||||
name={iconAndNameForOpenDomain.name}
|
||||
icon={iconAndNameForOpenSubject.icon}
|
||||
name={iconAndNameForOpenSubject.name}
|
||||
size={32}
|
||||
/>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
hideWarning,
|
||||
} from '../../../store/actions';
|
||||
import {
|
||||
getAddressConnectedDomainMap,
|
||||
getAddressConnectedSubjectMap,
|
||||
getMetaMaskAccountsOrdered,
|
||||
getMetaMaskKeyrings,
|
||||
getOriginOfCurrentTab,
|
||||
@ -31,7 +31,7 @@ function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
isAccountMenuOpen,
|
||||
addressConnectedDomainMap: getAddressConnectedDomainMap(state),
|
||||
addressConnectedSubjectMap: getAddressConnectedSubjectMap(state),
|
||||
originOfCurrentTab: origin,
|
||||
selectedAddress,
|
||||
keyrings: getMetaMaskKeyrings(state),
|
||||
|
@ -24,7 +24,7 @@ describe('Account Menu', () => {
|
||||
|
||||
const props = {
|
||||
isAccountMenuOpen: true,
|
||||
addressConnectedDomainMap: {},
|
||||
addressConnectedSubjectMap: {},
|
||||
accounts: [
|
||||
{
|
||||
address: '0x00',
|
||||
|
@ -65,7 +65,7 @@ describe('Unconnected Account Alert', () => {
|
||||
provider: {
|
||||
chainId: KOVAN_CHAIN_ID,
|
||||
},
|
||||
permissionsHistory: {
|
||||
permissionHistory: {
|
||||
'https://test.dapp': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
@ -74,26 +74,20 @@ describe('Unconnected Account Alert', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
domains: {
|
||||
subjects: {
|
||||
'https://test.dapp': {
|
||||
permissions: [
|
||||
{
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'primaryAccountOnly',
|
||||
type: 'limitResponseLength',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
|
||||
},
|
||||
],
|
||||
invoker: 'https://test.dapp',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SiteIcon from '../../ui/site-icon';
|
||||
import { stripHttpSchemes } from '../../../helpers/utils/util';
|
||||
import { stripHttpsSchemeWithoutPort } from '../../../helpers/utils/util';
|
||||
|
||||
export default class ConnectedSitesList extends Component {
|
||||
static contextTypes = {
|
||||
@ -9,42 +9,40 @@ export default class ConnectedSitesList extends Component {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
connectedDomains: PropTypes.arrayOf(
|
||||
connectedSubjects: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconUrl: PropTypes.string,
|
||||
origin: PropTypes.string,
|
||||
host: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
onDisconnect: PropTypes.func.isRequired,
|
||||
domainHostCount: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { connectedDomains, onDisconnect } = this.props;
|
||||
const { connectedSubjects, onDisconnect } = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
return (
|
||||
<main className="connected-sites-list__content-rows">
|
||||
{connectedDomains.map((domain) => (
|
||||
{connectedSubjects.map((subject) => (
|
||||
<div
|
||||
key={domain.origin}
|
||||
key={subject.origin}
|
||||
className="connected-sites-list__content-row"
|
||||
>
|
||||
<div className="connected-sites-list__domain-info">
|
||||
<SiteIcon icon={domain.icon} name={domain.name} size={32} />
|
||||
<div className="connected-sites-list__subject-info">
|
||||
<SiteIcon icon={subject.iconUrl} name={subject.name} size={32} />
|
||||
<span
|
||||
className="connected-sites-list__domain-name"
|
||||
title={domain.extensionId || domain.origin}
|
||||
className="connected-sites-list__subject-name"
|
||||
title={subject.extensionId || subject.origin}
|
||||
>
|
||||
{this.getDomainDisplayName(domain)}
|
||||
{this.getSubjectDisplayName(subject)}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
className="fas fa-trash-alt connected-sites-list__trash"
|
||||
title={t('disconnect')}
|
||||
onClick={() => onDisconnect(domain.origin)}
|
||||
onClick={() => onDisconnect(subject.origin)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -52,13 +50,12 @@ export default class ConnectedSitesList extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
getDomainDisplayName(domain) {
|
||||
if (domain.extensionId) {
|
||||
getSubjectDisplayName(subject) {
|
||||
if (subject.extensionId) {
|
||||
return this.context.t('externalExtension');
|
||||
}
|
||||
|
||||
return this.props.domainHostCount[domain.host] > 1
|
||||
? domain.origin
|
||||
: stripHttpSchemes(domain.origin);
|
||||
// We strip https schemes only, and only if the URL has no port.
|
||||
return stripHttpsSchemeWithoutPort(subject.origin);
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
&__domain-info {
|
||||
&__subject-info {
|
||||
@include H7;
|
||||
|
||||
display: flex;
|
||||
@ -24,7 +24,7 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__domain-name {
|
||||
&__subject-name {
|
||||
max-width: 215px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -11,7 +11,7 @@ import ColorIndicator from '../../ui/color-indicator';
|
||||
import { COLORS } from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import {
|
||||
getAddressConnectedDomainMap,
|
||||
getAddressConnectedSubjectMap,
|
||||
getOriginOfCurrentTab,
|
||||
getSelectedAddress,
|
||||
} from '../../../selectors';
|
||||
@ -20,17 +20,17 @@ export default function ConnectedStatusIndicator({ onClick }) {
|
||||
const t = useI18nContext();
|
||||
|
||||
const selectedAddress = useSelector(getSelectedAddress);
|
||||
const addressConnectedDomainMap = useSelector(getAddressConnectedDomainMap);
|
||||
const addressConnectedSubjectMap = useSelector(getAddressConnectedSubjectMap);
|
||||
const originOfCurrentTab = useSelector(getOriginOfCurrentTab);
|
||||
|
||||
const selectedAddressDomainMap = addressConnectedDomainMap[selectedAddress];
|
||||
const selectedAddressSubjectMap = addressConnectedSubjectMap[selectedAddress];
|
||||
const currentTabIsConnectedToSelectedAddress = Boolean(
|
||||
selectedAddressDomainMap && selectedAddressDomainMap[originOfCurrentTab],
|
||||
selectedAddressSubjectMap && selectedAddressSubjectMap[originOfCurrentTab],
|
||||
);
|
||||
let status;
|
||||
if (currentTabIsConnectedToSelectedAddress) {
|
||||
status = STATUS_CONNECTED;
|
||||
} else if (findKey(addressConnectedDomainMap, originOfCurrentTab)) {
|
||||
} else if (findKey(addressConnectedSubjectMap, originOfCurrentTab)) {
|
||||
status = STATUS_CONNECTED_TO_ANOTHER_ACCOUNT;
|
||||
} else {
|
||||
status = STATUS_NOT_CONNECTED;
|
||||
|
@ -6,10 +6,9 @@ import PermissionsConnectPermissionList from '../../permissions-connect-permissi
|
||||
|
||||
export default class PermissionPageContainerContent extends PureComponent {
|
||||
static propTypes = {
|
||||
domainMetadata: PropTypes.shape({
|
||||
subjectMetadata: PropTypes.shape({
|
||||
extensionId: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
host: PropTypes.string.isRequired,
|
||||
iconUrl: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
origin: PropTypes.string.isRequired,
|
||||
}),
|
||||
@ -68,14 +67,14 @@ export default class PermissionPageContainerContent extends PureComponent {
|
||||
|
||||
getTitle() {
|
||||
const {
|
||||
domainMetadata,
|
||||
subjectMetadata,
|
||||
selectedIdentities,
|
||||
allIdentitiesSelected,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
if (domainMetadata.extensionId) {
|
||||
return t('externalExtension', [domainMetadata.extensionId]);
|
||||
if (subjectMetadata.extensionId) {
|
||||
return t('externalExtension', [subjectMetadata.extensionId]);
|
||||
} else if (allIdentitiesSelected) {
|
||||
return t('connectToAll', [
|
||||
this.renderAccountTooltip(t('connectToAllAccounts')),
|
||||
@ -91,7 +90,7 @@ export default class PermissionPageContainerContent extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { domainMetadata } = this.props;
|
||||
const { subjectMetadata } = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
const title = this.getTitle();
|
||||
@ -100,15 +99,15 @@ export default class PermissionPageContainerContent extends PureComponent {
|
||||
<div className="permission-approval-container__content">
|
||||
<div className="permission-approval-container__content-container">
|
||||
<PermissionsConnectHeader
|
||||
icon={domainMetadata.icon}
|
||||
iconName={domainMetadata.name}
|
||||
icon={subjectMetadata.iconUrl}
|
||||
iconName={subjectMetadata.name}
|
||||
headerTitle={title}
|
||||
headerText={
|
||||
domainMetadata.extensionId
|
||||
? t('allowExternalExtensionTo', [domainMetadata.extensionId])
|
||||
subjectMetadata.extensionId
|
||||
? t('allowExternalExtensionTo', [subjectMetadata.extensionId])
|
||||
: t('allowThisSiteTo')
|
||||
}
|
||||
siteOrigin={domainMetadata.origin}
|
||||
siteOrigin={subjectMetadata.origin}
|
||||
/>
|
||||
<section className="permission-approval-container__permissions-container">
|
||||
{this.renderRequestedPermissions()}
|
||||
|
@ -13,10 +13,9 @@ export default class PermissionPageContainer extends Component {
|
||||
allIdentitiesSelected: PropTypes.bool,
|
||||
request: PropTypes.object,
|
||||
requestMetadata: PropTypes.object,
|
||||
targetDomainMetadata: PropTypes.shape({
|
||||
targetSubjectMetadata: PropTypes.shape({
|
||||
extensionId: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
host: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
origin: PropTypes.string.isRequired,
|
||||
}),
|
||||
@ -88,6 +87,9 @@ export default class PermissionPageContainer extends Component {
|
||||
const request = {
|
||||
..._request,
|
||||
permissions: { ..._request.permissions },
|
||||
approvedAccounts: selectedIdentities.map(
|
||||
(selectedIdentity) => selectedIdentity.address,
|
||||
),
|
||||
};
|
||||
|
||||
Object.keys(this.state.selectedPermissions).forEach((key) => {
|
||||
@ -97,10 +99,7 @@ export default class PermissionPageContainer extends Component {
|
||||
});
|
||||
|
||||
if (Object.keys(request.permissions).length > 0) {
|
||||
approvePermissionsRequest(
|
||||
request,
|
||||
selectedIdentities.map((selectedIdentity) => selectedIdentity.address),
|
||||
);
|
||||
approvePermissionsRequest(request);
|
||||
} else {
|
||||
rejectPermissionsRequest(request.metadata.id);
|
||||
}
|
||||
@ -109,7 +108,7 @@ export default class PermissionPageContainer extends Component {
|
||||
render() {
|
||||
const {
|
||||
requestMetadata,
|
||||
targetDomainMetadata,
|
||||
targetSubjectMetadata,
|
||||
selectedIdentities,
|
||||
allIdentitiesSelected,
|
||||
} = this.props;
|
||||
@ -118,7 +117,7 @@ export default class PermissionPageContainer extends Component {
|
||||
<div className="page-container permission-approval-container">
|
||||
<PermissionPageContainerContent
|
||||
requestMetadata={requestMetadata}
|
||||
domainMetadata={targetDomainMetadata}
|
||||
subjectMetadata={targetSubjectMetadata}
|
||||
selectedPermissions={this.state.selectedPermissions}
|
||||
selectedIdentities={selectedIdentities}
|
||||
allIdentitiesSelected={allIdentitiesSelected}
|
||||
|
@ -6,6 +6,7 @@ import { ObjectInspector } from 'react-inspector';
|
||||
import LedgerInstructionField from '../ledger-instruction-field';
|
||||
|
||||
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
|
||||
import { getURLHostName } from '../../../helpers/utils/util';
|
||||
import Identicon from '../../ui/identicon';
|
||||
import AccountListItem from '../account-list-item';
|
||||
import { conversionUtil } from '../../../../shared/modules/conversion.utils';
|
||||
@ -32,7 +33,7 @@ export default class SignatureRequestOriginal extends Component {
|
||||
requesterAddress: PropTypes.string,
|
||||
sign: PropTypes.func.isRequired,
|
||||
txData: PropTypes.object.isRequired,
|
||||
domainMetadata: PropTypes.object,
|
||||
subjectMetadata: PropTypes.object,
|
||||
hardwareWalletRequiresConnection: PropTypes.bool,
|
||||
isLedgerWallet: PropTypes.bool,
|
||||
nativeCurrency: PropTypes.string.isRequired,
|
||||
@ -121,11 +122,11 @@ export default class SignatureRequestOriginal extends Component {
|
||||
};
|
||||
|
||||
renderOriginInfo = () => {
|
||||
const { txData, domainMetadata } = this.props;
|
||||
const { txData, subjectMetadata } = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
const originMetadata = txData.msgParams.origin
|
||||
? domainMetadata?.[txData.msgParams.origin]
|
||||
const targetSubjectMetadata = txData.msgParams.origin
|
||||
? subjectMetadata?.[txData.msgParams.origin]
|
||||
: null;
|
||||
|
||||
return (
|
||||
@ -133,10 +134,13 @@ export default class SignatureRequestOriginal extends Component {
|
||||
<div className="request-signature__origin-label">
|
||||
{`${t('origin')}:`}
|
||||
</div>
|
||||
{originMetadata?.icon ? (
|
||||
{targetSubjectMetadata?.iconUrl ? (
|
||||
<SiteIcon
|
||||
icon={originMetadata.icon}
|
||||
name={originMetadata.hostname}
|
||||
icon={targetSubjectMetadata.iconUrl}
|
||||
name={
|
||||
getURLHostName(targetSubjectMetadata.origin) ||
|
||||
targetSubjectMetadata.origin
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -7,7 +7,7 @@ import { goHome } from '../../../store/actions';
|
||||
import {
|
||||
accountsWithSendEtherInfoSelector,
|
||||
conversionRateSelector,
|
||||
getDomainMetadata,
|
||||
getSubjectMetadata,
|
||||
doesAddressRequireLedgerHidConnection,
|
||||
} from '../../../selectors';
|
||||
import { getAccountByAddress } from '../../../helpers/utils/util';
|
||||
@ -40,7 +40,7 @@ function mapStateToProps(state, ownProps) {
|
||||
nativeCurrency: getNativeCurrency(state),
|
||||
// not passed to component
|
||||
allAccounts: accountsWithSendEtherInfoSelector(state),
|
||||
domainMetadata: getDomainMetadata(state),
|
||||
subjectMetadata: getSubjectMetadata(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -261,6 +261,21 @@ export function stripHttpsScheme(urlString) {
|
||||
return urlString.replace(/^https:\/\//u, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips `https` schemes from URL strings, if the URL does not have a port.
|
||||
* This is useful
|
||||
*
|
||||
* @param {string} urlString - The URL string to strip the scheme from.
|
||||
* @returns {string} The URL string, without the scheme, if it was stripped.
|
||||
*/
|
||||
export function stripHttpsSchemeWithoutPort(urlString) {
|
||||
if (getURL(urlString).port) {
|
||||
return urlString;
|
||||
}
|
||||
|
||||
return stripHttpsScheme(urlString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a URL-like value (object or string) is an extension URL.
|
||||
*
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getDomainMetadata } from '../selectors';
|
||||
import { getSubjectMetadata } from '../selectors';
|
||||
|
||||
/**
|
||||
* @typedef {Object} OriginMetadata
|
||||
* @property {string} host - The host of the origin
|
||||
* @property {string} hostname - The hostname of the origin (host + port)
|
||||
* @property {string} origin - The original origin string itself
|
||||
* @property {string} [icon] - The origin's site icon if available
|
||||
* @property {number} [lastUpdated] - Timestamp of the last update to the
|
||||
* origin's metadata
|
||||
* @property {string} [iconUrl] - The origin's site icon URL, if available
|
||||
* @property {string} [name] - The registered name of the origin if available
|
||||
*/
|
||||
|
||||
@ -20,7 +17,7 @@ import { getDomainMetadata } from '../selectors';
|
||||
* current origin
|
||||
*/
|
||||
export function useOriginMetadata(origin) {
|
||||
const domainMetaData = useSelector(getDomainMetadata);
|
||||
const subjectMetadata = useSelector(getSubjectMetadata);
|
||||
if (!origin) {
|
||||
return null;
|
||||
}
|
||||
@ -32,10 +29,10 @@ export function useOriginMetadata(origin) {
|
||||
origin,
|
||||
};
|
||||
|
||||
if (domainMetaData?.[origin]) {
|
||||
if (subjectMetadata?.[origin]) {
|
||||
return {
|
||||
...minimumOriginMetadata,
|
||||
...domainMetaData[origin],
|
||||
...subjectMetadata[origin],
|
||||
};
|
||||
}
|
||||
return minimumOriginMetadata;
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
transactionFeeSelector,
|
||||
txDataSelector,
|
||||
getCurrentCurrency,
|
||||
getDomainMetadata,
|
||||
getSubjectMetadata,
|
||||
getUseNonceField,
|
||||
getCustomNonceValue,
|
||||
getNextSuggestedNonce,
|
||||
@ -65,7 +65,7 @@ export default function ConfirmApprove() {
|
||||
const currentCurrency = useSelector(getCurrentCurrency);
|
||||
const nativeCurrency = useSelector(getNativeCurrency);
|
||||
const currentNetworkTxList = useSelector(currentNetworkTxListSelector);
|
||||
const domainMetadata = useSelector(getDomainMetadata);
|
||||
const subjectMetadata = useSelector(getSubjectMetadata);
|
||||
const tokens = useSelector(getTokens);
|
||||
const useNonceField = useSelector(getUseNonceField);
|
||||
const nextNonce = useSelector(getNextSuggestedNonce);
|
||||
@ -163,7 +163,7 @@ export default function ConfirmApprove() {
|
||||
const { origin } = transaction;
|
||||
const formattedOrigin = origin || '';
|
||||
|
||||
const { icon: siteImage = '' } = domainMetadata[origin] || {};
|
||||
const { iconUrl: siteImage = '' } = subjectMetadata[origin] || {};
|
||||
|
||||
const tokensText = `${Number(tokenAmount)} ${tokenSymbol}`;
|
||||
const tokenBalance = tokenTrackerBalance
|
||||
|
@ -7,7 +7,7 @@ import { updateMetamaskState } from '../../store/actions';
|
||||
import { currentNetworkTxListSelector } from '../../selectors/transactions';
|
||||
import { store, getNewState } from '../../../.storybook/preview';
|
||||
|
||||
import { domainMetadata } from '../../../.storybook/initial-states/approval-screens/token-approval';
|
||||
import { subjectMetadata } from '../../../.storybook/initial-states/approval-screens/token-approval';
|
||||
import ConfirmApprove from '.';
|
||||
|
||||
export default {
|
||||
@ -20,7 +20,7 @@ const txId = 7900715443136469;
|
||||
|
||||
const PageSet = ({ children }) => {
|
||||
const origin = text('Origin', 'https://metamask.github.io');
|
||||
const domainIconUrl = text(
|
||||
const subjectIconUrl = text(
|
||||
'Icon URL',
|
||||
'https://metamask.github.io/test-dapp/metamask-fox.svg',
|
||||
);
|
||||
@ -43,15 +43,15 @@ const PageSet = ({ children }) => {
|
||||
store.dispatch(
|
||||
updateMetamaskState(
|
||||
getNewState(state.metamask, {
|
||||
domainMetadata: {
|
||||
subjectMetadata: {
|
||||
[origin]: {
|
||||
icon: domainIconUrl,
|
||||
iconUrl: subjectIconUrl,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, [domainIconUrl, origin, state.metamask]);
|
||||
}, [subjectIconUrl, origin, state.metamask]);
|
||||
|
||||
const params = useParams();
|
||||
params.id = txId;
|
||||
@ -63,7 +63,7 @@ export const DefaultStory = () => {
|
||||
store.dispatch(
|
||||
updateMetamaskState(
|
||||
getNewState(state.metamask, {
|
||||
domainMetadata,
|
||||
subjectMetadata,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
@ -33,7 +33,7 @@ export default class ConfirmDecryptMessage extends Component {
|
||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||
requesterAddress: PropTypes.string,
|
||||
txData: PropTypes.object,
|
||||
domainMetadata: PropTypes.object,
|
||||
subjectMetadata: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -136,11 +136,11 @@ export default class ConfirmDecryptMessage extends Component {
|
||||
};
|
||||
|
||||
renderBody = () => {
|
||||
const { decryptMessageInline, domainMetadata, txData } = this.props;
|
||||
const { decryptMessageInline, subjectMetadata, txData } = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
const originMetadata = domainMetadata[txData.msgParams.origin];
|
||||
const name = originMetadata?.hostname || txData.msgParams.origin;
|
||||
const targetSubjectMetadata = subjectMetadata[txData.msgParams.origin];
|
||||
const name = targetSubjectMetadata?.name || txData.msgParams.origin;
|
||||
const notice = t('decryptMessageNotice', [txData.msgParams.origin]);
|
||||
|
||||
const {
|
||||
@ -157,10 +157,10 @@ export default class ConfirmDecryptMessage extends Component {
|
||||
{this.renderAccountInfo()}
|
||||
<div className="request-decrypt-message__visual">
|
||||
<section>
|
||||
{originMetadata?.icon ? (
|
||||
{targetSubjectMetadata?.iconUrl ? (
|
||||
<img
|
||||
className="request-decrypt-message__visual-identicon"
|
||||
src={originMetadata.icon}
|
||||
src={targetSubjectMetadata.iconUrl}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
|
@ -19,7 +19,7 @@ import ConfirmDecryptMessage from './confirm-decrypt-message.component';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {
|
||||
metamask: { domainMetadata = {} },
|
||||
metamask: { subjectMetadata = {} },
|
||||
} = state;
|
||||
|
||||
const unconfirmedTransactions = unconfirmedTransactionsListSelector(state);
|
||||
@ -34,7 +34,7 @@ function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
txData,
|
||||
domainMetadata,
|
||||
subjectMetadata,
|
||||
fromAccount,
|
||||
requester: null,
|
||||
requesterAddress: null,
|
||||
|
@ -26,7 +26,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
|
||||
history: PropTypes.object.isRequired,
|
||||
requesterAddress: PropTypes.string,
|
||||
txData: PropTypes.object,
|
||||
domainMetadata: PropTypes.object,
|
||||
subjectMetadata: PropTypes.object,
|
||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||
nativeCurrency: PropTypes.string.isRequired,
|
||||
};
|
||||
@ -113,22 +113,22 @@ export default class ConfirmEncryptionPublicKey extends Component {
|
||||
};
|
||||
|
||||
renderBody = () => {
|
||||
const { domainMetadata, txData } = this.props;
|
||||
const { subjectMetadata, txData } = this.props;
|
||||
const { t } = this.context;
|
||||
|
||||
const originMetadata = domainMetadata[txData.origin];
|
||||
const targetSubjectMetadata = subjectMetadata[txData.origin];
|
||||
const notice = t('encryptionPublicKeyNotice', [txData.origin]);
|
||||
const name = originMetadata?.hostname || txData.origin;
|
||||
const name = targetSubjectMetadata?.hostname || txData.origin;
|
||||
|
||||
return (
|
||||
<div className="request-encryption-public-key__body">
|
||||
{this.renderAccountInfo()}
|
||||
<div className="request-encryption-public-key__visual">
|
||||
<section>
|
||||
{originMetadata?.icon ? (
|
||||
{targetSubjectMetadata?.iconUrl ? (
|
||||
<img
|
||||
className="request-encryption-public-key__visual-identicon"
|
||||
src={originMetadata.icon}
|
||||
src={targetSubjectMetadata.iconUrl}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
|
@ -21,7 +21,7 @@ import ConfirmEncryptionPublicKey from './confirm-encryption-public-key.componen
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {
|
||||
metamask: { domainMetadata = {} },
|
||||
metamask: { subjectMetadata = {} },
|
||||
} = state;
|
||||
|
||||
const unconfirmedTransactions = unconfirmedTransactionsListSelector(state);
|
||||
@ -34,7 +34,7 @@ function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
txData,
|
||||
domainMetadata,
|
||||
subjectMetadata,
|
||||
fromAccount,
|
||||
requester: null,
|
||||
requesterAddress: null,
|
||||
|
@ -196,7 +196,7 @@ export default function ConfirmationPage() {
|
||||
label={stripHttpsScheme(originMetadata.origin)}
|
||||
leftIcon={
|
||||
<SiteIcon
|
||||
icon={originMetadata.icon}
|
||||
icon={originMetadata.iconUrl}
|
||||
name={originMetadata.hostname}
|
||||
size={32}
|
||||
/>
|
||||
|
@ -16,8 +16,7 @@ export default class ConnectedSites extends Component {
|
||||
static propTypes = {
|
||||
accountLabel: PropTypes.string.isRequired,
|
||||
closePopover: PropTypes.func.isRequired,
|
||||
connectedDomains: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
domainHostCount: PropTypes.objectOf(PropTypes.number).isRequired,
|
||||
connectedSubjects: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
disconnectAllAccounts: PropTypes.func.isRequired,
|
||||
disconnectAccount: PropTypes.func.isRequired,
|
||||
getOpenMetamaskTabsIds: PropTypes.func.isRequired,
|
||||
@ -37,10 +36,10 @@ export default class ConnectedSites extends Component {
|
||||
getOpenMetamaskTabsIds();
|
||||
}
|
||||
|
||||
setPendingDisconnect = (domainKey) => {
|
||||
setPendingDisconnect = (subjectKey) => {
|
||||
this.setState({
|
||||
sitePendingDisconnect: {
|
||||
domainKey,
|
||||
subjectKey,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -55,7 +54,7 @@ export default class ConnectedSites extends Component {
|
||||
const { disconnectAccount } = this.props;
|
||||
const { sitePendingDisconnect } = this.state;
|
||||
|
||||
disconnectAccount(sitePendingDisconnect.domainKey);
|
||||
disconnectAccount(sitePendingDisconnect.subjectKey);
|
||||
this.clearPendingDisconnect();
|
||||
};
|
||||
|
||||
@ -63,15 +62,14 @@ export default class ConnectedSites extends Component {
|
||||
const { disconnectAllAccounts } = this.props;
|
||||
const { sitePendingDisconnect } = this.state;
|
||||
|
||||
disconnectAllAccounts(sitePendingDisconnect.domainKey);
|
||||
disconnectAllAccounts(sitePendingDisconnect.subjectKey);
|
||||
this.clearPendingDisconnect();
|
||||
};
|
||||
|
||||
renderConnectedSitesList() {
|
||||
return (
|
||||
<ConnectedSitesList
|
||||
domainHostCount={this.props.domainHostCount}
|
||||
connectedDomains={this.props.connectedDomains}
|
||||
connectedSubjects={this.props.connectedSubjects}
|
||||
onDisconnect={this.setPendingDisconnect}
|
||||
/>
|
||||
);
|
||||
@ -81,7 +79,7 @@ export default class ConnectedSites extends Component {
|
||||
const {
|
||||
accountLabel,
|
||||
closePopover,
|
||||
connectedDomains,
|
||||
connectedSubjects,
|
||||
tabToConnect,
|
||||
requestAccountsPermission,
|
||||
} = this.props;
|
||||
@ -92,7 +90,7 @@ export default class ConnectedSites extends Component {
|
||||
className="connected-sites"
|
||||
title={t('connectedSites')}
|
||||
subtitle={
|
||||
connectedDomains.length
|
||||
connectedSubjects.length
|
||||
? t('connectedSitesDescription', [accountLabel])
|
||||
: t('connectedSitesEmptyDescription', [accountLabel])
|
||||
}
|
||||
@ -118,15 +116,15 @@ export default class ConnectedSites extends Component {
|
||||
const { closePopover, permittedAccountsByOrigin } = this.props;
|
||||
const { t } = this.context;
|
||||
const {
|
||||
sitePendingDisconnect: { domainKey },
|
||||
sitePendingDisconnect: { subjectKey },
|
||||
} = this.state;
|
||||
|
||||
const numPermittedAccounts = permittedAccountsByOrigin[domainKey].length;
|
||||
const numPermittedAccounts = permittedAccountsByOrigin[subjectKey].length;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="connected-sites"
|
||||
title={t('disconnectPrompt', [domainKey])}
|
||||
title={t('disconnectPrompt', [subjectKey])}
|
||||
subtitle={t('disconnectAllAccountsConfirmationDescription')}
|
||||
onClose={closePopover}
|
||||
footer={
|
||||
|
@ -6,11 +6,10 @@ import {
|
||||
removePermittedAccount,
|
||||
} from '../../store/actions';
|
||||
import {
|
||||
getConnectedDomainsForSelectedAddress,
|
||||
getConnectedSubjectsForSelectedAddress,
|
||||
getCurrentAccountWithSendEtherInfo,
|
||||
getOriginOfCurrentTab,
|
||||
getPermissionDomains,
|
||||
getPermissionsMetadataHostCounts,
|
||||
getPermissionSubjects,
|
||||
getPermittedAccountsByOrigin,
|
||||
getSelectedAddress,
|
||||
} from '../../selectors';
|
||||
@ -21,7 +20,7 @@ import ConnectedSites from './connected-sites.component';
|
||||
const mapStateToProps = (state) => {
|
||||
const { openMetaMaskTabs } = state.appState;
|
||||
const { id } = state.activeTab;
|
||||
const connectedDomains = getConnectedDomainsForSelectedAddress(state);
|
||||
const connectedSubjects = getConnectedSubjectsForSelectedAddress(state);
|
||||
const originOfCurrentTab = getOriginOfCurrentTab(state);
|
||||
const permittedAccountsByOrigin = getPermittedAccountsByOrigin(state);
|
||||
const selectedAddress = getSelectedAddress(state);
|
||||
@ -38,9 +37,8 @@ const mapStateToProps = (state) => {
|
||||
|
||||
return {
|
||||
accountLabel: getCurrentAccountWithSendEtherInfo(state).name,
|
||||
connectedDomains,
|
||||
domains: getPermissionDomains(state),
|
||||
domainHostCount: getPermissionsMetadataHostCounts(state),
|
||||
connectedSubjects,
|
||||
subjects: getPermissionSubjects(state),
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
permittedAccountsByOrigin,
|
||||
selectedAddress,
|
||||
@ -51,16 +49,16 @@ const mapStateToProps = (state) => {
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
getOpenMetamaskTabsIds: () => dispatch(getOpenMetamaskTabsIds()),
|
||||
disconnectAccount: (domainKey, address) => {
|
||||
dispatch(removePermittedAccount(domainKey, address));
|
||||
disconnectAccount: (subjectKey, address) => {
|
||||
dispatch(removePermittedAccount(subjectKey, address));
|
||||
},
|
||||
disconnectAllAccounts: (domainKey, domain) => {
|
||||
const permissionMethodNames = domain.permissions.map(
|
||||
disconnectAllAccounts: (subjectKey, subject) => {
|
||||
const permissionMethodNames = subject.permissions.map(
|
||||
({ parentCapability }) => parentCapability,
|
||||
);
|
||||
dispatch(
|
||||
removePermissionsFor({
|
||||
[domainKey]: permissionMethodNames,
|
||||
[subjectKey]: permissionMethodNames,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@ -71,8 +69,8 @@ const mapDispatchToProps = (dispatch) => {
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const {
|
||||
connectedDomains,
|
||||
domains,
|
||||
connectedSubjects,
|
||||
subjects,
|
||||
mostRecentOverviewPage,
|
||||
selectedAddress,
|
||||
tabToConnect,
|
||||
@ -92,15 +90,15 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
closePopover,
|
||||
disconnectAccount: (domainKey) => {
|
||||
disconnectAccount(domainKey, selectedAddress);
|
||||
if (connectedDomains.length === 1) {
|
||||
disconnectAccount: (subjectKey) => {
|
||||
disconnectAccount(subjectKey, selectedAddress);
|
||||
if (connectedSubjects.length === 1) {
|
||||
closePopover();
|
||||
}
|
||||
},
|
||||
disconnectAllAccounts: (domainKey) => {
|
||||
disconnectAllAccounts(domainKey, domains[domainKey]);
|
||||
if (connectedDomains.length === 1) {
|
||||
disconnectAllAccounts: (subjectKey) => {
|
||||
disconnectAllAccounts(subjectKey, subjects[subjectKey]);
|
||||
if (connectedSubjects.length === 1) {
|
||||
closePopover();
|
||||
}
|
||||
},
|
||||
|
@ -75,9 +75,7 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const firstPermissionsRequest = getFirstPermissionRequest(state);
|
||||
const firstPermissionsRequestId =
|
||||
firstPermissionsRequest && firstPermissionsRequest.metadata
|
||||
? firstPermissionsRequest.metadata.id
|
||||
: null;
|
||||
firstPermissionsRequest?.metadata.id || null;
|
||||
|
||||
const originOfCurrentTab = getOriginOfCurrentTab(state);
|
||||
const shouldShowWeb3ShimUsageNotification =
|
||||
|
@ -31,10 +31,9 @@ export default class ChooseAccount extends Component {
|
||||
cancelPermissionsRequest: PropTypes.func.isRequired,
|
||||
permissionsRequestId: PropTypes.string.isRequired,
|
||||
selectedAccountAddresses: PropTypes.object.isRequired,
|
||||
targetDomainMetadata: PropTypes.shape({
|
||||
targetSubjectMetadata: PropTypes.shape({
|
||||
extensionId: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
host: PropTypes.string.isRequired,
|
||||
iconUrl: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
origin: PropTypes.string.isRequired,
|
||||
}),
|
||||
@ -199,7 +198,7 @@ export default class ChooseAccount extends Component {
|
||||
selectAccounts,
|
||||
permissionsRequestId,
|
||||
cancelPermissionsRequest,
|
||||
targetDomainMetadata,
|
||||
targetSubjectMetadata,
|
||||
accounts,
|
||||
} = this.props;
|
||||
const { selectedAccounts } = this.state;
|
||||
@ -207,15 +206,15 @@ export default class ChooseAccount extends Component {
|
||||
return (
|
||||
<div className="permissions-connect-choose-account">
|
||||
<PermissionsConnectHeader
|
||||
icon={targetDomainMetadata.icon}
|
||||
iconName={targetDomainMetadata.name}
|
||||
icon={targetSubjectMetadata.iconUrl}
|
||||
iconName={targetSubjectMetadata.name}
|
||||
headerTitle={t('connectWithMetaMask')}
|
||||
headerText={
|
||||
accounts.length > 0
|
||||
? t('selectAccounts')
|
||||
: t('connectAccountOrCreate')
|
||||
}
|
||||
siteOrigin={targetDomainMetadata.origin}
|
||||
siteOrigin={targetSubjectMetadata.origin}
|
||||
/>
|
||||
{this.renderAccountsListHeader()}
|
||||
{this.renderAccountsList()}
|
||||
|
@ -31,10 +31,9 @@ export default class PermissionConnect extends Component {
|
||||
connectPath: PropTypes.string.isRequired,
|
||||
confirmPermissionPath: PropTypes.string.isRequired,
|
||||
page: PropTypes.string.isRequired,
|
||||
targetDomainMetadata: PropTypes.shape({
|
||||
targetSubjectMetadata: PropTypes.shape({
|
||||
extensionId: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
host: PropTypes.string.isRequired,
|
||||
iconUrl: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
origin: PropTypes.string.isRequired,
|
||||
}),
|
||||
@ -56,7 +55,7 @@ export default class PermissionConnect extends Component {
|
||||
selectedAccountAddresses: new Set([this.props.currentAddress]),
|
||||
permissionsApproved: null,
|
||||
origin: this.props.origin,
|
||||
targetDomainMetadata: this.props.targetDomainMetadata || {},
|
||||
targetSubjectMetadata: this.props.targetSubjectMetadata || {},
|
||||
};
|
||||
|
||||
beforeUnload = () => {
|
||||
@ -97,14 +96,14 @@ export default class PermissionConnect extends Component {
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { permissionsRequest, targetDomainMetadata } = props;
|
||||
const { targetDomainMetadata: savedMetadata } = state;
|
||||
const { permissionsRequest, targetSubjectMetadata } = props;
|
||||
const { targetSubjectMetadata: savedMetadata } = state;
|
||||
|
||||
if (
|
||||
permissionsRequest &&
|
||||
savedMetadata.origin !== targetDomainMetadata?.origin
|
||||
savedMetadata.origin !== targetSubjectMetadata?.origin
|
||||
) {
|
||||
return { targetDomainMetadata };
|
||||
return { targetSubjectMetadata };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -202,14 +201,14 @@ export default class PermissionConnect extends Component {
|
||||
selectedAccountAddresses,
|
||||
permissionsApproved,
|
||||
redirecting,
|
||||
targetDomainMetadata,
|
||||
targetSubjectMetadata,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className="permissions-connect">
|
||||
{this.renderTopBar()}
|
||||
{redirecting && permissionsApproved ? (
|
||||
<PermissionsRedirect domainMetadata={targetDomainMetadata} />
|
||||
<PermissionsRedirect subjectMetadata={targetSubjectMetadata} />
|
||||
) : (
|
||||
<Switch>
|
||||
<Route
|
||||
@ -233,7 +232,7 @@ export default class PermissionConnect extends Component {
|
||||
}
|
||||
permissionsRequestId={permissionsRequestId}
|
||||
selectedAccountAddresses={selectedAccountAddresses}
|
||||
targetDomainMetadata={targetDomainMetadata}
|
||||
targetSubjectMetadata={targetSubjectMetadata}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -253,7 +252,7 @@ export default class PermissionConnect extends Component {
|
||||
selectedIdentities={accounts.filter((account) =>
|
||||
selectedAccountAddresses.has(account.address),
|
||||
)}
|
||||
targetDomainMetadata={targetDomainMetadata}
|
||||
targetSubjectMetadata={targetSubjectMetadata}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
getPermissionsRequests,
|
||||
getAccountsWithLabels,
|
||||
getLastConnectedInfo,
|
||||
getDomainMetadata,
|
||||
getSubjectMetadata,
|
||||
getSelectedAddress,
|
||||
} from '../../selectors';
|
||||
import { getNativeCurrency } from '../../ducks/metamask/metamask';
|
||||
@ -41,15 +41,15 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const { origin } = metadata;
|
||||
const nativeCurrency = getNativeCurrency(state);
|
||||
|
||||
const domainMetadata = getDomainMetadata(state);
|
||||
const subjectMetadata = getSubjectMetadata(state);
|
||||
|
||||
let targetDomainMetadata = null;
|
||||
let targetSubjectMetadata = null;
|
||||
if (origin) {
|
||||
if (domainMetadata[origin]) {
|
||||
targetDomainMetadata = { ...domainMetadata[origin], origin };
|
||||
if (subjectMetadata[origin]) {
|
||||
targetSubjectMetadata = { ...subjectMetadata[origin], origin };
|
||||
} else {
|
||||
const targetUrl = new URL(origin);
|
||||
targetDomainMetadata = {
|
||||
targetSubjectMetadata = {
|
||||
host: targetUrl.host,
|
||||
name: targetUrl.hostname,
|
||||
origin,
|
||||
@ -94,14 +94,14 @@ const mapStateToProps = (state, ownProps) => {
|
||||
connectPath,
|
||||
confirmPermissionPath,
|
||||
page,
|
||||
targetDomainMetadata,
|
||||
targetSubjectMetadata,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
approvePermissionsRequest: (request, accounts) =>
|
||||
dispatch(approvePermissionsRequest(request, accounts)),
|
||||
approvePermissionsRequest: (request) =>
|
||||
dispatch(approvePermissionsRequest(request)),
|
||||
rejectPermissionsRequest: (requestId) =>
|
||||
dispatch(rejectPermissionsRequest(requestId)),
|
||||
showNewAccountModal: ({ onCreateNewAccount, newAccountNumber }) => {
|
||||
|
@ -21,10 +21,8 @@ export const ChooseAccountComponent = () => {
|
||||
'0xbe0eb53f46cd790cd13851d5eff43d12404d33e8',
|
||||
])
|
||||
}
|
||||
targetDomainMetadata={{
|
||||
host: 'gnosis-safe.io',
|
||||
icon: './gnosis.svg',
|
||||
lastUpdated: 1627423550860,
|
||||
targetSubjectMetadata={{
|
||||
iconUrl: './gnosis.svg',
|
||||
name: 'Gnosis - Manage Digital Assets',
|
||||
origin: 'https://gnosis-safe.io',
|
||||
}}
|
||||
@ -56,11 +54,9 @@ export const PermissionPageContainerComponent = () => {
|
||||
return (
|
||||
<div className="page-container permission-approval-container">
|
||||
<PermissionPageContainerContent
|
||||
domainMetadata={{
|
||||
subjectMetadata={{
|
||||
extensionId: '1',
|
||||
host: 'gnosis-safe.io',
|
||||
icon: './gnosis.svg',
|
||||
lastUpdated: 1627423550860,
|
||||
iconUrl: './gnosis.svg',
|
||||
name: 'Gnosis - Manage Digital Assets',
|
||||
origin: 'https://gnosis-safe.io',
|
||||
}}
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import SiteIcon from '../../../components/ui/site-icon';
|
||||
import { I18nContext } from '../../../contexts/i18n';
|
||||
|
||||
export default function PermissionsRedirect({ domainMetadata }) {
|
||||
export default function PermissionsRedirect({ subjectMetadata }) {
|
||||
const t = useContext(I18nContext);
|
||||
|
||||
return (
|
||||
@ -12,8 +12,8 @@ export default function PermissionsRedirect({ domainMetadata }) {
|
||||
{t('connecting')}
|
||||
<div className="permissions-redirect__icons">
|
||||
<SiteIcon
|
||||
icon={domainMetadata.icon}
|
||||
name={domainMetadata.name}
|
||||
icon={subjectMetadata.iconUrl}
|
||||
name={subjectMetadata.name}
|
||||
size={64}
|
||||
/>
|
||||
<div className="permissions-redirect__center-icon">
|
||||
@ -47,10 +47,9 @@ export default function PermissionsRedirect({ domainMetadata }) {
|
||||
}
|
||||
|
||||
PermissionsRedirect.propTypes = {
|
||||
domainMetadata: PropTypes.shape({
|
||||
subjectMetadata: PropTypes.shape({
|
||||
extensionId: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
host: PropTypes.string.isRequired,
|
||||
iconUrl: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
origin: PropTypes.string.isRequired,
|
||||
}),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { forOwn } from 'lodash';
|
||||
import { CAVEAT_NAMES } from '../../shared/constants/permissions';
|
||||
import { CaveatTypes } from '../../shared/constants/permissions';
|
||||
import {
|
||||
getMetaMaskAccountsOrdered,
|
||||
getOriginOfCurrentTab,
|
||||
@ -9,23 +8,23 @@ import {
|
||||
// selectors
|
||||
|
||||
/**
|
||||
* Get the permission domains object.
|
||||
* Get the permission subjects object.
|
||||
*
|
||||
* @param {Object} state - The current state.
|
||||
* @returns {Object} The permissions domains object.
|
||||
* @returns {Object} The permissions subjects object.
|
||||
*/
|
||||
export function getPermissionDomains(state) {
|
||||
return state.metamask.domains || {};
|
||||
export function getPermissionSubjects(state) {
|
||||
return state.metamask.subjects || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permission domains metadata object.
|
||||
* Get the permission subjects metadata object.
|
||||
*
|
||||
* @param {Object} state - The current state.
|
||||
* @returns {Object} The permission domains metadata object.
|
||||
* @returns {Object} The permission subjects metadata object.
|
||||
*/
|
||||
export function getPermissionDomainsMetadata(state) {
|
||||
return state.metamask.domainMetadata || {};
|
||||
export function getPermissionSubjectsMetadata(state) {
|
||||
return state.metamask.subjectMetadata || {};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,12 +32,12 @@ export function getPermissionDomainsMetadata(state) {
|
||||
* and an origin.
|
||||
*
|
||||
* @param {Object} state - The current state.
|
||||
* @param {string} origin - The origin/domain to get the permitted accounts for.
|
||||
* @param {string} origin - The origin/subject to get the permitted accounts for.
|
||||
* @returns {Array<string>} An empty array or an array of accounts.
|
||||
*/
|
||||
export function getPermittedAccounts(state, origin) {
|
||||
return getAccountsFromPermission(
|
||||
getAccountsPermissionFromDomain(domainSelector(state, origin)),
|
||||
getAccountsPermissionFromSubject(subjectSelector(state, origin)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -60,82 +59,79 @@ export function getPermittedAccountsForCurrentTab(state) {
|
||||
* @returns {Object} Permitted accounts by origin.
|
||||
*/
|
||||
export function getPermittedAccountsByOrigin(state) {
|
||||
const domains = getPermissionDomains(state);
|
||||
return Object.keys(domains).reduce((acc, domainKey) => {
|
||||
const accounts = getAccountsFromPermission(
|
||||
getAccountsPermissionFromDomain(domains[domainKey]),
|
||||
);
|
||||
const subjects = getPermissionSubjects(state);
|
||||
return Object.keys(subjects).reduce((acc, subjectKey) => {
|
||||
const accounts = getAccountsFromSubject(subjects[subjectKey]);
|
||||
if (accounts.length > 0) {
|
||||
acc[domainKey] = accounts;
|
||||
acc[subjectKey] = accounts;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of connected domain objects, with the following properties:
|
||||
* Returns an array of connected subject objects, with the following properties:
|
||||
* - extensionId
|
||||
* - key (i.e. origin)
|
||||
* - name
|
||||
* - icon
|
||||
*
|
||||
* @param {Object} state - The current state.
|
||||
* @returns {Array<Object>} An array of connected domain objects.
|
||||
* @returns {Array<Object>} An array of connected subject objects.
|
||||
*/
|
||||
export function getConnectedDomainsForSelectedAddress(state) {
|
||||
export function getConnectedSubjectsForSelectedAddress(state) {
|
||||
const { selectedAddress } = state.metamask;
|
||||
const domains = getPermissionDomains(state);
|
||||
const domainMetadata = getPermissionDomainsMetadata(state);
|
||||
const subjects = getPermissionSubjects(state);
|
||||
const subjectMetadata = getPermissionSubjectsMetadata(state);
|
||||
|
||||
const connectedDomains = [];
|
||||
const connectedSubjects = [];
|
||||
|
||||
forOwn(domains, (domainValue, domainKey) => {
|
||||
const exposedAccounts = getAccountsFromDomain(domainValue);
|
||||
Object.entries(subjects).forEach(([subjectKey, subjectValue]) => {
|
||||
const exposedAccounts = getAccountsFromSubject(subjectValue);
|
||||
if (!exposedAccounts.includes(selectedAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { extensionId, name, icon, host } = domainMetadata[domainKey] || {};
|
||||
const { extensionId, name, iconUrl } = subjectMetadata[subjectKey] || {};
|
||||
|
||||
connectedDomains.push({
|
||||
connectedSubjects.push({
|
||||
extensionId,
|
||||
origin: domainKey,
|
||||
origin: subjectKey,
|
||||
name,
|
||||
icon,
|
||||
host,
|
||||
iconUrl,
|
||||
});
|
||||
});
|
||||
|
||||
return connectedDomains;
|
||||
return connectedSubjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object mapping addresses to objects mapping origins to connected
|
||||
* domain info. Domain info objects have the following properties:
|
||||
* - icon
|
||||
* subject info. Subject info objects have the following properties:
|
||||
* - iconUrl
|
||||
* - name
|
||||
*
|
||||
* @param {Object} state - The current state.
|
||||
* @returns {Object} A mapping of addresses to a mapping of origins to
|
||||
* connected domain info.
|
||||
* connected subject info.
|
||||
*/
|
||||
export function getAddressConnectedDomainMap(state) {
|
||||
const domainMetadata = getPermissionDomainsMetadata(state);
|
||||
export function getAddressConnectedSubjectMap(state) {
|
||||
const subjectMetadata = getPermissionSubjectsMetadata(state);
|
||||
const accountsMap = getPermittedAccountsByOrigin(state);
|
||||
const addressConnectedIconMap = {};
|
||||
|
||||
Object.keys(accountsMap).forEach((domainKey) => {
|
||||
const { icon, name } = domainMetadata[domainKey] || {};
|
||||
Object.keys(accountsMap).forEach((subjectKey) => {
|
||||
const { iconUrl, name } = subjectMetadata[subjectKey] || {};
|
||||
|
||||
accountsMap[domainKey].forEach((address) => {
|
||||
const nameToRender = name || domainKey;
|
||||
accountsMap[subjectKey].forEach((address) => {
|
||||
const nameToRender = name || subjectKey;
|
||||
|
||||
addressConnectedIconMap[address] = addressConnectedIconMap[address]
|
||||
? {
|
||||
...addressConnectedIconMap[address],
|
||||
[domainKey]: { icon, name: nameToRender },
|
||||
[subjectKey]: { iconUrl, name: nameToRender },
|
||||
}
|
||||
: { [domainKey]: { icon, name: nameToRender } };
|
||||
: { [subjectKey]: { iconUrl, name: nameToRender } };
|
||||
});
|
||||
});
|
||||
|
||||
@ -144,16 +140,12 @@ export function getAddressConnectedDomainMap(state) {
|
||||
|
||||
// selector helpers
|
||||
|
||||
function getAccountsFromDomain(domain) {
|
||||
return getAccountsFromPermission(getAccountsPermissionFromDomain(domain));
|
||||
function getAccountsFromSubject(subject) {
|
||||
return getAccountsFromPermission(getAccountsPermissionFromSubject(subject));
|
||||
}
|
||||
|
||||
function getAccountsPermissionFromDomain(domain = {}) {
|
||||
return Array.isArray(domain.permissions)
|
||||
? domain.permissions.find(
|
||||
(perm) => perm.parentCapability === 'eth_accounts',
|
||||
)
|
||||
: {};
|
||||
function getAccountsPermissionFromSubject(subject = {}) {
|
||||
return subject.permissions?.eth_accounts || {};
|
||||
}
|
||||
|
||||
function getAccountsFromPermission(accountsPermission) {
|
||||
@ -167,13 +159,13 @@ function getAccountsCaveatFromPermission(accountsPermission = {}) {
|
||||
return (
|
||||
Array.isArray(accountsPermission.caveats) &&
|
||||
accountsPermission.caveats.find(
|
||||
(c) => c.name === CAVEAT_NAMES.exposedAccounts,
|
||||
(caveat) => caveat.type === CaveatTypes.restrictReturnedAccounts,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function domainSelector(state, origin) {
|
||||
return origin && state.metamask.domains?.[origin];
|
||||
function subjectSelector(state, origin) {
|
||||
return origin && state.metamask.subjects?.[origin];
|
||||
}
|
||||
|
||||
export function getAccountToConnectToActiveTab(state) {
|
||||
@ -203,12 +195,12 @@ export function getAccountToConnectToActiveTab(state) {
|
||||
export function getOrderedConnectedAccountsForActiveTab(state) {
|
||||
const {
|
||||
activeTab,
|
||||
metamask: { permissionsHistory },
|
||||
metamask: { permissionHistory },
|
||||
} = state;
|
||||
|
||||
const permissionsHistoryByAccount =
|
||||
const permissionHistoryByAccount =
|
||||
// eslint-disable-next-line camelcase
|
||||
permissionsHistory[activeTab.origin]?.eth_accounts?.accounts;
|
||||
permissionHistory[activeTab.origin]?.eth_accounts?.accounts;
|
||||
const orderedAccounts = getMetaMaskAccountsOrdered(state);
|
||||
const connectedAccounts = getPermittedAccountsForCurrentTab(state);
|
||||
|
||||
@ -216,7 +208,7 @@ export function getOrderedConnectedAccountsForActiveTab(state) {
|
||||
.filter((account) => connectedAccounts.includes(account.address))
|
||||
.map((account) => ({
|
||||
...account,
|
||||
lastActive: permissionsHistoryByAccount?.[account.address],
|
||||
lastActive: permissionHistoryByAccount?.[account.address],
|
||||
}))
|
||||
.sort(
|
||||
({ lastSelected: lastSelectedA }, { lastSelected: lastSelectedB }) => {
|
||||
@ -235,27 +227,31 @@ export function getOrderedConnectedAccountsForActiveTab(state) {
|
||||
|
||||
export function getPermissionsForActiveTab(state) {
|
||||
const { activeTab, metamask } = state;
|
||||
const { domains = {} } = metamask;
|
||||
const { subjects = {} } = metamask;
|
||||
|
||||
return domains[activeTab.origin]?.permissions?.map(({ parentCapability }) => {
|
||||
return {
|
||||
key: parentCapability,
|
||||
};
|
||||
});
|
||||
return Object.keys(subjects[activeTab.origin]?.permissions || {}).map(
|
||||
(parentCapability) => {
|
||||
return {
|
||||
key: parentCapability,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function activeTabHasPermissions(state) {
|
||||
const { activeTab, metamask } = state;
|
||||
const { domains = {} } = metamask;
|
||||
const { subjects = {} } = metamask;
|
||||
|
||||
return Boolean(domains[activeTab.origin]?.permissions?.length > 0);
|
||||
return Boolean(
|
||||
Object.keys(subjects[activeTab.origin]?.permissions || {}).length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
export function getLastConnectedInfo(state) {
|
||||
const { permissionsHistory = {} } = state.metamask;
|
||||
return Object.keys(permissionsHistory).reduce((acc, origin) => {
|
||||
const { permissionHistory = {} } = state.metamask;
|
||||
return Object.keys(permissionHistory).reduce((acc, origin) => {
|
||||
const ethAccountsHistory = JSON.parse(
|
||||
JSON.stringify(permissionsHistory[origin].eth_accounts),
|
||||
JSON.stringify(permissionHistory[origin].eth_accounts),
|
||||
);
|
||||
return {
|
||||
...acc,
|
||||
@ -264,22 +260,10 @@ export function getLastConnectedInfo(state) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function getPermissionsMetadataHostCounts(state) {
|
||||
const metadata = getPermissionDomainsMetadata(state);
|
||||
return Object.values(metadata).reduce((counts, { host }) => {
|
||||
if (host) {
|
||||
if (counts[host]) {
|
||||
counts[host] += 1;
|
||||
} else {
|
||||
counts[host] = 1;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function getPermissionsRequests(state) {
|
||||
return state.metamask.permissionsRequests || [];
|
||||
return Object.values(state.metamask.pendingApprovals)
|
||||
.filter(({ type }) => type === 'wallet_requestPermissions')
|
||||
.map(({ requestData }) => requestData);
|
||||
}
|
||||
|
||||
export function getFirstPermissionRequest(state) {
|
||||
|
@ -1,154 +1,139 @@
|
||||
import { KOVAN_CHAIN_ID } from '../../shared/constants/network';
|
||||
import {
|
||||
getConnectedDomainsForSelectedAddress,
|
||||
getConnectedSubjectsForSelectedAddress,
|
||||
getOrderedConnectedAccountsForActiveTab,
|
||||
getPermissionsForActiveTab,
|
||||
} from './permissions';
|
||||
|
||||
describe('selectors', () => {
|
||||
describe('getConnectedDomainsForSelectedAddress', () => {
|
||||
it('should return the list of connected domains when there is 1 connected account', () => {
|
||||
describe('getConnectedSubjectsForSelectedAddress', () => {
|
||||
it('should return the list of connected subjects when there is 1 connected account', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
selectedAddress: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
|
||||
domainMetadata: {
|
||||
subjectMetadata: {
|
||||
'peepeth.com': {
|
||||
icon: 'https://peepeth.com/favicon-32x32.png',
|
||||
iconUrl: 'https://peepeth.com/favicon-32x32.png',
|
||||
name: 'Peepeth',
|
||||
host: 'peepeth.com',
|
||||
},
|
||||
'https://remix.ethereum.org': {
|
||||
icon: 'https://remix.ethereum.org/icon.png',
|
||||
iconUrl: 'https://remix.ethereum.org/icon.png',
|
||||
name: 'Remix - Ethereum IDE',
|
||||
host: 'remix.ethereum.org',
|
||||
},
|
||||
},
|
||||
domains: {
|
||||
subjects: {
|
||||
'peepeth.com': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
|
||||
},
|
||||
],
|
||||
'date': 1585676177970,
|
||||
'id': '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
'invoker': 'peepeth.com',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585676177970,
|
||||
id: '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
invoker: 'peepeth.com',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'https://remix.ethereum.org': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
|
||||
name: 'exposedAccounts',
|
||||
},
|
||||
],
|
||||
'date': 1585685128948,
|
||||
'id': '6b9615cc-64e4-4317-afab-3c4f8ee0244a',
|
||||
'invoker': 'https://remix.ethereum.org',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585685128948,
|
||||
id: '6b9615cc-64e4-4317-afab-3c4f8ee0244a',
|
||||
invoker: 'https://remix.ethereum.org',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const extensionId = undefined;
|
||||
expect(getConnectedDomainsForSelectedAddress(mockState)).toStrictEqual([
|
||||
expect(getConnectedSubjectsForSelectedAddress(mockState)).toStrictEqual([
|
||||
{
|
||||
extensionId,
|
||||
icon: 'https://peepeth.com/favicon-32x32.png',
|
||||
iconUrl: 'https://peepeth.com/favicon-32x32.png',
|
||||
origin: 'peepeth.com',
|
||||
name: 'Peepeth',
|
||||
host: 'peepeth.com',
|
||||
},
|
||||
{
|
||||
extensionId,
|
||||
name: 'Remix - Ethereum IDE',
|
||||
icon: 'https://remix.ethereum.org/icon.png',
|
||||
iconUrl: 'https://remix.ethereum.org/icon.png',
|
||||
origin: 'https://remix.ethereum.org',
|
||||
host: 'remix.ethereum.org',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the list of connected domains when there are 2 connected accounts', () => {
|
||||
it('should return the list of connected subjects when there are 2 connected accounts', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
selectedAddress: '0x7250739de134d33ec7ab1ee592711e15098c9d2d',
|
||||
domainMetadata: {
|
||||
subjectMetadata: {
|
||||
'peepeth.com': {
|
||||
icon: 'https://peepeth.com/favicon-32x32.png',
|
||||
iconUrl: 'https://peepeth.com/favicon-32x32.png',
|
||||
name: 'Peepeth',
|
||||
host: 'peepeth.com',
|
||||
},
|
||||
'https://remix.ethereum.org': {
|
||||
icon: 'https://remix.ethereum.org/icon.png',
|
||||
iconUrl: 'https://remix.ethereum.org/icon.png',
|
||||
name: 'Remix - Ethereum IDE',
|
||||
host: 'remix.ethereum.com',
|
||||
},
|
||||
},
|
||||
domains: {
|
||||
subjects: {
|
||||
'peepeth.com': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
|
||||
},
|
||||
],
|
||||
'date': 1585676177970,
|
||||
'id': '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
'invoker': 'peepeth.com',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585676177970,
|
||||
id: '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
invoker: 'peepeth.com',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'https://remix.ethereum.org': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: [
|
||||
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
|
||||
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
|
||||
],
|
||||
name: 'exposedAccounts',
|
||||
},
|
||||
],
|
||||
'date': 1585685128948,
|
||||
'id': '6b9615cc-64e4-4317-afab-3c4f8ee0244a',
|
||||
'invoker': 'https://remix.ethereum.org',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585685128948,
|
||||
id: '6b9615cc-64e4-4317-afab-3c4f8ee0244a',
|
||||
invoker: 'https://remix.ethereum.org',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const extensionId = undefined;
|
||||
expect(getConnectedDomainsForSelectedAddress(mockState)).toStrictEqual([
|
||||
expect(getConnectedSubjectsForSelectedAddress(mockState)).toStrictEqual([
|
||||
{
|
||||
extensionId,
|
||||
name: 'Remix - Ethereum IDE',
|
||||
icon: 'https://remix.ethereum.org/icon.png',
|
||||
iconUrl: 'https://remix.ethereum.org/icon.png',
|
||||
origin: 'https://remix.ethereum.org',
|
||||
host: 'remix.ethereum.com',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@ -184,15 +169,13 @@ describe('selectors', () => {
|
||||
},
|
||||
},
|
||||
cachedBalances: {},
|
||||
domains: {
|
||||
subjects: {
|
||||
'https://remix.ethereum.org': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: [
|
||||
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
|
||||
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
|
||||
@ -202,30 +185,28 @@ describe('selectors', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
'date': 1586359844177,
|
||||
'id': '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
|
||||
'invoker': 'https://remix.ethereum.org',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1586359844177,
|
||||
id: '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
|
||||
invoker: 'https://remix.ethereum.org',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'peepeth.com': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
|
||||
},
|
||||
],
|
||||
'date': 1585676177970,
|
||||
'id': '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
'invoker': 'peepeth.com',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585676177970,
|
||||
id: '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
invoker: 'peepeth.com',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
identities: {
|
||||
@ -264,7 +245,7 @@ describe('selectors', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
permissionsHistory: {
|
||||
permissionHistory: {
|
||||
'https://remix.ethereum.org': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
@ -338,72 +319,66 @@ describe('selectors', () => {
|
||||
name: 'Account 2',
|
||||
},
|
||||
},
|
||||
domains: {
|
||||
subjects: {
|
||||
'https://remix.ethereum.org': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: [
|
||||
'0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5',
|
||||
'0x7250739de134d33ec7ab1ee592711e15098c9d2d',
|
||||
],
|
||||
},
|
||||
],
|
||||
'date': 1586359844177,
|
||||
'id': '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
|
||||
'invoker': 'https://remix.ethereum.org',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1586359844177,
|
||||
id: '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b',
|
||||
invoker: 'https://remix.ethereum.org',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'peepeth.com': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
|
||||
},
|
||||
],
|
||||
'date': 1585676177970,
|
||||
'id': '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
'invoker': 'peepeth.com',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585676177970,
|
||||
id: '840d72a0-925f-449f-830a-1aa1dd5ce151',
|
||||
invoker: 'peepeth.com',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'uniswap.exchange': {
|
||||
permissions: [
|
||||
{
|
||||
'@context': ['https://github.com/MetaMask/rpc-cap'],
|
||||
'caveats': [
|
||||
permissions: {
|
||||
eth_accounts: {
|
||||
caveats: [
|
||||
{
|
||||
name: 'exposedAccounts',
|
||||
type: 'filterResponse',
|
||||
type: 'restrictReturnedAccounts',
|
||||
value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'],
|
||||
},
|
||||
],
|
||||
'date': 1585616816623,
|
||||
'id': 'ce625215-f2e9-48e7-93ca-21ba193244ff',
|
||||
'invoker': 'uniswap.exchange',
|
||||
'parentCapability': 'eth_accounts',
|
||||
date: 1585616816623,
|
||||
id: 'ce625215-f2e9-48e7-93ca-21ba193244ff',
|
||||
invoker: 'uniswap.exchange',
|
||||
parentCapability: 'eth_accounts',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
domainMetadata: {
|
||||
subjectMetadata: {
|
||||
'https://remix.ethereum.org': {
|
||||
icon: 'https://remix.ethereum.org/icon.png',
|
||||
iconUrl: 'https://remix.ethereum.org/icon.png',
|
||||
name: 'Remix - Ethereum IDE',
|
||||
},
|
||||
},
|
||||
permissionsHistory: {
|
||||
permissionHistory: {
|
||||
'https://remix.ethereum.org': {
|
||||
eth_accounts: {
|
||||
accounts: {
|
||||
|
@ -508,8 +508,8 @@ export function getCustomNonceValue(state) {
|
||||
return String(state.metamask.customNonceValue);
|
||||
}
|
||||
|
||||
export function getDomainMetadata(state) {
|
||||
return state.metamask.domainMetadata;
|
||||
export function getSubjectMetadata(state) {
|
||||
return state.metamask.subjectMetadata;
|
||||
}
|
||||
|
||||
export function getRpcPrefsForCurrentProvider(state) {
|
||||
|
@ -2477,12 +2477,11 @@ export function requestAccountsPermissionWithId(origin) {
|
||||
|
||||
/**
|
||||
* Approves the permissions request.
|
||||
* @param {Object} request - The permissions request to approve
|
||||
* @param {string[]} accounts - The accounts to expose, if any.
|
||||
* @param {Object} request - The permissions request to approve.
|
||||
*/
|
||||
export function approvePermissionsRequest(request, accounts) {
|
||||
export function approvePermissionsRequest(request) {
|
||||
return (dispatch) => {
|
||||
background.approvePermissionsRequest(request, accounts, (err) => {
|
||||
background.approvePermissionsRequest(request, (err) => {
|
||||
if (err) {
|
||||
dispatch(displayWarning(err.message));
|
||||
}
|
||||
@ -2512,9 +2511,9 @@ export function rejectPermissionsRequest(requestId) {
|
||||
/**
|
||||
* Clears the given permissions for the given origin.
|
||||
*/
|
||||
export function removePermissionsFor(domains) {
|
||||
export function removePermissionsFor(subjects) {
|
||||
return (dispatch) => {
|
||||
background.removePermissionsFor(domains, (err) => {
|
||||
background.removePermissionsFor(subjects, (err) => {
|
||||
if (err) {
|
||||
dispatch(displayWarning(err.message));
|
||||
}
|
||||
|
138
yarn.lock
138
yarn.lock
@ -2601,11 +2601,48 @@
|
||||
semver "^7.3.5"
|
||||
yargs "^17.0.1"
|
||||
|
||||
"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.30.0", "@metamask/contract-metadata@^1.31.0":
|
||||
"@metamask/contract-metadata@^1.29.0", "@metamask/contract-metadata@^1.30.0", "@metamask/contract-metadata@^1.31.0":
|
||||
version "1.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.31.0.tgz#9e3e46de7a955ea1ca61f7db20d9a17b5e91d3d0"
|
||||
integrity sha512-4FBJkg/vDiYp/thIiZknxrJ0lfsj2eWIPenwlNZmoqOhoL4VqhK5eKWxi+EuGMvv9taP+QBRk6Key7wC1uL78A==
|
||||
|
||||
"@metamask/controllers@^17.0.0":
|
||||
version "17.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-17.0.0.tgz#7ef00b4f7583d8075115e8a2f074d7b66646bbe8"
|
||||
integrity sha512-myPlAk8SpNm5SwHHKGgm2XDLP4bxNR2UsKoQlYtV7bJq3l8FV1agSFwHBwDhg61/52Xvqdqy+1YDVdV3kOwPgg==
|
||||
dependencies:
|
||||
"@ethereumjs/common" "^2.3.1"
|
||||
"@ethereumjs/tx" "^3.2.1"
|
||||
"@metamask/contract-metadata" "^1.29.0"
|
||||
"@types/uuid" "^8.3.0"
|
||||
abort-controller "^3.0.0"
|
||||
async-mutex "^0.2.6"
|
||||
babel-runtime "^6.26.0"
|
||||
eth-ens-namehash "^2.0.8"
|
||||
eth-json-rpc-infura "^5.1.0"
|
||||
eth-keyring-controller "^6.2.1"
|
||||
eth-method-registry "1.1.0"
|
||||
eth-phishing-detect "^1.1.14"
|
||||
eth-query "^2.1.2"
|
||||
eth-rpc-errors "^4.0.0"
|
||||
eth-sig-util "^3.0.0"
|
||||
ethereumjs-util "^7.0.10"
|
||||
ethereumjs-wallet "^1.0.1"
|
||||
ethers "^5.4.1"
|
||||
ethjs-unit "^0.1.6"
|
||||
ethjs-util "^0.1.6"
|
||||
human-standard-collectible-abi "^1.0.2"
|
||||
human-standard-token-abi "^2.0.0"
|
||||
immer "^9.0.6"
|
||||
isomorphic-fetch "^3.0.0"
|
||||
jsonschema "^1.2.4"
|
||||
nanoid "^3.1.12"
|
||||
punycode "^2.1.1"
|
||||
single-call-balance-checker-abi "^1.0.0"
|
||||
uuid "^8.3.2"
|
||||
web3 "^0.20.7"
|
||||
web3-provider-engine "^16.0.3"
|
||||
|
||||
"@metamask/controllers@^20.1.0":
|
||||
version "20.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-20.1.0.tgz#1d8386dc22d14f9fd9feb8b3cc8314d663587550"
|
||||
@ -2644,34 +2681,6 @@
|
||||
web3 "^0.20.7"
|
||||
web3-provider-engine "^16.0.3"
|
||||
|
||||
"@metamask/controllers@^5.0.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-5.1.0.tgz#02c1957295bcb6db1655a716d165665d170e7f34"
|
||||
integrity sha512-4piqkIrpphe+9nEy68WH+yBw9wsXZyCMVeBZeRtliVHAJFXUdz+KZDUi/R1Y+568JBzqAvsOtOzbUIU4btD3Fw==
|
||||
dependencies:
|
||||
"@metamask/contract-metadata" "^1.19.0"
|
||||
await-semaphore "^0.1.3"
|
||||
eth-ens-namehash "^2.0.8"
|
||||
eth-json-rpc-infura "^5.1.0"
|
||||
eth-keyring-controller "^6.1.0"
|
||||
eth-method-registry "1.1.0"
|
||||
eth-phishing-detect "^1.1.13"
|
||||
eth-query "^2.1.2"
|
||||
eth-rpc-errors "^4.0.0"
|
||||
eth-sig-util "^3.0.0"
|
||||
ethereumjs-util "^6.1.0"
|
||||
ethereumjs-wallet "^0.6.4"
|
||||
ethjs-query "^0.3.8"
|
||||
human-standard-collectible-abi "^1.0.2"
|
||||
human-standard-token-abi "^2.0.0"
|
||||
isomorphic-fetch "^3.0.0"
|
||||
jsonschema "^1.2.4"
|
||||
nanoid "^3.1.12"
|
||||
single-call-balance-checker-abi "^1.0.0"
|
||||
uuid "^3.3.2"
|
||||
web3 "^0.20.7"
|
||||
web3-provider-engine "^16.0.1"
|
||||
|
||||
"@metamask/eslint-config-jest@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/eslint-config-jest/-/eslint-config-jest-6.0.0.tgz#9e10cfbca31236afd7be2058be70365084e540d6"
|
||||
@ -2759,6 +2768,15 @@
|
||||
readable-stream "^2.2.2"
|
||||
through2 "^2.0.3"
|
||||
|
||||
"@metamask/obs-store@^6.0.2":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-6.0.2.tgz#1fbc458cc617557a4557f9ab58e6676c474df2b1"
|
||||
integrity sha512-MjnP+xNZGBx46YZrR8ZYPb+ScPfxJUbs09MTByuQKxMsf7Lxz17oBTI5ZMkOZOTSBBxhknKdjJg+nAM8mMopwg==
|
||||
dependencies:
|
||||
"@metamask/safe-event-emitter" "^2.0.0"
|
||||
readable-stream "^2.2.2"
|
||||
through2 "^2.0.3"
|
||||
|
||||
"@metamask/obs-store@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-7.0.0.tgz#6cae5f28306bb3e83a381bc9ae22682316095bd3"
|
||||
@ -2767,7 +2785,7 @@
|
||||
"@metamask/safe-event-emitter" "^2.0.0"
|
||||
through2 "^2.0.3"
|
||||
|
||||
"@metamask/post-message-stream@^4.0.0":
|
||||
"@metamask/post-message-stream@4.0.0", "@metamask/post-message-stream@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-4.0.0.tgz#72f120e562346ca86ccc9b3684023ad44265f0df"
|
||||
integrity sha512-r0JcoWXNuHycProx8ClxiIElJY/GVb/0/WWXTMsZu7qDejLo52VNXlwfydCdVjbMXeoT2nK1Yt3d5gjmHy5BWw==
|
||||
@ -2797,6 +2815,32 @@
|
||||
resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c"
|
||||
integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==
|
||||
|
||||
"@metamask/snap-controllers@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/snap-controllers/-/snap-controllers-0.4.0.tgz#20647f061e20263f462347c2bf0d59f7aedd6898"
|
||||
integrity sha512-pZ9S72Y9u2KBrMdzFauz6t4LIxJBqcT6uzxstxdY8y0qroeeTJum6Z0L9HFkVSsTLP3JMyVSDD6FwRsHIwXewg==
|
||||
dependencies:
|
||||
"@metamask/controllers" "^17.0.0"
|
||||
"@metamask/object-multiplex" "^1.1.0"
|
||||
"@metamask/obs-store" "^6.0.2"
|
||||
"@metamask/post-message-stream" "4.0.0"
|
||||
"@metamask/safe-event-emitter" "^2.0.0"
|
||||
"@metamask/snap-workers" "^0.4.0"
|
||||
"@types/deep-freeze-strict" "^1.1.0"
|
||||
deep-freeze-strict "^1.1.1"
|
||||
eth-rpc-errors "^4.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
immer "^9.0.6"
|
||||
json-rpc-engine "^6.1.0"
|
||||
json-rpc-middleware-stream "^3.0.0"
|
||||
nanoid "^3.1.28"
|
||||
pump "^3.0.0"
|
||||
|
||||
"@metamask/snap-workers@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/snap-workers/-/snap-workers-0.4.0.tgz#ba561eb15a7b7e7b353738ad5635a68c03cf64b0"
|
||||
integrity sha512-usPEnwRXIwaDc06f8Jis4/CxXzmZJpPOLucOMqkxGAAz3hepA/T5fbfus12sibo5h6QsG0VTqBQ5AqKFlTr0zQ==
|
||||
|
||||
"@metamask/test-dapp@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@metamask/test-dapp/-/test-dapp-4.0.1.tgz#fbc66069687f0502ebb4c6ac0fa7c9862ea6563c"
|
||||
@ -4105,6 +4149,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/deep-freeze-strict@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.0.tgz#447a6a2576191344aa42310131dd3df5c41492c4"
|
||||
integrity sha1-RHpqJXYZE0SqQjEBMd099cQUksQ=
|
||||
|
||||
"@types/estree@^0.0.48":
|
||||
version "0.0.48"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.48.tgz#18dc8091b285df90db2f25aa7d906cfc394b7f74"
|
||||
@ -9494,7 +9543,7 @@ deep-extend@^0.6.0, deep-extend@~0.6.0:
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-freeze-strict@1.1.1:
|
||||
deep-freeze-strict@1.1.1, deep-freeze-strict@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0"
|
||||
integrity sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA=
|
||||
@ -11331,7 +11380,7 @@ eth-json-rpc-middleware@^8.0.0:
|
||||
node-fetch "^2.6.1"
|
||||
pify "^3.0.0"
|
||||
|
||||
eth-keyring-controller@^6.1.0, eth-keyring-controller@^6.2.0, eth-keyring-controller@^6.2.1:
|
||||
eth-keyring-controller@^6.2.0, eth-keyring-controller@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-6.2.1.tgz#61901071fc74059ed37cb5ae93870fdcae6e3781"
|
||||
integrity sha512-x2gTM1iHp2Kbvdtd9Eslysw0qzVZiqOzpVB3AU/ni2Xiit+rlcv2H80zYKjrEwlfWFDj4YILD3bOqlnEMmRJOA==
|
||||
@ -11393,7 +11442,7 @@ eth-method-registry@^2.0.0:
|
||||
dependencies:
|
||||
ethjs "^0.4.0"
|
||||
|
||||
eth-phishing-detect@^1.1.13, eth-phishing-detect@^1.1.14:
|
||||
eth-phishing-detect@^1.1.14:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/eth-phishing-detect/-/eth-phishing-detect-1.1.15.tgz#c42e1aad6cd1c5eeee41c6bf932dcfd0e523d499"
|
||||
integrity sha512-RVNSGMVIuO6VZ1Uv4v8dljjj0ephW+APVAU5QL5mBu3VEqfBluPMNb6jw66kxYrIFrSNalnb/pMeDpAA+W3cvg==
|
||||
@ -11636,7 +11685,7 @@ ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^1.2.2, ethereumjs-tx@
|
||||
ethereum-common "^0.0.18"
|
||||
ethereumjs-util "^5.0.0"
|
||||
|
||||
ethereumjs-util@6.2.1, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0, ethereumjs-util@^6.2.0:
|
||||
ethereumjs-util@6.2.1, ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69"
|
||||
integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==
|
||||
@ -19884,10 +19933,10 @@ nanoid@^2.0.0, nanoid@^2.1.6:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
|
||||
|
||||
nanoid@^3.1.12, nanoid@^3.1.23:
|
||||
version "3.1.23"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
||||
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
||||
nanoid@^3.1.12, nanoid@^3.1.23, nanoid@^3.1.28:
|
||||
version "3.1.30"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
|
||||
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.9"
|
||||
@ -24538,17 +24587,6 @@ roarr@^2.15.3:
|
||||
semver-compare "^1.0.0"
|
||||
sprintf-js "^1.1.2"
|
||||
|
||||
rpc-cap@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/rpc-cap/-/rpc-cap-3.2.1.tgz#95aba886c60562626261667d3637f9b4fe05b078"
|
||||
integrity sha512-aNevLIJ/jIGR3by0b3pd7MWBUSJOkbOkjOCZ13Kk2ddToGZV1TaxclBsasLIdomFbxl0lyUQ8EeoMwYrS7yuBA==
|
||||
dependencies:
|
||||
"@metamask/controllers" "^5.0.0"
|
||||
eth-rpc-errors "^3.0.0"
|
||||
is-subset "^0.1.1"
|
||||
json-rpc-engine "^5.3.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
rsa-pem-to-jwk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/rsa-pem-to-jwk/-/rsa-pem-to-jwk-1.1.3.tgz#245e76bdb7e7234cfee7ca032d31b54c38fab98e"
|
||||
@ -28379,7 +28417,7 @@ web3-provider-engine@14.2.1:
|
||||
xhr "^2.2.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
web3-provider-engine@^16.0.1, web3-provider-engine@^16.0.3:
|
||||
web3-provider-engine@^16.0.3:
|
||||
version "16.0.3"
|
||||
resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-16.0.3.tgz#8ff93edf3a8da2f70d7f85c5116028c06a0d9f07"
|
||||
integrity sha512-Q3bKhGqLfMTdLvkd4TtkGYJHcoVQ82D1l8jTIwwuJp/sAp7VHnRYb9YJ14SW/69VMWoOhSpPLZV2tWb9V0WJoA==
|
||||
|
Loading…
Reference in New Issue
Block a user