@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,443 @@
1
+ import { PortalError, PortalMpcError } from '@portal-hq/utils'
2
+
3
+ import Portal from '../index'
4
+ import {
5
+ BackupArgs,
6
+ DecryptArgs,
7
+ DecryptResult,
8
+ EncryptArgs,
9
+ EncryptResult,
10
+ GenerateArgs,
11
+ GenerateResult,
12
+ IframeConfigurationOptions,
13
+ MpcOptions,
14
+ RecoverArgs,
15
+ SignArgs,
16
+ WorkerResult,
17
+ } from '../../types'
18
+
19
+ const WEB_SDK_VERSION = '0.0.1-beta-1'
20
+
21
+ class Mpc {
22
+ private apiHost: string
23
+ private apiKey: string
24
+ private autoApprove: boolean
25
+ private chainId: number
26
+ private iframe?: HTMLIFrameElement
27
+ private mpcHost: string
28
+ private portal: Portal
29
+ private rpcUrl: string
30
+ private webHost: string
31
+
32
+ constructor({
33
+ apiKey,
34
+ chainId,
35
+ portal,
36
+ rpcUrl,
37
+
38
+ // Optional
39
+ apiHost = 'api.portalhq.io',
40
+ autoApprove = false,
41
+ mpcHost = 'mpc.portalhq.io',
42
+ webHost = 'web.portalhq.io',
43
+ }: MpcOptions) {
44
+ this.apiHost = apiHost
45
+ this.apiKey = apiKey
46
+ this.autoApprove = autoApprove
47
+ this.chainId = chainId
48
+ this.mpcHost = mpcHost
49
+ this.portal = portal
50
+ this.rpcUrl = rpcUrl
51
+ this.webHost = webHost
52
+
53
+ // Handle scoping of certain functions
54
+ this.configureIframe = this.configureIframe.bind(this)
55
+
56
+ // Create the iFrame for MPC operations
57
+ this.appendIframe()
58
+ }
59
+
60
+ // In Progress
61
+ public async backup(data: BackupArgs): Promise<GenerateResult> {
62
+ const message = {
63
+ type: 'portal:wasm:backup',
64
+ data,
65
+ }
66
+
67
+ return new Promise((resolve, reject) => {
68
+ // Create a function to be bound when backup is triggered
69
+ const handleBackup = (event: MessageEvent<GenerateResult>) => {
70
+ const { type, data: result } = event
71
+
72
+ // Check that the message is the one we're looking for
73
+ if (type === 'portal:wasm:backupResult') {
74
+ // Check if the result is an error
75
+ if (result.error && result.error.code > 0) {
76
+ // Remove the event listener
77
+ window.removeEventListener('message', handleBackup)
78
+
79
+ // Reject the promise with the error
80
+ return reject(new PortalMpcError(result.error))
81
+ }
82
+
83
+ // Remove the event listener
84
+ window.removeEventListener('message', handleBackup)
85
+
86
+ // Resolve the promise with the result
87
+ resolve(result)
88
+ }
89
+ }
90
+
91
+ // Bind the function to the message event
92
+ window.addEventListener('message', handleBackup)
93
+
94
+ // Send the request to the iframe
95
+ this.postMessage(message)
96
+ })
97
+ }
98
+
99
+ // In Progress
100
+ public async decrypt(data: DecryptArgs): Promise<DecryptResult> {
101
+ const message = {
102
+ type: 'portal:wasm:decrypt',
103
+ data,
104
+ }
105
+
106
+ return new Promise((resolve, reject) => {
107
+ const handleDecrypt = (event: MessageEvent<DecryptResult>) => {
108
+ const { type, data: result } = event
109
+
110
+ // Check that the message is the one we're looking for
111
+ if (type === 'portal:wasm:decryptResult') {
112
+ // Check if the result is an error
113
+ if (result.error && result.error.code > 0) {
114
+ // Remove the event listener
115
+ window.removeEventListener('message', handleDecrypt)
116
+
117
+ // Reject the promise with the error
118
+ return reject(new PortalMpcError(result.error))
119
+ }
120
+
121
+ // Remove the event listener
122
+ window.removeEventListener('message', handleDecrypt)
123
+
124
+ // Resolve the promise with the result
125
+ resolve(result)
126
+ }
127
+ }
128
+
129
+ // Bind the function to the message event
130
+ window.addEventListener('message', handleDecrypt)
131
+
132
+ // Send the request to the iframe
133
+ this.postMessage(message)
134
+ })
135
+ }
136
+
137
+ // In Progress
138
+ public async encrypt(data: EncryptArgs): Promise<EncryptResult> {
139
+ const message = {
140
+ type: 'portal:wasm:encrypt',
141
+ data,
142
+ }
143
+
144
+ return new Promise((resolve, reject) => {
145
+ const handleEncrypt = (event: MessageEvent<EncryptResult>) => {
146
+ const { type, data: result } = event
147
+
148
+ // Check that the message is the one we're looking for
149
+ if (type === 'portal:wasm:encryptResult') {
150
+ // Check if the result is an error
151
+ if (result.error && result.error.code > 0) {
152
+ // Remove the event listener
153
+ window.removeEventListener('message', handleEncrypt)
154
+
155
+ // Reject the promise with the error
156
+ return reject(new PortalMpcError(result.error))
157
+ }
158
+
159
+ // Remove the event listener
160
+ window.removeEventListener('message', handleEncrypt)
161
+
162
+ // Resolve the promise with the result
163
+ resolve(result)
164
+ }
165
+ }
166
+
167
+ // Bind the function to the message event
168
+ window.addEventListener('message', handleEncrypt)
169
+
170
+ // Send the request to the iframe
171
+ this.postMessage(message)
172
+ })
173
+ }
174
+
175
+ public async generate(data: GenerateArgs): Promise<string> {
176
+ const message = {
177
+ type: 'portal:wasm:generate',
178
+ data,
179
+ }
180
+
181
+ return new Promise((resolve, reject) => {
182
+ // Create a function to be bound when generate is triggered
183
+ const handleGenerate = (event: MessageEvent<WorkerResult>) => {
184
+ console.log(`[MPC] Temporary listener received event: `, event)
185
+
186
+ const { type, data: result } = event.data
187
+
188
+ // Handle errors
189
+ if (type === 'portal:wasm:generateError') {
190
+ // Remove the event listener
191
+ window.removeEventListener('message', handleGenerate)
192
+
193
+ // Reject the promise with the error
194
+ return reject(new PortalMpcError(result))
195
+ } else if (type === 'portal:wasm:generateResult') {
196
+ // Remove the event listener
197
+ window.removeEventListener('message', handleGenerate)
198
+
199
+ // Resolve the promise with the result
200
+ resolve(result)
201
+ }
202
+ }
203
+
204
+ // Bind the function to the message event
205
+ window.addEventListener('message', handleGenerate)
206
+
207
+ // Send the request to the iframe
208
+ this.postMessage(message)
209
+ })
210
+ }
211
+
212
+ public async getAddress(): Promise<string> {
213
+ const message = {
214
+ type: 'portal:address',
215
+ data: {},
216
+ }
217
+
218
+ return new Promise((resolve) => {
219
+ // Create a function to be bound when getAddress is triggered
220
+ const handleGetAddress = (event: MessageEvent<WorkerResult>) => {
221
+ const { type, data: result } = event.data
222
+
223
+ // Check that the message is the one we're looking for
224
+ if (type === 'portal:addressResult') {
225
+ // Remove the event listener
226
+ window.removeEventListener('message', handleGetAddress)
227
+
228
+ // Resolve the promise with the result
229
+ resolve(result)
230
+ }
231
+ }
232
+
233
+ // Bind the function to the message event
234
+ window.addEventListener('message', handleGetAddress)
235
+
236
+ // Send the request to the iframe
237
+ this.postMessage(message)
238
+ })
239
+ }
240
+
241
+ // In Progress
242
+ public async recover(data: RecoverArgs): Promise<string> {
243
+ await this.recoverBackup(data)
244
+ // TODO: Write the DKG Result to local storage
245
+ const cipherText = await this.recoverSigning(data)
246
+
247
+ return JSON.stringify(cipherText)
248
+ }
249
+
250
+ // In Progress
251
+ private async recoverBackup(data: RecoverArgs): Promise<GenerateResult> {
252
+ const message = {
253
+ type: 'portal:wasm:recoverBackup',
254
+ data,
255
+ }
256
+
257
+ return new Promise((resolve, reject) => {
258
+ // Create a function to be bound when recoverSigning is triggered
259
+ const handleRecovery = (event: MessageEvent<GenerateResult>) => {
260
+ const { type, data: result } = event
261
+
262
+ // Check that the message is the one we're looking for
263
+ if (type === 'portal:wasm:recoverBackupResult') {
264
+ // Check if the result is an error
265
+ if (result.error && result.error.code > 0) {
266
+ // Remove the event listener
267
+ window.removeEventListener('message', handleRecovery)
268
+
269
+ // Reject the promise with the error
270
+ return reject(new PortalMpcError(result.error))
271
+ }
272
+
273
+ // Remove the event listener
274
+ window.removeEventListener('message', handleRecovery)
275
+
276
+ // Resolve the promise with the result
277
+ resolve(result)
278
+ }
279
+ }
280
+
281
+ // Bind the function to the message event
282
+ window.addEventListener('message', handleRecovery)
283
+
284
+ // Send the request to the iframe
285
+ this.postMessage(message)
286
+ })
287
+ }
288
+
289
+ // In Progress
290
+ private async recoverSigning(data: RecoverArgs): Promise<GenerateResult> {
291
+ const message = {
292
+ type: 'portal:wasm:recoverSigning',
293
+ data,
294
+ }
295
+
296
+ return new Promise((resolve, reject) => {
297
+ // Create a function to be bound when recoverSigning is triggered
298
+ const handleRecovery = (event: MessageEvent<GenerateResult>) => {
299
+ const { type, data: result } = event
300
+
301
+ // Check that the message is the one we're looking for
302
+ if (type === 'portal:wasm:recoverSigningResult') {
303
+ // Check if the result is an error
304
+ if (result.error && result.error.code > 0) {
305
+ // Remove the event listener
306
+ window.removeEventListener('message', handleRecovery)
307
+
308
+ // Reject the promise with the error
309
+ return reject(new PortalMpcError(result.error))
310
+ }
311
+
312
+ // Remove the event listener
313
+ window.removeEventListener('message', handleRecovery)
314
+
315
+ // Resolve the promise with the result
316
+ resolve(result)
317
+ }
318
+ }
319
+
320
+ // Bind the function to the message event
321
+ window.addEventListener('message', handleRecovery)
322
+
323
+ // Send the request to the iframe
324
+ this.postMessage(message)
325
+ })
326
+ }
327
+
328
+ public async sign(data: SignArgs): Promise<string> {
329
+ const message = {
330
+ type: 'portal:wasm:sign',
331
+ data,
332
+ }
333
+
334
+ return new Promise((resolve, reject) => {
335
+ // Create a function to be bound when sign is triggered
336
+ const handleSign = (event: MessageEvent<WorkerResult>) => {
337
+ const { type, data: result } = event.data
338
+
339
+ if (type === 'portal:wasm:signError') {
340
+ // Remove the event listener
341
+ window.removeEventListener('message', handleSign)
342
+
343
+ // Reject the promise with the error
344
+ return reject(new PortalMpcError(result as PortalError))
345
+ } else if (type === 'portal:wasm:signResult') {
346
+ // Remove the event listener
347
+ window.removeEventListener('message', handleSign)
348
+
349
+ // Resolve the promise with the result
350
+ resolve(result as string)
351
+ }
352
+ }
353
+
354
+ // Bind the function to the message event
355
+ window.addEventListener('message', handleSign)
356
+
357
+ // Send the request to the iframe
358
+ this.postMessage(message)
359
+ })
360
+ }
361
+
362
+ /***************************
363
+ * Private Methods
364
+ ***************************/
365
+
366
+ /**
367
+ * Appends the iframe to the document body
368
+ */
369
+ private appendIframe() {
370
+ const source = this.webHost.startsWith('locahost:')
371
+ ? `http://${this.webHost}/${WEB_SDK_VERSION}/iframe/index.html?t=${Date.now()}`
372
+ : `https://${this.webHost}/${WEB_SDK_VERSION}/iframe/index.html?t=${Date.now()}`
373
+
374
+ const iframe = document.createElement('iframe')
375
+ iframe.height = '0'
376
+ iframe.width = '0'
377
+ iframe.src = source
378
+ iframe.addEventListener('load', this.configureIframe)
379
+
380
+ document.body.appendChild(iframe)
381
+
382
+ this.iframe = iframe
383
+
384
+ }
385
+
386
+ private configureIframe() {
387
+ const config: IframeConfigurationOptions = {
388
+ apiHost: this.apiHost,
389
+ apiKey: this.apiKey,
390
+ autoApprove: this.autoApprove,
391
+ chainId: this.chainId,
392
+ mpcHost: this.mpcHost,
393
+ rpcUrl: this.rpcUrl,
394
+ }
395
+
396
+ const message = {
397
+ type: 'portal:configure',
398
+ data: config
399
+ }
400
+
401
+ this.postMessage(message)
402
+
403
+ this.waitForReadyMessage()
404
+ }
405
+
406
+ private getOrigin(): string {
407
+ const origin = this.webHost.startsWith('localhost:')
408
+ ? `http://${this.webHost}`
409
+ : `https://${this.webHost}`
410
+
411
+ return origin
412
+ }
413
+
414
+ private postMessage(event: { type: string, data: any }) {
415
+ console.log(`[MpcProxy] Sending new message: `, event, this.iframe?.contentWindow)
416
+ this.iframe?.contentWindow?.postMessage(event, this.getOrigin())
417
+ }
418
+
419
+ private waitForReadyMessage() {
420
+ const handleReady = async (message: MessageEvent<WorkerResult>) => {
421
+ const { type, data } = message.data
422
+
423
+ if (type === 'portal:wasm:ready' && data === true) {
424
+ // Unbind the event listener
425
+ window.removeEventListener('message', handleReady)
426
+
427
+ // Update ready state
428
+ this.portal.ready = true
429
+
430
+ // Update the address
431
+ const address = await this.getAddress()
432
+ this.portal.address = address
433
+
434
+ // Trigger the ready callback
435
+ this.portal.triggerReady()
436
+ }
437
+ }
438
+
439
+ window.addEventListener('message', handleReady)
440
+ }
441
+ }
442
+
443
+ export default Mpc