This reverts commit 466ece4588
, which has
the message:
"Revert "Merge pull request #7599 from MetaMask/Version-v7.7.0" (#7648)"
This effectively re-introduces the changes from the "LoginPerSite" PR.
feature/default_network_editable
parent
5e8a80e21d
commit
9a624dd24c
After Width: | Height: | Size: 188 B |
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 937 B |
@ -0,0 +1,377 @@ |
|||||||
|
const JsonRpcEngine = require('json-rpc-engine') |
||||||
|
const asMiddleware = require('json-rpc-engine/src/asMiddleware') |
||||||
|
const ObservableStore = require('obs-store') |
||||||
|
const RpcCap = require('rpc-cap').CapabilitiesController |
||||||
|
const { ethErrors } = require('eth-json-rpc-errors') |
||||||
|
|
||||||
|
const getRestrictedMethods = require('./restrictedMethods') |
||||||
|
const createMethodMiddleware = require('./methodMiddleware') |
||||||
|
const createLoggerMiddleware = require('./loggerMiddleware') |
||||||
|
|
||||||
|
// Methods that do not require any permissions to use:
|
||||||
|
const SAFE_METHODS = require('./permissions-safe-methods.json') |
||||||
|
|
||||||
|
// some constants
|
||||||
|
const METADATA_STORE_KEY = 'domainMetadata' |
||||||
|
const LOG_STORE_KEY = 'permissionsLog' |
||||||
|
const HISTORY_STORE_KEY = 'permissionsHistory' |
||||||
|
const WALLET_METHOD_PREFIX = 'wallet_' |
||||||
|
const CAVEAT_NAMES = { |
||||||
|
exposedAccounts: 'exposedAccounts', |
||||||
|
} |
||||||
|
const ACCOUNTS_CHANGED_NOTIFICATION = 'wallet_accountsChanged' |
||||||
|
|
||||||
|
class PermissionsController { |
||||||
|
|
||||||
|
constructor ( |
||||||
|
{ |
||||||
|
platform, notifyDomain, notifyAllDomains, keyringController, |
||||||
|
} = {}, |
||||||
|
restoredPermissions = {}, |
||||||
|
restoredState = {}) { |
||||||
|
this.store = new ObservableStore({ |
||||||
|
[METADATA_STORE_KEY]: restoredState[METADATA_STORE_KEY] || {}, |
||||||
|
[LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [], |
||||||
|
[HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {}, |
||||||
|
}) |
||||||
|
this.notifyDomain = notifyDomain |
||||||
|
this.notifyAllDomains = notifyAllDomains |
||||||
|
this.keyringController = keyringController |
||||||
|
this._platform = platform |
||||||
|
this._restrictedMethods = getRestrictedMethods(this) |
||||||
|
this._initializePermissions(restoredPermissions) |
||||||
|
} |
||||||
|
|
||||||
|
createMiddleware ({ origin, extensionId }) { |
||||||
|
|
||||||
|
if (extensionId) { |
||||||
|
this.store.updateState({ |
||||||
|
[METADATA_STORE_KEY]: { |
||||||
|
...this.store.getState()[METADATA_STORE_KEY], |
||||||
|
[origin]: { extensionId }, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const engine = new JsonRpcEngine() |
||||||
|
|
||||||
|
engine.push(createLoggerMiddleware({ |
||||||
|
walletPrefix: WALLET_METHOD_PREFIX, |
||||||
|
restrictedMethods: Object.keys(this._restrictedMethods), |
||||||
|
ignoreMethods: [ 'wallet_sendDomainMetadata' ], |
||||||
|
store: this.store, |
||||||
|
logStoreKey: LOG_STORE_KEY, |
||||||
|
historyStoreKey: HISTORY_STORE_KEY, |
||||||
|
})) |
||||||
|
|
||||||
|
engine.push(createMethodMiddleware({ |
||||||
|
store: this.store, |
||||||
|
storeKey: METADATA_STORE_KEY, |
||||||
|
getAccounts: this.getAccounts.bind(this, origin), |
||||||
|
requestAccountsPermission: this._requestPermissions.bind( |
||||||
|
this, origin, { eth_accounts: {} } |
||||||
|
), |
||||||
|
})) |
||||||
|
|
||||||
|
engine.push(this.permissions.providerMiddlewareFunction.bind( |
||||||
|
this.permissions, { origin } |
||||||
|
)) |
||||||
|
return asMiddleware(engine) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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, () => {}, _end |
||||||
|
) |
||||||
|
|
||||||
|
function _end () { |
||||||
|
if (res.error || !Array.isArray(res.result)) { |
||||||
|
resolve([]) |
||||||
|
} else { |
||||||
|
resolve(res.result) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Submits a permissions request to rpc-cap. Internal use only. |
||||||
|
* |
||||||
|
* @param {string} origin - The origin string. |
||||||
|
* @param {IRequestedPermissions} permissions - The requested permissions. |
||||||
|
*/ |
||||||
|
_requestPermissions (origin, permissions) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
|
||||||
|
const req = { method: 'wallet_requestPermissions', params: [permissions] } |
||||||
|
const res = {} |
||||||
|
this.permissions.providerMiddlewareFunction( |
||||||
|
{ origin }, req, res, () => {}, _end |
||||||
|
) |
||||||
|
|
||||||
|
function _end (err) { |
||||||
|
if (err || res.error) { |
||||||
|
reject(err || res.error) |
||||||
|
} else { |
||||||
|
resolve(res.result) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* User approval callback. The request can fail if the request is invalid. |
||||||
|
* |
||||||
|
* @param {object} approved the approved request object |
||||||
|
*/ |
||||||
|
async approvePermissionsRequest (approved, accounts) { |
||||||
|
|
||||||
|
const { id } = approved.metadata |
||||||
|
const approval = this.pendingApprovals[id] |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
// attempt to finalize the request and resolve it
|
||||||
|
await this.finalizePermissionsRequest(approved.permissions, accounts) |
||||||
|
approval.resolve(approved.permissions) |
||||||
|
|
||||||
|
} catch (err) { |
||||||
|
|
||||||
|
// if finalization fails, reject the request
|
||||||
|
approval.reject(ethErrors.rpc.invalidRequest({ |
||||||
|
message: err.message, data: err, |
||||||
|
})) |
||||||
|
} |
||||||
|
|
||||||
|
delete this.pendingApprovals[id] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* User rejection callback. |
||||||
|
* |
||||||
|
* @param {string} id the id of the rejected request |
||||||
|
*/ |
||||||
|
async rejectPermissionsRequest (id) { |
||||||
|
const approval = this.pendingApprovals[id] |
||||||
|
approval.reject(ethErrors.provider.userRejectedRequest()) |
||||||
|
delete this.pendingApprovals[id] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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) { |
||||||
|
|
||||||
|
const permissions = { |
||||||
|
eth_accounts: {}, |
||||||
|
} |
||||||
|
|
||||||
|
await this.finalizePermissionsRequest(permissions, accounts) |
||||||
|
|
||||||
|
let error |
||||||
|
try { |
||||||
|
await new Promise((resolve, reject) => { |
||||||
|
this.permissions.grantNewPermissions(origin, permissions, {}, err => err ? resolve() : reject(err)) |
||||||
|
}) |
||||||
|
} catch (err) { |
||||||
|
error = err |
||||||
|
} |
||||||
|
|
||||||
|
if (error) { |
||||||
|
if (error.code === 4001) { |
||||||
|
throw error |
||||||
|
} else { |
||||||
|
throw ethErrors.rpc.internal({ |
||||||
|
message: `Failed to add 'eth_accounts' to '${origin}'.`, |
||||||
|
data: { |
||||||
|
originalError: error, |
||||||
|
accounts, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Update the accounts exposed to the given origin. |
||||||
|
* Throws error if the update fails. |
||||||
|
* |
||||||
|
* @param {string} origin - The origin to change the exposed accounts for. |
||||||
|
* @param {string[]} accounts - The new account(s) to expose. |
||||||
|
*/ |
||||||
|
async updateExposedAccounts (origin, accounts) { |
||||||
|
|
||||||
|
await this.validateExposedAccounts(accounts) |
||||||
|
|
||||||
|
this.permissions.updateCaveatFor( |
||||||
|
origin, 'eth_accounts', CAVEAT_NAMES.exposedAccounts, accounts |
||||||
|
) |
||||||
|
|
||||||
|
this.notifyDomain(origin, { |
||||||
|
method: ACCOUNTS_CHANGED_NOTIFICATION, |
||||||
|
result: accounts, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Finalizes a permissions request. |
||||||
|
* Throws if request validation fails. |
||||||
|
* |
||||||
|
* @param {Object} requestedPermissions - The requested permissions. |
||||||
|
* @param {string[]} accounts - The accounts to expose, if any. |
||||||
|
*/ |
||||||
|
async finalizePermissionsRequest (requestedPermissions, accounts) { |
||||||
|
|
||||||
|
const { eth_accounts: ethAccounts } = requestedPermissions |
||||||
|
|
||||||
|
if (ethAccounts) { |
||||||
|
|
||||||
|
await this.validateExposedAccounts(accounts) |
||||||
|
|
||||||
|
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 |
||||||
|
)) |
||||||
|
|
||||||
|
ethAccounts.caveats.push( |
||||||
|
{ |
||||||
|
type: 'filterResponse', |
||||||
|
value: accounts, |
||||||
|
name: CAVEAT_NAMES.exposedAccounts, |
||||||
|
}, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
async validateExposedAccounts (accounts) { |
||||||
|
|
||||||
|
if (!Array.isArray(accounts) || accounts.length === 0) { |
||||||
|
throw new Error('Must provide non-empty array of account(s).') |
||||||
|
} |
||||||
|
|
||||||
|
// assert accounts exist
|
||||||
|
const allAccounts = await this.keyringController.getAccounts() |
||||||
|
accounts.forEach(acc => { |
||||||
|
if (!allAccounts.includes(acc)) { |
||||||
|
throw new Error(`Unknown account: ${acc}`) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes the given permissions for the given domain. |
||||||
|
* @param {object} domains { origin: [permissions] } |
||||||
|
*/ |
||||||
|
removePermissionsFor (domains) { |
||||||
|
|
||||||
|
Object.entries(domains).forEach(([origin, perms]) => { |
||||||
|
|
||||||
|
this.permissions.removePermissionsFor( |
||||||
|
origin, |
||||||
|
perms.map(methodName => { |
||||||
|
|
||||||
|
if (methodName === 'eth_accounts') { |
||||||
|
this.notifyDomain( |
||||||
|
origin, |
||||||
|
{ method: ACCOUNTS_CHANGED_NOTIFICATION, result: [] } |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return { parentCapability: methodName } |
||||||
|
}) |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes all known domains and their related permissions. |
||||||
|
*/ |
||||||
|
clearPermissions () { |
||||||
|
this.permissions.clearDomains() |
||||||
|
this.notifyAllDomains({ |
||||||
|
method: ACCOUNTS_CHANGED_NOTIFICATION, |
||||||
|
result: [], |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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.pendingApprovals = {} |
||||||
|
|
||||||
|
this.permissions = new RpcCap({ |
||||||
|
|
||||||
|
// Supports passthrough methods:
|
||||||
|
safeMethods: SAFE_METHODS, |
||||||
|
|
||||||
|
// optional prefix for internal methods
|
||||||
|
methodPrefix: WALLET_METHOD_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 } } = req |
||||||
|
|
||||||
|
this._platform.openExtensionInBrowser('connect') |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
this.pendingApprovals[id] = { resolve, reject } |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, initState) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
PermissionsController, |
||||||
|
addInternalMethodPrefix: prefix, |
||||||
|
CAVEAT_NAMES, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function prefix (method) { |
||||||
|
return WALLET_METHOD_PREFIX + method |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
|
||||||
|
const clone = require('clone') |
||||||
|
const { isValidAddress } = require('ethereumjs-util') |
||||||
|
|
||||||
|
const LOG_LIMIT = 100 |
||||||
|
|
||||||
|
/** |
||||||
|
* Create middleware for logging requests and responses to restricted and |
||||||
|
* permissions-related methods. |
||||||
|
*/ |
||||||
|
module.exports = function createLoggerMiddleware ({ |
||||||
|
walletPrefix, restrictedMethods, store, logStoreKey, historyStoreKey, ignoreMethods, |
||||||
|
}) { |
||||||
|
return (req, res, next, _end) => { |
||||||
|
let activityEntry, requestedMethods |
||||||
|
const { origin, method } = req |
||||||
|
const isInternal = method.startsWith(walletPrefix) |
||||||
|
if ((isInternal || restrictedMethods.includes(method)) && !ignoreMethods.includes(method)) { |
||||||
|
activityEntry = logActivity(req, isInternal) |
||||||
|
if (method === `${walletPrefix}requestPermissions`) { |
||||||
|
requestedMethods = getRequestedMethods(req) |
||||||
|
} |
||||||
|
} else if (method === 'eth_requestAccounts') { |
||||||
|
activityEntry = logActivity(req, isInternal) |
||||||
|
requestedMethods = [ 'eth_accounts' ] |
||||||
|
} else { |
||||||
|
return next() |
||||||
|
} |
||||||
|
|
||||||
|
next(cb => { |
||||||
|
const time = Date.now() |
||||||
|
addResponse(activityEntry, res, time) |
||||||
|
if (!res.error && requestedMethods) { |
||||||
|
logHistory(requestedMethods, origin, res.result, time, method === 'eth_requestAccounts') |
||||||
|
} |
||||||
|
cb() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function logActivity (request, isInternal) { |
||||||
|
const activityEntry = { |
||||||
|
id: request.id, |
||||||
|
method: request.method, |
||||||
|
methodType: isInternal ? 'internal' : 'restricted', |
||||||
|
origin: request.origin, |
||||||
|
request: cloneObj(request), |
||||||
|
requestTime: Date.now(), |
||||||
|
response: null, |
||||||
|
responseTime: null, |
||||||
|
success: null, |
||||||
|
} |
||||||
|
commitActivity(activityEntry) |
||||||
|
return activityEntry |
||||||
|
} |
||||||
|
|
||||||
|
function addResponse (activityEntry, response, time) { |
||||||
|
if (!response) { |
||||||
|
return |
||||||
|
} |
||||||
|
activityEntry.response = cloneObj(response) |
||||||
|
activityEntry.responseTime = time |
||||||
|
activityEntry.success = !response.error |
||||||
|
} |
||||||
|
|
||||||
|
function commitActivity (entry) { |
||||||
|
const logs = store.getState()[logStoreKey] |
||||||
|
if (logs.length > LOG_LIMIT - 2) { |
||||||
|
logs.pop() |
||||||
|
} |
||||||
|
logs.push(entry) |
||||||
|
store.updateState({ [logStoreKey]: logs }) |
||||||
|
} |
||||||
|
|
||||||
|
function getRequestedMethods (request) { |
||||||
|
if ( |
||||||
|
!request.params || |
||||||
|
typeof request.params[0] !== 'object' || |
||||||
|
Array.isArray(request.params[0]) |
||||||
|
) { |
||||||
|
return null |
||||||
|
} |
||||||
|
return Object.keys(request.params[0]) |
||||||
|
} |
||||||
|
|
||||||
|
function logHistory (requestedMethods, origin, result, time, isEthRequestAccounts) { |
||||||
|
let accounts, entries |
||||||
|
if (isEthRequestAccounts) { |
||||||
|
accounts = result |
||||||
|
const accountToTimeMap = accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {}) |
||||||
|
entries = { 'eth_accounts': { accounts: accountToTimeMap, lastApproved: time } } |
||||||
|
} else { |
||||||
|
entries = result |
||||||
|
? result |
||||||
|
.map(perm => { |
||||||
|
if (perm.parentCapability === 'eth_accounts') { |
||||||
|
accounts = getAccountsFromPermission(perm) |
||||||
|
} |
||||||
|
return perm.parentCapability |
||||||
|
}) |
||||||
|
.reduce((acc, m) => { |
||||||
|
if (requestedMethods.includes(m)) { |
||||||
|
if (m === 'eth_accounts') { |
||||||
|
const accountToTimeMap = accounts.reduce((acc, account) => ({ ...acc, [account]: time }), {}) |
||||||
|
acc[m] = { lastApproved: time, accounts: accountToTimeMap } |
||||||
|
} else { |
||||||
|
acc[m] = { lastApproved: time } |
||||||
|
} |
||||||
|
} |
||||||
|
return acc |
||||||
|
}, {}) |
||||||
|
: {} |
||||||
|
} |
||||||
|
|
||||||
|
if (Object.keys(entries).length > 0) { |
||||||
|
commitHistory(origin, entries) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function commitHistory (origin, entries) { |
||||||
|
const history = store.getState()[historyStoreKey] || {} |
||||||
|
const newOriginHistory = { |
||||||
|
...history[origin], |
||||||
|
...entries, |
||||||
|
} |
||||||
|
|
||||||
|
if (history[origin] && history[origin]['eth_accounts'] && entries['eth_accounts']) { |
||||||
|
newOriginHistory['eth_accounts'] = { |
||||||
|
lastApproved: entries['eth_accounts'].lastApproved, |
||||||
|
accounts: { |
||||||
|
...history[origin]['eth_accounts'].accounts, |
||||||
|
...entries['eth_accounts'].accounts, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
history[origin] = newOriginHistory |
||||||
|
|
||||||
|
store.updateState({ [historyStoreKey]: history }) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// the call to clone is set to disallow circular references
|
||||||
|
// we attempt cloning at a depth of 3 and 2, then return a
|
||||||
|
// shallow copy of the object
|
||||||
|
function cloneObj (obj) { |
||||||
|
for (let i = 3; i > 1; i--) { |
||||||
|
try { |
||||||
|
return clone(obj, false, i) |
||||||
|
} catch (_) {} |
||||||
|
} |
||||||
|
return { ...obj } |
||||||
|
} |
||||||
|
|
||||||
|
function getAccountsFromPermission (perm) { |
||||||
|
if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) { |
||||||
|
return [] |
||||||
|
} |
||||||
|
const accounts = {} |
||||||
|
for (const c of perm.caveats) { |
||||||
|
if (c.type === 'filterResponse' && Array.isArray(c.value)) { |
||||||
|
for (const v of c.value) { |
||||||
|
if (isValidAddress(v)) { |
||||||
|
accounts[v] = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return Object.keys(accounts) |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
|
||||||
|
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') |
||||||
|
const { ethErrors } = require('eth-json-rpc-errors') |
||||||
|
|
||||||
|
/** |
||||||
|
* Create middleware for handling certain methods and preprocessing permissions requests. |
||||||
|
*/ |
||||||
|
module.exports = function createMethodMiddleware ({ |
||||||
|
store, storeKey, getAccounts, requestAccountsPermission, |
||||||
|
}) { |
||||||
|
return createAsyncMiddleware(async (req, res, next) => { |
||||||
|
|
||||||
|
if (typeof req.method !== 'string') { |
||||||
|
res.error = ethErrors.rpc.invalidRequest({ data: req}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
switch (req.method) { |
||||||
|
|
||||||
|
// intercepting eth_accounts requests for backwards compatibility,
|
||||||
|
// i.e. return an empty array instead of an error
|
||||||
|
case 'eth_accounts': |
||||||
|
|
||||||
|
res.result = await getAccounts() |
||||||
|
return |
||||||
|
|
||||||
|
case 'eth_requestAccounts': |
||||||
|
|
||||||
|
// 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() |
||||||
|
if (accounts.length > 0) { |
||||||
|
res.result = accounts |
||||||
|
} else { |
||||||
|
// this should never happen
|
||||||
|
res.error = ethErrors.rpc.internal( |
||||||
|
'Accounts unexpectedly unavailable. Please report this bug.' |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return |
||||||
|
|
||||||
|
// custom method for getting metadata from the requesting domain
|
||||||
|
case 'wallet_sendDomainMetadata': |
||||||
|
|
||||||
|
const storeState = store.getState()[storeKey] |
||||||
|
const extensionId = storeState[req.origin] |
||||||
|
? storeState[req.origin].extensionId |
||||||
|
: undefined |
||||||
|
|
||||||
|
if ( |
||||||
|
req.domainMetadata && |
||||||
|
typeof req.domainMetadata.name === 'string' |
||||||
|
) { |
||||||
|
|
||||||
|
store.updateState({ |
||||||
|
[storeKey]: { |
||||||
|
...storeState, |
||||||
|
[req.origin]: { |
||||||
|
extensionId, |
||||||
|
...req.domainMetadata, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
res.result = true |
||||||
|
return |
||||||
|
|
||||||
|
default: |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
next() |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
[ |
||||||
|
"web3_sha3", |
||||||
|
"net_listening", |
||||||
|
"net_peerCount", |
||||||
|
"net_version", |
||||||
|
"eth_blockNumber", |
||||||
|
"eth_call", |
||||||
|
"eth_chainId", |
||||||
|
"eth_coinbase", |
||||||
|
"eth_estimateGas", |
||||||
|
"eth_gasPrice", |
||||||
|
"eth_getBalance", |
||||||
|
"eth_getBlockByHash", |
||||||
|
"eth_getBlockByNumber", |
||||||
|
"eth_getBlockTransactionCountByHash", |
||||||
|
"eth_getBlockTransactionCountByNumber", |
||||||
|
"eth_getCode", |
||||||
|
"eth_getFilterChanges", |
||||||
|
"eth_getFilterLogs", |
||||||
|
"eth_getLogs", |
||||||
|
"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", |
||||||
|
"personal_sign", |
||||||
|
"eth_signTypedData", |
||||||
|
"eth_signTypedData_v1", |
||||||
|
"eth_signTypedData_v3", |
||||||
|
"eth_submitHashrate", |
||||||
|
"eth_submitWork", |
||||||
|
"eth_syncing", |
||||||
|
"eth_uninstallFilter" |
||||||
|
] |
@ -0,0 +1,20 @@ |
|||||||
|
|
||||||
|
module.exports = function getRestrictedMethods (permissionsController) { |
||||||
|
return { |
||||||
|
|
||||||
|
'eth_accounts': { |
||||||
|
description: 'View the address of the selected account', |
||||||
|
method: (_, res, __, end) => { |
||||||
|
permissionsController.keyringController.getAccounts() |
||||||
|
.then((accounts) => { |
||||||
|
res.result = accounts |
||||||
|
end() |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
res.error = err |
||||||
|
end(err) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
@ -1,175 +0,0 @@ |
|||||||
const ObservableStore = require('obs-store') |
|
||||||
const SafeEventEmitter = require('safe-event-emitter') |
|
||||||
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') |
|
||||||
const { errors: rpcErrors } = require('eth-json-rpc-errors') |
|
||||||
|
|
||||||
/** |
|
||||||
* A controller that services user-approved requests for a full Ethereum provider API |
|
||||||
*/ |
|
||||||
class ProviderApprovalController extends SafeEventEmitter { |
|
||||||
/** |
|
||||||
* Creates a ProviderApprovalController |
|
||||||
* |
|
||||||
* @param {Object} [config] - Options to configure controller |
|
||||||
*/ |
|
||||||
constructor ({ closePopup, initState, keyringController, openPopup, preferencesController } = {}) { |
|
||||||
super() |
|
||||||
this.closePopup = closePopup |
|
||||||
this.keyringController = keyringController |
|
||||||
this.openPopup = openPopup |
|
||||||
this.preferencesController = preferencesController |
|
||||||
this.memStore = new ObservableStore({ |
|
||||||
providerRequests: [], |
|
||||||
}) |
|
||||||
|
|
||||||
const defaultState = { approvedOrigins: {} } |
|
||||||
this.store = new ObservableStore(Object.assign(defaultState, initState)) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Called when a user approves access to a full Ethereum provider API |
|
||||||
* |
|
||||||
* @param {object} opts - opts for the middleware contains the origin for the middleware |
|
||||||
*/ |
|
||||||
createMiddleware ({ senderUrl, extensionId, getSiteMetadata }) { |
|
||||||
return createAsyncMiddleware(async (req, res, next) => { |
|
||||||
// only handle requestAccounts
|
|
||||||
if (req.method !== 'eth_requestAccounts') return next() |
|
||||||
// if already approved or privacy mode disabled, return early
|
|
||||||
const isUnlocked = this.keyringController.memStore.getState().isUnlocked |
|
||||||
const origin = senderUrl.hostname |
|
||||||
if (this.shouldExposeAccounts(origin) && isUnlocked) { |
|
||||||
res.result = [this.preferencesController.getSelectedAddress()] |
|
||||||
return |
|
||||||
} |
|
||||||
// register the provider request
|
|
||||||
const metadata = { hostname: senderUrl.hostname, origin } |
|
||||||
if (extensionId) { |
|
||||||
metadata.extensionId = extensionId |
|
||||||
} else { |
|
||||||
const siteMetadata = await getSiteMetadata(origin) |
|
||||||
Object.assign(metadata, { siteTitle: siteMetadata.name, siteImage: siteMetadata.icon}) |
|
||||||
} |
|
||||||
this._handleProviderRequest(metadata) |
|
||||||
// wait for resolution of request
|
|
||||||
const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) |
|
||||||
if (approved) { |
|
||||||
res.result = [this.preferencesController.getSelectedAddress()] |
|
||||||
} else { |
|
||||||
throw rpcErrors.eth.userRejectedRequest('User denied account authorization') |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @typedef {Object} SiteMetadata |
|
||||||
* @param {string} hostname - The hostname of the site |
|
||||||
* @param {string} origin - The origin of the site |
|
||||||
* @param {string} [siteTitle] - The title of the site |
|
||||||
* @param {string} [siteImage] - The icon for the site |
|
||||||
* @param {string} [extensionId] - The extension ID of the extension |
|
||||||
*/ |
|
||||||
/** |
|
||||||
* Called when a tab requests access to a full Ethereum provider API |
|
||||||
* |
|
||||||
* @param {SiteMetadata} siteMetadata - The metadata for the site requesting full provider access |
|
||||||
*/ |
|
||||||
_handleProviderRequest (siteMetadata) { |
|
||||||
const { providerRequests } = this.memStore.getState() |
|
||||||
const origin = siteMetadata.origin |
|
||||||
this.memStore.updateState({ |
|
||||||
providerRequests: [ |
|
||||||
...providerRequests, |
|
||||||
siteMetadata, |
|
||||||
], |
|
||||||
}) |
|
||||||
const isUnlocked = this.keyringController.memStore.getState().isUnlocked |
|
||||||
const { approvedOrigins } = this.store.getState() |
|
||||||
const originAlreadyHandled = approvedOrigins[origin] |
|
||||||
if (originAlreadyHandled && isUnlocked) { |
|
||||||
return |
|
||||||
} |
|
||||||
this.openPopup && this.openPopup() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Called when a user approves access to a full Ethereum provider API |
|
||||||
* |
|
||||||
* @param {string} origin - origin of the domain that had provider access approved |
|
||||||
*/ |
|
||||||
approveProviderRequestByOrigin (origin) { |
|
||||||
if (this.closePopup) { |
|
||||||
this.closePopup() |
|
||||||
} |
|
||||||
|
|
||||||
const { approvedOrigins } = this.store.getState() |
|
||||||
const { providerRequests } = this.memStore.getState() |
|
||||||
const providerRequest = providerRequests.find((request) => request.origin === origin) |
|
||||||
const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) |
|
||||||
this.store.updateState({ |
|
||||||
approvedOrigins: { |
|
||||||
...approvedOrigins, |
|
||||||
[origin]: { |
|
||||||
siteTitle: providerRequest ? providerRequest.siteTitle : null, |
|
||||||
siteImage: providerRequest ? providerRequest.siteImage : null, |
|
||||||
hostname: providerRequest ? providerRequest.hostname : null, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
this.memStore.updateState({ providerRequests: remainingProviderRequests }) |
|
||||||
this.emit(`resolvedRequest:${origin}`, { approved: true }) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Called when a tab rejects access to a full Ethereum provider API |
|
||||||
* |
|
||||||
* @param {string} origin - origin of the domain that had provider access approved |
|
||||||
*/ |
|
||||||
rejectProviderRequestByOrigin (origin) { |
|
||||||
if (this.closePopup) { |
|
||||||
this.closePopup() |
|
||||||
} |
|
||||||
|
|
||||||
const { approvedOrigins } = this.store.getState() |
|
||||||
const { providerRequests } = this.memStore.getState() |
|
||||||
const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) |
|
||||||
|
|
||||||
// We're cloning and deleting keys here because we don't want to keep unneeded keys
|
|
||||||
const _approvedOrigins = Object.assign({}, approvedOrigins) |
|
||||||
delete _approvedOrigins[origin] |
|
||||||
|
|
||||||
this.store.putState({ approvedOrigins: _approvedOrigins }) |
|
||||||
this.memStore.putState({ providerRequests: remainingProviderRequests }) |
|
||||||
this.emit(`resolvedRequest:${origin}`, { approved: false }) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Clears any approvals for user-approved origins |
|
||||||
*/ |
|
||||||
clearApprovedOrigins () { |
|
||||||
this.store.updateState({ |
|
||||||
approvedOrigins: {}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Determines if a given origin should have accounts exposed |
|
||||||
* |
|
||||||
* @param {string} origin - Domain origin to check for approval status |
|
||||||
* @returns {boolean} - True if the origin has been approved |
|
||||||
*/ |
|
||||||
shouldExposeAccounts (origin) { |
|
||||||
return Boolean(this.store.getState().approvedOrigins[origin]) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns a merged state representation |
|
||||||
* @return {object} |
|
||||||
* @private |
|
||||||
*/ |
|
||||||
_getMergedState () { |
|
||||||
return Object.assign({}, this.memStore.getState(), this.store.getState()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = ProviderApprovalController |
|
@ -1,73 +0,0 @@ |
|||||||
class StandardProvider { |
|
||||||
_isConnected |
|
||||||
_provider |
|
||||||
|
|
||||||
constructor (provider) { |
|
||||||
this._provider = provider |
|
||||||
this._subscribe() |
|
||||||
// indicate that we've connected, mostly just for standard compliance
|
|
||||||
setTimeout(() => { |
|
||||||
this._onConnect() |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
_onClose () { |
|
||||||
if (this._isConnected === undefined || this._isConnected) { |
|
||||||
this._provider.emit('close', { |
|
||||||
code: 1011, |
|
||||||
reason: 'Network connection error', |
|
||||||
}) |
|
||||||
} |
|
||||||
this._isConnected = false |
|
||||||
} |
|
||||||
|
|
||||||
_onConnect () { |
|
||||||
!this._isConnected && this._provider.emit('connect') |
|
||||||
this._isConnected = true |
|
||||||
} |
|
||||||
|
|
||||||
_subscribe () { |
|
||||||
this._provider.on('data', (error, { method, params }) => { |
|
||||||
if (!error && method === 'eth_subscription') { |
|
||||||
this._provider.emit('notification', params.result) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Initiate an RPC method call |
|
||||||
* |
|
||||||
* @param {string} method - RPC method name to call |
|
||||||
* @param {string[]} params - Array of RPC method parameters |
|
||||||
* @returns {Promise<*>} Promise resolving to the result if successful |
|
||||||
*/ |
|
||||||
send (method, params = []) { |
|
||||||
return new Promise((resolve, reject) => { |
|
||||||
try { |
|
||||||
this._provider.sendAsync({ id: 1, jsonrpc: '2.0', method, params }, (error, response) => { |
|
||||||
error = error || response.error |
|
||||||
error ? reject(error) : resolve(response) |
|
||||||
}) |
|
||||||
} catch (error) { |
|
||||||
reject(error) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Converts a legacy provider into an EIP-1193-compliant standard provider |
|
||||||
* @param {Object} provider - Legacy provider to convert |
|
||||||
* @returns {Object} Standard provider |
|
||||||
*/ |
|
||||||
export default function createStandardProvider (provider) { |
|
||||||
const standardProvider = new StandardProvider(provider) |
|
||||||
const sendLegacy = provider.send |
|
||||||
provider.send = (methodOrPayload, callbackOrArgs) => { |
|
||||||
if (typeof methodOrPayload === 'string' && !callbackOrArgs || Array.isArray(callbackOrArgs)) { |
|
||||||
return standardProvider.send(methodOrPayload, callbackOrArgs) |
|
||||||
} |
|
||||||
return sendLegacy.call(provider, methodOrPayload, callbackOrArgs) |
|
||||||
} |
|
||||||
return provider |
|
||||||
} |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue