1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +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:
Erik Marks 2020-06-04 12:15:52 -07:00 committed by GitHub
parent ab06595a5d
commit c8a995dd9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 157 additions and 61 deletions

View File

@ -47,7 +47,7 @@ export class PermissionsController {
this.getKeyringAccounts = getKeyringAccounts
this._getUnlockPromise = getUnlockPromise
this._notifyDomain = notifyDomain
this.notifyAllDomains = notifyAllDomains
this._notifyAllDomains = notifyAllDomains
this._showPermissionRequest = showPermissionRequest
this._restrictedMethods = getRestrictedMethods({
@ -95,6 +95,7 @@ export class PermissionsController {
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: {} },
),
@ -196,6 +197,7 @@ export class PermissionsController {
* 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
@ -206,7 +208,7 @@ export class PermissionsController {
const approval = this.pendingApprovals.get(id)
if (!approval) {
log.error(`Permissions request with id '${id}' not found`)
log.debug(`Permissions request with id '${id}' not found`)
return
}
@ -241,6 +243,7 @@ export class PermissionsController {
/**
* 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
*/
@ -248,7 +251,7 @@ export class PermissionsController {
const approval = this.pendingApprovals.get(id)
if (!approval) {
log.error(`Permissions request with id '${id}' not found`)
log.debug(`Permissions request with id '${id}' not found`)
return
}
@ -289,10 +292,7 @@ export class PermissionsController {
const permittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: permittedAccounts,
})
this.notifyAccountsChanged(origin, permittedAccounts)
}
/**
@ -338,10 +338,7 @@ export class PermissionsController {
newPermittedAccounts = await this.getAccounts(origin)
}
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: newPermittedAccounts,
})
this.notifyAccountsChanged(origin, 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,
// update "last seen" time for the origin and account(s)
// exception: no accounts -> no times to update
if (
payload.method === NOTIFICATION_NAMES.accountsChanged &&
Array.isArray(payload.result)
) {
this.permissionsLog.updateAccountsHistory(
origin, payload.result
)
}
this._notifyDomain(origin, payload)
this.permissionsLog.updateAccountsHistory(
origin, newAccounts
)
// NOTE:
// 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
* 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) {
@ -449,10 +460,7 @@ export class PermissionsController {
perms.map((methodName) => {
if (methodName === 'eth_accounts') {
this.notifyDomain(
origin,
{ method: NOTIFICATION_NAMES.accountsChanged, result: [] }
)
this.notifyAccountsChanged(origin, [])
}
return { parentCapability: methodName }
@ -466,7 +474,7 @@ export class PermissionsController {
*/
clearPermissions () {
this.permissions.clearDomains()
this.notifyAllDomains({
this._notifyAllDomains({
method: NOTIFICATION_NAMES.accountsChanged,
result: [],
})
@ -583,6 +591,7 @@ export class PermissionsController {
* @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.')
}
@ -618,10 +627,7 @@ export class PermissionsController {
async _handleConnectedAccountSelected (origin) {
const permittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: permittedAccounts,
})
this.notifyAccountsChanged(origin, permittedAccounts)
}
/**

View File

@ -9,6 +9,7 @@ export default function createMethodMiddleware ({
getAccounts,
getUnlockPromise,
hasPermission,
notifyAccountsChanged,
requestAccountsPermission,
}) {
@ -16,6 +17,8 @@ export default function createMethodMiddleware ({
return createAsyncMiddleware(async (req, res, next) => {
let responseHandler
switch (req.method) {
// Intercepting eth_accounts requests for backwards compatibility:
@ -81,10 +84,33 @@ export default function createMethodMiddleware ({
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
}
next()
// when this promise resolves, the response is on its way back
await next()
if (responseHandler) {
responseHandler()
}
})
}

View File

@ -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,
}
},
/**
* 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,
}
},
},
/**

View File

@ -1163,7 +1163,7 @@ describe('permissions controller', function () {
})
})
describe('notifyDomain', function () {
describe('notifyAccountsChanged', function () {
let notifications, permController
@ -1173,11 +1173,11 @@ describe('permissions controller', function () {
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,
NOTIFICATIONS.newAccounts(ACCOUNTS.a.permitted),
ACCOUNTS.a.permitted,
)
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.ok(
permController.permissionsLog.updateAccountsHistory.notCalled,
'permissionsLog.updateAccountsHistory should not have been called'
assert.throws(
() => permController.notifyAccountsChanged(
4,
ACCOUNTS.a.permitted,
),
ERRORS.notifyAccountsChanged.invalidOrigin(4),
'should throw expected error for non-string origin'
)
assert.deepEqual(
notifications[ORIGINS.a],
[ NOTIFICATIONS.test() ],
'origin should have correct notification'
assert.throws(
() => permController.notifyAccountsChanged(
'',
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'
)
})
})

View File

@ -1,5 +1,5 @@
import { strict as assert } from 'assert'
import { useFakeTimers } from 'sinon'
import sinon from 'sinon'
import {
METADATA_STORE_KEY,
@ -58,6 +58,7 @@ describe('permissions middleware', function () {
beforeEach(function () {
permController = initPermController()
permController.notifyAccountsChanged = sinon.fake()
})
it('grants permissions on user approval', async function () {
@ -107,6 +108,13 @@ describe('permissions middleware', function () {
aAccounts, [ACCOUNTS.a.primary],
'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 () {
@ -157,6 +165,13 @@ describe('permissions middleware', function () {
'origin should have correct accounts'
)
assert.ok(
permController.notifyAccountsChanged.calledOnceWith(
ORIGINS.a, accounts1,
),
'expected notification call should have been made'
)
// create second request
const requestedPerms2 = {
@ -211,6 +226,18 @@ describe('permissions middleware', function () {
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(
ORIGINS.a, accounts2,
),
'expected notification call should have been made'
)
})
it('rejects permissions on user rejection', async function () {
@ -252,6 +279,11 @@ describe('permissions middleware', function () {
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 () {
@ -288,6 +320,11 @@ describe('permissions middleware', function () {
),
'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 () {
@ -695,7 +732,7 @@ describe('permissions middleware', function () {
beforeEach(function () {
permController = initPermController()
clock = useFakeTimers(1)
clock = sinon.useFakeTimers(1)
})
afterEach(function () {