@portal-hq/provider 4.1.1 → 4.1.3

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.
@@ -74,6 +74,7 @@ class Provider {
74
74
  mpcHost,
75
75
  keychain,
76
76
  version,
77
+ portalApi: this.portalApi,
77
78
  featureFlags,
78
79
  });
79
80
  }
@@ -187,8 +188,8 @@ class Provider {
187
188
  * @param args The arguments of the request being made
188
189
  * @returns Promise<any>
189
190
  */
190
- request(_a) {
191
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, connect, }) {
191
+ request({ method, params, chainId, connect, }) {
192
+ return __awaiter(this, void 0, void 0, function* () {
192
193
  chainId = chainId !== null && chainId !== void 0 ? chainId : this.chainId;
193
194
  if (!chainId) {
194
195
  throw new Error('[PortalProvider] No chainId provided');
@@ -320,8 +321,8 @@ class Provider {
320
321
  *
321
322
  * @param args The arguments of the request being made
322
323
  */
323
- getApproval(_a) {
324
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, connect, }) {
324
+ getApproval({ method, params, chainId, connect, }) {
325
+ return __awaiter(this, void 0, void 0, function* () {
325
326
  // If autoApprove is enabled, just resolve to true
326
327
  if (this.autoApprove) {
327
328
  return true;
@@ -386,8 +387,8 @@ class Provider {
386
387
  * @param args The arguments of the request being made
387
388
  * @returns Promise<any>
388
389
  */
389
- handleGatewayRequests(_a) {
390
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, }) {
390
+ handleGatewayRequests({ method, params, chainId, }) {
391
+ return __awaiter(this, void 0, void 0, function* () {
391
392
  const gatewayUrl = this.getGatewayUrl(chainId);
392
393
  // Pass request off to the gateway
393
394
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
@@ -407,9 +408,9 @@ class Provider {
407
408
  * @param args The arguments of the request being made
408
409
  * @returns Promise<any>
409
410
  */
410
- handleSigningRequests(_a) {
411
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, connect, }) {
412
- var _b, _c;
411
+ handleSigningRequests({ method, params, chainId, connect, }) {
412
+ var _a, _b;
413
+ return __awaiter(this, void 0, void 0, function* () {
413
414
  const isApproved = passiveSignerMethods.includes(method)
414
415
  ? true
415
416
  : yield this.getApproval({ method, params, chainId, connect });
@@ -440,7 +441,7 @@ class Provider {
440
441
  case 'sol_signAndSendTransaction':
441
442
  case 'sol_signMessage':
442
443
  case 'sol_signTransaction': {
443
- const result = yield ((_b = this.signer) === null || _b === void 0 ? void 0 : _b.sign({
444
+ const result = yield ((_a = this.signer) === null || _a === void 0 ? void 0 : _a.sign({
444
445
  chainId: `${namespace}:${reference}`,
445
446
  method,
446
447
  params,
@@ -450,7 +451,7 @@ class Provider {
450
451
  return result;
451
452
  }
452
453
  case 'raw_sign': {
453
- const result = yield ((_c = this.signer) === null || _c === void 0 ? void 0 : _c.sign({
454
+ const result = yield ((_b = this.signer) === null || _b === void 0 ? void 0 : _b.sign({
454
455
  chainId: '',
455
456
  method: '',
456
457
  params,
@@ -8,12 +8,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  const core_1 = require("@portal-hq/core");
13
16
  const utils_1 = require("@portal-hq/utils");
14
17
  const react_native_1 = require("react-native");
18
+ const react_native_uuid_1 = __importDefault(require("react-native-uuid"));
19
+ var Operation;
20
+ (function (Operation) {
21
+ Operation["SIGN"] = "sign";
22
+ Operation["RAW_SIGN"] = "raw_sign";
23
+ })(Operation || (Operation = {}));
15
24
  class MpcSigner {
16
- constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', featureFlags = {}, }) {
25
+ constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', portalApi, featureFlags = {}, }) {
17
26
  this.version = 'v6';
18
27
  this.buildParams = (method, txParams) => {
19
28
  let params = txParams;
@@ -44,61 +53,164 @@ class MpcSigner {
44
53
  this.mpc = react_native_1.NativeModules.PortalMobileMpc;
45
54
  this.mpcHost = mpcHost;
46
55
  this.version = version;
56
+ this.portalApi = portalApi;
47
57
  if (!this.mpc) {
48
58
  throw new Error(`[Portal.Provider.MpcSigner] The MPC module could not be found by the signer. This is usually an issue with React Native linking. Please verify that the 'PortalReactNative' module is properly linked to this project.`);
49
59
  }
50
60
  }
51
61
  sign(message, provider) {
52
62
  return __awaiter(this, void 0, void 0, function* () {
53
- const eip155Address = yield this.keychain.getEip155Address();
54
- const apiKey = provider.apiKey;
55
- const { method, chainId, curve, isRaw } = message;
56
- switch (method) {
57
- case 'eth_requestAccounts':
58
- return [eip155Address];
59
- case 'eth_accounts':
60
- return [eip155Address];
61
- default:
62
- break;
63
- }
64
- const shares = yield this.keychain.getShares();
65
- let signingShare = shares.secp256k1.share;
66
- if (curve === core_1.PortalCurve.ED25519) {
67
- if (!shares.ed25519) {
68
- throw new Error('[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.');
69
- }
70
- signingShare = shares.ed25519.share;
71
- }
72
- const metadata = {
73
- clientPlatform: 'REACT_NATIVE',
74
- clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
75
- isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
76
- mpcServerVersion: this.version,
77
- optimized: true,
78
- curve,
79
- chainId,
80
- isRaw,
63
+ // Always track metrics, but only send if feature flag is enabled
64
+ const shouldSendMetrics = this.featureFlags.enableSdkPerformanceMetrics === true;
65
+ const signStartTime = performance.now();
66
+ const preOperationStartTime = performance.now();
67
+ // Generate a traceId for this operation
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
69
+ const traceId = react_native_uuid_1.default.v4();
70
+ const metrics = {
71
+ hasError: false,
72
+ operation: Operation.SIGN,
73
+ signingMethod: message.method,
74
+ traceId,
81
75
  };
82
- const stringifiedMetadata = JSON.stringify(metadata);
83
- let formattedParams;
84
- let rpcUrl;
85
- if (isRaw) {
86
- formattedParams = this.buildParams(method, message.params);
87
- rpcUrl = '';
76
+ try {
77
+ const eip155Address = yield this.keychain.getEip155Address();
78
+ const apiKey = provider.apiKey;
79
+ const { method, chainId, curve, isRaw } = message;
80
+ // Add chainId to metrics
81
+ if (chainId) {
82
+ metrics.chainId = chainId;
83
+ }
84
+ switch (method) {
85
+ case 'eth_requestAccounts':
86
+ return [eip155Address];
87
+ case 'eth_accounts':
88
+ return [eip155Address];
89
+ default:
90
+ break;
91
+ }
92
+ const shares = yield this.keychain.getShares();
93
+ let signingShare = shares.secp256k1.share;
94
+ if (curve === core_1.PortalCurve.ED25519) {
95
+ if (!shares.ed25519) {
96
+ throw new Error('[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.');
97
+ }
98
+ signingShare = shares.ed25519.share;
99
+ }
100
+ const metadata = {
101
+ clientPlatform: 'REACT_NATIVE',
102
+ clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
103
+ isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
104
+ mpcServerVersion: this.version,
105
+ optimized: true,
106
+ curve,
107
+ chainId,
108
+ isRaw,
109
+ reqId: traceId,
110
+ connectionTracingEnabled: shouldSendMetrics,
111
+ };
112
+ const stringifiedMetadata = JSON.stringify(metadata);
113
+ let formattedParams;
114
+ let rpcUrl;
115
+ if (isRaw) {
116
+ formattedParams = this.buildParams(method, message.params);
117
+ rpcUrl = '';
118
+ metrics.operation = Operation.RAW_SIGN;
119
+ }
120
+ else {
121
+ formattedParams = JSON.stringify(this.buildParams(method, message.params));
122
+ rpcUrl = provider.getGatewayUrl(chainId);
123
+ }
124
+ if (typeof formattedParams !== 'string') {
125
+ throw new Error(`[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
126
+ }
127
+ // Record pre-operation time
128
+ metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
129
+ // Measure MPC signing operation time
130
+ const mpcSignStartTime = performance.now();
131
+ const result = yield this.mpc.sign(apiKey, this.mpcHost, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId, stringifiedMetadata);
132
+ // Post-operation processing time starts
133
+ const postOperationStartTime = performance.now();
134
+ // Record native call time
135
+ metrics.mpcNativeCallMs = performance.now() - mpcSignStartTime;
136
+ // Parse result and extract binary metrics if available
137
+ const parsedResponse = JSON.parse(String(result));
138
+ const { data, error, meta } = parsedResponse;
139
+ // Add binary metrics to our metrics object
140
+ if (meta === null || meta === void 0 ? void 0 : meta.metrics) {
141
+ const binaryMetrics = meta.metrics;
142
+ if (binaryMetrics.wsConnectDurationMs) {
143
+ metrics.sdkBinaryWSConnectMs = binaryMetrics.wsConnectDurationMs;
144
+ }
145
+ if (binaryMetrics.operationDurationMs) {
146
+ metrics.sdkBinaryOperationMs = binaryMetrics.operationDurationMs;
147
+ }
148
+ if (binaryMetrics.tlsHandshakeMs) {
149
+ metrics.sdkBinaryTlsHandshakeMs = binaryMetrics.tlsHandshakeMs;
150
+ }
151
+ if (binaryMetrics.firstResponseMs) {
152
+ metrics.sdkBinaryFirstResponseMs = binaryMetrics.firstResponseMs;
153
+ }
154
+ if (binaryMetrics.dnsLookupMs) {
155
+ metrics.sdkBinaryDnsLookupMs = binaryMetrics.dnsLookupMs;
156
+ }
157
+ if (binaryMetrics.connectMs) {
158
+ metrics.sdkBinaryConnectMs = binaryMetrics.connectMs;
159
+ }
160
+ }
161
+ if ((error === null || error === void 0 ? void 0 : error.code) > 0) {
162
+ throw new utils_1.PortalMpcError(error);
163
+ }
164
+ // Record post-operation time
165
+ metrics.sdkPostOperationMs = performance.now() - postOperationStartTime;
166
+ // Calculate total SDK signing time
167
+ metrics.sdkOperationMs = performance.now() - signStartTime;
168
+ // Only send metrics if the feature flag is enabled
169
+ if (shouldSendMetrics && this.portalApi) {
170
+ try {
171
+ yield this.sendMetrics(metrics, apiKey);
172
+ }
173
+ catch (err) {
174
+ // No-op
175
+ }
176
+ }
177
+ return data;
88
178
  }
89
- else {
90
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
91
- rpcUrl = provider.getGatewayUrl(chainId);
179
+ catch (error) {
180
+ // Calculate total time even in error case
181
+ metrics.sdkOperationMs = performance.now() - signStartTime;
182
+ // Only send metrics if the feature flag is enabled
183
+ if (shouldSendMetrics) {
184
+ const apiKey = provider.apiKey;
185
+ metrics.hasError = true;
186
+ try {
187
+ yield this.sendMetrics(metrics, apiKey);
188
+ }
189
+ catch (_a) {
190
+ // No-op
191
+ }
192
+ }
193
+ throw error;
92
194
  }
93
- if (typeof formattedParams !== 'string') {
94
- throw new Error(`[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
195
+ });
196
+ }
197
+ // Add debug logs to sendMetrics method too
198
+ sendMetrics(metrics, apiKey) {
199
+ return __awaiter(this, void 0, void 0, function* () {
200
+ try {
201
+ if (this.portalApi) {
202
+ yield this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
203
+ headers: {
204
+ Authorization: `Bearer ${apiKey}`,
205
+ 'Content-Type': 'application/json',
206
+ },
207
+ body: metrics,
208
+ });
209
+ }
95
210
  }
96
- const result = yield this.mpc.sign(apiKey, this.mpcHost, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId, stringifiedMetadata);
97
- const { data, error } = JSON.parse(String(result));
98
- if (error && error.code > 0) {
99
- throw new utils_1.PortalMpcError(error);
211
+ catch (_a) {
212
+ // No-op
100
213
  }
101
- return data;
102
214
  });
103
215
  }
104
216
  }
@@ -72,6 +72,7 @@ class Provider {
72
72
  mpcHost,
73
73
  keychain,
74
74
  version,
75
+ portalApi: this.portalApi,
75
76
  featureFlags,
76
77
  });
77
78
  }
@@ -185,8 +186,8 @@ class Provider {
185
186
  * @param args The arguments of the request being made
186
187
  * @returns Promise<any>
187
188
  */
188
- request(_a) {
189
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, connect, }) {
189
+ request({ method, params, chainId, connect, }) {
190
+ return __awaiter(this, void 0, void 0, function* () {
190
191
  chainId = chainId !== null && chainId !== void 0 ? chainId : this.chainId;
191
192
  if (!chainId) {
192
193
  throw new Error('[PortalProvider] No chainId provided');
@@ -318,8 +319,8 @@ class Provider {
318
319
  *
319
320
  * @param args The arguments of the request being made
320
321
  */
321
- getApproval(_a) {
322
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, connect, }) {
322
+ getApproval({ method, params, chainId, connect, }) {
323
+ return __awaiter(this, void 0, void 0, function* () {
323
324
  // If autoApprove is enabled, just resolve to true
324
325
  if (this.autoApprove) {
325
326
  return true;
@@ -384,8 +385,8 @@ class Provider {
384
385
  * @param args The arguments of the request being made
385
386
  * @returns Promise<any>
386
387
  */
387
- handleGatewayRequests(_a) {
388
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, }) {
388
+ handleGatewayRequests({ method, params, chainId, }) {
389
+ return __awaiter(this, void 0, void 0, function* () {
389
390
  const gatewayUrl = this.getGatewayUrl(chainId);
390
391
  // Pass request off to the gateway
391
392
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
@@ -405,9 +406,9 @@ class Provider {
405
406
  * @param args The arguments of the request being made
406
407
  * @returns Promise<any>
407
408
  */
408
- handleSigningRequests(_a) {
409
- return __awaiter(this, arguments, void 0, function* ({ method, params, chainId, connect, }) {
410
- var _b, _c;
409
+ handleSigningRequests({ method, params, chainId, connect, }) {
410
+ var _a, _b;
411
+ return __awaiter(this, void 0, void 0, function* () {
411
412
  const isApproved = passiveSignerMethods.includes(method)
412
413
  ? true
413
414
  : yield this.getApproval({ method, params, chainId, connect });
@@ -438,7 +439,7 @@ class Provider {
438
439
  case 'sol_signAndSendTransaction':
439
440
  case 'sol_signMessage':
440
441
  case 'sol_signTransaction': {
441
- const result = yield ((_b = this.signer) === null || _b === void 0 ? void 0 : _b.sign({
442
+ const result = yield ((_a = this.signer) === null || _a === void 0 ? void 0 : _a.sign({
442
443
  chainId: `${namespace}:${reference}`,
443
444
  method,
444
445
  params,
@@ -448,7 +449,7 @@ class Provider {
448
449
  return result;
449
450
  }
450
451
  case 'raw_sign': {
451
- const result = yield ((_c = this.signer) === null || _c === void 0 ? void 0 : _c.sign({
452
+ const result = yield ((_b = this.signer) === null || _b === void 0 ? void 0 : _b.sign({
452
453
  chainId: '',
453
454
  method: '',
454
455
  params,
@@ -10,8 +10,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { PortalCurve } from '@portal-hq/core';
11
11
  import { PortalMpcError, getClientPlatformVersion, } from '@portal-hq/utils';
12
12
  import { NativeModules } from 'react-native';
13
+ import UUID from 'react-native-uuid';
14
+ var Operation;
15
+ (function (Operation) {
16
+ Operation["SIGN"] = "sign";
17
+ Operation["RAW_SIGN"] = "raw_sign";
18
+ })(Operation || (Operation = {}));
13
19
  class MpcSigner {
14
- constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', featureFlags = {}, }) {
20
+ constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', portalApi, featureFlags = {}, }) {
15
21
  this.version = 'v6';
16
22
  this.buildParams = (method, txParams) => {
17
23
  let params = txParams;
@@ -42,61 +48,164 @@ class MpcSigner {
42
48
  this.mpc = NativeModules.PortalMobileMpc;
43
49
  this.mpcHost = mpcHost;
44
50
  this.version = version;
51
+ this.portalApi = portalApi;
45
52
  if (!this.mpc) {
46
53
  throw new Error(`[Portal.Provider.MpcSigner] The MPC module could not be found by the signer. This is usually an issue with React Native linking. Please verify that the 'PortalReactNative' module is properly linked to this project.`);
47
54
  }
48
55
  }
49
56
  sign(message, provider) {
50
57
  return __awaiter(this, void 0, void 0, function* () {
51
- const eip155Address = yield this.keychain.getEip155Address();
52
- const apiKey = provider.apiKey;
53
- const { method, chainId, curve, isRaw } = message;
54
- switch (method) {
55
- case 'eth_requestAccounts':
56
- return [eip155Address];
57
- case 'eth_accounts':
58
- return [eip155Address];
59
- default:
60
- break;
61
- }
62
- const shares = yield this.keychain.getShares();
63
- let signingShare = shares.secp256k1.share;
64
- if (curve === PortalCurve.ED25519) {
65
- if (!shares.ed25519) {
66
- throw new Error('[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.');
67
- }
68
- signingShare = shares.ed25519.share;
69
- }
70
- const metadata = {
71
- clientPlatform: 'REACT_NATIVE',
72
- clientPlatformVersion: getClientPlatformVersion(),
73
- isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
74
- mpcServerVersion: this.version,
75
- optimized: true,
76
- curve,
77
- chainId,
78
- isRaw,
58
+ // Always track metrics, but only send if feature flag is enabled
59
+ const shouldSendMetrics = this.featureFlags.enableSdkPerformanceMetrics === true;
60
+ const signStartTime = performance.now();
61
+ const preOperationStartTime = performance.now();
62
+ // Generate a traceId for this operation
63
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
64
+ const traceId = UUID.v4();
65
+ const metrics = {
66
+ hasError: false,
67
+ operation: Operation.SIGN,
68
+ signingMethod: message.method,
69
+ traceId,
79
70
  };
80
- const stringifiedMetadata = JSON.stringify(metadata);
81
- let formattedParams;
82
- let rpcUrl;
83
- if (isRaw) {
84
- formattedParams = this.buildParams(method, message.params);
85
- rpcUrl = '';
71
+ try {
72
+ const eip155Address = yield this.keychain.getEip155Address();
73
+ const apiKey = provider.apiKey;
74
+ const { method, chainId, curve, isRaw } = message;
75
+ // Add chainId to metrics
76
+ if (chainId) {
77
+ metrics.chainId = chainId;
78
+ }
79
+ switch (method) {
80
+ case 'eth_requestAccounts':
81
+ return [eip155Address];
82
+ case 'eth_accounts':
83
+ return [eip155Address];
84
+ default:
85
+ break;
86
+ }
87
+ const shares = yield this.keychain.getShares();
88
+ let signingShare = shares.secp256k1.share;
89
+ if (curve === PortalCurve.ED25519) {
90
+ if (!shares.ed25519) {
91
+ throw new Error('[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.');
92
+ }
93
+ signingShare = shares.ed25519.share;
94
+ }
95
+ const metadata = {
96
+ clientPlatform: 'REACT_NATIVE',
97
+ clientPlatformVersion: getClientPlatformVersion(),
98
+ isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
99
+ mpcServerVersion: this.version,
100
+ optimized: true,
101
+ curve,
102
+ chainId,
103
+ isRaw,
104
+ reqId: traceId,
105
+ connectionTracingEnabled: shouldSendMetrics,
106
+ };
107
+ const stringifiedMetadata = JSON.stringify(metadata);
108
+ let formattedParams;
109
+ let rpcUrl;
110
+ if (isRaw) {
111
+ formattedParams = this.buildParams(method, message.params);
112
+ rpcUrl = '';
113
+ metrics.operation = Operation.RAW_SIGN;
114
+ }
115
+ else {
116
+ formattedParams = JSON.stringify(this.buildParams(method, message.params));
117
+ rpcUrl = provider.getGatewayUrl(chainId);
118
+ }
119
+ if (typeof formattedParams !== 'string') {
120
+ throw new Error(`[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
121
+ }
122
+ // Record pre-operation time
123
+ metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
124
+ // Measure MPC signing operation time
125
+ const mpcSignStartTime = performance.now();
126
+ const result = yield this.mpc.sign(apiKey, this.mpcHost, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId, stringifiedMetadata);
127
+ // Post-operation processing time starts
128
+ const postOperationStartTime = performance.now();
129
+ // Record native call time
130
+ metrics.mpcNativeCallMs = performance.now() - mpcSignStartTime;
131
+ // Parse result and extract binary metrics if available
132
+ const parsedResponse = JSON.parse(String(result));
133
+ const { data, error, meta } = parsedResponse;
134
+ // Add binary metrics to our metrics object
135
+ if (meta === null || meta === void 0 ? void 0 : meta.metrics) {
136
+ const binaryMetrics = meta.metrics;
137
+ if (binaryMetrics.wsConnectDurationMs) {
138
+ metrics.sdkBinaryWSConnectMs = binaryMetrics.wsConnectDurationMs;
139
+ }
140
+ if (binaryMetrics.operationDurationMs) {
141
+ metrics.sdkBinaryOperationMs = binaryMetrics.operationDurationMs;
142
+ }
143
+ if (binaryMetrics.tlsHandshakeMs) {
144
+ metrics.sdkBinaryTlsHandshakeMs = binaryMetrics.tlsHandshakeMs;
145
+ }
146
+ if (binaryMetrics.firstResponseMs) {
147
+ metrics.sdkBinaryFirstResponseMs = binaryMetrics.firstResponseMs;
148
+ }
149
+ if (binaryMetrics.dnsLookupMs) {
150
+ metrics.sdkBinaryDnsLookupMs = binaryMetrics.dnsLookupMs;
151
+ }
152
+ if (binaryMetrics.connectMs) {
153
+ metrics.sdkBinaryConnectMs = binaryMetrics.connectMs;
154
+ }
155
+ }
156
+ if ((error === null || error === void 0 ? void 0 : error.code) > 0) {
157
+ throw new PortalMpcError(error);
158
+ }
159
+ // Record post-operation time
160
+ metrics.sdkPostOperationMs = performance.now() - postOperationStartTime;
161
+ // Calculate total SDK signing time
162
+ metrics.sdkOperationMs = performance.now() - signStartTime;
163
+ // Only send metrics if the feature flag is enabled
164
+ if (shouldSendMetrics && this.portalApi) {
165
+ try {
166
+ yield this.sendMetrics(metrics, apiKey);
167
+ }
168
+ catch (err) {
169
+ // No-op
170
+ }
171
+ }
172
+ return data;
86
173
  }
87
- else {
88
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
89
- rpcUrl = provider.getGatewayUrl(chainId);
174
+ catch (error) {
175
+ // Calculate total time even in error case
176
+ metrics.sdkOperationMs = performance.now() - signStartTime;
177
+ // Only send metrics if the feature flag is enabled
178
+ if (shouldSendMetrics) {
179
+ const apiKey = provider.apiKey;
180
+ metrics.hasError = true;
181
+ try {
182
+ yield this.sendMetrics(metrics, apiKey);
183
+ }
184
+ catch (_a) {
185
+ // No-op
186
+ }
187
+ }
188
+ throw error;
90
189
  }
91
- if (typeof formattedParams !== 'string') {
92
- throw new Error(`[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
190
+ });
191
+ }
192
+ // Add debug logs to sendMetrics method too
193
+ sendMetrics(metrics, apiKey) {
194
+ return __awaiter(this, void 0, void 0, function* () {
195
+ try {
196
+ if (this.portalApi) {
197
+ yield this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
198
+ headers: {
199
+ Authorization: `Bearer ${apiKey}`,
200
+ 'Content-Type': 'application/json',
201
+ },
202
+ body: metrics,
203
+ });
204
+ }
93
205
  }
94
- const result = yield this.mpc.sign(apiKey, this.mpcHost, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId, stringifiedMetadata);
95
- const { data, error } = JSON.parse(String(result));
96
- if (error && error.code > 0) {
97
- throw new PortalMpcError(error);
206
+ catch (_a) {
207
+ // No-op
98
208
  }
99
- return data;
100
209
  });
101
210
  }
102
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portal-hq/provider",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "license": "MIT",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/esm/index",
@@ -19,8 +19,10 @@
19
19
  "test": "jest"
20
20
  },
21
21
  "dependencies": {
22
- "@portal-hq/connect": "^4.1.1",
23
- "@portal-hq/utils": "^4.1.1"
22
+ "@portal-hq/connect": "^4.1.3",
23
+ "@portal-hq/utils": "^4.1.3",
24
+ "@types/react-native-uuid": "^2.0.0",
25
+ "react-native-uuid": "^2.0.3"
24
26
  },
25
27
  "devDependencies": {
26
28
  "@babel/preset-typescript": "^7.18.6",
@@ -30,5 +32,5 @@
30
32
  "ts-jest": "^29.0.3",
31
33
  "typescript": "^4.8.4"
32
34
  },
33
- "gitHead": "3c03bd4a6573b23865f655856cd0161706f3f30d"
35
+ "gitHead": "912b03f1152f4dbb7e017919c63bdc950ca867cf"
34
36
  }
@@ -115,6 +115,7 @@ class Provider implements IPortalProvider {
115
115
  mpcHost,
116
116
  keychain,
117
117
  version,
118
+ portalApi: this.portalApi,
118
119
  featureFlags,
119
120
  })
120
121
  }
@@ -1,6 +1,7 @@
1
1
  import { PortalCurve } from '@portal-hq/core'
2
2
  import { FeatureFlags } from '@portal-hq/core/types'
3
3
  import {
4
+ HttpRequester,
4
5
  IPortalProvider,
5
6
  KeychainAdapter,
6
7
  PortalMpcError,
@@ -8,6 +9,7 @@ import {
8
9
  getClientPlatformVersion,
9
10
  } from '@portal-hq/utils'
10
11
  import { NativeModules } from 'react-native'
12
+ import UUID from 'react-native-uuid'
11
13
 
12
14
  import {
13
15
  type MpcSignerOptions,
@@ -17,17 +19,24 @@ import {
17
19
  } from '../../types'
18
20
  import Signer from './abstract'
19
21
 
22
+ enum Operation {
23
+ SIGN = 'sign',
24
+ RAW_SIGN = 'raw_sign',
25
+ }
26
+
20
27
  class MpcSigner implements Signer {
21
28
  private featureFlags: FeatureFlags
22
29
  private keychain: KeychainAdapter
23
30
  private mpc: PortalMobileMpc
24
- private mpcHost: string // should we add a default here mpc.portalhq.io
31
+ private mpcHost: string
25
32
  private version = 'v6'
33
+ private portalApi?: HttpRequester
26
34
 
27
35
  constructor({
28
36
  keychain,
29
37
  mpcHost = 'mpc.portalhq.io',
30
38
  version = 'v6',
39
+ portalApi,
31
40
  featureFlags = {},
32
41
  }: MpcSignerOptions) {
33
42
  this.featureFlags = featureFlags
@@ -35,6 +44,7 @@ class MpcSigner implements Signer {
35
44
  this.mpc = NativeModules.PortalMobileMpc
36
45
  this.mpcHost = mpcHost
37
46
  this.version = version
47
+ this.portalApi = portalApi
38
48
 
39
49
  if (!this.mpc) {
40
50
  throw new Error(
@@ -47,82 +57,195 @@ class MpcSigner implements Signer {
47
57
  message: SigningRequestArguments,
48
58
  provider: IPortalProvider,
49
59
  ): Promise<any> {
50
- const eip155Address = await this.keychain.getEip155Address()
60
+ // Always track metrics, but only send if feature flag is enabled
61
+ const shouldSendMetrics =
62
+ this.featureFlags.enableSdkPerformanceMetrics === true
63
+ const signStartTime = performance.now()
64
+ const preOperationStartTime = performance.now()
65
+ // Generate a traceId for this operation
66
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
67
+ const traceId = (UUID as any).v4() as string
68
+ const metrics: Record<string, number | string | boolean> = {
69
+ hasError: false,
70
+ operation: Operation.SIGN,
71
+ signingMethod: message.method,
72
+ traceId,
73
+ }
74
+ try {
75
+ const eip155Address = await this.keychain.getEip155Address()
51
76
 
52
- const apiKey = provider.apiKey
77
+ const apiKey = provider.apiKey
53
78
 
54
- const { method, chainId, curve, isRaw } = message
79
+ const { method, chainId, curve, isRaw } = message
80
+ // Add chainId to metrics
81
+ if (chainId) {
82
+ metrics.chainId = chainId
83
+ }
55
84
 
56
- switch (method) {
57
- case 'eth_requestAccounts':
58
- return [eip155Address]
59
- case 'eth_accounts':
60
- return [eip155Address]
61
- default:
62
- break
63
- }
85
+ switch (method) {
86
+ case 'eth_requestAccounts':
87
+ return [eip155Address]
88
+ case 'eth_accounts':
89
+ return [eip155Address]
90
+ default:
91
+ break
92
+ }
93
+
94
+ const shares = await this.keychain.getShares()
64
95
 
65
- const shares = await this.keychain.getShares()
96
+ let signingShare = shares.secp256k1.share
66
97
 
67
- let signingShare = shares.secp256k1.share
98
+ if (curve === PortalCurve.ED25519) {
99
+ if (!shares.ed25519) {
100
+ throw new Error(
101
+ '[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.',
102
+ )
103
+ }
104
+ signingShare = shares.ed25519.share
105
+ }
68
106
 
69
- if (curve === PortalCurve.ED25519) {
70
- if (!shares.ed25519) {
107
+ const metadata: PortalMobileMpcMetadata = {
108
+ clientPlatform: 'REACT_NATIVE',
109
+ clientPlatformVersion: getClientPlatformVersion(),
110
+ isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
111
+ mpcServerVersion: this.version,
112
+ optimized: true,
113
+ curve,
114
+ chainId,
115
+ isRaw,
116
+ reqId: traceId,
117
+ connectionTracingEnabled: shouldSendMetrics,
118
+ }
119
+
120
+ const stringifiedMetadata = JSON.stringify(metadata)
121
+
122
+ let formattedParams: string
123
+ let rpcUrl: string
124
+
125
+ if (isRaw) {
126
+ formattedParams = this.buildParams(method, message.params)
127
+ rpcUrl = ''
128
+ metrics.operation = Operation.RAW_SIGN
129
+ } else {
130
+ formattedParams = JSON.stringify(
131
+ this.buildParams(method, message.params),
132
+ )
133
+ rpcUrl = provider.getGatewayUrl(chainId)
134
+ }
135
+
136
+ if (typeof formattedParams !== 'string') {
71
137
  throw new Error(
72
- '[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.',
138
+ `[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`,
73
139
  )
74
140
  }
75
- signingShare = shares.ed25519.share
76
- }
77
141
 
78
- const metadata: PortalMobileMpcMetadata = {
79
- clientPlatform: 'REACT_NATIVE',
80
- clientPlatformVersion: getClientPlatformVersion(),
81
- isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
82
- mpcServerVersion: this.version,
83
- optimized: true,
84
- curve,
85
- chainId,
86
- isRaw,
87
- }
142
+ // Record pre-operation time
143
+ metrics.sdkPreOperationMs = performance.now() - preOperationStartTime
88
144
 
89
- const stringifiedMetadata = JSON.stringify(metadata)
145
+ // Measure MPC signing operation time
146
+ const mpcSignStartTime = performance.now()
90
147
 
91
- let formattedParams: string
92
- let rpcUrl: string
148
+ const result = await this.mpc.sign(
149
+ apiKey,
150
+ this.mpcHost,
151
+ JSON.stringify(signingShare),
152
+ message.method,
153
+ formattedParams,
154
+ rpcUrl,
155
+ chainId,
156
+ stringifiedMetadata,
157
+ )
93
158
 
94
- if (isRaw) {
95
- formattedParams = this.buildParams(method, message.params)
96
- rpcUrl = ''
97
- } else {
98
- formattedParams = JSON.stringify(this.buildParams(method, message.params))
99
- rpcUrl = provider.getGatewayUrl(chainId)
100
- }
159
+ // Post-operation processing time starts
160
+ const postOperationStartTime = performance.now()
101
161
 
102
- if (typeof formattedParams !== 'string') {
103
- throw new Error(
104
- `[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`,
105
- )
106
- }
162
+ // Record native call time
163
+ metrics.mpcNativeCallMs = performance.now() - mpcSignStartTime
164
+
165
+ // Parse result and extract binary metrics if available
166
+ const parsedResponse = JSON.parse(String(result)) as SigningResponse
167
+ const { data, error, meta } = parsedResponse
168
+
169
+ // Add binary metrics to our metrics object
170
+ if (meta?.metrics) {
171
+ const binaryMetrics = meta.metrics
172
+ if (binaryMetrics.wsConnectDurationMs) {
173
+ metrics.sdkBinaryWSConnectMs = binaryMetrics.wsConnectDurationMs
174
+ }
175
+ if (binaryMetrics.operationDurationMs) {
176
+ metrics.sdkBinaryOperationMs = binaryMetrics.operationDurationMs
177
+ }
178
+ if (binaryMetrics.tlsHandshakeMs) {
179
+ metrics.sdkBinaryTlsHandshakeMs = binaryMetrics.tlsHandshakeMs
180
+ }
181
+ if (binaryMetrics.firstResponseMs) {
182
+ metrics.sdkBinaryFirstResponseMs = binaryMetrics.firstResponseMs
183
+ }
184
+ if (binaryMetrics.dnsLookupMs) {
185
+ metrics.sdkBinaryDnsLookupMs = binaryMetrics.dnsLookupMs
186
+ }
187
+ if (binaryMetrics.connectMs) {
188
+ metrics.sdkBinaryConnectMs = binaryMetrics.connectMs
189
+ }
190
+ }
191
+
192
+ if (error?.code > 0) {
193
+ throw new PortalMpcError(error)
194
+ }
107
195
 
108
- const result = await this.mpc.sign(
109
- apiKey,
110
- this.mpcHost,
111
- JSON.stringify(signingShare),
112
- message.method,
113
- formattedParams,
114
- rpcUrl,
115
- chainId,
116
- stringifiedMetadata,
117
- )
118
-
119
- const { data, error } = JSON.parse(String(result)) as SigningResponse
120
-
121
- if (error && error.code > 0) {
122
- throw new PortalMpcError(error)
196
+ // Record post-operation time
197
+ metrics.sdkPostOperationMs = performance.now() - postOperationStartTime
198
+
199
+ // Calculate total SDK signing time
200
+ metrics.sdkOperationMs = performance.now() - signStartTime
201
+
202
+ // Only send metrics if the feature flag is enabled
203
+ if (shouldSendMetrics && this.portalApi) {
204
+ try {
205
+ await this.sendMetrics(metrics, apiKey)
206
+ } catch (err) {
207
+ // No-op
208
+ }
209
+ }
210
+
211
+ return data
212
+ } catch (error) {
213
+ // Calculate total time even in error case
214
+ metrics.sdkOperationMs = performance.now() - signStartTime
215
+
216
+ // Only send metrics if the feature flag is enabled
217
+ if (shouldSendMetrics) {
218
+ const apiKey = provider.apiKey
219
+ metrics.hasError = true
220
+ try {
221
+ await this.sendMetrics(metrics, apiKey)
222
+ } catch {
223
+ // No-op
224
+ }
225
+ }
226
+
227
+ throw error
123
228
  }
229
+ }
124
230
 
125
- return data
231
+ // Add debug logs to sendMetrics method too
232
+ private async sendMetrics(
233
+ metrics: Record<string, number | string | boolean>,
234
+ apiKey: string,
235
+ ): Promise<void> {
236
+ try {
237
+ if (this.portalApi) {
238
+ await this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
239
+ headers: {
240
+ Authorization: `Bearer ${apiKey}`,
241
+ 'Content-Type': 'application/json',
242
+ },
243
+ body: metrics,
244
+ })
245
+ }
246
+ } catch {
247
+ // No-op
248
+ }
126
249
  }
127
250
 
128
251
  private buildParams = (method: string, txParams: any) => {
package/types.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  // Types
2
2
  import PortalConnect from '@portal-hq/connect'
3
- import { BackupMethods } from '@portal-hq/core'
4
- import { KeychainAdapter, type PortalError } from '@portal-hq/utils'
3
+ import {
4
+ KeychainAdapter,
5
+ type PortalError,
6
+ HttpRequester,
7
+ } from '@portal-hq/utils'
5
8
 
6
9
  export type EventHandler = (data: any) => void
7
10
 
@@ -21,6 +24,7 @@ export interface MpcSignerOptions extends SignerOptions {
21
24
  // Optional
22
25
  isSimulator?: boolean
23
26
  mpcHost?: string
27
+ portalApi?: HttpRequester
24
28
  version?: string
25
29
  featureFlags?: FeatureFlags
26
30
  }
@@ -41,6 +45,8 @@ export interface PortalMobileMpcMetadata {
41
45
  mpcServerVersion: string
42
46
  optimized: true
43
47
  isRaw?: boolean
48
+ reqId?: string
49
+ connectionTracingEnabled?: boolean
44
50
  }
45
51
 
46
52
  export interface PortalMobileMpc {
@@ -120,6 +126,16 @@ export type SigningRequestParams = Eip1559 | LegacyTx
120
126
  export interface SigningResponse {
121
127
  data: string
122
128
  error: PortalError
129
+ meta?: {
130
+ metrics?: {
131
+ wsConnectDurationMs?: number
132
+ operationDurationMs?: number
133
+ tlsHandshakeMs?: number
134
+ firstResponseMs?: number
135
+ dnsLookupMs?: number
136
+ connectMs?: number
137
+ }
138
+ }
123
139
  }
124
140
 
125
141
  export interface SwitchEthereumChainParameter {