@portal-hq/web 0.0.1-beta-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,314 @@
1
+ import { ProviderRpcError, RpcErrorCodes } from '@portal-hq/utils'
2
+
3
+ import Portal from '../index'
4
+
5
+ import type { EventHandler, ProviderOptions, RegisteredEventHandler, RequestArguments } from '../../types'
6
+
7
+ const passiveSignerMethods = [
8
+ 'eth_accounts',
9
+ 'eth_chainId',
10
+ 'eth_requestAccounts',
11
+ ]
12
+
13
+ const signerMethods = [
14
+ 'eth_accounts',
15
+ 'eth_chainId',
16
+ 'eth_requestAccounts',
17
+ 'eth_sendTransaction',
18
+ 'eth_sign',
19
+ 'eth_signTransaction',
20
+ 'eth_signTypedData_v3',
21
+ 'eth_signTypedData_v4',
22
+ 'personal_sign',
23
+ ]
24
+
25
+ class Provider {
26
+ public events: Record<string, RegisteredEventHandler[]>
27
+
28
+ private autoApprove: boolean
29
+ private portal: Portal
30
+
31
+ constructor({
32
+ autoApprove = false,
33
+ portal,
34
+ }: ProviderOptions) {
35
+ this.autoApprove = autoApprove
36
+ this.events = {}
37
+ this.portal = portal
38
+ }
39
+
40
+ /**
41
+ * Invokes all registered event handlers with the data provided
42
+ * - If any `once` handlers exist, they are removed after all handlers are invoked
43
+ *
44
+ * @param event The name of the event to be handled
45
+ * @param data The data to be passed to registered event handlers
46
+ * @returns BaseProvider
47
+ */
48
+ public emit(event: string, data: any): Provider {
49
+ // Grab the registered event handlers if any are available
50
+ const handlers = this.events[event] || []
51
+
52
+ // Execute every event handler
53
+ for (const registeredEventHandler of handlers) {
54
+ registeredEventHandler.handler(data)
55
+ }
56
+
57
+ // Remove any registered event handlers with the `once` flag
58
+ this.events[event] = handlers.filter((handler) => !handler.once)
59
+
60
+ return this
61
+ }
62
+
63
+ /**
64
+ * Registers an event handler for the provided event
65
+ *
66
+ * @param event The event name to add a handler to
67
+ * @param callback The callback to be invoked when the event is emitted
68
+ * @returns BaseProvider
69
+ */
70
+ public on(event: string, callback: EventHandler): Provider {
71
+ // If no handlers are registered for this event, create an entry for the event
72
+ if (!this.events[event]) {
73
+ this.events[event] = []
74
+ }
75
+
76
+ // Register event handler with the rudimentary event bus
77
+ if (typeof callback !== 'undefined') {
78
+ this.events[event].push({
79
+ handler: callback,
80
+ once: false,
81
+ })
82
+ }
83
+
84
+ return this
85
+ }
86
+
87
+ public removeEventListener(
88
+ event: string,
89
+ listenerToRemove?: EventHandler,
90
+ ): void {
91
+ if (!this.events[event]) {
92
+ return
93
+ }
94
+
95
+ if (!listenerToRemove) {
96
+ this.events[event] = []
97
+ } else {
98
+ const filterEventHandlers = (
99
+ registeredEventHandler: RegisteredEventHandler,
100
+ ) => {
101
+ return registeredEventHandler.handler !== listenerToRemove
102
+ }
103
+
104
+ this.events[event] = this.events[event].filter(filterEventHandlers)
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Handles request routing in compliance with the EIP-1193 Ethereum Javascript Provider API
110
+ * - See here for more info: https://eips.ethereum.org/EIPS/eip-1193
111
+ *
112
+ * @param args The arguments of the request being made
113
+ * @returns Promise<any>
114
+ */
115
+ public async request({ method, params }: RequestArguments): Promise<any> {
116
+ if (method === 'eth_chainId') {
117
+ return this.portal.chainId
118
+ }
119
+
120
+ const isSignerMethod = signerMethods.includes(method)
121
+
122
+ let result: any
123
+ if (!isSignerMethod && !method.startsWith('wallet_')) {
124
+ // Send to Gateway for RPC calls
125
+ const response = await this.handleGatewayRequest({ method, params })
126
+
127
+ this.emit('portal_signatureReceived', {
128
+ method,
129
+ params,
130
+ signature: response,
131
+ })
132
+
133
+ if (response.error) {
134
+ throw new ProviderRpcError(response.error)
135
+ }
136
+
137
+ result = response.result
138
+ } else if (isSignerMethod) {
139
+ // Handle signing
140
+ const transactionHash = await this.handleSigningRequest({
141
+ method,
142
+ params,
143
+ })
144
+
145
+ if (transactionHash) {
146
+ this.emit('portal_signatureReceived', {
147
+ method,
148
+ params,
149
+ signature: transactionHash,
150
+ })
151
+
152
+ result = transactionHash
153
+ }
154
+ } else {
155
+ // Unsupported method
156
+ throw new ProviderRpcError({
157
+ code: RpcErrorCodes.UnsupportedMethod,
158
+ data: {
159
+ method,
160
+ params,
161
+ },
162
+ })
163
+ }
164
+
165
+ return result
166
+ }
167
+
168
+ /************************
169
+ * Private Methods
170
+ ************************/
171
+
172
+ /**
173
+ * Kicks off the approval flow for a given request
174
+ *
175
+ * @param args The arguments of the request being made
176
+ */
177
+ protected async getApproval({
178
+ method,
179
+ params,
180
+ }: RequestArguments): Promise<boolean> {
181
+ // If autoApprove is enabled, just resolve to true
182
+ if (this.autoApprove) {
183
+ return true
184
+ }
185
+
186
+ const signingHandlers = this.events['portal_signingRequested']
187
+
188
+ if (!signingHandlers || signingHandlers.length === 0) {
189
+ throw new Error(
190
+ `[PortalProvider] Auto-approve is disabled. Cannot perform signing requests without an event handler for the 'portal_signingRequested' event.`,
191
+ )
192
+ }
193
+
194
+ return new Promise((resolve) => {
195
+ // Remove already used listeners
196
+ this.removeEventListener('portal_signingApproved')
197
+ this.removeEventListener('portal_signingRejected')
198
+
199
+ const handleApproval = ({ method: approvedMethod, params: approvedParams }: RequestArguments) => {
200
+ // Remove already used listeners
201
+ this.removeEventListener('portal_signingApproved')
202
+ this.removeEventListener('portal_signingRejected')
203
+
204
+ // First verify that this is the same signing request
205
+ if (
206
+ method === approvedMethod &&
207
+ JSON.stringify(params) === JSON.stringify(approvedParams)
208
+ ) {
209
+ resolve(true)
210
+ }
211
+ }
212
+
213
+ const handleRejection = ({ method: rejectedMethod, params: rejectedParams }: RequestArguments) => {
214
+ // Remove already used listeners
215
+ this.removeEventListener('portal_signingApproved')
216
+ this.removeEventListener('portal_signingRejected')
217
+
218
+ // First verify that this is the same signing request
219
+ if (
220
+ method === rejectedMethod &&
221
+ JSON.stringify(params) === JSON.stringify(rejectedParams)
222
+ ) {
223
+ resolve(false)
224
+ }
225
+ }
226
+
227
+ // If the signing has been approved, resolve to true
228
+ this.on('portal_signingApproved', handleApproval)
229
+
230
+ // If the signing request has been rejected, resolve to false
231
+ this.on('portal_signingRejected', handleRejection)
232
+
233
+ // Tell any listening clients that signing has been requested
234
+ this.emit('portal_signingRequested', {
235
+ method,
236
+ params,
237
+ })
238
+ })
239
+ }
240
+
241
+ /**
242
+ * Sends the provided request payload along to the RPC HttpRequester
243
+ *
244
+ * @param args The arguments of the request being made
245
+ * @returns Promise<any>
246
+ */
247
+ private async handleGatewayRequest({
248
+ method,
249
+ params,
250
+ }: RequestArguments): Promise<any> {
251
+ // Pass request off to the gateway
252
+ const result = await fetch(this.portal.getRpcUrl(), {
253
+ body: JSON.stringify({
254
+ jsonrpc: '2.0',
255
+ id: this.portal.chainId,
256
+ method,
257
+ params,
258
+ }),
259
+ method: 'POST',
260
+ })
261
+
262
+ return result.json()
263
+ }
264
+
265
+ /**
266
+ * Sends the provided request payload along to the Signer
267
+ *
268
+ * @param args The arguments of the request being made
269
+ * @returns Promise<any>
270
+ */
271
+ private async handleSigningRequest({
272
+ method,
273
+ params,
274
+ }: RequestArguments): Promise<any> {
275
+ const isApproved = passiveSignerMethods.includes(method)
276
+ ? true
277
+ : await this.getApproval({ method, params })
278
+
279
+ if (!isApproved) {
280
+ console.info(
281
+ `[PortalProvider] Request for signing method '${method}' could not be completed because it was not approved by the user.`,
282
+ )
283
+ return
284
+ }
285
+
286
+ switch (method) {
287
+ case 'eth_chainId':
288
+ return `0x${this.portal.chainId.toString(16)}`
289
+ case 'eth_accounts':
290
+ case 'eth_requestAccounts':
291
+ return [this.portal.address]
292
+ case 'eth_sendTransaction':
293
+ case 'eth_sign':
294
+ case 'eth_signTransaction':
295
+ case 'eth_signTypedData_v3':
296
+ case 'eth_signTypedData_v4':
297
+ case 'personal_sign':
298
+ const result = await this.portal.mpc.sign({
299
+ host: this.portal.mpcHost,
300
+ method,
301
+ mpcVersion: this.portal.mpcVersion,
302
+ params,
303
+ })
304
+
305
+ return result
306
+ default:
307
+ throw new Error(
308
+ '[PortalProvider] Method "' + method + '" not supported',
309
+ )
310
+ }
311
+ }
312
+ }
313
+
314
+ export default Provider
@@ -0,0 +1,29 @@
1
+ import { Storage } from '@portal-hq/utils'
2
+
3
+ class DefaultBackupStorage implements Storage {
4
+ public delete(): Promise<boolean> {
5
+ throw new Error(
6
+ '[Portal] Storage method delete cannot be called directly. Please extend Storage.',
7
+ )
8
+ }
9
+
10
+ public read(): Promise<string> {
11
+ throw new Error(
12
+ '[Portal] Storage method read cannot be called directly. Please extend Storage.',
13
+ )
14
+ }
15
+
16
+ public write(privateKey: string): Promise<string> {
17
+ throw new Error(
18
+ `[Portal] Storage method write(${privateKey}) cannot be called directly. Please extend Storage.`,
19
+ )
20
+ }
21
+
22
+ public validateOperations(): Promise<boolean> {
23
+ throw new Error(
24
+ '[Portal] Storage method validateOperations cannot be called directly. Please extend Storage.',
25
+ )
26
+ }
27
+ }
28
+
29
+ export default DefaultBackupStorage
package/types.d.ts ADDED
@@ -0,0 +1,341 @@
1
+ import {
2
+ type DkgDataV1 as DkgData,
3
+ Storage,
4
+ PortalError,
5
+ } from '@portal-hq/utils'
6
+ import { MpcErrorCodes } from './src/mpc/errors'
7
+ import { BackupMethods } from './src/mpc'
8
+ import Provider from './src/provider'
9
+ import Portal from './src/index'
10
+
11
+ export type GatewayLike = GatewayConfig | string
12
+ export type EventHandler = (event: Event<any>) => void | Promise<void>
13
+ export type MessageData =
14
+ | BackupArgs
15
+ | DecryptArgs
16
+ | EncryptArgs
17
+ | GenerateArgs
18
+ | RecoverArgs
19
+ | SignArgs
20
+
21
+ export type EthereumTransaction = EIP1559Transaction | LegacyTransaction
22
+
23
+ // Interfaces
24
+
25
+ export interface AddressResult {
26
+ data: string
27
+ type: string
28
+ }
29
+
30
+ export interface BackupArgs extends MpcOperationArgs {
31
+ }
32
+
33
+ export interface BackupResult {}
34
+
35
+ export interface BackupOptions {
36
+ default?: Storage
37
+ gdrive?: Storage
38
+ icloud?: Storage
39
+ }
40
+ export interface Bk {
41
+ X: string
42
+ Rank: number
43
+ }
44
+
45
+ export interface Address {
46
+ id: string
47
+ network: Network
48
+ value: string
49
+ }
50
+
51
+ export interface DappOnNetwork {
52
+ clientUrl?: string
53
+ dapp: Dapp
54
+ id: string
55
+ network: Network
56
+ }
57
+
58
+ export interface Dapp {
59
+ id: string
60
+ name: string
61
+
62
+ addresses: Address[]
63
+ dappOnNetworks: DappOnNetwork[]
64
+ image: DappImage
65
+ }
66
+
67
+ export interface DappImage {
68
+ id: string
69
+ filename: string
70
+ data: string
71
+ }
72
+
73
+ export interface DecryptArgs {
74
+ cipherText: string
75
+ privateKey: string
76
+ }
77
+
78
+ export interface DkgResult {
79
+ share: string
80
+ pubkey: Pubkey
81
+ bks: Bk[]
82
+ }
83
+
84
+ export interface EIP1559Transaction {
85
+ from: string
86
+ to: string
87
+
88
+ // Optional
89
+ data?: string
90
+ gasLimit?: string
91
+ maxFeePerGas?: string
92
+ maxPriorityFeePerGas?: string
93
+ nonce?: string
94
+ value?: string
95
+ }
96
+
97
+ export interface EncryptArgs {
98
+ dkgData: string
99
+ }
100
+
101
+ export interface EncryptedData {
102
+ cipherText: string
103
+ privateKey: string
104
+ }
105
+
106
+ export interface EncryptResult {
107
+ data: EncryptData
108
+ error: PortalError
109
+ }
110
+
111
+ export interface DecryptData {
112
+ plaintext: string
113
+ }
114
+
115
+ export interface DecryptResult {
116
+ data: DecryptData
117
+ error: PortalError
118
+ }
119
+
120
+ export interface GatewayConfig {
121
+ [key: number]: string
122
+ }
123
+
124
+ export interface GenerateArgs extends MpcOperationArgs {}
125
+
126
+ export interface GenerateResult {
127
+ data: {
128
+ address: string
129
+ dkgResult: DkgData
130
+ }
131
+ error: PortalError
132
+ }
133
+
134
+ export interface GenerateResponse {
135
+ address: string
136
+ cipherText?: string
137
+ share?: string
138
+ }
139
+
140
+ export interface IframeConfigurationOptions {
141
+ apiHost: string
142
+ apiKey: string
143
+ autoApprove: boolean
144
+ chainId: number
145
+ mpcHost: string
146
+ rpcUrl: string
147
+ }
148
+
149
+ export interface LegacyTransaction {
150
+ from: string
151
+ to: string
152
+
153
+ // Optional
154
+ data?: string
155
+ gasLimit?: string
156
+ gasPrice?: string
157
+ nonce?: string
158
+ value?: string
159
+ }
160
+
161
+ export interface MpcErrorData {
162
+ code: MpcErrorCodes
163
+ message: (str?: string) => string
164
+ }
165
+
166
+ export interface MpcOperationArgs {
167
+ host: string
168
+ mpcVersion: string
169
+ }
170
+
171
+ export interface MpcOptions {
172
+ apiHost: string
173
+ apiKey: string
174
+ autoApprove: boolean
175
+ chainId: number
176
+ mpcHost: string
177
+ rpcUrl: string
178
+ webHost: string
179
+ portal: Portal
180
+ }
181
+
182
+ export interface PortalApiOptions {
183
+ apiKey: string
184
+ chainId: number
185
+ host: string
186
+ portal: Portal
187
+ }
188
+
189
+ export interface PortalMpcOptions {
190
+ // Required options
191
+ chainId: number
192
+ keychain: KeychainAdapter
193
+ portal: Portal
194
+ rpc: string
195
+ storage: BackupOptions
196
+ token: string
197
+
198
+ // Optional options
199
+ mpcHost?: string
200
+ mpcVersion?: string
201
+ webHost?: string
202
+ }
203
+
204
+ export interface PortalOptions {
205
+ // Required options
206
+ apiKey: string
207
+ gatewayConfig: GatewayLike
208
+
209
+ // Optional options
210
+ apiHost?: string
211
+ autoApprove?: boolean
212
+ backup?: BackupOptions
213
+ chainId?: number
214
+ isSimulator?: boolean
215
+ keychain?: KeychainAdapter
216
+ mpcHost?: string
217
+ mpcVersion?: string
218
+ webHost?: string
219
+ }
220
+
221
+ export interface ProviderOptions {
222
+ // Required options
223
+ portal: Portal
224
+
225
+ // Optional options
226
+ autoApprove?: boolean
227
+ mpcHost?: string
228
+ mpcVersion?: string
229
+ }
230
+
231
+ export interface Pubkey {
232
+ X: string
233
+ Y: string
234
+ }
235
+
236
+ export interface QuoteArgs {
237
+ affiliateAddress?: string
238
+ buyAmount?: number
239
+ buyToken: string
240
+ buyTokenPercentageFee?: number
241
+ enableSlippageProtection?: boolean
242
+ excludedSources?: string
243
+ feeRecipient?: string
244
+ gasPrice?: number
245
+ includedSources?: string
246
+ intentOnFilling?: boolean
247
+ priceImpactProtectionPercentage?: number
248
+ sellAmount?: number
249
+ sellToken: string
250
+ skipValidation?: boolean
251
+ slippagePercentage?: number
252
+ takerAddress?: string
253
+ }
254
+
255
+ export interface QuoteResponse {
256
+ cost: string
257
+ transaction: Eip1559 | LegacyTx
258
+ }
259
+
260
+ export interface RegisteredEventHandler {
261
+ handler: EventHandler
262
+ once?: boolean
263
+ }
264
+
265
+ export interface RequestArguments {
266
+ method: string
267
+ params?: unknown[] | SigningRequestParams
268
+ }
269
+
270
+ export interface RecoverArgs extends MpcOperationArgs {
271
+ }
272
+
273
+ export interface RotateResult {
274
+ data: RotateData
275
+ error: PortalError
276
+ }
277
+
278
+ export interface RotateData {
279
+ address: string
280
+ dkgResult: DkgData
281
+ }
282
+
283
+ export interface SignArgs extends MpcOperationArgs {
284
+ method: string
285
+ params: string
286
+ }
287
+
288
+ export interface SignData {
289
+ R: string
290
+ S: string
291
+ }
292
+
293
+ export interface SignResult {
294
+ data: string
295
+ error: PortalError
296
+ }
297
+
298
+ export interface SignerOptions {
299
+ portal: Portal
300
+ }
301
+
302
+ export interface SwitchEthereumChainParameter {
303
+ chainId: number
304
+ }
305
+
306
+ export interface TypedData {
307
+ types: Type[]
308
+ primaryType: string
309
+ domain: string
310
+ message: string
311
+ }
312
+
313
+ export interface Type {
314
+ name: string
315
+ type: string
316
+ }
317
+
318
+ export interface WorkerMessage {
319
+ type: string
320
+ data:
321
+ | BackupArgs
322
+ | DecryptArgs
323
+ | EncryptArgs
324
+ | GenerateArgs
325
+ | RecoverArgs
326
+ | SignArgs
327
+ }
328
+
329
+ export interface WorkerResult {
330
+ type: string
331
+ data:
332
+ | boolean
333
+ | string
334
+ | BackupResult
335
+ | DecryptResult
336
+ | EncryptResult
337
+ | GenerateResult
338
+ | ReadyResult
339
+ | RecoverResult
340
+ | SignResult
341
+ }