@portal-hq/web 2.0.1 → 3.0.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.
package/src/index.ts CHANGED
@@ -1,24 +1,32 @@
1
+ import {
2
+ Connection,
3
+ PublicKey,
4
+ Transaction as SolanaTransaction,
5
+ SystemProgram,
6
+ } from '@solana/web3.js'
7
+
1
8
  import type {
2
9
  BackupConfigs,
3
10
  BackupResponse,
4
- BackupSharePairMetadata,
5
11
  Balance,
6
12
  ClientResponse,
7
13
  EthereumTransaction,
8
14
  FeatureFlags,
9
15
  GDriveConfig,
10
- GatewayLike,
16
+ RpcConfig,
11
17
  NFT,
12
18
  PasskeyConfig,
13
19
  PortalOptions,
14
20
  ProgressCallback,
15
21
  QuoteArgs,
16
22
  QuoteResponse,
17
- SigningSharePairMetadata,
18
23
  SimulateTransactionParam,
19
24
  SimulatedTransaction,
20
25
  Transaction,
21
26
  TypedData,
27
+ SharesOnDeviceResponse,
28
+ RequestArguments,
29
+ EjectResult,
22
30
  } from '../types'
23
31
  import Mpc from './mpc'
24
32
  import Provider from './provider'
@@ -29,7 +37,6 @@ class Portal {
29
37
  public authToken?: string
30
38
  public authUrl?: string
31
39
  public autoApprove: boolean
32
- public chainId: number
33
40
  public gDriveConfig?: GDriveConfig
34
41
  public passkeyConfig?: PasskeyConfig
35
42
  public host: string
@@ -41,19 +48,18 @@ class Portal {
41
48
  public featureFlags: FeatureFlags
42
49
 
43
50
  private errorCallbacks: ((reason: string) => any | Promise<any>)[] = []
44
- private gatewayConfig: GatewayLike
51
+ private rpcConfig: RpcConfig
45
52
  private readyCallbacks: (() => any | Promise<any>)[] = []
46
53
 
47
54
  constructor({
48
55
  // Required
49
- gatewayConfig,
56
+ rpcConfig,
50
57
 
51
58
  // Optional
52
59
  apiKey,
53
60
  authToken,
54
61
  authUrl,
55
62
  autoApprove = false,
56
- chainId = 1,
57
63
  gdrive,
58
64
  passkey,
59
65
  host = 'web.portalhq.io',
@@ -67,8 +73,7 @@ class Portal {
67
73
  this.authToken = authToken
68
74
  this.authUrl = authUrl
69
75
  this.autoApprove = autoApprove
70
- this.chainId = chainId
71
- this.gatewayConfig = gatewayConfig
76
+ this.rpcConfig = rpcConfig
72
77
  this.host = host
73
78
  this.mpcHost = mpcHost
74
79
  this.mpcVersion = mpcVersion
@@ -220,12 +225,12 @@ class Portal {
220
225
  return this.recoverWallet(cipherText, backupMethod, backupConfigs, progress)
221
226
  }
222
227
 
223
- public async ejectPrivateKey(
228
+ public async eject(
224
229
  clientBackupCipherText: string,
225
230
  backupMethod: BackupMethods,
226
231
  backupConfigs: BackupConfigs,
227
232
  orgBackupShare: string,
228
- ): Promise<string> {
233
+ ): Promise<EjectResult> {
229
234
  if (clientBackupCipherText === '') {
230
235
  throw new Error('clientBackupCipherText cannot be empty string.')
231
236
  }
@@ -234,7 +239,7 @@ class Portal {
234
239
  throw new Error('orgBackupShare cannot be empty string.')
235
240
  }
236
241
 
237
- const privateKey = await this.mpc.eject({
242
+ const { SECP256K1, ED25519 } = await this.mpc.eject({
238
243
  cipherText: clientBackupCipherText,
239
244
  backupMethod,
240
245
  backupConfigs,
@@ -244,17 +249,73 @@ class Portal {
244
249
  featureFlags: this.featureFlags,
245
250
  })
246
251
 
247
- return privateKey
252
+ return {
253
+ SECP256K1,
254
+ ED25519,
255
+ }
256
+ }
257
+
258
+ public async getEip155Address(): Promise<string> {
259
+ const client = await this.mpc?.getClient()
260
+ const eip155Address = client?.metadata?.namespaces?.eip155?.address || ''
261
+ return eip155Address
262
+ }
263
+
264
+ public async getSolanaAddress(): Promise<string> {
265
+ const client = await this.mpc?.getClient()
266
+ const solAddress = client?.metadata?.namespaces?.solana?.address || ''
267
+ return solAddress
268
+ }
269
+
270
+ public async doesWalletExist(chainId?: string): Promise<boolean> {
271
+ const client = await this.mpc?.getClient()
272
+
273
+ if (chainId) {
274
+ const namespace = chainId?.split(':')?.[0] || ''
275
+ const namespaceInfo = client?.metadata?.namespaces?.[namespace]
276
+ return !!namespaceInfo
277
+ }
278
+
279
+ return client?.wallets?.length > 0
280
+ }
281
+
282
+ public async isWalletOnDevice(chainId?: string): Promise<boolean> {
283
+ const client = await this.mpc?.getClient()
284
+ const sharesOnDevice = await this.mpc?.checkSharesOnDevice()
285
+
286
+ if (chainId) {
287
+ const namespace = chainId?.split(':')?.[0] || ''
288
+ const curve = client?.metadata?.namespaces?.[namespace]?.curve || ''
289
+ return sharesOnDevice[curve as keyof SharesOnDeviceResponse] || false
290
+ }
291
+
292
+ return sharesOnDevice.ED25519 && sharesOnDevice.SECP256K1
248
293
  }
249
294
 
250
295
  /****************************
251
296
  * Provider Methods
252
297
  ****************************/
253
298
 
299
+ public async request(request: RequestArguments): Promise<any> {
300
+ return this.provider.request(request)
301
+ }
302
+
303
+ /**
304
+ * Estimates the amount of gas that will be required to execute an Ethereum transaction.
305
+ *
306
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_estimateGas' instead.
307
+ *
308
+ * @param {string} chainId - The chain ID of the Ethereum network.
309
+ * @param {EthereumTransaction} transaction - The transaction object containing the necessary transaction details.
310
+ * @returns {Promise<any>} A Promise that resolves to the gas estimate.
311
+ */
254
312
  public async ethEstimateGas(
313
+ chainId: string,
255
314
  transaction: EthereumTransaction,
256
- chainId?: string,
257
315
  ): Promise<any> {
316
+ console.warn(
317
+ '"portal.ethEstimateGas" is deprecated. Use "portal.request" instead.',
318
+ )
258
319
  return this.provider.request({
259
320
  chainId,
260
321
  method: 'eth_estimateGas',
@@ -262,7 +323,18 @@ class Portal {
262
323
  })
263
324
  }
264
325
 
265
- public async ethGasPrice(chainId?: string): Promise<string> {
326
+ /**
327
+ * Gets the current gas price for the Ethereum network.
328
+ *
329
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_gasPrice' instead.
330
+ *
331
+ * @param {string} chainId - The chain ID of the Ethereum network.
332
+ * @returns {Promise<string>} A Promise that resolves to the current gas price.
333
+ */
334
+ public async ethGasPrice(chainId: string): Promise<string> {
335
+ console.warn(
336
+ '"portal.ethGasPrice" is deprecated. Use "portal.request" instead.',
337
+ )
266
338
  return this.provider.request({
267
339
  chainId,
268
340
  method: 'eth_gasPrice',
@@ -270,7 +342,18 @@ class Portal {
270
342
  }) as Promise<string>
271
343
  }
272
344
 
273
- public async ethGetBalance(chainId?: string): Promise<string> {
345
+ /**
346
+ * Gets the balance of the current Ethereum address.
347
+ *
348
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_getBalance' instead.
349
+ *
350
+ * @param {string} chainId - The chain ID of the Ethereum network.
351
+ * @returns {Promise<string>} A Promise that resolves to the current balance.
352
+ */
353
+ public async ethGetBalance(chainId: string): Promise<string> {
354
+ console.warn(
355
+ '"portal.ethGetBalance" is deprecated. Use "portal.request" instead.',
356
+ )
274
357
  return this.provider.request({
275
358
  chainId,
276
359
  method: 'eth_getBalance',
@@ -278,10 +361,22 @@ class Portal {
278
361
  }) as Promise<string>
279
362
  }
280
363
 
364
+ /**
365
+ * Sends an Ethereum transaction.
366
+ *
367
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_getTransactionCount' instead.
368
+ *
369
+ * @param {string} chainId - The chain ID of the Ethereum network.
370
+ * @param {EthereumTransaction} transaction - The transaction object containing the necessary transaction details.
371
+ * @returns {Promise<string>} A Promise that resolves to the transaction hash.
372
+ */
281
373
  public async ethSendTransaction(
374
+ chainId: string,
282
375
  transaction: EthereumTransaction,
283
- chainId?: string,
284
376
  ): Promise<string> {
377
+ console.warn(
378
+ '"portal.ethSendTransaction" is deprecated. Use "portal.request" instead.',
379
+ )
285
380
  return this.provider.request({
286
381
  chainId,
287
382
  method: 'eth_sendTransaction',
@@ -289,18 +384,22 @@ class Portal {
289
384
  }) as Promise<string>
290
385
  }
291
386
 
292
- public async ethSign(message: string, chainId?: string): Promise<string> {
293
- return this.provider.request({
294
- chainId,
295
- method: 'eth_sign',
296
- params: [this.address, this.stringToHex(message)],
297
- }) as Promise<string>
298
- }
299
-
387
+ /**
388
+ * Signs an Ethereum transaction.
389
+ *
390
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTransaction' instead.
391
+ *
392
+ * @param {string} chainId - The chain ID of the Ethereum network.
393
+ * @param {EthereumTransaction} transaction - The transaction object containing the necessary transaction details.
394
+ * @returns {Promise<string>} A Promise that resolves to the signed transaction.
395
+ */
300
396
  public async ethSignTransaction(
397
+ chainId: string,
301
398
  transaction: EthereumTransaction,
302
- chainId?: string,
303
399
  ): Promise<string> {
400
+ console.warn(
401
+ '"portal.ethSignTransaction" is deprecated. Use "portal.request" instead.',
402
+ )
304
403
  return this.provider.request({
305
404
  chainId,
306
405
  method: 'eth_signTransaction',
@@ -308,10 +407,22 @@ class Portal {
308
407
  }) as Promise<string>
309
408
  }
310
409
 
410
+ /**
411
+ * Signs an Ethereum message using EIP-712.
412
+ *
413
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTypedData' instead.
414
+ *
415
+ * @param {string} chainId - The chain ID of the Ethereum network.
416
+ * @param {TypedData} data - The typed data object to sign.
417
+ * @returns {Promise<string>} A Promise that resolves to the signed message.
418
+ */
311
419
  public async ethSignTypedData(
420
+ chainId: string,
312
421
  data: TypedData,
313
- chainId?: string,
314
422
  ): Promise<string> {
423
+ console.warn(
424
+ '"portal.ethSignTypedData" is deprecated. Use "portal.request" instead.',
425
+ )
315
426
  return this.provider.request({
316
427
  chainId,
317
428
  method: 'eth_signTypedData',
@@ -319,10 +430,22 @@ class Portal {
319
430
  }) as Promise<string>
320
431
  }
321
432
 
433
+ /**
434
+ * Signs an Ethereum message using EIP-712 (legacy).
435
+ *
436
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTypedData_v3' instead.
437
+ *
438
+ * @param {string} chainId - The chain ID of the Ethereum network.
439
+ * @param {TypedData} data - The typed data object to sign.
440
+ * @returns {Promise<string>} A Promise that resolves to the signed message.
441
+ */
322
442
  public async ethSignTypedDataV3(
443
+ chainId: string,
323
444
  data: TypedData,
324
- chainId?: string,
325
445
  ): Promise<string> {
446
+ console.warn(
447
+ '"portal.ethSignTypedDataV3" is deprecated. Use "portal.request" instead.',
448
+ )
326
449
  return this.provider.request({
327
450
  chainId,
328
451
  method: 'eth_signTypedData_v3',
@@ -330,10 +453,22 @@ class Portal {
330
453
  }) as Promise<string>
331
454
  }
332
455
 
456
+ /**
457
+ * Signs an Ethereum message using EIP-712 (v4).
458
+ *
459
+ * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTypedData_v4' instead.
460
+ *
461
+ * @param {string} chainId - The chain ID of the Ethereum network.
462
+ * @param {TypedData} data - The typed data object to sign.
463
+ * @returns {Promise<string>} A Promise that resolves to the signed message.
464
+ */
333
465
  public async ethSignTypedDataV4(
466
+ chainId: string,
334
467
  data: TypedData,
335
- chainId?: string,
336
468
  ): Promise<string> {
469
+ console.warn(
470
+ '"portal.ethSignTypedDataV4" is deprecated. Use "portal.request" instead.',
471
+ )
337
472
  return this.provider.request({
338
473
  chainId,
339
474
  method: 'eth_signTypedData_v4',
@@ -341,10 +476,10 @@ class Portal {
341
476
  }) as Promise<string>
342
477
  }
343
478
 
344
- public async personalSign(
345
- message: string,
346
- chainId?: string,
347
- ): Promise<string> {
479
+ public async personalSign(chainId: string, message: string): Promise<string> {
480
+ console.warn(
481
+ '"portal.personalSign" is deprecated. Use "portal.request" instead.',
482
+ )
348
483
  return (await this.provider.request({
349
484
  chainId,
350
485
  method: 'personal_sign',
@@ -352,43 +487,162 @@ class Portal {
352
487
  })) as Promise<string>
353
488
  }
354
489
 
490
+ public async sendSol({
491
+ chainId,
492
+ to,
493
+ lamports,
494
+ }: {
495
+ chainId: string
496
+ to: string
497
+ lamports: number
498
+ }): Promise<string> {
499
+ // Ensure the chainId is solana.
500
+ if (!chainId.startsWith('solana:')) {
501
+ throw new Error(
502
+ '[Portal] Invalid chainId. Please provide a chainId that starts with "solana:"',
503
+ )
504
+ }
505
+
506
+ // Ensure the to address is a valid Solana address.
507
+ if (!to || typeof to !== 'string' || to.length !== 44) {
508
+ throw new Error(
509
+ '[Portal] Invalid "to" Solana address provided, must be 44 characters',
510
+ )
511
+ }
512
+
513
+ // Validate the lamports.
514
+ if (typeof lamports !== 'number' || lamports <= 0) {
515
+ throw new Error(
516
+ '[Portal] Invalid lamports amount, must be a positive number greater than 0',
517
+ )
518
+ }
519
+
520
+ // Get the most recent blockhash.
521
+ const blockhashResponse = await this.provider.request({
522
+ chainId: chainId,
523
+ method: 'getLatestBlockhash',
524
+ params: [],
525
+ })
526
+ const blockhash = blockhashResponse?.value?.blockhash || ''
527
+
528
+ // If we didn't get a blockhash, throw an error.
529
+ if (!blockhash) {
530
+ throw new Error('[Portal] Failed to get most recent blockhash')
531
+ }
532
+
533
+ // Get the Solana address from the client, validate the addresses.
534
+ const solanaAddress = await this.getSolanaAddress()
535
+ if (!solanaAddress) {
536
+ throw new Error('[Portal] Failed to get Solana address')
537
+ }
538
+
539
+ // Get the Solana gateway URL.
540
+ const gatewayUrl = this.getRpcUrl(chainId)
541
+ if (!gatewayUrl) {
542
+ throw new Error('[Portal] No RPC endpoint configured for chainId')
543
+ }
544
+
545
+ // Create a new connection to the Solana network.
546
+ new Connection(gatewayUrl, 'confirmed')
547
+
548
+ // The sender's public key.
549
+ const fromPublicKey = new PublicKey(solanaAddress)
550
+
551
+ // The recipient's public key.
552
+ const toPublicKey = new PublicKey(to)
553
+
554
+ // Create a new transaction.
555
+ const transaction = new SolanaTransaction().add(
556
+ SystemProgram.transfer({
557
+ fromPubkey: fromPublicKey,
558
+ toPubkey: toPublicKey,
559
+ lamports,
560
+ }),
561
+ )
562
+ transaction.recentBlockhash = blockhash
563
+ transaction.feePayer = fromPublicKey
564
+ const compiledMessage = transaction.compileMessage()
565
+
566
+ // Build the transaction and its message.
567
+ const message = {
568
+ accountKeys: compiledMessage.accountKeys.map((key) => key.toBase58()),
569
+ header: compiledMessage.header,
570
+ instructions: compiledMessage.instructions,
571
+ recentBlockhash: blockhash,
572
+ }
573
+ const formattedTransaction = {
574
+ signatures: null,
575
+ message,
576
+ }
577
+
578
+ // Attempt to sign and send the transaction
579
+ const transactionResult = await this.provider.request({
580
+ chainId: chainId,
581
+ method: 'sol_signAndSendTransaction',
582
+ params: [formattedTransaction],
583
+ })
584
+
585
+ // If we didn't get a transactionResult, throw an error.
586
+ if (!transactionResult) {
587
+ throw new Error('[Portal] Failed to send Solana transaction')
588
+ }
589
+
590
+ // Return the transactionResult.
591
+ return transactionResult
592
+ }
593
+
594
+ public sendEth = async ({
595
+ chainId,
596
+ to,
597
+ value,
598
+ }: {
599
+ chainId: string
600
+ to: string
601
+ value: string
602
+ }) => {
603
+ return this.provider.request({
604
+ chainId,
605
+ method: 'eth_sendTransaction',
606
+ params: [
607
+ {
608
+ from: this.address,
609
+ to,
610
+ value,
611
+ },
612
+ ],
613
+ })
614
+ }
615
+
355
616
  /*******************************
356
617
  * API Methods
357
618
  *******************************/
358
619
 
359
- public async getBalances(): Promise<Balance[]> {
360
- return await this.mpc?.getBalances()
620
+ public async getBalances(chainId: string): Promise<Balance[]> {
621
+ return await this.mpc?.getBalances(chainId)
361
622
  }
362
623
 
363
624
  public async getClient(): Promise<ClientResponse> {
364
625
  return this.mpc?.getClient()
365
626
  }
366
627
 
367
- public async getNFTs(): Promise<NFT[]> {
368
- return this.mpc?.getNFTs()
369
- }
370
-
371
- public async getBackupShareMetadata(): Promise<BackupSharePairMetadata[]> {
372
- return this.mpc?.getBackupShareMetadata()
373
- }
374
-
375
- public async getSigningShareMetadata(): Promise<SigningSharePairMetadata[]> {
376
- return this.mpc?.getSigningShareMetadata()
628
+ public async getNFTs(chainId: string): Promise<NFT[]> {
629
+ return this.mpc?.getNFTs(chainId)
377
630
  }
378
631
 
379
632
  public async getTransactions(
633
+ chainId: string,
380
634
  limit?: number,
381
635
  offset?: number,
382
636
  order?: GetTransactionsOrder,
383
- chainId?: string,
384
637
  ): Promise<Transaction[]> {
385
- return this.mpc?.getTransactions(limit, offset, order, chainId)
638
+ return this.mpc?.getTransactions(chainId, limit, offset, order)
386
639
  }
387
640
 
388
641
  public async simulateTransaction(
642
+ chainId: string,
389
643
  transaction: SimulateTransactionParam,
390
644
  ): Promise<SimulatedTransaction> {
391
- return this.mpc?.simulateTransaction(transaction)
645
+ return this.mpc?.simulateTransaction(transaction, chainId)
392
646
  }
393
647
 
394
648
  /*******************************
@@ -398,14 +652,14 @@ class Portal {
398
652
  public async getQuote(
399
653
  apiKey: string,
400
654
  args: QuoteArgs,
401
- chainId?: string,
655
+ chainId: string,
402
656
  ): Promise<QuoteResponse> {
403
657
  return this.mpc?.getQuote(apiKey, args, chainId)
404
658
  }
405
659
 
406
660
  public async getSources(
407
661
  apiKey: string,
408
- chainId?: string,
662
+ chainId: string,
409
663
  ): Promise<Record<string, string>> {
410
664
  return this.mpc?.getSources(apiKey, chainId)
411
665
  }
@@ -425,31 +679,33 @@ class Portal {
425
679
  * RPC Methods
426
680
  ****************************/
427
681
 
428
- public getRpcUrl() {
429
- if (typeof this.gatewayConfig === 'string') {
430
- // If the gatewayConfig is just a static URL, return that
431
- return this.gatewayConfig
432
- } else if (
433
- typeof this.gatewayConfig === 'object' &&
434
- // eslint-disable-next-line no-prototype-builtins
435
- !this.gatewayConfig.hasOwnProperty(this.chainId)
436
- ) {
437
- // If there's no explicit mapping for the current chainId, error out
682
+ public getRpcUrl(chainId?: string) {
683
+ // Ensure a chainId is provided.
684
+ if (!chainId) {
685
+ throw new Error(
686
+ '[Portal] No chainId provided. Please provide a chainId to get the RPC endpoint',
687
+ )
688
+ }
689
+
690
+ // Ensure the chainId is configured in the rpcConfig.
691
+ // eslint-disable-next-line no-prototype-builtins
692
+ if (!this.rpcConfig.hasOwnProperty(chainId)) {
438
693
  throw new Error(
439
- `[PortalProvider] No RPC endpoint configured for chainId: ${this.chainId}`,
694
+ `[Portal] No RPC endpoint configured for chainId: ${chainId}`,
440
695
  )
441
696
  }
442
697
 
443
- // Get the entry for the current chainId from the gatewayConfig
444
- const config = this.gatewayConfig[this.chainId]
698
+ // Derive the RPC endpoint from the rpcConfig.
699
+ const gatewayUrl = this.rpcConfig[chainId]
445
700
 
446
- if (typeof config === 'string') {
447
- return config
701
+ // If the RPC endpoint is a string, return it as-is.
702
+ if (typeof gatewayUrl === 'string') {
703
+ return gatewayUrl
448
704
  }
449
705
 
450
- // If we got this far, there's no way to support the chain with the current config
706
+ // Otherwise, something is wrong with the configuration.
451
707
  throw new Error(
452
- `[PortalProvider] Could not find a valid gatewayConfig entry for chainId: ${this.chainId}`,
708
+ `[Portal] Could not find a valid rpcConfig entry for chainId: ${chainId}`,
453
709
  )
454
710
  }
455
711
 
@@ -72,7 +72,7 @@ export class MpcError extends Error {
72
72
  super(error.message(context))
73
73
 
74
74
  // Custom error context
75
- this.code = error.code
75
+ this.code = error?.code || 999
76
76
  if (context) {
77
77
  this.context = context
78
78
  }
@@ -160,7 +160,7 @@ export class PortalMpcError extends Error {
160
160
  public constructor(error: PortalError) {
161
161
  super(error.message)
162
162
 
163
- this.code = error.code
163
+ this.code = error?.code || 999
164
164
  }
165
165
  }
166
166