@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.
@@ -31,7 +31,8 @@ describe('EvmAccountType', () => {
31
31
  .mockImplementation((message, origin) => {
32
32
  const { type, data } = message;
33
33
  expect(type).toEqual('portal:evmAccountType:getStatus');
34
- expect(data).toEqual(args);
34
+ expect(data).toMatchObject(args);
35
+ expect(typeof message.traceId).toBe('string');
35
36
  expect(origin).toEqual(mockHostOrigin);
36
37
  window.dispatchEvent(new MessageEvent('message', {
37
38
  origin: mockHostOrigin,
@@ -59,7 +60,8 @@ describe('EvmAccountType', () => {
59
60
  .mockImplementationOnce((message, origin) => {
60
61
  const { type, data } = message;
61
62
  expect(type).toEqual('portal:evmAccountType:getStatus');
62
- expect(data).toEqual(args);
63
+ expect(data).toMatchObject(args);
64
+ expect(typeof message.traceId).toBe('string');
63
65
  expect(origin).toEqual(mockHostOrigin);
64
66
  window.dispatchEvent(new MessageEvent('message', {
65
67
  origin: mockHostOrigin,
@@ -99,7 +101,8 @@ describe('EvmAccountType', () => {
99
101
  .mockImplementation((message, origin) => {
100
102
  const { type, data } = message;
101
103
  expect(type).toEqual('portal:evmAccountType:getAddresses');
102
- expect(data).toEqual(args);
104
+ expect(data).toMatchObject(args);
105
+ expect(typeof message.traceId).toBe('string');
103
106
  expect(origin).toEqual(mockHostOrigin);
104
107
  window.dispatchEvent(new MessageEvent('message', {
105
108
  origin: mockHostOrigin,
@@ -127,7 +130,8 @@ describe('EvmAccountType', () => {
127
130
  .mockImplementationOnce((message, origin) => {
128
131
  const { type, data } = message;
129
132
  expect(type).toEqual('portal:evmAccountType:getAddresses');
130
- expect(data).toEqual(args);
133
+ expect(data).toMatchObject(args);
134
+ expect(typeof message.traceId).toBe('string');
131
135
  expect(origin).toEqual(mockHostOrigin);
132
136
  window.dispatchEvent(new MessageEvent('message', {
133
137
  origin: mockHostOrigin,
@@ -149,6 +153,7 @@ describe('EvmAccountType', () => {
149
153
  .catch((error) => {
150
154
  expect(error).toBeInstanceOf(PortalMpcError);
151
155
  expect(error.message).toEqual('test');
156
+ expect(error.code).toEqual(1);
152
157
  done();
153
158
  });
154
159
  });
@@ -166,7 +171,8 @@ describe('EvmAccountType', () => {
166
171
  .mockImplementation((message, origin) => {
167
172
  const { type, data } = message;
168
173
  expect(type).toEqual('portal:evmAccountType:buildAuthorizationList');
169
- expect(data).toEqual(args);
174
+ expect(data).toMatchObject(args);
175
+ expect(typeof message.traceId).toBe('string');
170
176
  expect(origin).toEqual(mockHostOrigin);
171
177
  window.dispatchEvent(new MessageEvent('message', {
172
178
  origin: mockHostOrigin,
@@ -198,7 +204,8 @@ describe('EvmAccountType', () => {
198
204
  .mockImplementation((message, origin) => {
199
205
  const { type, data } = message;
200
206
  expect(type).toEqual('portal:evmAccountType:buildAuthorizationList');
201
- expect(data).toEqual(argsWithoutSubsidize);
207
+ expect(data).toMatchObject(argsWithoutSubsidize);
208
+ expect(typeof message.traceId).toBe('string');
202
209
  expect(origin).toEqual(mockHostOrigin);
203
210
  window.dispatchEvent(new MessageEvent('message', {
204
211
  origin: mockHostOrigin,
@@ -226,7 +233,8 @@ describe('EvmAccountType', () => {
226
233
  .mockImplementationOnce((message, origin) => {
227
234
  const { type, data } = message;
228
235
  expect(type).toEqual('portal:evmAccountType:buildAuthorizationList');
229
- expect(data).toEqual(args);
236
+ expect(data).toMatchObject(args);
237
+ expect(typeof message.traceId).toBe('string');
230
238
  expect(origin).toEqual(mockHostOrigin);
231
239
  window.dispatchEvent(new MessageEvent('message', {
232
240
  origin: mockHostOrigin,
@@ -266,7 +274,8 @@ describe('EvmAccountType', () => {
266
274
  .mockImplementation((message, origin) => {
267
275
  const { type, data } = message;
268
276
  expect(type).toEqual('portal:evmAccountType:build7702UpgradeTx');
269
- expect(data).toEqual(args);
277
+ expect(data).toMatchObject(args);
278
+ expect(typeof message.traceId).toBe('string');
270
279
  expect(origin).toEqual(mockHostOrigin);
271
280
  window.dispatchEvent(new MessageEvent('message', {
272
281
  origin: mockHostOrigin,
@@ -299,7 +308,8 @@ describe('EvmAccountType', () => {
299
308
  .mockImplementation((message, origin) => {
300
309
  const { type, data } = message;
301
310
  expect(type).toEqual('portal:evmAccountType:build7702UpgradeTx');
302
- expect(data).toEqual(argsWithoutSubsidize);
311
+ expect(data).toMatchObject(argsWithoutSubsidize);
312
+ expect(typeof message.traceId).toBe('string');
303
313
  expect(origin).toEqual(mockHostOrigin);
304
314
  window.dispatchEvent(new MessageEvent('message', {
305
315
  origin: mockHostOrigin,
@@ -327,7 +337,8 @@ describe('EvmAccountType', () => {
327
337
  .mockImplementationOnce((message, origin) => {
328
338
  const { type, data } = message;
329
339
  expect(type).toEqual('portal:evmAccountType:build7702UpgradeTx');
330
- expect(data).toEqual(args);
340
+ expect(data).toMatchObject(args);
341
+ expect(typeof message.traceId).toBe('string');
331
342
  expect(origin).toEqual(mockHostOrigin);
332
343
  window.dispatchEvent(new MessageEvent('message', {
333
344
  origin: mockHostOrigin,
@@ -367,7 +378,8 @@ describe('EvmAccountType', () => {
367
378
  .mockImplementation((message, _origin) => {
368
379
  const { type, data } = message;
369
380
  if (type === 'portal:evmAccountType:getStatus') {
370
- expect(data).toEqual({ chain: args.chain });
381
+ expect(data).toMatchObject({ chain: args.chain });
382
+ expect(typeof message.traceId).toBe('string');
371
383
  window.dispatchEvent(new MessageEvent('message', {
372
384
  origin: mockHostOrigin,
373
385
  data: {
@@ -377,7 +389,8 @@ describe('EvmAccountType', () => {
377
389
  }));
378
390
  }
379
391
  else if (type === 'portal:evmAccountType:buildAuthorizationList') {
380
- expect(data).toEqual({ chain: args.chain, subsidize: true });
392
+ expect(data).toMatchObject({ chain: args.chain, subsidize: true });
393
+ expect(typeof message.traceId).toBe('string');
381
394
  window.dispatchEvent(new MessageEvent('message', {
382
395
  origin: mockHostOrigin,
383
396
  data: {
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { ProviderRpcError, RpcErrorCodes } from './errors';
11
11
  import { sdkLogger } from '../logger';
12
+ import { generateTraceId, X_PORTAL_TRACE_ID_HEADER } from '../shared/trace';
12
13
  export var RequestMethod;
13
14
  (function (RequestMethod) {
14
15
  RequestMethod["eth_accounts"] = "eth_accounts";
@@ -258,8 +259,10 @@ class Provider {
258
259
  * @param args The arguments of the request being made
259
260
  * @returns Promise<any>
260
261
  */
261
- request({ chainId: requestChainId, method, params, sponsorGas, signatureApprovalMemo, }) {
262
+ request({ chainId: requestChainId, method, params, sponsorGas, signatureApprovalMemo, traceId: providedTraceId, }) {
262
263
  return __awaiter(this, void 0, void 0, function* () {
264
+ const traceId = providedTraceId !== null && providedTraceId !== void 0 ? providedTraceId : generateTraceId();
265
+ sdkLogger.info(`[PortalProvider] request started | method=${method} | traceId=${traceId}`);
263
266
  const isSignerMethod = signerMethods.includes(method);
264
267
  const chainId = this.getCAIP2ChainId(requestChainId);
265
268
  if (!isSignerMethod && !method.startsWith('wallet_')) {
@@ -268,6 +271,7 @@ class Provider {
268
271
  chainId,
269
272
  method,
270
273
  params,
274
+ traceId,
271
275
  });
272
276
  this.emit('portal_signatureReceived', {
273
277
  chainId,
@@ -288,6 +292,7 @@ class Provider {
288
292
  params,
289
293
  sponsorGas,
290
294
  signatureApprovalMemo,
295
+ traceId,
291
296
  });
292
297
  if (transactionHash) {
293
298
  this.emit('portal_signatureReceived', {
@@ -377,9 +382,8 @@ class Provider {
377
382
  * @param args The arguments of the request being made
378
383
  * @returns Promise<any>
379
384
  */
380
- handleGatewayRequest({ chainId, method, params, }) {
385
+ handleGatewayRequest({ chainId, method, params, traceId, }) {
381
386
  return __awaiter(this, void 0, void 0, function* () {
382
- // Prepare the request body
383
387
  const requestBody = {
384
388
  body: JSON.stringify({
385
389
  jsonrpc: '2.0',
@@ -388,11 +392,8 @@ class Provider {
388
392
  params,
389
393
  }),
390
394
  method: 'POST',
391
- headers: {
392
- 'Content-Type': 'application/json',
393
- },
395
+ headers: Object.assign({ 'Content-Type': 'application/json' }, (traceId && { [X_PORTAL_TRACE_ID_HEADER]: traceId })),
394
396
  };
395
- // Pass request off to the gateway
396
397
  const result = yield fetch(this.portal.getRpcUrl(chainId), requestBody);
397
398
  return result.json();
398
399
  });
@@ -403,7 +404,7 @@ class Provider {
403
404
  * @param args The arguments of the request being made
404
405
  * @returns Promise<any>
405
406
  */
406
- handleSigningRequest({ chainId, method, params, sponsorGas, signatureApprovalMemo, }) {
407
+ handleSigningRequest({ chainId, method, params, sponsorGas, signatureApprovalMemo, traceId, }) {
407
408
  return __awaiter(this, void 0, void 0, function* () {
408
409
  const isApproved = passiveSignerMethods.includes(method)
409
410
  ? true
@@ -430,9 +431,9 @@ class Provider {
430
431
  case RequestMethod.eth_signTypedData_v4:
431
432
  case RequestMethod.personal_sign: {
432
433
  this.enforceEip155ChainId(chainId);
433
- const result = yield this.portal.mpc.sign(Object.assign({ chainId: chainId, method, params: this.buildParams(method, params), rpcUrl: this.portal.getRpcUrl(chainId), sponsorGas }, (signatureApprovalMemo !== undefined && {
434
+ const result = yield this.portal.mpc.sign(Object.assign(Object.assign({ chainId: chainId, method, params: this.buildParams(method, params), rpcUrl: this.portal.getRpcUrl(chainId), sponsorGas }, (signatureApprovalMemo !== undefined && {
434
435
  signatureApprovalMemo,
435
- })));
436
+ })), { traceId }));
436
437
  return result;
437
438
  }
438
439
  case RequestMethod.sol_signAndConfirmTransaction:
@@ -440,14 +441,10 @@ class Provider {
440
441
  case RequestMethod.sol_signMessage:
441
442
  case RequestMethod.sol_signTransaction: {
442
443
  this.enforceSolanaChainId(chainId);
443
- const result = yield this.portal.mpc.sign({
444
- chainId: chainId,
445
- method,
446
- params: this.buildParams(method, params),
447
- rpcUrl: this.portal.getRpcUrl(chainId),
448
- sponsorGas,
444
+ const result = yield this.portal.mpc.sign(Object.assign({ chainId: chainId, method, params: this.buildParams(method, params), rpcUrl: this.portal.getRpcUrl(chainId), sponsorGas,
445
+ traceId }, (signatureApprovalMemo !== undefined && {
449
446
  signatureApprovalMemo,
450
- });
447
+ })));
451
448
  return result;
452
449
  }
453
450
  default:
@@ -17,6 +17,8 @@ import { ProviderRpcError, RpcErrorCodes } from './errors';
17
17
  import { RequestMethod } from '../';
18
18
  import mpcMock from '../__mocks/portal/mpc';
19
19
  import { sdkLogger } from '../logger';
20
+ import { X_PORTAL_TRACE_ID_HEADER } from '../shared/trace';
21
+ jest.mock('../shared/trace', () => (Object.assign(Object.assign({}, jest.requireActual('../shared/trace')), { generateTraceId: jest.fn(() => 'mock-trace-id-12345'), X_PORTAL_TRACE_ID_HEADER: 'X-Portal-Trace-Id' })));
20
22
  describe('Provider', () => {
21
23
  beforeEach(() => {
22
24
  portal.autoApprove = true;
@@ -152,6 +154,7 @@ describe('Provider', () => {
152
154
  method: RequestMethod.eth_sendTransaction,
153
155
  params: 'test',
154
156
  rpcUrl: mockRpcUrl,
157
+ traceId: 'mock-trace-id-12345',
155
158
  });
156
159
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
157
160
  chainId: 'eip155:1',
@@ -188,6 +191,7 @@ describe('Provider', () => {
188
191
  method: RequestMethod.eth_signTransaction,
189
192
  params: 'test',
190
193
  rpcUrl: mockRpcUrl,
194
+ traceId: 'mock-trace-id-12345',
191
195
  });
192
196
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
193
197
  chainId: 'eip155:1',
@@ -224,6 +228,7 @@ describe('Provider', () => {
224
228
  method: RequestMethod.eth_sign,
225
229
  params: ['test'],
226
230
  rpcUrl: mockRpcUrl,
231
+ traceId: 'mock-trace-id-12345',
227
232
  });
228
233
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
229
234
  chainId: 'eip155:1',
@@ -260,6 +265,7 @@ describe('Provider', () => {
260
265
  method: RequestMethod.eth_signTypedData_v3,
261
266
  params: ['test'],
262
267
  rpcUrl: mockRpcUrl,
268
+ traceId: 'mock-trace-id-12345',
263
269
  });
264
270
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
265
271
  chainId: 'eip155:1',
@@ -296,6 +302,7 @@ describe('Provider', () => {
296
302
  method: RequestMethod.eth_signTypedData_v4,
297
303
  params: ['test'],
298
304
  rpcUrl: mockRpcUrl,
305
+ traceId: 'mock-trace-id-12345',
299
306
  });
300
307
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
301
308
  chainId: 'eip155:1',
@@ -332,6 +339,7 @@ describe('Provider', () => {
332
339
  method: RequestMethod.personal_sign,
333
340
  params: ['test'],
334
341
  rpcUrl: mockRpcUrl,
342
+ traceId: 'mock-trace-id-12345',
335
343
  });
336
344
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
337
345
  chainId: 'eip155:1',
@@ -368,6 +376,7 @@ describe('Provider', () => {
368
376
  method: RequestMethod.sol_signAndConfirmTransaction,
369
377
  params: ['test'],
370
378
  rpcUrl: mockRpcUrl,
379
+ traceId: 'mock-trace-id-12345',
371
380
  });
372
381
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
373
382
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -404,6 +413,7 @@ describe('Provider', () => {
404
413
  method: RequestMethod.sol_signAndSendTransaction,
405
414
  params: ['test'],
406
415
  rpcUrl: mockRpcUrl,
416
+ traceId: 'mock-trace-id-12345',
407
417
  });
408
418
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
409
419
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -440,6 +450,7 @@ describe('Provider', () => {
440
450
  method: RequestMethod.sol_signMessage,
441
451
  params: ['test'],
442
452
  rpcUrl: mockRpcUrl,
453
+ traceId: 'mock-trace-id-12345',
443
454
  });
444
455
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
445
456
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -476,6 +487,7 @@ describe('Provider', () => {
476
487
  method: RequestMethod.sol_signTransaction,
477
488
  params: ['test'],
478
489
  rpcUrl: mockRpcUrl,
490
+ traceId: 'mock-trace-id-12345',
479
491
  });
480
492
  expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
481
493
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -576,6 +588,7 @@ describe('Provider', () => {
576
588
  method: RequestMethod.eth_sendTransaction,
577
589
  params: 'test',
578
590
  rpcUrl: mockRpcUrl,
591
+ traceId: 'mock-trace-id-12345',
579
592
  });
580
593
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
581
594
  chainId: 'eip155:1',
@@ -627,6 +640,7 @@ describe('Provider', () => {
627
640
  method: RequestMethod.eth_signTransaction,
628
641
  params: 'test',
629
642
  rpcUrl: mockRpcUrl,
643
+ traceId: 'mock-trace-id-12345',
630
644
  });
631
645
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
632
646
  chainId: 'eip155:1',
@@ -678,6 +692,7 @@ describe('Provider', () => {
678
692
  method: RequestMethod.eth_sign,
679
693
  params: ['test'],
680
694
  rpcUrl: mockRpcUrl,
695
+ traceId: 'mock-trace-id-12345',
681
696
  });
682
697
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
683
698
  chainId: 'eip155:1',
@@ -729,6 +744,7 @@ describe('Provider', () => {
729
744
  method: RequestMethod.eth_signTypedData_v3,
730
745
  params: ['test'],
731
746
  rpcUrl: mockRpcUrl,
747
+ traceId: 'mock-trace-id-12345',
732
748
  });
733
749
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
734
750
  chainId: 'eip155:1',
@@ -780,6 +796,7 @@ describe('Provider', () => {
780
796
  method: RequestMethod.eth_signTypedData_v4,
781
797
  params: ['test'],
782
798
  rpcUrl: mockRpcUrl,
799
+ traceId: 'mock-trace-id-12345',
783
800
  });
784
801
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
785
802
  chainId: 'eip155:1',
@@ -831,6 +848,7 @@ describe('Provider', () => {
831
848
  method: RequestMethod.personal_sign,
832
849
  params: ['test'],
833
850
  rpcUrl: mockRpcUrl,
851
+ traceId: 'mock-trace-id-12345',
834
852
  });
835
853
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
836
854
  chainId: 'eip155:1',
@@ -882,6 +900,7 @@ describe('Provider', () => {
882
900
  method: RequestMethod.sol_signAndConfirmTransaction,
883
901
  params: ['test'],
884
902
  rpcUrl: mockRpcUrl,
903
+ traceId: 'mock-trace-id-12345',
885
904
  });
886
905
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
887
906
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -933,6 +952,7 @@ describe('Provider', () => {
933
952
  method: RequestMethod.sol_signAndSendTransaction,
934
953
  params: ['test'],
935
954
  rpcUrl: mockRpcUrl,
955
+ traceId: 'mock-trace-id-12345',
936
956
  });
937
957
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
938
958
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -984,6 +1004,7 @@ describe('Provider', () => {
984
1004
  method: RequestMethod.sol_signMessage,
985
1005
  params: ['test'],
986
1006
  rpcUrl: mockRpcUrl,
1007
+ traceId: 'mock-trace-id-12345',
987
1008
  });
988
1009
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
989
1010
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -1035,6 +1056,7 @@ describe('Provider', () => {
1035
1056
  method: RequestMethod.sol_signTransaction,
1036
1057
  params: ['test'],
1037
1058
  rpcUrl: mockRpcUrl,
1059
+ traceId: 'mock-trace-id-12345',
1038
1060
  });
1039
1061
  expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1040
1062
  chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
@@ -1078,9 +1100,10 @@ describe('Provider', () => {
1078
1100
  expect(result).toEqual('test');
1079
1101
  expect(global.fetch).toHaveBeenCalledWith(mockRpcUrl, {
1080
1102
  method: 'POST',
1081
- headers: {
1103
+ headers: expect.objectContaining({
1082
1104
  'Content-Type': 'application/json',
1083
- },
1105
+ [X_PORTAL_TRACE_ID_HEADER]: 'mock-trace-id-12345',
1106
+ }),
1084
1107
  body: JSON.stringify({
1085
1108
  jsonrpc: '2.0',
1086
1109
  id: '0',
@@ -0,0 +1,56 @@
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
+ const LOG_LEVEL_PRIORITY = {
6
+ none: 0,
7
+ error: 1,
8
+ warn: 2,
9
+ info: 3,
10
+ debug: 4,
11
+ };
12
+ class SdkLoggerImpl {
13
+ constructor() {
14
+ this.logLevel = 'none';
15
+ }
16
+ configure(logLevel) {
17
+ this.logLevel = logLevel;
18
+ }
19
+ getLogLevel() {
20
+ return this.logLevel;
21
+ }
22
+ shouldLog(level) {
23
+ return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.logLevel];
24
+ }
25
+ log(level, message, ...args) {
26
+ if (!this.shouldLog(level))
27
+ return;
28
+ if (typeof console === 'undefined')
29
+ return;
30
+ const prefix = `[PortalMpc] ${message}`;
31
+ try {
32
+ if (level === 'debug' && console.debug) {
33
+ console.debug(prefix, ...args);
34
+ }
35
+ else if (level === 'warn' && console.warn) {
36
+ console.warn(prefix, ...args);
37
+ }
38
+ else if (level === 'error' && console.error) {
39
+ console.error(prefix, ...args);
40
+ }
41
+ }
42
+ catch (_a) {
43
+ // no-op if console is unavailable or throws
44
+ }
45
+ }
46
+ debug(message, ...args) {
47
+ this.log('debug', message, ...args);
48
+ }
49
+ warn(message, ...args) {
50
+ this.log('warn', message, ...args);
51
+ }
52
+ error(message, ...args) {
53
+ this.log('error', message, ...args);
54
+ }
55
+ }
56
+ export const sdkLogger = new SdkLoggerImpl();
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Request tracing for Portal API calls.
3
+ * Aligns with portal-react-native X-Portal-Trace-Id / traceId behavior.
4
+ *
5
+ * Propagation in the Web SDK:
6
+ * - Browser: High-level methods (backup, recover, sendAsset, provider.request) accept optional
7
+ * traceId or generate one. They pass it to handleRequestToIframeAndPost (postMessage) or
8
+ * to fetch headers (X-Portal-Trace-Id).
9
+ * - Iframe: On each portal:* message, the iframe reads traceId from the message (top-level or
10
+ * data.traceId), calls setRequestTraceId(traceId), which sets it on Api, Mpc, ZeroX, etc.
11
+ * All REST and MPC calls made during that request then use the same trace ID (header or reqId).
12
+ * - After the request completes, clearRequestTraceId() is called so the next message gets a
13
+ * fresh or caller-provided trace ID.
14
+ */
15
+ /**
16
+ * HTTP header name for request tracing.
17
+ * Used by connect-api (client + custodian APIs) in the Web SDK.
18
+ * Note: Other internal services may use different header names.
19
+ */
20
+ export const X_PORTAL_TRACE_ID_HEADER = 'X-Portal-Trace-Id';
21
+ /**
22
+ * Generates a UUID v4 trace ID for request correlation.
23
+ * Uses crypto.randomUUID() when available; otherwise crypto.getRandomValues() for better
24
+ * quality than Math.random(); falls back to Math.random() only when no crypto API exists.
25
+ */
26
+ export function generateTraceId() {
27
+ if (typeof crypto !== 'undefined' &&
28
+ typeof crypto.randomUUID === 'function') {
29
+ return crypto.randomUUID();
30
+ }
31
+ if (typeof crypto !== 'undefined' &&
32
+ typeof crypto.getRandomValues === 'function') {
33
+ return fallbackUuidV4WithCrypto(crypto);
34
+ }
35
+ return fallbackUuidV4WithMathRandom();
36
+ }
37
+ function fallbackUuidV4WithCrypto(crypto) {
38
+ const bytes = new Uint8Array(16);
39
+ crypto.getRandomValues(bytes);
40
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
41
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
42
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
43
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
44
+ }
45
+ function fallbackUuidV4WithMathRandom() {
46
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
47
+ const r = (Math.random() * 16) | 0;
48
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
49
+ return v.toString(16);
50
+ });
51
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Shared types barrel export
3
3
  *
4
- * This module exports all shared types used by both browser and iframe packages
4
+ * This module exports all shared types used by both browser and iframe packages.
5
+ * For trace ID helpers (X_PORTAL_TRACE_ID_HEADER, generateTraceId), use shared/trace.
5
6
  */
6
7
  // Common types
7
8
  export * from './common';
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Portal MPC Support for Web",
4
4
  "author": "Portal Labs, Inc.",
5
5
  "homepage": "https://portalhq.io/",
6
- "version": "3.11.0",
6
+ "version": "3.12.0",
7
7
  "license": "MIT",
8
8
  "main": "lib/commonjs/index",
9
9
  "module": "lib/esm/index",
package/src/index.test.ts CHANGED
@@ -1033,6 +1033,7 @@ describe('Portal', () => {
1033
1033
  mockAddress,
1034
1034
  'USDC',
1035
1035
  '1',
1036
+ undefined,
1036
1037
  )
1037
1038
  })
1038
1039
  })
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ import Delegations from './integrations/delegations'
56
56
  import PasskeyService from './passkeys'
57
57
  import { EvmAccountType } from './namespaces/evmAccountType'
58
58
  import { sdkLogger } from './logger'
59
+ import { generateTraceId } from './shared/trace'
59
60
  import type { LogLevel, ILogger } from '../types'
60
61
 
61
62
  class Portal {
@@ -627,22 +628,29 @@ class Portal {
627
628
  chain: string,
628
629
  params: SendAssetParams,
629
630
  ): Promise<string> {
631
+ const traceId = params.traceId ?? generateTraceId()
632
+ sdkLogger.debug(
633
+ '[Portal] sendAsset started (single traceId for build + sign)',
634
+ { traceId },
635
+ )
636
+
630
637
  try {
631
638
  // Convert the chain to a chain ID
632
639
  const chainId = this.convertChainToChainId(chain)
633
640
 
634
- // Build the transaction
641
+ // Build the transaction (same traceId for correlation with backend)
635
642
  const buildTxResponse = await this.buildTransaction(
636
643
  chainId,
637
644
  params.to,
638
645
  params.token,
639
646
  params.amount,
647
+ traceId,
640
648
  )
641
649
 
642
650
  // Get the chain namespace
643
651
  const chainNamespace = chainId.split(':')[0] ?? ''
644
652
 
645
- // Send the transaction based on chain namespace
653
+ // Send the transaction based on chain namespace (same traceId on request)
646
654
  let response
647
655
  switch (chainNamespace) {
648
656
  case 'eip155': {
@@ -652,6 +660,7 @@ class Portal {
652
660
  params: [buildTxResponse.transaction],
653
661
  sponsorGas: params.sponsorGas,
654
662
  signatureApprovalMemo: params.signatureApprovalMemo,
663
+ traceId,
655
664
  })
656
665
  break
657
666
  }
@@ -662,6 +671,7 @@ class Portal {
662
671
  params: [buildTxResponse.transaction],
663
672
  sponsorGas: params.sponsorGas,
664
673
  signatureApprovalMemo: params.signatureApprovalMemo,
674
+ traceId,
665
675
  })
666
676
  break
667
677
  }
@@ -1076,8 +1086,9 @@ class Portal {
1076
1086
  to: string,
1077
1087
  token: string,
1078
1088
  amount: string,
1089
+ traceId?: string,
1079
1090
  ): Promise<BuiltTransaction> {
1080
- return this.mpc?.buildTransaction(chainId, to, token, amount)
1091
+ return this.mpc?.buildTransaction(chainId, to, token, amount, traceId)
1081
1092
  }
1082
1093
 
1083
1094
  /*******************************
@@ -1,11 +1,4 @@
1
- export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug'
2
-
3
- export interface ILogger {
4
- error(...args: unknown[]): void
5
- warn(...args: unknown[]): void
6
- info(...args: unknown[]): void
7
- debug(...args: unknown[]): void
8
- }
1
+ import type { LogLevel, ILogger } from '../../types'
9
2
 
10
3
  const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
11
4
  none: 0,