@portal-hq/provider 3.0.6 → 4.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.
@@ -1,24 +1,25 @@
1
1
  import PortalConnect from '@portal-hq/connect'
2
+ import { PortalCurve } from '@portal-hq/core'
2
3
  import {
3
4
  Events,
4
5
  HttpRequester,
5
6
  IPortalProvider,
6
7
  InvalidApiKeyError,
7
- InvalidChainIdError,
8
8
  InvalidGatewayConfigError,
9
9
  KeychainAdapter,
10
+ PortalRequests,
10
11
  ProviderRpcError,
11
12
  type RequestArguments,
12
13
  RpcErrorCodes,
13
14
  } from '@portal-hq/utils'
14
- import { RpcErrorOptions } from '@portal-hq/utils/types'
15
+ import { AddressesByNamespace, RpcErrorOptions } from '@portal-hq/utils/types'
15
16
 
16
17
  import {
17
18
  type EventHandler,
18
19
  type GatewayLike,
19
20
  type ProviderOptions,
20
21
  type RegisteredEventHandler,
21
- type SwitchEthereumChainParameter,
22
+ SwitchEthereumChainParameter,
22
23
  } from '../../types'
23
24
  import { MpcSigner, Signer } from '../signers'
24
25
 
@@ -38,52 +39,52 @@ const signerMethods = [
38
39
  'eth_signTypedData_v3',
39
40
  'eth_signTypedData_v4',
40
41
  'personal_sign',
42
+ 'sol_signAndConfirmTransaction',
43
+ 'sol_signAndSendTransaction',
44
+ 'sol_signMessage',
45
+ 'sol_signTransaction',
46
+ 'raw_sign',
41
47
  ]
42
48
 
43
49
  class Provider implements IPortalProvider {
44
50
  public apiKey: string
45
51
  public autoApprove: boolean
46
- public chainId: number
52
+ public chainId?: string
47
53
  public log: Console
48
- public gateway: HttpRequester
54
+ public gateway: PortalRequests
49
55
 
50
56
  private events: Record<string, RegisteredEventHandler[]>
51
57
  private gatewayConfig: GatewayLike
52
- private isSimulator: boolean
53
58
  private keychain: KeychainAdapter
54
59
  private readonly portalApi: HttpRequester
55
60
  private signer: Signer
56
61
 
57
- public get address(): Promise<string | undefined> {
58
- return this.keychain.getAddress(this.isSimulator)
62
+ public get addresses(): Promise<AddressesByNamespace | undefined> {
63
+ return this.keychain.getAddresses()
59
64
  }
60
65
 
61
- public get gatewayUrl(): string {
62
- return this.getGatewayUrl()
66
+ public get address(): Promise<string | undefined> {
67
+ return this.keychain.getAddress()
63
68
  }
64
69
 
65
70
  constructor({
66
71
  // Required
67
72
  apiKey,
68
- chainId,
69
73
  keychain,
70
74
  gatewayConfig,
71
75
 
72
76
  // Optional
73
- isSimulator = false,
74
77
  autoApprove = false,
75
78
  apiHost = 'api.portalhq.io',
76
79
  mpcHost = 'mpc.portalhq.io',
77
80
  version = 'v6',
78
- featureFlags = { optimized: false },
81
+ chainId = 'eip155:11155111',
82
+ featureFlags = {},
79
83
  }: ProviderOptions) {
80
84
  // Handle required fields
81
85
  if (!apiKey || apiKey.length === 0) {
82
86
  throw new InvalidApiKeyError()
83
87
  }
84
- if (!chainId) {
85
- throw new InvalidChainIdError()
86
- }
87
88
  if (!gatewayConfig) {
88
89
  throw new InvalidGatewayConfigError()
89
90
  }
@@ -94,7 +95,6 @@ class Provider implements IPortalProvider {
94
95
  this.chainId = chainId
95
96
  this.events = {}
96
97
  this.gatewayConfig = gatewayConfig
97
- this.isSimulator = isSimulator
98
98
  this.keychain = keychain
99
99
 
100
100
  // Add a logger
@@ -108,9 +108,7 @@ class Provider implements IPortalProvider {
108
108
  })
109
109
 
110
110
  // Initialize Gateway HttpRequester
111
- this.gateway = new HttpRequester({
112
- baseUrl: this.getGatewayUrl(),
113
- })
111
+ this.gateway = new PortalRequests()
114
112
 
115
113
  // Initialize an MpcSigner
116
114
  this.signer = new MpcSigner({
@@ -149,23 +147,27 @@ class Provider implements IPortalProvider {
149
147
  *
150
148
  * @returns string
151
149
  */
152
- public getGatewayUrl(): string {
150
+ public getGatewayUrl(chainId?: string): string {
151
+ chainId = chainId ?? this.chainId
152
+ if (!chainId) {
153
+ throw new Error('[PortalProvider] No chainId provided')
154
+ }
153
155
  if (typeof this.gatewayConfig === 'string') {
154
156
  // If the gatewayConfig is just a static URL, return that
155
157
  return this.gatewayConfig
156
158
  } else if (
157
159
  typeof this.gatewayConfig === 'object' &&
158
160
  // eslint-disable-next-line no-prototype-builtins
159
- !this.gatewayConfig.hasOwnProperty(this.chainId)
161
+ !this.gatewayConfig.hasOwnProperty(chainId)
160
162
  ) {
161
163
  // If there's no explicit mapping for the current chainId, error out
162
164
  throw new Error(
163
- `[PortalProvider] No RPC endpoint configured for chainId: ${this.chainId}`,
165
+ `[PortalProvider] No RPC endpoint configured for chainId: ${chainId}`,
164
166
  )
165
167
  }
166
168
 
167
169
  // Get the entry for the current chainId from the gatewayConfig
168
- const config = this.gatewayConfig[this.chainId]
170
+ const config = this.gatewayConfig[chainId]
169
171
 
170
172
  if (typeof config === 'string') {
171
173
  return config
@@ -173,7 +175,7 @@ class Provider implements IPortalProvider {
173
175
 
174
176
  // If we got this far, there's no way to support the chain with the current config
175
177
  throw new Error(
176
- `[PortalProvider] Could not find a valid gatewayConfig entry for chainId: ${this.chainId}`,
178
+ `[PortalProvider] Could not find a valid gatewayConfig entry for chainId: ${chainId}`,
177
179
  )
178
180
  }
179
181
 
@@ -259,8 +261,18 @@ class Provider implements IPortalProvider {
259
261
  chainId,
260
262
  connect,
261
263
  }: RequestArguments): Promise<any> {
264
+ chainId = chainId ?? this.chainId
265
+ if (!chainId) {
266
+ throw new Error('[PortalProvider] No chainId provided')
267
+ }
262
268
  if (method === 'eth_chainId') {
263
- return this.chainId
269
+ if (typeof chainId === 'string' && chainId.includes(':')) {
270
+ return chainId.split(':')[1]
271
+ } else {
272
+ throw new Error(
273
+ "[PortalProvider] Invalid chainId format. Must be 'namespace:reference'",
274
+ )
275
+ }
264
276
  }
265
277
 
266
278
  // Handle changing chains
@@ -268,7 +280,7 @@ class Provider implements IPortalProvider {
268
280
  const param = (params as any[])[0]
269
281
  const chainId = parseInt(param.chainId as string, 16)
270
282
 
271
- this.chainId = chainId
283
+ this.chainId = `eip155:${String(chainId)}`
272
284
 
273
285
  this.emit('portal_signatureReceived', {
274
286
  method,
@@ -282,7 +294,11 @@ class Provider implements IPortalProvider {
282
294
  let result: any
283
295
  if (!isSignerMethod && !method.startsWith('wallet_')) {
284
296
  // Send to Gateway for RPC calls
285
- const response = await this.handleGatewayRequests({ method, params })
297
+ const response = await this.handleGatewayRequests({
298
+ method,
299
+ params,
300
+ chainId,
301
+ })
286
302
 
287
303
  this.emit('portal_signatureReceived', {
288
304
  method,
@@ -304,6 +320,24 @@ class Provider implements IPortalProvider {
304
320
  connect,
305
321
  })
306
322
 
323
+ if (transactionHash) {
324
+ try {
325
+ await this.portalApi.post('/api/v1/analytics/track', {
326
+ headers: {
327
+ Authentication: `Bearer ${this.apiKey}`,
328
+ },
329
+ body: {
330
+ event: Events.TransactionSigned,
331
+ properties: {
332
+ method,
333
+ },
334
+ },
335
+ })
336
+ } catch (error) {
337
+ // Do nothing, because we don't want metrics gathering
338
+ // to block the SDK
339
+ }
340
+ }
307
341
  if (transactionHash) {
308
342
  this.emit('portal_signatureReceived', {
309
343
  method,
@@ -326,7 +360,6 @@ class Provider implements IPortalProvider {
326
360
 
327
361
  return result
328
362
  }
329
-
330
363
  /**
331
364
  * Updates the chainId of this instance and builds a new RPC HttpRequester for
332
365
  * the gateway used for the new chain
@@ -335,29 +368,35 @@ class Provider implements IPortalProvider {
335
368
  * @returns BaseProvider
336
369
  */
337
370
  public async setChainId(
338
- chainId: number,
371
+ chainId: string | number,
339
372
  connect?: PortalConnect,
340
- ): Promise<Provider> {
373
+ ): Promise<IPortalProvider> {
374
+ if (typeof chainId === 'number') {
375
+ chainId = `eip155:${String(chainId)}`
376
+ }
341
377
  // Update the chainId
342
378
  this.chainId = chainId
379
+ if (chainId.includes(':')) {
380
+ const chainIdParts = chainId.split(':')
381
+ const reference = parseInt(chainIdParts[1])
343
382
 
344
- // Re-initialize the Gateway HttpRequester
345
- this.gateway = new HttpRequester({
346
- baseUrl: this.getGatewayUrl(),
347
- })
348
-
349
- // Emit event for update
350
- this.emit('chainChanged', {
351
- chainId: this.chainId,
352
- } as SwitchEthereumChainParameter)
353
-
354
- if (connect && connect.connected) {
355
- connect.emit('portalConnect_chainChanged', {
356
- chainId: this.chainId,
383
+ // Emit event for update
384
+ this.emit('chainChanged', {
385
+ chainId: reference,
357
386
  } as SwitchEthereumChainParameter)
387
+
388
+ if (connect && connect.connected) {
389
+ connect.emit('portalConnect_chainChanged', {
390
+ chainId: reference,
391
+ } as SwitchEthereumChainParameter)
392
+ }
393
+ // Dispatch 'connect' event
394
+ this.dispatchConnect(chainId)
395
+ } else {
396
+ console.error(
397
+ `[PortalProvider] Invalid chainId format. Must be 'namespace:reference', but got ${chainId}`,
398
+ )
358
399
  }
359
- // Dispatch 'connect' event
360
- this.dispatchConnect()
361
400
 
362
401
  return new Promise((resolve) => resolve(this))
363
402
  }
@@ -444,9 +483,10 @@ class Provider implements IPortalProvider {
444
483
  })
445
484
  }
446
485
 
447
- private dispatchConnect() {
486
+ private dispatchConnect(chainId: string) {
487
+ const reference = chainId.split(':')[1]
448
488
  this.emit('connect', {
449
- chainId: `0x${this.chainId.toString(16)}`,
489
+ chainId: `0x${Number(reference).toString(16)}`,
450
490
  })
451
491
  }
452
492
 
@@ -459,12 +499,15 @@ class Provider implements IPortalProvider {
459
499
  private async handleGatewayRequests({
460
500
  method,
461
501
  params,
502
+ chainId,
462
503
  }: RequestArguments): Promise<any> {
504
+ const gatewayUrl = this.getGatewayUrl(chainId)
463
505
  // Pass request off to the gateway
464
- return await this.gateway.post<any>('', {
506
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
507
+ return await this.gateway.post<any>(gatewayUrl, '', {
465
508
  body: {
466
509
  jsonrpc: '2.0',
467
- id: this.chainId,
510
+ id: chainId,
468
511
  method,
469
512
  params,
470
513
  },
@@ -493,10 +536,20 @@ class Provider implements IPortalProvider {
493
536
  )
494
537
  return
495
538
  }
539
+ let namespace = 'eip155'
540
+ let reference = chainId
541
+ if (typeof chainId == 'string' && chainId.includes(':')) {
542
+ const tmp = chainId.split(':')
543
+ namespace = tmp[0]
544
+ reference = tmp[1]
545
+ }
546
+
547
+ const curve =
548
+ namespace == 'eip155' ? PortalCurve.SECP256K1 : PortalCurve.ED25519
496
549
 
497
550
  switch (method) {
498
551
  case 'eth_chainId':
499
- return `0x${this.chainId.toString(16)}`
552
+ return `0x${Number(reference).toString(16)}`
500
553
  case 'eth_accounts':
501
554
  case 'eth_requestAccounts':
502
555
  case 'eth_sendTransaction':
@@ -504,30 +557,35 @@ class Provider implements IPortalProvider {
504
557
  case 'eth_signTransaction':
505
558
  case 'eth_signTypedData_v3':
506
559
  case 'eth_signTypedData_v4':
507
- case 'personal_sign': {
560
+ case 'personal_sign':
561
+ case 'sol_signAndConfirmTransaction':
562
+ case 'sol_signAndSendTransaction':
563
+ case 'sol_signMessage':
564
+ case 'sol_signTransaction': {
508
565
  const result = await this.signer?.sign(
509
- { chainId: this.chainId, method, params },
566
+ {
567
+ chainId: `${namespace}:${reference}`,
568
+ method,
569
+ params,
570
+ curve,
571
+ isRaw: false,
572
+ },
510
573
  this,
511
574
  )
512
575
 
513
- if (result) {
514
- try {
515
- await this.portalApi.post('/api/v1/analytics/track', {
516
- headers: {
517
- Authentication: `Bearer ${this.apiKey}`,
518
- },
519
- body: {
520
- event: Events.TransactionSigned,
521
- properties: {
522
- method,
523
- },
524
- },
525
- })
526
- } catch (error) {
527
- // Do nothing, because we don't want metrics gathering
528
- // to block the SDK
529
- }
530
- }
576
+ return result
577
+ }
578
+ case 'raw_sign': {
579
+ const result = await this.signer?.sign(
580
+ {
581
+ chainId: '',
582
+ method: '',
583
+ params,
584
+ curve,
585
+ isRaw: true,
586
+ },
587
+ this,
588
+ )
531
589
 
532
590
  return result
533
591
  }
@@ -1,3 +1,4 @@
1
+ import { PortalCurve } from '@portal-hq/core'
1
2
  import { FeatureFlags } from '@portal-hq/core/types'
2
3
  import {
3
4
  IPortalProvider,
@@ -16,29 +17,18 @@ import {
16
17
  import Signer from './abstract'
17
18
 
18
19
  class MpcSigner implements Signer {
19
- private isSimulator: boolean
20
20
  private keychain: KeychainAdapter
21
21
  private mpc: PortalMobileMpc
22
22
  private mpcHost: string // should we add a default here mpc.portalhq.io
23
23
  private version = 'v6'
24
24
  private featureFlags: FeatureFlags
25
25
 
26
- private get address(): Promise<string | undefined> {
27
- return this.keychain.getAddress(this.isSimulator)
28
- }
29
-
30
- private get signingShare(): Promise<string> {
31
- return this.keychain.getDkgResult(this.isSimulator)
32
- }
33
-
34
26
  constructor({
35
27
  keychain,
36
- isSimulator = false,
37
28
  mpcHost = 'mpc.portalhq.io',
38
29
  version = 'v6',
39
- featureFlags = { optimized: false },
30
+ featureFlags = {},
40
31
  }: MpcSignerOptions) {
41
- this.isSimulator = isSimulator
42
32
  this.keychain = keychain
43
33
  this.mpc = NativeModules.PortalMobileMpc
44
34
  this.mpcHost = mpcHost
@@ -56,37 +46,71 @@ class MpcSigner implements Signer {
56
46
  message: SigningRequestArguments,
57
47
  provider: IPortalProvider,
58
48
  ): Promise<any> {
59
- const address = await this.address
49
+ const eip155Address = await this.keychain.getEip155Address()
50
+
60
51
  const apiKey = provider.apiKey
61
52
 
62
- const { method, params } = message
53
+ const { method, chainId, curve, isRaw } = message
63
54
 
64
55
  switch (method) {
65
56
  case 'eth_requestAccounts':
66
- return [address]
57
+ return [eip155Address]
67
58
  case 'eth_accounts':
68
- return [address]
59
+ return [eip155Address]
69
60
  default:
70
61
  break
71
62
  }
72
63
 
73
- const signingShare = await this.signingShare
64
+ const shares = await this.keychain.getShares()
65
+
66
+ let signingShare = shares.secp256k1.share
67
+
68
+ if (curve === PortalCurve.ED25519) {
69
+ if (!shares.ed25519) {
70
+ throw new Error(
71
+ '[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.',
72
+ )
73
+ }
74
+ signingShare = shares.ed25519.share
75
+ }
74
76
 
75
77
  const metadata: PortalMobileMpcMetadata = {
76
78
  clientPlatform: 'REACT_NATIVE',
77
79
  isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
78
80
  mpcServerVersion: this.version,
79
- optimized: this.featureFlags.optimized,
81
+ optimized: true,
82
+ curve,
83
+ chainId,
84
+ isRaw,
80
85
  }
86
+
81
87
  const stringifiedMetadata = JSON.stringify(metadata)
88
+
89
+ let formattedParams: string
90
+ let rpcUrl: string
91
+
92
+ if (isRaw) {
93
+ formattedParams = this.buildParams(method, message.params)
94
+ rpcUrl = ''
95
+ } else {
96
+ formattedParams = JSON.stringify(this.buildParams(method, message.params))
97
+ rpcUrl = provider.getGatewayUrl(chainId)
98
+ }
99
+
100
+ if (typeof formattedParams !== 'string') {
101
+ throw new Error(
102
+ `[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`,
103
+ )
104
+ }
105
+
82
106
  const result = await this.mpc.sign(
83
107
  apiKey,
84
108
  this.mpcHost,
85
- signingShare,
109
+ JSON.stringify(signingShare),
86
110
  message.method,
87
- JSON.stringify(this.buildParams(method, params)),
88
- provider.gatewayUrl,
89
- provider.chainId.toString(),
111
+ formattedParams,
112
+ rpcUrl,
113
+ chainId,
90
114
  stringifiedMetadata,
91
115
  )
92
116
 
@@ -107,16 +131,21 @@ class MpcSigner implements Signer {
107
131
  case 'personal_sign':
108
132
  case 'eth_signTypedData_v3':
109
133
  case 'eth_signTypedData_v4':
134
+ case 'sol_signMessage':
135
+ case 'sol_signTransaction':
136
+ case 'sol_signAndSendTransaction':
137
+ case 'sol_signAndConfirmTransaction':
110
138
  if (!Array.isArray(txParams)) {
111
139
  params = [txParams]
112
140
  }
113
141
  break
114
142
  default:
115
143
  if (Array.isArray(txParams)) {
116
- params = txParams[0]
144
+ if (txParams.length === 1) {
145
+ params = txParams[0]
146
+ }
117
147
  }
118
148
  }
119
-
120
149
  return params
121
150
  }
122
151
  }
package/types.d.ts CHANGED
@@ -12,7 +12,7 @@ export type ValidHttpRequestMethods = 'DELETE' | 'GET' | 'POST' | 'PUT'
12
12
  // Interfaces
13
13
 
14
14
  export interface GatewayConfig {
15
- [key: number]: string
15
+ [key: string]: string
16
16
  }
17
17
 
18
18
  export interface MpcSignerOptions extends SignerOptions {
@@ -33,10 +33,13 @@ export interface PortalMobileMpcMetadata {
33
33
  | 'PASSKEY'
34
34
  | 'PASSWORD'
35
35
  | 'UNKNOWN'
36
+ chainId?: string
37
+ curve?: PortalCurve
36
38
  clientPlatform: string
37
39
  isMultiBackupEnabled?: boolean
38
40
  mpcServerVersion: string
39
- optimized: boolean
41
+ optimized: true
42
+ isRaw?: boolean
40
43
  }
41
44
 
42
45
  export interface PortalMobileMpc {
@@ -67,11 +70,11 @@ export interface PortalMobileMpc {
67
70
  export interface ProviderOptions {
68
71
  // Required
69
72
  apiKey: string
70
- chainId: number
71
73
  keychain: KeychainAdapter
72
74
  gatewayConfig: GatewayLike
73
75
 
74
76
  // Optional
77
+ chainId?: string
75
78
  isSimulator?: boolean
76
79
  autoApprove?: boolean
77
80
  apiHost?: string