Merge pull request #7599 from MetaMask/Version-v7.7.0
Version v7.7.0 RCfeature/default_network_editable
commit
1110287fe1
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