mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Send accountsChanged notification for wallet_requestPermissions (#8742)
* emit accountsChanged for eth_accounts via wallet_requestPermissions * add/update tests
This commit is contained in:
parent
ab06595a5d
commit
c8a995dd9b
@ -47,7 +47,7 @@ export class PermissionsController {
|
|||||||
this.getKeyringAccounts = getKeyringAccounts
|
this.getKeyringAccounts = getKeyringAccounts
|
||||||
this._getUnlockPromise = getUnlockPromise
|
this._getUnlockPromise = getUnlockPromise
|
||||||
this._notifyDomain = notifyDomain
|
this._notifyDomain = notifyDomain
|
||||||
this.notifyAllDomains = notifyAllDomains
|
this._notifyAllDomains = notifyAllDomains
|
||||||
this._showPermissionRequest = showPermissionRequest
|
this._showPermissionRequest = showPermissionRequest
|
||||||
|
|
||||||
this._restrictedMethods = getRestrictedMethods({
|
this._restrictedMethods = getRestrictedMethods({
|
||||||
@ -95,6 +95,7 @@ export class PermissionsController {
|
|||||||
getAccounts: this.getAccounts.bind(this, origin),
|
getAccounts: this.getAccounts.bind(this, origin),
|
||||||
getUnlockPromise: () => this._getUnlockPromise(true),
|
getUnlockPromise: () => this._getUnlockPromise(true),
|
||||||
hasPermission: this.hasPermission.bind(this, origin),
|
hasPermission: this.hasPermission.bind(this, origin),
|
||||||
|
notifyAccountsChanged: this.notifyAccountsChanged.bind(this, origin),
|
||||||
requestAccountsPermission: this._requestPermissions.bind(
|
requestAccountsPermission: this._requestPermissions.bind(
|
||||||
this, { origin }, { eth_accounts: {} },
|
this, { origin }, { eth_accounts: {} },
|
||||||
),
|
),
|
||||||
@ -196,6 +197,7 @@ export class PermissionsController {
|
|||||||
* User approval callback. Resolves the Promise for the permissions request
|
* User approval callback. Resolves the Promise for the permissions request
|
||||||
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
|
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
|
||||||
* The request will be rejected if finalizePermissionsRequest fails.
|
* 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 {Object} approved - The request object approved by the user
|
||||||
* @param {Array} accounts - The accounts to expose, if any
|
* @param {Array} accounts - The accounts to expose, if any
|
||||||
@ -206,7 +208,7 @@ export class PermissionsController {
|
|||||||
const approval = this.pendingApprovals.get(id)
|
const approval = this.pendingApprovals.get(id)
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
log.error(`Permissions request with id '${id}' not found`)
|
log.debug(`Permissions request with id '${id}' not found`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +243,7 @@ export class PermissionsController {
|
|||||||
/**
|
/**
|
||||||
* User rejection callback. Rejects the Promise for the permissions request
|
* User rejection callback. Rejects the Promise for the permissions request
|
||||||
* waited upon by rpc-cap, see requestUserApproval in _initializePermissions.
|
* 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
|
* @param {string} id - The id of the request rejected by the user
|
||||||
*/
|
*/
|
||||||
@ -248,7 +251,7 @@ export class PermissionsController {
|
|||||||
const approval = this.pendingApprovals.get(id)
|
const approval = this.pendingApprovals.get(id)
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
log.error(`Permissions request with id '${id}' not found`)
|
log.debug(`Permissions request with id '${id}' not found`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,10 +292,7 @@ export class PermissionsController {
|
|||||||
|
|
||||||
const permittedAccounts = await this.getAccounts(origin)
|
const permittedAccounts = await this.getAccounts(origin)
|
||||||
|
|
||||||
this.notifyDomain(origin, {
|
this.notifyAccountsChanged(origin, permittedAccounts)
|
||||||
method: NOTIFICATION_NAMES.accountsChanged,
|
|
||||||
result: permittedAccounts,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -338,10 +338,7 @@ export class PermissionsController {
|
|||||||
newPermittedAccounts = await this.getAccounts(origin)
|
newPermittedAccounts = await this.getAccounts(origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifyDomain(origin, {
|
this.notifyAccountsChanged(origin, newPermittedAccounts)
|
||||||
method: NOTIFICATION_NAMES.accountsChanged,
|
|
||||||
result: newPermittedAccounts,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -410,21 +407,34 @@ export class PermissionsController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyDomain (origin, payload) {
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._notifyDomain(origin, {
|
||||||
|
method: NOTIFICATION_NAMES.accountsChanged,
|
||||||
|
result: newAccounts,
|
||||||
|
})
|
||||||
|
|
||||||
// if the accounts changed from the perspective of the dapp,
|
// if the accounts changed from the perspective of the dapp,
|
||||||
// update "last seen" time for the origin and account(s)
|
// update "last seen" time for the origin and account(s)
|
||||||
// exception: no accounts -> no times to update
|
// exception: no accounts -> no times to update
|
||||||
if (
|
this.permissionsLog.updateAccountsHistory(
|
||||||
payload.method === NOTIFICATION_NAMES.accountsChanged &&
|
origin, newAccounts
|
||||||
Array.isArray(payload.result)
|
)
|
||||||
) {
|
|
||||||
this.permissionsLog.updateAccountsHistory(
|
|
||||||
origin, payload.result
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._notifyDomain(origin, payload)
|
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
// we don't check for accounts changing in the notifyAllDomains case,
|
// we don't check for accounts changing in the notifyAllDomains case,
|
||||||
@ -438,7 +448,8 @@ export class PermissionsController {
|
|||||||
* Should only be called after confirming that the permissions exist, to
|
* Should only be called after confirming that the permissions exist, to
|
||||||
* avoid sending unnecessary notifications.
|
* avoid sending unnecessary notifications.
|
||||||
*
|
*
|
||||||
* @param {Object} domains { origin: [permissions] }
|
* @param {Object} domains { origin: [permissions] } - The map of domain
|
||||||
|
* origins to permissions to remove.
|
||||||
*/
|
*/
|
||||||
removePermissionsFor (domains) {
|
removePermissionsFor (domains) {
|
||||||
|
|
||||||
@ -449,10 +460,7 @@ export class PermissionsController {
|
|||||||
perms.map((methodName) => {
|
perms.map((methodName) => {
|
||||||
|
|
||||||
if (methodName === 'eth_accounts') {
|
if (methodName === 'eth_accounts') {
|
||||||
this.notifyDomain(
|
this.notifyAccountsChanged(origin, [])
|
||||||
origin,
|
|
||||||
{ method: NOTIFICATION_NAMES.accountsChanged, result: [] }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { parentCapability: methodName }
|
return { parentCapability: methodName }
|
||||||
@ -466,7 +474,7 @@ export class PermissionsController {
|
|||||||
*/
|
*/
|
||||||
clearPermissions () {
|
clearPermissions () {
|
||||||
this.permissions.clearDomains()
|
this.permissions.clearDomains()
|
||||||
this.notifyAllDomains({
|
this._notifyAllDomains({
|
||||||
method: NOTIFICATION_NAMES.accountsChanged,
|
method: NOTIFICATION_NAMES.accountsChanged,
|
||||||
result: [],
|
result: [],
|
||||||
})
|
})
|
||||||
@ -583,6 +591,7 @@ export class PermissionsController {
|
|||||||
* @param {string} account - The newly selected account's address.
|
* @param {string} account - The newly selected account's address.
|
||||||
*/
|
*/
|
||||||
async _handleAccountSelected (account) {
|
async _handleAccountSelected (account) {
|
||||||
|
|
||||||
if (typeof account !== 'string') {
|
if (typeof account !== 'string') {
|
||||||
throw new Error('Selected account should be a non-empty string.')
|
throw new Error('Selected account should be a non-empty string.')
|
||||||
}
|
}
|
||||||
@ -618,10 +627,7 @@ export class PermissionsController {
|
|||||||
async _handleConnectedAccountSelected (origin) {
|
async _handleConnectedAccountSelected (origin) {
|
||||||
const permittedAccounts = await this.getAccounts(origin)
|
const permittedAccounts = await this.getAccounts(origin)
|
||||||
|
|
||||||
this.notifyDomain(origin, {
|
this.notifyAccountsChanged(origin, permittedAccounts)
|
||||||
method: NOTIFICATION_NAMES.accountsChanged,
|
|
||||||
result: permittedAccounts,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,6 +9,7 @@ export default function createMethodMiddleware ({
|
|||||||
getAccounts,
|
getAccounts,
|
||||||
getUnlockPromise,
|
getUnlockPromise,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
|
notifyAccountsChanged,
|
||||||
requestAccountsPermission,
|
requestAccountsPermission,
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ export default function createMethodMiddleware ({
|
|||||||
|
|
||||||
return createAsyncMiddleware(async (req, res, next) => {
|
return createAsyncMiddleware(async (req, res, next) => {
|
||||||
|
|
||||||
|
let responseHandler
|
||||||
|
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
|
|
||||||
// Intercepting eth_accounts requests for backwards compatibility:
|
// Intercepting eth_accounts requests for backwards compatibility:
|
||||||
@ -81,10 +84,33 @@ export default function createMethodMiddleware ({
|
|||||||
res.result = true
|
res.result = true
|
||||||
return
|
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:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
// when this promise resolves, the response is on its way back
|
||||||
|
await next()
|
||||||
|
|
||||||
|
if (responseHandler) {
|
||||||
|
responseHandler()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -446,6 +446,19 @@ export const getters = deepFreeze({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
notifyAccountsChanged: {
|
||||||
|
invalidOrigin: (origin) => {
|
||||||
|
return {
|
||||||
|
message: `Invalid origin: '${origin}'`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invalidAccounts: () => {
|
||||||
|
return {
|
||||||
|
message: 'Invalid accounts',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -477,18 +490,6 @@ export const getters = deepFreeze({
|
|||||||
result: accounts,
|
result: accounts,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a test notification that doesn't occur in practice.
|
|
||||||
*
|
|
||||||
* @returns {Object} A notification with the 'test_notification' method name
|
|
||||||
*/
|
|
||||||
test: () => {
|
|
||||||
return {
|
|
||||||
method: 'test_notification',
|
|
||||||
result: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1163,7 +1163,7 @@ describe('permissions controller', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('notifyDomain', function () {
|
describe('notifyAccountsChanged', function () {
|
||||||
|
|
||||||
let notifications, permController
|
let notifications, permController
|
||||||
|
|
||||||
@ -1173,11 +1173,11 @@ describe('permissions controller', function () {
|
|||||||
sinon.spy(permController.permissionsLog, 'updateAccountsHistory')
|
sinon.spy(permController.permissionsLog, 'updateAccountsHistory')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('notifyDomain handles accountsChanged', async function () {
|
it('notifyAccountsChanged records history and sends notification', async function () {
|
||||||
|
|
||||||
permController.notifyDomain(
|
permController.notifyAccountsChanged(
|
||||||
ORIGINS.a,
|
ORIGINS.a,
|
||||||
NOTIFICATIONS.newAccounts(ACCOUNTS.a.permitted),
|
ACCOUNTS.a.permitted,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
@ -1192,19 +1192,45 @@ describe('permissions controller', function () {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('notifyDomain handles notifications other than accountsChanged', async function () {
|
it('notifyAccountsChanged throws on invalid origin', async function () {
|
||||||
|
|
||||||
permController.notifyDomain(ORIGINS.a, NOTIFICATIONS.test())
|
assert.throws(
|
||||||
|
() => permController.notifyAccountsChanged(
|
||||||
assert.ok(
|
4,
|
||||||
permController.permissionsLog.updateAccountsHistory.notCalled,
|
ACCOUNTS.a.permitted,
|
||||||
'permissionsLog.updateAccountsHistory should not have been called'
|
),
|
||||||
|
ERRORS.notifyAccountsChanged.invalidOrigin(4),
|
||||||
|
'should throw expected error for non-string origin'
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.throws(
|
||||||
notifications[ORIGINS.a],
|
() => permController.notifyAccountsChanged(
|
||||||
[ NOTIFICATIONS.test() ],
|
'',
|
||||||
'origin should have correct notification'
|
ACCOUNTS.a.permitted,
|
||||||
|
),
|
||||||
|
ERRORS.notifyAccountsChanged.invalidOrigin(''),
|
||||||
|
'should throw expected error for empty string origin'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notifyAccountsChanged throws on invalid accounts', async function () {
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => permController.notifyAccountsChanged(
|
||||||
|
ORIGINS.a,
|
||||||
|
4,
|
||||||
|
),
|
||||||
|
ERRORS.notifyAccountsChanged.invalidAccounts(),
|
||||||
|
'should throw expected error for truthy non-array accounts'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => permController.notifyAccountsChanged(
|
||||||
|
ORIGINS.a,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
ERRORS.notifyAccountsChanged.invalidAccounts(),
|
||||||
|
'should throw expected error for falsy non-array accounts'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { strict as assert } from 'assert'
|
import { strict as assert } from 'assert'
|
||||||
import { useFakeTimers } from 'sinon'
|
import sinon from 'sinon'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
METADATA_STORE_KEY,
|
METADATA_STORE_KEY,
|
||||||
@ -58,6 +58,7 @@ describe('permissions middleware', function () {
|
|||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
permController = initPermController()
|
permController = initPermController()
|
||||||
|
permController.notifyAccountsChanged = sinon.fake()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('grants permissions on user approval', async function () {
|
it('grants permissions on user approval', async function () {
|
||||||
@ -107,6 +108,13 @@ describe('permissions middleware', function () {
|
|||||||
aAccounts, [ACCOUNTS.a.primary],
|
aAccounts, [ACCOUNTS.a.primary],
|
||||||
'origin should have correct accounts'
|
'origin should have correct accounts'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
permController.notifyAccountsChanged.calledOnceWith(
|
||||||
|
ORIGINS.a, aAccounts,
|
||||||
|
),
|
||||||
|
'expected notification call should have been made'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles serial approved requests that overwrite existing permissions', async function () {
|
it('handles serial approved requests that overwrite existing permissions', async function () {
|
||||||
@ -157,6 +165,13 @@ describe('permissions middleware', function () {
|
|||||||
'origin should have correct accounts'
|
'origin should have correct accounts'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
permController.notifyAccountsChanged.calledOnceWith(
|
||||||
|
ORIGINS.a, accounts1,
|
||||||
|
),
|
||||||
|
'expected notification call should have been made'
|
||||||
|
)
|
||||||
|
|
||||||
// create second request
|
// create second request
|
||||||
|
|
||||||
const requestedPerms2 = {
|
const requestedPerms2 = {
|
||||||
@ -211,6 +226,18 @@ describe('permissions middleware', function () {
|
|||||||
accounts2, [ACCOUNTS.b.primary],
|
accounts2, [ACCOUNTS.b.primary],
|
||||||
'origin should have correct accounts'
|
'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(
|
||||||
|
ORIGINS.a, accounts2,
|
||||||
|
),
|
||||||
|
'expected notification call should have been made'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects permissions on user rejection', async function () {
|
it('rejects permissions on user rejection', async function () {
|
||||||
@ -252,6 +279,11 @@ describe('permissions middleware', function () {
|
|||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
aAccounts, [], 'origin should have have correct accounts'
|
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 () {
|
it('rejects requests with unknown permissions', async function () {
|
||||||
@ -288,6 +320,11 @@ describe('permissions middleware', function () {
|
|||||||
),
|
),
|
||||||
'response should have expected error and no result'
|
'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 () {
|
it('accepts only a single pending permissions request per origin', async function () {
|
||||||
@ -695,7 +732,7 @@ describe('permissions middleware', function () {
|
|||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
permController = initPermController()
|
permController = initPermController()
|
||||||
clock = useFakeTimers(1)
|
clock = sinon.useFakeTimers(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user