aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/controllers/provider-approval.js
blob: 3beda6d535b0c1f6e87d4d2095491c5821271524 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
const ObservableStore = require('obs-store')
const SafeEventEmitter = require('safe-event-emitter')
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')

/**
 * 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 ({ origin, 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
      if (this.shouldExposeAccounts(origin) && isUnlocked) {
        res.result = [this.preferencesController.getSelectedAddress()]
        return
      }
      // register the provider request
      const metadata = await getSiteMetadata(origin)
      this._handleProviderRequest(origin, metadata.name, metadata.icon)
      // 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 new Error('User denied account authorization')
      }
    })
  }

  /**
   * Called when a tab requests access to a full Ethereum provider API
   *
   * @param {string} origin - Origin of the window requesting full provider access
   * @param {string} siteTitle - The title of the document requesting full provider access
   * @param {string} siteImage - The icon of the window requesting full provider access
   */
  _handleProviderRequest (origin, siteTitle, siteImage) {
    const { providerRequests } = this.memStore.getState()
    this.memStore.updateState({
      providerRequests: [
        ...providerRequests,
        { origin, siteTitle, siteImage },
      ],
    })
    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,
        },
      },
    })
    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