1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Manually connect via the full connect flow (#8666)

This commit is contained in:
Whymarrh Whitby 2020-05-28 09:08:12 -02:30 committed by GitHub
commit 5da369d723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 46 additions and 291 deletions

View File

@ -1,3 +1,4 @@
import nanoid from 'nanoid'
import JsonRpcEngine from 'json-rpc-engine' import JsonRpcEngine from 'json-rpc-engine'
import asMiddleware from 'json-rpc-engine/src/asMiddleware' import asMiddleware from 'json-rpc-engine/src/asMiddleware'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
@ -95,7 +96,7 @@ export class PermissionsController {
getUnlockPromise: () => this._getUnlockPromise(true), getUnlockPromise: () => this._getUnlockPromise(true),
hasPermission: this.hasPermission.bind(this, origin), hasPermission: this.hasPermission.bind(this, origin),
requestAccountsPermission: this._requestPermissions.bind( requestAccountsPermission: this._requestPermissions.bind(
this, origin, { eth_accounts: {} } this, { origin }, { eth_accounts: {} },
), ),
})) }))
@ -106,6 +107,17 @@ export class PermissionsController {
return asMiddleware(engine) return asMiddleware(engine)
} }
/**
* Request {@code eth_accounts} permissions
* @param {string} origin - The origin
* @returns {Promise<string>} the request ID
*/
async requestAccountsPermission (origin) {
const id = nanoid()
this._requestPermissions({ origin, id }, { eth_accounts: {} })
return id
}
/** /**
* Returns the accounts that should be exposed for the given origin domain, * 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 * if any. This method exists for when a trusted context needs to know
@ -155,10 +167,10 @@ export class PermissionsController {
/** /**
* Submits a permissions request to rpc-cap. Internal, background use only. * Submits a permissions request to rpc-cap. Internal, background use only.
* *
* @param {string} origin - The origin string. * @param {IOriginMetadata} metadata - The origin metadata.
* @param {IRequestedPermissions} permissions - The requested permissions. * @param {IRequestedPermissions} permissions - The requested permissions.
*/ */
_requestPermissions (origin, permissions) { _requestPermissions (metadata, permissions) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// rpc-cap assigns an id to the request if there is none, as expected by // rpc-cap assigns an id to the request if there is none, as expected by
@ -166,7 +178,7 @@ export class PermissionsController {
const req = { method: 'wallet_requestPermissions', params: [permissions] } const req = { method: 'wallet_requestPermissions', params: [permissions] }
const res = {} const res = {}
this.permissions.providerMiddlewareFunction( this.permissions.providerMiddlewareFunction(
{ origin }, req, res, () => {}, _end metadata, req, res, () => {}, _end
) )
function _end (_err) { function _end (_err) {
@ -244,70 +256,6 @@ export class PermissionsController {
this._removePendingApproval(id) this._removePendingApproval(id)
} }
/**
* @deprecated
* Grants the given origin the eth_accounts permission for the given account(s).
* This method should ONLY be called as a result of direct user action in the UI,
* with the intention of supporting legacy dapps that don't support EIP 1102.
*
* @param {string} origin - The origin to expose the account(s) to.
* @param {Array<string>} accounts - The account(s) to expose.
*/
async legacyExposeAccounts (origin, accounts) {
// accounts are validated by finalizePermissionsRequest
if (typeof origin !== 'string' || !origin.length) {
throw new Error('Must provide non-empty string origin.')
}
const existingAccounts = await this.getAccounts(origin)
if (existingAccounts.length > 0) {
throw new Error(
'May not call legacyExposeAccounts on origin with exposed accounts.'
)
}
const permissions = await this.finalizePermissionsRequest(
{ eth_accounts: {} }, accounts
)
try {
await new Promise((resolve, reject) => {
this.permissions.grantNewPermissions(
origin, permissions, {}, _end
)
function _end (err) {
if (err) {
reject(err)
} else {
resolve()
}
}
})
const newPermittedAccounts = await this.getAccounts(origin)
this.notifyDomain(origin, {
method: NOTIFICATION_NAMES.accountsChanged,
result: newPermittedAccounts,
})
this.permissionsLog.logAccountExposure(origin, accounts)
} catch (error) {
throw ethErrors.rpc.internal({
message: `Failed to add 'eth_accounts' to '${origin}'.`,
data: {
originalError: error,
accounts,
},
})
}
}
/** /**
* Expose an account to the given origin. Changes the eth_accounts * Expose an account to the given origin. Changes the eth_accounts
* permissions and emits accountsChanged. * permissions and emits accountsChanged.

View File

@ -202,33 +202,6 @@ export default class PermissionsLogController {
this.updateActivityLog(logs) this.updateActivityLog(logs)
} }
/**
* Record account exposure and eth_accounts permissions history for the given
* origin.
*
* @param {string} origin - The origin accounts were exposed to.
* @param {Array<string>} accounts - The accounts that were exposed.
*/
logAccountExposure (origin, accounts) {
if (
typeof origin !== 'string' || !origin.length ||
!Array.isArray(accounts) || accounts.length === 0
) {
throw new Error(
'Must provide non-empty string origin and array of accounts.'
)
}
this.logPermissionsHistory(
['eth_accounts'],
origin,
accounts,
Date.now(),
true
)
}
/** /**
* Create new permissions history log entries, if any, and commit them. * Create new permissions history log entries, if any, and commit them.
* *

View File

@ -573,7 +573,7 @@ export default class MetamaskController extends EventEmitter {
removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController), removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController),
addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController), addPermittedAccount: nodeify(permissionsController.addPermittedAccount, permissionsController),
removePermittedAccount: nodeify(permissionsController.removePermittedAccount, permissionsController), removePermittedAccount: nodeify(permissionsController.removePermittedAccount, permissionsController),
legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), requestAccountsPermission: nodeify(permissionsController.requestAccountsPermission, permissionsController),
getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()), getRequestAccountTabIds: (cb) => cb(null, this.getRequestAccountTabIds()),
getOpenMetamaskTabsIds: (cb) => cb(null, this.getOpenMetamaskTabsIds()), getOpenMetamaskTabsIds: (cb) => cb(null, this.getOpenMetamaskTabsIds()),

View File

@ -380,20 +380,6 @@ export const getters = deepFreeze({
}, },
}, },
legacyExposeAccounts: {
badOrigin: () => {
return {
message: 'Must provide non-empty string origin.',
}
},
forbiddenUsage: () => {
return {
name: 'Error',
message: 'May not call legacyExposeAccounts on origin with exposed accounts.',
}
},
},
_handleAccountSelected: { _handleAccountSelected: {
invalidParams: () => { invalidParams: () => {
return { return {
@ -440,14 +426,6 @@ export const getters = deepFreeze({
}, },
}, },
logAccountExposure: {
invalidParams: () => {
return {
message: 'Must provide non-empty string origin and array of accounts.',
}
},
},
pendingApprovals: { pendingApprovals: {
duplicateOriginOrId: (id, origin) => { duplicateOriginOrId: (id, origin) => {
return { return {

View File

@ -716,119 +716,6 @@ describe('permissions controller', function () {
}) })
}) })
describe('legacyExposeAccounts', function () {
let permController, notifications
beforeEach(function () {
notifications = initNotifications()
permController = initPermController(notifications)
})
it('successfully exposes accounts and updates permissions history', async function () {
let aAccounts = await permController.getAccounts(ORIGINS.a)
assert.deepEqual(aAccounts, [], 'origin should have no accounts')
await permController.legacyExposeAccounts(ORIGINS.a, ACCOUNTS.a.permitted)
aAccounts = await permController.getAccounts(ORIGINS.a)
assert.deepEqual(aAccounts, [ACCOUNTS.a.primary], 'origin should have correct accounts')
// now, permissions history should be updated
const permissionsHistory = permController.permissionsLog.getHistory()
const historyOrigins = Object.keys(permissionsHistory)
assert.equal(historyOrigins.length, 1, 'should have single origin')
assert.equal(historyOrigins[0], ORIGINS.a, 'should have correct origin')
assert.ok(
permissionsHistory[ORIGINS.a].eth_accounts,
'history should have eth_accounts entry'
)
assert.deepEqual(
Object.keys(permissionsHistory[ORIGINS.a].eth_accounts.accounts),
ACCOUNTS.a.permitted,
'should have expected eth_accounts entry accounts'
)
// notification should also have been sent
assert.deepEqual(
notifications[ORIGINS.a][0],
NOTIFICATIONS.newAccounts([ACCOUNTS.a.primary]),
'first origin should have correct notification'
)
})
it('throws if called on origin with existing exposed accounts', async function () {
grantPermissions(
permController, ORIGINS.a,
PERMS.finalizedRequests.eth_accounts(ACCOUNTS.a.permitted)
)
const aAccounts = await permController.getAccounts(ORIGINS.a)
assert.deepEqual(aAccounts, [ACCOUNTS.a.primary], 'origin should have correct accounts')
await assert.rejects(
permController.legacyExposeAccounts(ORIGINS.a, ACCOUNTS.b.permitted),
ERRORS.legacyExposeAccounts.forbiddenUsage(),
'should throw if called on origin with existing exposed accounts'
)
const permissionsHistory = permController.permissionsLog.getHistory()
assert.deepEqual(
permissionsHistory, {},
'should not have modified history'
)
assert.deepEqual(
notifications[ORIGINS.a], [],
'should not have sent notification'
)
})
it('throws if called with bad accounts', async function () {
await assert.rejects(
permController.legacyExposeAccounts(ORIGINS.a, []),
ERRORS.validatePermittedAccounts.invalidParam(),
'should throw if called with no accounts'
)
const permissionsHistory = permController.permissionsLog.getHistory()
assert.deepEqual(
permissionsHistory, {},
'should not have modified history'
)
assert.deepEqual(
notifications[ORIGINS.a], [],
'should not have sent notification'
)
})
it('throws if called with bad origin', async function () {
await assert.rejects(
permController.legacyExposeAccounts(null, ACCOUNTS.a.permitted),
ERRORS.legacyExposeAccounts.badOrigin(),
'should throw if called with invalid origin'
)
const permissionsHistory = permController.permissionsLog.getHistory()
assert.deepEqual(
permissionsHistory, {},
'should not have modified history'
)
Object.keys(notifications).forEach((domain) => {
assert.deepEqual(
notifications[domain], [],
'should not have sent notification'
)
})
})
})
describe('preferences state update', function () { describe('preferences state update', function () {
let permController, notifications, preferences, identities let permController, notifications, preferences, identities
@ -1504,6 +1391,16 @@ describe('permissions controller', function () {
permController = initPermController() permController = initPermController()
}) })
it('requestAccountsPermission calls _requestAccountsPermission with an explicit request ID', async function () {
const _requestPermissions = sinon.stub(permController, '_requestPermissions').resolves()
await permController.requestAccountsPermission('example.com')
assert.ok(_requestPermissions.calledOnceWithExactly(
sinon.match.object.and(sinon.match.has('origin')).and(sinon.match.has('id')),
{ eth_accounts: {} },
))
_requestPermissions.restore()
})
it('_addPendingApproval: should throw if adding origin twice', function () { it('_addPendingApproval: should throw if adding origin twice', function () {
const id = nanoid() const id = nanoid()

View File

@ -22,7 +22,6 @@ import {
} from './mocks' } from './mocks'
const { const {
ERRORS,
PERMS, PERMS,
RPC_REQUESTS, RPC_REQUESTS,
} = getters } = getters
@ -647,44 +646,4 @@ describe('permissions log', function () {
) )
}) })
}) })
describe('instance method edge cases', function () {
it('logAccountExposure errors on invalid params', function () {
const permLog = initPermLog()
assert.throws(
() => {
permLog.logAccountExposure('', ACCOUNTS.a.permitted)
},
ERRORS.logAccountExposure.invalidParams(),
'should throw expected error'
)
assert.throws(
() => {
permLog.logAccountExposure(null, ACCOUNTS.a.permitted)
},
ERRORS.logAccountExposure.invalidParams(),
'should throw expected error'
)
assert.throws(
() => {
permLog.logAccountExposure('foo', {})
},
ERRORS.logAccountExposure.invalidParams(),
'should throw expected error'
)
assert.throws(
() => {
permLog.logAccountExposure('foo', [])
},
ERRORS.logAccountExposure.invalidParams(),
'should throw expected error'
)
})
})
}) })

View File

@ -20,11 +20,11 @@ export default class ConnectedSites extends Component {
disconnectAllAccounts: PropTypes.func.isRequired, disconnectAllAccounts: PropTypes.func.isRequired,
disconnectAccount: PropTypes.func.isRequired, disconnectAccount: PropTypes.func.isRequired,
getOpenMetamaskTabsIds: PropTypes.func.isRequired, getOpenMetamaskTabsIds: PropTypes.func.isRequired,
legacyExposeAccount: PropTypes.func.isRequired,
permittedAccountsByOrigin: PropTypes.objectOf( permittedAccountsByOrigin: PropTypes.objectOf(
PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.string),
).isRequired, ).isRequired,
tabToConnect: PropTypes.object, tabToConnect: PropTypes.object,
requestAccountsPermission: PropTypes.func.isRequired,
} }
state = { state = {
@ -76,13 +76,12 @@ export default class ConnectedSites extends Component {
} }
renderConnectedSitesPopover () { renderConnectedSitesPopover () {
const { const {
accountLabel, accountLabel,
closePopover, closePopover,
connectedDomains, connectedDomains,
legacyExposeAccount,
tabToConnect, tabToConnect,
requestAccountsPermission,
} = this.props } = this.props
const { t } = this.context const { t } = this.context
@ -100,7 +99,7 @@ export default class ConnectedSites extends Component {
? ( ? (
<a <a
className="connected-sites__text-button" className="connected-sites__text-button"
onClick={legacyExposeAccount} onClick={requestAccountsPermission}
> >
{t('connectManually')} {t('connectManually')}
</a> </a>

View File

@ -2,7 +2,7 @@ import { connect } from 'react-redux'
import ConnectedSites from './connected-sites.component' import ConnectedSites from './connected-sites.component'
import { import {
getOpenMetamaskTabsIds, getOpenMetamaskTabsIds,
legacyExposeAccounts, requestAccountsPermission,
removePermissionsFor, removePermissionsFor,
removePermittedAccount, removePermittedAccount,
} from '../../store/actions' } from '../../store/actions'
@ -14,7 +14,7 @@ import {
getPermittedAccountsByOrigin, getPermittedAccountsByOrigin,
getSelectedAddress, getSelectedAddress,
} from '../../selectors' } from '../../selectors'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes' import { CONNECT_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { openMetaMaskTabs } = state.appState const { openMetaMaskTabs } = state.appState
@ -57,7 +57,7 @@ const mapDispatchToProps = (dispatch) => {
[domainKey]: permissionMethodNames, [domainKey]: permissionMethodNames,
})) }))
}, },
legacyExposeAccounts: (origin, account) => dispatch(legacyExposeAccounts(origin, [account])), requestAccountsPermission: (origin) => dispatch(requestAccountsPermission(origin)),
} }
} }
@ -71,7 +71,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { const {
disconnectAccount, disconnectAccount,
disconnectAllAccounts, disconnectAllAccounts,
legacyExposeAccounts: dispatchLegacyExposeAccounts, requestAccountsPermission: dispatchRequestAccountsPermission,
} = dispatchProps } = dispatchProps
const { history } = ownProps const { history } = ownProps
@ -94,7 +94,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
closePopover() closePopover()
} }
}, },
legacyExposeAccount: () => dispatchLegacyExposeAccounts(tabToConnect.origin, selectedAddress), requestAccountsPermission: async () => {
const id = await dispatchRequestAccountsPermission(tabToConnect.origin)
history.push(`${CONNECT_ROUTE}/${id}`)
},
} }
} }

View File

@ -2035,6 +2035,14 @@ export function setPendingTokens (pendingTokens) {
// Permissions // Permissions
export function requestAccountsPermission (origin) {
return async (dispatch) => {
const id = await promisifiedBackground.requestAccountsPermission(origin)
await forceUpdateMetamaskState(dispatch)
return id
}
}
/** /**
* Approves the permissions request. * Approves the permissions request.
* @param {Object} request - The permissions request to approve * @param {Object} request - The permissions request to approve
@ -2064,16 +2072,6 @@ export function rejectPermissionsRequest (requestId) {
} }
} }
/**
* Exposes the given account(s) to the given origin.
* Call ONLY as a result of direct user action.
*/
export function legacyExposeAccounts (origin, accounts) {
return () => {
return background.legacyExposeAccounts(origin, accounts)
}
}
/** /**
* Clears the given permissions for the given origin. * Clears the given permissions for the given origin.
*/ */