@portal-hq/web 3.11.0 → 3.12.0

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/mpc/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PortalMpcError } from './errors'
2
+ import { sdkLogger } from '../logger'
2
3
 
3
4
  import Portal, {
4
5
  BackupMethods,
@@ -107,14 +108,16 @@ import {
107
108
  ScreenAddressApiResponse,
108
109
  ScreenAddressRequestOptions,
109
110
  } from '../../hypernative'
111
+ import { generateTraceId } from '../shared/trace'
110
112
 
111
- const WEB_SDK_VERSION = '3.11.0'
113
+ const WEB_SDK_VERSION = '3.12.0'
112
114
 
113
115
  class Mpc {
114
116
  public iframe?: HTMLIFrameElement
115
117
 
116
118
  private portal: Portal
117
119
  private _ready = false
120
+ private presignatureLogHandler: ((event: MessageEvent) => void) | null = null
118
121
 
119
122
  public get ready() {
120
123
  return this._ready
@@ -143,10 +146,16 @@ class Mpc {
143
146
  progress: ProgressCallback = () => {
144
147
  // Noop
145
148
  },
149
+ traceId?: string,
146
150
  ): Promise<BackupResponse> {
147
151
  // validates password config for password backup
148
152
  this.validateBackupConfig(data)
149
153
 
154
+ const resolvedTraceId = traceId ?? generateTraceId()
155
+ sdkLogger.info(
156
+ `[Portal MPC] backup started | backupMethod=${data.backupMethod} | traceId=${resolvedTraceId}`,
157
+ )
158
+
150
159
  return this.handleRequestToIframeAndPost({
151
160
  methodMessage: 'portal:wasm:backup',
152
161
  errorMessage: 'portal:wasm:backupError',
@@ -154,6 +163,7 @@ class Mpc {
154
163
  data,
155
164
  progressMessage: 'portal:wasm:backupProgress',
156
165
  progressCallback: progress,
166
+ traceId: resolvedTraceId,
157
167
  mapReturnValue: (result: Record<string, unknown>) => {
158
168
  const resultData = result as Record<string, unknown>
159
169
  const backupIds = Array.isArray(resultData.backupIds)
@@ -183,6 +193,9 @@ class Mpc {
183
193
  resultMessage: 'portal:destroyResult',
184
194
  data: {},
185
195
  mapReturnValue: () => true,
196
+ }).then((result) => {
197
+ this.teardownPresignatureLogForwarding()
198
+ return result
186
199
  })
187
200
  }
188
201
 
@@ -191,7 +204,11 @@ class Mpc {
191
204
  progress: ProgressCallback = () => {
192
205
  // Noop
193
206
  },
207
+ traceId?: string,
194
208
  ): Promise<string> {
209
+ const resolvedTraceId = traceId ?? generateTraceId()
210
+ sdkLogger.info(`[Portal MPC] generate started | traceId=${resolvedTraceId}`)
211
+
195
212
  return this.handleRequestToIframeAndPost({
196
213
  methodMessage: 'portal:wasm:generate',
197
214
  errorMessage: 'portal:wasm:generateError',
@@ -199,6 +216,7 @@ class Mpc {
199
216
  data,
200
217
  progressMessage: 'portal:wasm:generateProgress',
201
218
  progressCallback: progress,
219
+ traceId: resolvedTraceId,
202
220
  })
203
221
  }
204
222
 
@@ -216,7 +234,13 @@ class Mpc {
216
234
  progress: ProgressCallback = () => {
217
235
  // Noop
218
236
  },
237
+ traceId?: string,
219
238
  ): Promise<string> {
239
+ const resolvedTraceId = traceId ?? generateTraceId()
240
+ sdkLogger.info(
241
+ `[Portal MPC] recover started | backupMethod=${data.backupMethod} | traceId=${resolvedTraceId}`,
242
+ )
243
+
220
244
  return this.handleRequestToIframeAndPost({
221
245
  methodMessage: 'portal:wasm:recover',
222
246
  errorMessage: 'portal:wasm:recoverError',
@@ -224,26 +248,30 @@ class Mpc {
224
248
  data,
225
249
  progressMessage: 'portal:wasm:recoverProgress',
226
250
  progressCallback: progress,
251
+ traceId: resolvedTraceId,
227
252
  })
228
253
  }
229
254
 
230
- public async eject(data: EjectArgs): Promise<EjectResult> {
255
+ public async eject(data: EjectArgs, traceId?: string): Promise<EjectResult> {
231
256
  return this.handleRequestToIframeAndPost({
232
257
  methodMessage: 'portal:wasm:eject',
233
258
  errorMessage: 'portal:wasm:ejectError',
234
259
  resultMessage: 'portal:wasm:ejectResult',
235
260
  data,
261
+ traceId,
236
262
  })
237
263
  }
238
264
 
239
265
  public async ejectPrivateKeys(
240
266
  data: EjectPrivateKeysArgs,
267
+ traceId?: string,
241
268
  ): Promise<EjectPrivateKeysResult> {
242
269
  return this.handleRequestToIframeAndPost({
243
270
  methodMessage: 'portal:wasm:ejectPrivateKeys',
244
271
  errorMessage: 'portal:wasm:ejectPrivateKeysError',
245
272
  resultMessage: 'portal:wasm:ejectPrivateKeysResult',
246
273
  data,
274
+ traceId,
247
275
  })
248
276
  }
249
277
 
@@ -272,13 +300,19 @@ class Mpc {
272
300
  // Noop
273
301
  },
274
302
  ): Promise<string> {
303
+ const resolvedTraceId = data.traceId ?? generateTraceId()
304
+ sdkLogger.info(
305
+ `[Portal MPC] sign started | method=${data.method} | traceId=${resolvedTraceId} | chainId=${data.chainId}`,
306
+ )
307
+
275
308
  return this.handleRequestToIframeAndPost({
276
309
  methodMessage: 'portal:wasm:sign',
277
310
  errorMessage: 'portal:wasm:signError',
278
311
  resultMessage: 'portal:wasm:signResult',
279
- data,
312
+ data: { ...data, traceId: resolvedTraceId },
280
313
  progressMessage: 'portal:wasm:signProgress',
281
314
  progressCallback: progress,
315
+ traceId: resolvedTraceId,
282
316
  })
283
317
  }
284
318
 
@@ -360,12 +394,14 @@ class Mpc {
360
394
  to: string,
361
395
  token: string,
362
396
  amount: string,
397
+ traceId?: string,
363
398
  ): Promise<BuiltTransaction> {
364
399
  return this.handleRequestToIframeAndPost({
365
400
  methodMessage: 'portal:buildTransaction',
366
401
  errorMessage: 'portal:buildTransactionError',
367
402
  resultMessage: 'portal:buildTransactionResult',
368
403
  data: { chainId, to, token, amount },
404
+ traceId,
369
405
  })
370
406
  }
371
407
 
@@ -879,6 +915,7 @@ class Mpc {
879
915
  progressMessage,
880
916
  progressCallback,
881
917
  mapReturnValue,
918
+ traceId: providedTraceId,
882
919
  }: {
883
920
  methodMessage: string
884
921
  errorMessage: string
@@ -887,7 +924,16 @@ class Mpc {
887
924
  progressMessage?: string
888
925
  progressCallback?: (status: MpcStatus) => void
889
926
  mapReturnValue?: (result: any) => ResponseType
927
+ traceId?: string
890
928
  }): Promise<ResponseType> {
929
+ // Single traceId per operation: use provided or generate. Propagates to iframe and all internal REST/MPC calls.
930
+ const traceId = providedTraceId ?? generateTraceId()
931
+ sdkLogger.debug(
932
+ '[Portal] request traceId:',
933
+ traceId,
934
+ 'method:',
935
+ methodMessage,
936
+ )
891
937
  return new Promise((resolve, reject) => {
892
938
  const handleRequest = (event: MessageEvent<WorkerResult>) => {
893
939
  const { type, data: result } = event.data
@@ -920,10 +966,11 @@ class Mpc {
920
966
  // Bind the function to the message event
921
967
  window.addEventListener('message', handleRequest)
922
968
 
923
- // Send the request to the iframe
969
+ // Send the request to the iframe (traceId at top-level so data shape stays unchanged)
924
970
  this.postMessage({
925
971
  type: methodMessage,
926
972
  data,
973
+ traceId,
927
974
  })
928
975
  })
929
976
  }
@@ -960,6 +1007,7 @@ class Mpc {
960
1007
  mpcHost: this.portal.mpcHost,
961
1008
  mpcVersion: this.portal.mpcVersion,
962
1009
  featureFlags: this.portal.featureFlags,
1010
+ logLevel: this.portal.getLogLevel(),
963
1011
  }
964
1012
 
965
1013
  const message = {
@@ -969,9 +1017,29 @@ class Mpc {
969
1017
 
970
1018
  this.postMessage(message)
971
1019
 
1020
+ this.setupPresignatureLogForwarding()
972
1021
  this.waitForReadyMessage()
973
1022
  }
974
1023
 
1024
+ private setupPresignatureLogForwarding() {
1025
+ if (this.presignatureLogHandler) return
1026
+ const handler = (event: MessageEvent) => {
1027
+ if (event.origin !== this.getOrigin()) return
1028
+ const { type, data } = event.data || {}
1029
+ if (type === 'portal:presignature:log' && data?.message) {
1030
+ sdkLogger.warn(data.message)
1031
+ }
1032
+ }
1033
+ this.presignatureLogHandler = handler
1034
+ window.addEventListener('message', handler)
1035
+ }
1036
+
1037
+ private teardownPresignatureLogForwarding() {
1038
+ if (!this.presignatureLogHandler) return
1039
+ window.removeEventListener('message', this.presignatureLogHandler)
1040
+ this.presignatureLogHandler = null
1041
+ }
1042
+
975
1043
  private getOrigin(): string {
976
1044
  const host = this.portal.host
977
1045
  const origin = host.startsWith('localhost:')
@@ -1028,7 +1096,7 @@ class Mpc {
1028
1096
  })
1029
1097
  }
1030
1098
 
1031
- private postMessage(event: { type: string; data: any }) {
1099
+ private postMessage(event: { type: string; data: any; traceId?: string }) {
1032
1100
  this.iframe?.contentWindow?.postMessage(event, this.getOrigin())
1033
1101
  }
1034
1102
 
@@ -50,7 +50,8 @@ describe('EvmAccountType', () => {
50
50
  const { type, data } = message
51
51
 
52
52
  expect(type).toEqual('portal:evmAccountType:getStatus')
53
- expect(data).toEqual(args)
53
+ expect(data).toMatchObject(args)
54
+ expect(typeof message.traceId).toBe('string')
54
55
  expect(origin).toEqual(mockHostOrigin)
55
56
 
56
57
  window.dispatchEvent(
@@ -83,7 +84,8 @@ describe('EvmAccountType', () => {
83
84
  const { type, data } = message
84
85
 
85
86
  expect(type).toEqual('portal:evmAccountType:getStatus')
86
- expect(data).toEqual(args)
87
+ expect(data).toMatchObject(args)
88
+ expect(typeof message.traceId).toBe('string')
87
89
  expect(origin).toEqual(mockHostOrigin)
88
90
 
89
91
  window.dispatchEvent(
@@ -130,7 +132,8 @@ describe('EvmAccountType', () => {
130
132
  const { type, data } = message
131
133
 
132
134
  expect(type).toEqual('portal:evmAccountType:getAddresses')
133
- expect(data).toEqual(args)
135
+ expect(data).toMatchObject(args)
136
+ expect(typeof message.traceId).toBe('string')
134
137
  expect(origin).toEqual(mockHostOrigin)
135
138
 
136
139
  window.dispatchEvent(
@@ -163,7 +166,8 @@ describe('EvmAccountType', () => {
163
166
  const { type, data } = message
164
167
 
165
168
  expect(type).toEqual('portal:evmAccountType:getAddresses')
166
- expect(data).toEqual(args)
169
+ expect(data).toMatchObject(args)
170
+ expect(typeof message.traceId).toBe('string')
167
171
  expect(origin).toEqual(mockHostOrigin)
168
172
 
169
173
  window.dispatchEvent(
@@ -189,6 +193,7 @@ describe('EvmAccountType', () => {
189
193
  .catch((error) => {
190
194
  expect(error).toBeInstanceOf(PortalMpcError)
191
195
  expect(error.message).toEqual('test')
196
+ expect(error.code).toEqual(1)
192
197
  done()
193
198
  })
194
199
  })
@@ -209,7 +214,8 @@ describe('EvmAccountType', () => {
209
214
  const { type, data } = message
210
215
 
211
216
  expect(type).toEqual('portal:evmAccountType:buildAuthorizationList')
212
- expect(data).toEqual(args)
217
+ expect(data).toMatchObject(args)
218
+ expect(typeof message.traceId).toBe('string')
213
219
  expect(origin).toEqual(mockHostOrigin)
214
220
 
215
221
  window.dispatchEvent(
@@ -249,7 +255,8 @@ describe('EvmAccountType', () => {
249
255
  expect(type).toEqual(
250
256
  'portal:evmAccountType:buildAuthorizationList',
251
257
  )
252
- expect(data).toEqual(argsWithoutSubsidize)
258
+ expect(data).toMatchObject(argsWithoutSubsidize)
259
+ expect(typeof message.traceId).toBe('string')
253
260
  expect(origin).toEqual(mockHostOrigin)
254
261
 
255
262
  window.dispatchEvent(
@@ -290,7 +297,8 @@ describe('EvmAccountType', () => {
290
297
  expect(type).toEqual(
291
298
  'portal:evmAccountType:buildAuthorizationList',
292
299
  )
293
- expect(data).toEqual(args)
300
+ expect(data).toMatchObject(args)
301
+ expect(typeof message.traceId).toBe('string')
294
302
  expect(origin).toEqual(mockHostOrigin)
295
303
 
296
304
  window.dispatchEvent(
@@ -337,7 +345,8 @@ describe('EvmAccountType', () => {
337
345
  const { type, data } = message
338
346
 
339
347
  expect(type).toEqual('portal:evmAccountType:build7702UpgradeTx')
340
- expect(data).toEqual(args)
348
+ expect(data).toMatchObject(args)
349
+ expect(typeof message.traceId).toBe('string')
341
350
  expect(origin).toEqual(mockHostOrigin)
342
351
 
343
352
  window.dispatchEvent(
@@ -378,7 +387,8 @@ describe('EvmAccountType', () => {
378
387
  expect(type).toEqual(
379
388
  'portal:evmAccountType:build7702UpgradeTx',
380
389
  )
381
- expect(data).toEqual(argsWithoutSubsidize)
390
+ expect(data).toMatchObject(argsWithoutSubsidize)
391
+ expect(typeof message.traceId).toBe('string')
382
392
  expect(origin).toEqual(mockHostOrigin)
383
393
 
384
394
  window.dispatchEvent(
@@ -419,7 +429,8 @@ describe('EvmAccountType', () => {
419
429
  expect(type).toEqual(
420
430
  'portal:evmAccountType:build7702UpgradeTx',
421
431
  )
422
- expect(data).toEqual(args)
432
+ expect(data).toMatchObject(args)
433
+ expect(typeof message.traceId).toBe('string')
423
434
  expect(origin).toEqual(mockHostOrigin)
424
435
 
425
436
  window.dispatchEvent(
@@ -468,7 +479,8 @@ describe('EvmAccountType', () => {
468
479
  const { type, data } = message
469
480
 
470
481
  if (type === 'portal:evmAccountType:getStatus') {
471
- expect(data).toEqual({ chain: args.chain })
482
+ expect(data).toMatchObject({ chain: args.chain })
483
+ expect(typeof message.traceId).toBe('string')
472
484
  window.dispatchEvent(
473
485
  new MessageEvent('message', {
474
486
  origin: mockHostOrigin,
@@ -479,7 +491,8 @@ describe('EvmAccountType', () => {
479
491
  }),
480
492
  )
481
493
  } else if (type === 'portal:evmAccountType:buildAuthorizationList') {
482
- expect(data).toEqual({ chain: args.chain, subsidize: true })
494
+ expect(data).toMatchObject({ chain: args.chain, subsidize: true })
495
+ expect(typeof message.traceId).toBe('string')
483
496
  window.dispatchEvent(
484
497
  new MessageEvent('message', {
485
498
  origin: mockHostOrigin,
@@ -9,6 +9,13 @@ import { ProviderRpcError, RpcErrorCodes } from './errors'
9
9
  import { RequestMethod } from '../'
10
10
  import mpcMock from '../__mocks/portal/mpc'
11
11
  import { sdkLogger } from '../logger'
12
+ import { X_PORTAL_TRACE_ID_HEADER } from '../shared/trace'
13
+
14
+ jest.mock('../shared/trace', () => ({
15
+ ...jest.requireActual('../shared/trace'),
16
+ generateTraceId: jest.fn(() => 'mock-trace-id-12345'),
17
+ X_PORTAL_TRACE_ID_HEADER: 'X-Portal-Trace-Id',
18
+ }))
12
19
 
13
20
  describe('Provider', () => {
14
21
  beforeEach(() => {
@@ -221,6 +228,7 @@ describe('Provider', () => {
221
228
  method: RequestMethod.eth_sendTransaction,
222
229
  params: 'test',
223
230
  rpcUrl: mockRpcUrl,
231
+ traceId: 'mock-trace-id-12345',
224
232
  })
225
233
  expect(provider.emit).toHaveBeenCalledWith(
226
234
  'portal_signatureReceived',
@@ -276,6 +284,7 @@ describe('Provider', () => {
276
284
  method: RequestMethod.eth_signTransaction,
277
285
  params: 'test',
278
286
  rpcUrl: mockRpcUrl,
287
+ traceId: 'mock-trace-id-12345',
279
288
  })
280
289
  expect(provider.emit).toHaveBeenCalledWith(
281
290
  'portal_signatureReceived',
@@ -331,6 +340,7 @@ describe('Provider', () => {
331
340
  method: RequestMethod.eth_sign,
332
341
  params: ['test'],
333
342
  rpcUrl: mockRpcUrl,
343
+ traceId: 'mock-trace-id-12345',
334
344
  })
335
345
  expect(provider.emit).toHaveBeenCalledWith(
336
346
  'portal_signatureReceived',
@@ -386,6 +396,7 @@ describe('Provider', () => {
386
396
  method: RequestMethod.eth_signTypedData_v3,
387
397
  params: ['test'],
388
398
  rpcUrl: mockRpcUrl,
399
+ traceId: 'mock-trace-id-12345',
389
400
  })
390
401
  expect(provider.emit).toHaveBeenCalledWith(
391
402
  'portal_signatureReceived',
@@ -441,6 +452,7 @@ describe('Provider', () => {
441
452
  method: RequestMethod.eth_signTypedData_v4,
442
453
  params: ['test'],
443
454
  rpcUrl: mockRpcUrl,
455
+ traceId: 'mock-trace-id-12345',
444
456
  })
445
457
  expect(provider.emit).toHaveBeenCalledWith(
446
458
  'portal_signatureReceived',
@@ -496,6 +508,7 @@ describe('Provider', () => {
496
508
  method: RequestMethod.personal_sign,
497
509
  params: ['test'],
498
510
  rpcUrl: mockRpcUrl,
511
+ traceId: 'mock-trace-id-12345',
499
512
  })
500
513
  expect(provider.emit).toHaveBeenCalledWith(
501
514
  'portal_signatureReceived',
@@ -551,6 +564,7 @@ describe('Provider', () => {
551
564
  method: RequestMethod.sol_signAndConfirmTransaction,
552
565
  params: ['test'],
553
566
  rpcUrl: mockRpcUrl,
567
+ traceId: 'mock-trace-id-12345',
554
568
  })
555
569
  expect(provider.emit).toHaveBeenCalledWith(
556
570
  'portal_signatureReceived',
@@ -606,6 +620,7 @@ describe('Provider', () => {
606
620
  method: RequestMethod.sol_signAndSendTransaction,
607
621
  params: ['test'],
608
622
  rpcUrl: mockRpcUrl,
623
+ traceId: 'mock-trace-id-12345',
609
624
  })
610
625
  expect(provider.emit).toHaveBeenCalledWith(
611
626
  'portal_signatureReceived',
@@ -661,6 +676,7 @@ describe('Provider', () => {
661
676
  method: RequestMethod.sol_signMessage,
662
677
  params: ['test'],
663
678
  rpcUrl: mockRpcUrl,
679
+ traceId: 'mock-trace-id-12345',
664
680
  })
665
681
  expect(provider.emit).toHaveBeenCalledWith(
666
682
  'portal_signatureReceived',
@@ -716,6 +732,7 @@ describe('Provider', () => {
716
732
  method: RequestMethod.sol_signTransaction,
717
733
  params: ['test'],
718
734
  rpcUrl: mockRpcUrl,
735
+ traceId: 'mock-trace-id-12345',
719
736
  })
720
737
  expect(provider.emit).toHaveBeenCalledWith(
721
738
  'portal_signatureReceived',
@@ -830,6 +847,7 @@ describe('Provider', () => {
830
847
  method: RequestMethod.eth_sendTransaction,
831
848
  params: 'test',
832
849
  rpcUrl: mockRpcUrl,
850
+ traceId: 'mock-trace-id-12345',
833
851
  })
834
852
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
835
853
  chainId: 'eip155:1',
@@ -887,6 +905,7 @@ describe('Provider', () => {
887
905
  method: RequestMethod.eth_signTransaction,
888
906
  params: 'test',
889
907
  rpcUrl: mockRpcUrl,
908
+ traceId: 'mock-trace-id-12345',
890
909
  })
891
910
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
892
911
  chainId: 'eip155:1',
@@ -944,6 +963,7 @@ describe('Provider', () => {
944
963
  method: RequestMethod.eth_sign,
945
964
  params: ['test'],
946
965
  rpcUrl: mockRpcUrl,
966
+ traceId: 'mock-trace-id-12345',
947
967
  })
948
968
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
949
969
  chainId: 'eip155:1',
@@ -1001,6 +1021,7 @@ describe('Provider', () => {
1001
1021
  method: RequestMethod.eth_signTypedData_v3,
1002
1022
  params: ['test'],
1003
1023
  rpcUrl: mockRpcUrl,
1024
+ traceId: 'mock-trace-id-12345',
1004
1025
  })
1005
1026
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1006
1027
  chainId: 'eip155:1',
@@ -1058,6 +1079,7 @@ describe('Provider', () => {
1058
1079
  method: RequestMethod.eth_signTypedData_v4,
1059
1080
  params: ['test'],
1060
1081
  rpcUrl: mockRpcUrl,
1082
+ traceId: 'mock-trace-id-12345',
1061
1083
  })
1062
1084
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1063
1085
  chainId: 'eip155:1',
@@ -1115,6 +1137,7 @@ describe('Provider', () => {
1115
1137
  method: RequestMethod.personal_sign,
1116
1138
  params: ['test'],
1117
1139
  rpcUrl: mockRpcUrl,
1140
+ traceId: 'mock-trace-id-12345',
1118
1141
  })
1119
1142
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1120
1143
  chainId: 'eip155:1',
@@ -1172,6 +1195,7 @@ describe('Provider', () => {
1172
1195
  method: RequestMethod.sol_signAndConfirmTransaction,
1173
1196
  params: ['test'],
1174
1197
  rpcUrl: mockRpcUrl,
1198
+ traceId: 'mock-trace-id-12345',
1175
1199
  })
1176
1200
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1177
1201
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -1229,6 +1253,7 @@ describe('Provider', () => {
1229
1253
  method: RequestMethod.sol_signAndSendTransaction,
1230
1254
  params: ['test'],
1231
1255
  rpcUrl: mockRpcUrl,
1256
+ traceId: 'mock-trace-id-12345',
1232
1257
  })
1233
1258
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1234
1259
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -1286,6 +1311,7 @@ describe('Provider', () => {
1286
1311
  method: RequestMethod.sol_signMessage,
1287
1312
  params: ['test'],
1288
1313
  rpcUrl: mockRpcUrl,
1314
+ traceId: 'mock-trace-id-12345',
1289
1315
  })
1290
1316
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1291
1317
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -1343,6 +1369,7 @@ describe('Provider', () => {
1343
1369
  method: RequestMethod.sol_signTransaction,
1344
1370
  params: ['test'],
1345
1371
  rpcUrl: mockRpcUrl,
1372
+ traceId: 'mock-trace-id-12345',
1346
1373
  })
1347
1374
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1348
1375
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -1393,9 +1420,10 @@ describe('Provider', () => {
1393
1420
  expect(result).toEqual('test')
1394
1421
  expect(global.fetch).toHaveBeenCalledWith(mockRpcUrl, {
1395
1422
  method: 'POST',
1396
- headers: {
1423
+ headers: expect.objectContaining({
1397
1424
  'Content-Type': 'application/json',
1398
- },
1425
+ [X_PORTAL_TRACE_ID_HEADER]: 'mock-trace-id-12345',
1426
+ }),
1399
1427
  body: JSON.stringify({
1400
1428
  jsonrpc: '2.0',
1401
1429
  id: '0',
@@ -2,6 +2,7 @@ import { ProviderRpcError, RpcErrorCodes } from './errors'
2
2
 
3
3
  import Portal from '../index'
4
4
  import { sdkLogger } from '../logger'
5
+ import { generateTraceId, X_PORTAL_TRACE_ID_HEADER } from '../shared/trace'
5
6
 
6
7
  import type {
7
8
  EventHandler,
@@ -251,7 +252,13 @@ class Provider {
251
252
  params,
252
253
  sponsorGas,
253
254
  signatureApprovalMemo,
255
+ traceId: providedTraceId,
254
256
  }: RequestArguments): Promise<any> {
257
+ const traceId = providedTraceId ?? generateTraceId()
258
+ sdkLogger.info(
259
+ `[PortalProvider] request started | method=${method} | traceId=${traceId}`,
260
+ )
261
+
255
262
  const isSignerMethod = signerMethods.includes(method)
256
263
  const chainId = this.getCAIP2ChainId(requestChainId)
257
264
 
@@ -261,6 +268,7 @@ class Provider {
261
268
  chainId,
262
269
  method,
263
270
  params,
271
+ traceId,
264
272
  })
265
273
 
266
274
  this.emit('portal_signatureReceived', {
@@ -283,6 +291,7 @@ class Provider {
283
291
  params,
284
292
  sponsorGas,
285
293
  signatureApprovalMemo,
294
+ traceId,
286
295
  })
287
296
 
288
297
  if (transactionHash) {
@@ -408,8 +417,8 @@ class Provider {
408
417
  chainId,
409
418
  method,
410
419
  params,
420
+ traceId,
411
421
  }: RequestArguments): Promise<any> {
412
- // Prepare the request body
413
422
  const requestBody = {
414
423
  body: JSON.stringify({
415
424
  jsonrpc: '2.0',
@@ -420,10 +429,10 @@ class Provider {
420
429
  method: 'POST',
421
430
  headers: {
422
431
  'Content-Type': 'application/json',
432
+ ...(traceId && { [X_PORTAL_TRACE_ID_HEADER]: traceId }),
423
433
  },
424
434
  }
425
435
 
426
- // Pass request off to the gateway
427
436
  const result = await fetch(this.portal.getRpcUrl(chainId), requestBody)
428
437
 
429
438
  return result.json()
@@ -441,6 +450,7 @@ class Provider {
441
450
  params,
442
451
  sponsorGas,
443
452
  signatureApprovalMemo,
453
+ traceId,
444
454
  }: RequestArguments): Promise<any> {
445
455
  const isApproved = passiveSignerMethods.includes(method)
446
456
  ? true
@@ -480,6 +490,7 @@ class Provider {
480
490
  ...(signatureApprovalMemo !== undefined && {
481
491
  signatureApprovalMemo,
482
492
  }),
493
+ traceId,
483
494
  })
484
495
  return result
485
496
  }
@@ -494,7 +505,10 @@ class Provider {
494
505
  params: this.buildParams(method, params),
495
506
  rpcUrl: this.portal.getRpcUrl(chainId),
496
507
  sponsorGas,
497
- signatureApprovalMemo,
508
+ traceId,
509
+ ...(signatureApprovalMemo !== undefined && {
510
+ signatureApprovalMemo,
511
+ }),
498
512
  })
499
513
  return result
500
514
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Configurable logger for the iframe SDK. Respects logLevel so that when the
3
+ * parent sets logLevel to 'none', no messages are written to the console.
4
+ */
5
+
6
+ export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug'
7
+
8
+ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
9
+ none: 0,
10
+ error: 1,
11
+ warn: 2,
12
+ info: 3,
13
+ debug: 4,
14
+ }
15
+
16
+ interface SdkLoggerInterface {
17
+ debug(message: string, ...args: unknown[]): void
18
+ warn(message: string, ...args: unknown[]): void
19
+ error(message: string, ...args: unknown[]): void
20
+ configure(logLevel: LogLevel): void
21
+ getLogLevel(): LogLevel
22
+ }
23
+
24
+ class SdkLoggerImpl implements SdkLoggerInterface {
25
+ private logLevel: LogLevel = 'none'
26
+
27
+ configure(logLevel: LogLevel): void {
28
+ this.logLevel = logLevel
29
+ }
30
+
31
+ getLogLevel(): LogLevel {
32
+ return this.logLevel
33
+ }
34
+
35
+ private shouldLog(level: Exclude<LogLevel, 'none'>): boolean {
36
+ return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.logLevel]
37
+ }
38
+
39
+ private log(level: 'debug' | 'warn' | 'error', message: string, ...args: unknown[]): void {
40
+ if (!this.shouldLog(level)) return
41
+ if (typeof console === 'undefined') return
42
+ const prefix = `[PortalMpc] ${message}`
43
+ try {
44
+ if (level === 'debug' && console.debug) {
45
+ console.debug(prefix, ...args)
46
+ } else if (level === 'warn' && console.warn) {
47
+ console.warn(prefix, ...args)
48
+ } else if (level === 'error' && console.error) {
49
+ console.error(prefix, ...args)
50
+ }
51
+ } catch {
52
+ // no-op if console is unavailable or throws
53
+ }
54
+ }
55
+
56
+ debug(message: string, ...args: unknown[]): void {
57
+ this.log('debug', message, ...args)
58
+ }
59
+
60
+ warn(message: string, ...args: unknown[]): void {
61
+ this.log('warn', message, ...args)
62
+ }
63
+
64
+ error(message: string, ...args: unknown[]): void {
65
+ this.log('error', message, ...args)
66
+ }
67
+ }
68
+
69
+ export const sdkLogger = new SdkLoggerImpl()