@portal-hq/provider 4.1.0 → 4.1.2

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
  }
@@ -12,8 +12,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const core_1 = require("@portal-hq/core");
13
13
  const utils_1 = require("@portal-hq/utils");
14
14
  const react_native_1 = require("react-native");
15
+ var Operation;
16
+ (function (Operation) {
17
+ Operation["SIGN"] = "sign";
18
+ Operation["RAW_SIGN"] = "raw_sign";
19
+ })(Operation || (Operation = {}));
15
20
  class MpcSigner {
16
- constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', featureFlags = {}, }) {
21
+ constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', portalApi, featureFlags = {}, }) {
17
22
  this.version = 'v6';
18
23
  this.buildParams = (method, txParams) => {
19
24
  let params = txParams;
@@ -44,61 +49,144 @@ class MpcSigner {
44
49
  this.mpc = react_native_1.NativeModules.PortalMobileMpc;
45
50
  this.mpcHost = mpcHost;
46
51
  this.version = version;
52
+ this.portalApi = portalApi;
47
53
  if (!this.mpc) {
48
54
  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
55
  }
50
56
  }
51
57
  sign(message, provider) {
52
58
  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,
59
+ // Always track metrics, but only send if feature flag is enabled
60
+ const shouldSendMetrics = this.featureFlags.enableSdkPerformanceMetrics === true;
61
+ const signStartTime = performance.now();
62
+ const preOperationStartTime = performance.now();
63
+ const metrics = {
64
+ operation: Operation.SIGN,
65
+ hasError: false,
81
66
  };
82
- const stringifiedMetadata = JSON.stringify(metadata);
83
- let formattedParams;
84
- let rpcUrl;
85
- if (isRaw) {
86
- formattedParams = this.buildParams(method, message.params);
87
- rpcUrl = '';
67
+ try {
68
+ const eip155Address = yield this.keychain.getEip155Address();
69
+ const apiKey = provider.apiKey;
70
+ const { method, chainId, curve, isRaw } = message;
71
+ // Add chainId to metrics
72
+ if (chainId) {
73
+ metrics.chainId = chainId;
74
+ }
75
+ switch (method) {
76
+ case 'eth_requestAccounts':
77
+ return [eip155Address];
78
+ case 'eth_accounts':
79
+ return [eip155Address];
80
+ default:
81
+ break;
82
+ }
83
+ const shares = yield this.keychain.getShares();
84
+ let signingShare = shares.secp256k1.share;
85
+ if (curve === core_1.PortalCurve.ED25519) {
86
+ if (!shares.ed25519) {
87
+ throw new Error('[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.');
88
+ }
89
+ signingShare = shares.ed25519.share;
90
+ }
91
+ const metadata = {
92
+ clientPlatform: 'REACT_NATIVE',
93
+ clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
94
+ isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
95
+ mpcServerVersion: this.version,
96
+ optimized: true,
97
+ curve,
98
+ chainId,
99
+ isRaw,
100
+ };
101
+ const stringifiedMetadata = JSON.stringify(metadata);
102
+ let formattedParams;
103
+ let rpcUrl;
104
+ if (isRaw) {
105
+ formattedParams = this.buildParams(method, message.params);
106
+ rpcUrl = '';
107
+ metrics.operation = Operation.RAW_SIGN;
108
+ }
109
+ else {
110
+ formattedParams = JSON.stringify(this.buildParams(method, message.params));
111
+ rpcUrl = provider.getGatewayUrl(chainId);
112
+ }
113
+ if (typeof formattedParams !== 'string') {
114
+ throw new Error(`[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
115
+ }
116
+ // Record pre-operation time
117
+ metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
118
+ // Measure MPC signing operation time
119
+ const mpcSignStartTime = performance.now();
120
+ const result = yield this.mpc.sign(apiKey, this.mpcHost, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId, stringifiedMetadata);
121
+ // Post-operation processing time starts
122
+ const postOperationStartTime = performance.now();
123
+ // Record native call time
124
+ metrics.mpcNativeCallMs = performance.now() - mpcSignStartTime;
125
+ // Parse result and extract binary metrics if available
126
+ const parsedResponse = JSON.parse(String(result));
127
+ const { data, error, meta } = parsedResponse;
128
+ // Add binary metrics to our metrics object
129
+ if (meta === null || meta === void 0 ? void 0 : meta.metrics) {
130
+ if (meta.metrics.wsConnectDurationMs) {
131
+ metrics.sdkBinaryWSConnectMs = meta.metrics.wsConnectDurationMs;
132
+ }
133
+ if (meta.metrics.operationDurationMs) {
134
+ metrics.sdkBinaryOperationMs = meta.metrics.operationDurationMs;
135
+ }
136
+ }
137
+ if ((error === null || error === void 0 ? void 0 : error.code) > 0) {
138
+ throw new utils_1.PortalMpcError(error);
139
+ }
140
+ // Record post-operation time
141
+ metrics.sdkPostOperationMs = performance.now() - postOperationStartTime;
142
+ // Calculate total SDK signing time
143
+ metrics.sdkOperationMs = performance.now() - signStartTime;
144
+ // Only send metrics if the feature flag is enabled
145
+ if (shouldSendMetrics && this.portalApi) {
146
+ try {
147
+ yield this.sendMetrics(metrics, apiKey);
148
+ }
149
+ catch (_a) {
150
+ // No-op
151
+ }
152
+ }
153
+ return data;
88
154
  }
89
- else {
90
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
91
- rpcUrl = provider.getGatewayUrl(chainId);
155
+ catch (error) {
156
+ // Calculate total time even in error case
157
+ metrics.sdkOperationMs = performance.now() - signStartTime;
158
+ // Only send metrics if the feature flag is enabled
159
+ if (shouldSendMetrics) {
160
+ const apiKey = provider.apiKey;
161
+ metrics.hasError = true;
162
+ try {
163
+ yield this.sendMetrics(metrics, apiKey);
164
+ }
165
+ catch (_b) {
166
+ // No-op
167
+ }
168
+ }
169
+ throw error;
92
170
  }
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}`);
171
+ });
172
+ }
173
+ // Add debug logs to sendMetrics method too
174
+ sendMetrics(metrics, apiKey) {
175
+ return __awaiter(this, void 0, void 0, function* () {
176
+ try {
177
+ if (this.portalApi) {
178
+ yield this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
179
+ headers: {
180
+ Authorization: `Bearer ${apiKey}`,
181
+ 'Content-Type': 'application/json',
182
+ },
183
+ body: metrics,
184
+ });
185
+ }
95
186
  }
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);
187
+ catch (_a) {
188
+ // No-op
100
189
  }
101
- return data;
102
190
  });
103
191
  }
104
192
  }
@@ -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
  }
@@ -10,8 +10,13 @@ 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
+ var Operation;
14
+ (function (Operation) {
15
+ Operation["SIGN"] = "sign";
16
+ Operation["RAW_SIGN"] = "raw_sign";
17
+ })(Operation || (Operation = {}));
13
18
  class MpcSigner {
14
- constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', featureFlags = {}, }) {
19
+ constructor({ keychain, mpcHost = 'mpc.portalhq.io', version = 'v6', portalApi, featureFlags = {}, }) {
15
20
  this.version = 'v6';
16
21
  this.buildParams = (method, txParams) => {
17
22
  let params = txParams;
@@ -42,61 +47,144 @@ class MpcSigner {
42
47
  this.mpc = NativeModules.PortalMobileMpc;
43
48
  this.mpcHost = mpcHost;
44
49
  this.version = version;
50
+ this.portalApi = portalApi;
45
51
  if (!this.mpc) {
46
52
  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
53
  }
48
54
  }
49
55
  sign(message, provider) {
50
56
  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,
57
+ // Always track metrics, but only send if feature flag is enabled
58
+ const shouldSendMetrics = this.featureFlags.enableSdkPerformanceMetrics === true;
59
+ const signStartTime = performance.now();
60
+ const preOperationStartTime = performance.now();
61
+ const metrics = {
62
+ operation: Operation.SIGN,
63
+ hasError: false,
79
64
  };
80
- const stringifiedMetadata = JSON.stringify(metadata);
81
- let formattedParams;
82
- let rpcUrl;
83
- if (isRaw) {
84
- formattedParams = this.buildParams(method, message.params);
85
- rpcUrl = '';
65
+ try {
66
+ const eip155Address = yield this.keychain.getEip155Address();
67
+ const apiKey = provider.apiKey;
68
+ const { method, chainId, curve, isRaw } = message;
69
+ // Add chainId to metrics
70
+ if (chainId) {
71
+ metrics.chainId = chainId;
72
+ }
73
+ switch (method) {
74
+ case 'eth_requestAccounts':
75
+ return [eip155Address];
76
+ case 'eth_accounts':
77
+ return [eip155Address];
78
+ default:
79
+ break;
80
+ }
81
+ const shares = yield this.keychain.getShares();
82
+ let signingShare = shares.secp256k1.share;
83
+ if (curve === PortalCurve.ED25519) {
84
+ if (!shares.ed25519) {
85
+ throw new Error('[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.');
86
+ }
87
+ signingShare = shares.ed25519.share;
88
+ }
89
+ const metadata = {
90
+ clientPlatform: 'REACT_NATIVE',
91
+ clientPlatformVersion: getClientPlatformVersion(),
92
+ isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
93
+ mpcServerVersion: this.version,
94
+ optimized: true,
95
+ curve,
96
+ chainId,
97
+ isRaw,
98
+ };
99
+ const stringifiedMetadata = JSON.stringify(metadata);
100
+ let formattedParams;
101
+ let rpcUrl;
102
+ if (isRaw) {
103
+ formattedParams = this.buildParams(method, message.params);
104
+ rpcUrl = '';
105
+ metrics.operation = Operation.RAW_SIGN;
106
+ }
107
+ else {
108
+ formattedParams = JSON.stringify(this.buildParams(method, message.params));
109
+ rpcUrl = provider.getGatewayUrl(chainId);
110
+ }
111
+ if (typeof formattedParams !== 'string') {
112
+ throw new Error(`[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
113
+ }
114
+ // Record pre-operation time
115
+ metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
116
+ // Measure MPC signing operation time
117
+ const mpcSignStartTime = performance.now();
118
+ const result = yield this.mpc.sign(apiKey, this.mpcHost, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId, stringifiedMetadata);
119
+ // Post-operation processing time starts
120
+ const postOperationStartTime = performance.now();
121
+ // Record native call time
122
+ metrics.mpcNativeCallMs = performance.now() - mpcSignStartTime;
123
+ // Parse result and extract binary metrics if available
124
+ const parsedResponse = JSON.parse(String(result));
125
+ const { data, error, meta } = parsedResponse;
126
+ // Add binary metrics to our metrics object
127
+ if (meta === null || meta === void 0 ? void 0 : meta.metrics) {
128
+ if (meta.metrics.wsConnectDurationMs) {
129
+ metrics.sdkBinaryWSConnectMs = meta.metrics.wsConnectDurationMs;
130
+ }
131
+ if (meta.metrics.operationDurationMs) {
132
+ metrics.sdkBinaryOperationMs = meta.metrics.operationDurationMs;
133
+ }
134
+ }
135
+ if ((error === null || error === void 0 ? void 0 : error.code) > 0) {
136
+ throw new PortalMpcError(error);
137
+ }
138
+ // Record post-operation time
139
+ metrics.sdkPostOperationMs = performance.now() - postOperationStartTime;
140
+ // Calculate total SDK signing time
141
+ metrics.sdkOperationMs = performance.now() - signStartTime;
142
+ // Only send metrics if the feature flag is enabled
143
+ if (shouldSendMetrics && this.portalApi) {
144
+ try {
145
+ yield this.sendMetrics(metrics, apiKey);
146
+ }
147
+ catch (_a) {
148
+ // No-op
149
+ }
150
+ }
151
+ return data;
86
152
  }
87
- else {
88
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
89
- rpcUrl = provider.getGatewayUrl(chainId);
153
+ catch (error) {
154
+ // Calculate total time even in error case
155
+ metrics.sdkOperationMs = performance.now() - signStartTime;
156
+ // Only send metrics if the feature flag is enabled
157
+ if (shouldSendMetrics) {
158
+ const apiKey = provider.apiKey;
159
+ metrics.hasError = true;
160
+ try {
161
+ yield this.sendMetrics(metrics, apiKey);
162
+ }
163
+ catch (_b) {
164
+ // No-op
165
+ }
166
+ }
167
+ throw error;
90
168
  }
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}`);
169
+ });
170
+ }
171
+ // Add debug logs to sendMetrics method too
172
+ sendMetrics(metrics, apiKey) {
173
+ return __awaiter(this, void 0, void 0, function* () {
174
+ try {
175
+ if (this.portalApi) {
176
+ yield this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
177
+ headers: {
178
+ Authorization: `Bearer ${apiKey}`,
179
+ 'Content-Type': 'application/json',
180
+ },
181
+ body: metrics,
182
+ });
183
+ }
93
184
  }
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);
185
+ catch (_a) {
186
+ // No-op
98
187
  }
99
- return data;
100
188
  });
101
189
  }
102
190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portal-hq/provider",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "license": "MIT",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/esm/index",
@@ -19,8 +19,8 @@
19
19
  "test": "jest"
20
20
  },
21
21
  "dependencies": {
22
- "@portal-hq/connect": "^4.1.0",
23
- "@portal-hq/utils": "^4.1.0"
22
+ "@portal-hq/connect": "^4.1.2",
23
+ "@portal-hq/utils": "^4.1.2"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@babel/preset-typescript": "^7.18.6",
@@ -30,5 +30,5 @@
30
30
  "ts-jest": "^29.0.3",
31
31
  "typescript": "^4.8.4"
32
32
  },
33
- "gitHead": "d53139e2dae6d0ede12d647ae4d11f4915b896f1"
33
+ "gitHead": "4504ed5bdfde36df2b9c1acab9916ad412ba6fd9"
34
34
  }
@@ -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,
@@ -17,17 +18,24 @@ import {
17
18
  } from '../../types'
18
19
  import Signer from './abstract'
19
20
 
21
+ enum Operation {
22
+ SIGN = 'sign',
23
+ RAW_SIGN = 'raw_sign',
24
+ }
25
+
20
26
  class MpcSigner implements Signer {
21
27
  private featureFlags: FeatureFlags
22
28
  private keychain: KeychainAdapter
23
29
  private mpc: PortalMobileMpc
24
- private mpcHost: string // should we add a default here mpc.portalhq.io
30
+ private mpcHost: string
25
31
  private version = 'v6'
32
+ private portalApi?: HttpRequester
26
33
 
27
34
  constructor({
28
35
  keychain,
29
36
  mpcHost = 'mpc.portalhq.io',
30
37
  version = 'v6',
38
+ portalApi,
31
39
  featureFlags = {},
32
40
  }: MpcSignerOptions) {
33
41
  this.featureFlags = featureFlags
@@ -35,6 +43,7 @@ class MpcSigner implements Signer {
35
43
  this.mpc = NativeModules.PortalMobileMpc
36
44
  this.mpcHost = mpcHost
37
45
  this.version = version
46
+ this.portalApi = portalApi
38
47
 
39
48
  if (!this.mpc) {
40
49
  throw new Error(
@@ -47,82 +56,175 @@ class MpcSigner implements Signer {
47
56
  message: SigningRequestArguments,
48
57
  provider: IPortalProvider,
49
58
  ): Promise<any> {
50
- const eip155Address = await this.keychain.getEip155Address()
59
+ // Always track metrics, but only send if feature flag is enabled
60
+ const shouldSendMetrics =
61
+ this.featureFlags.enableSdkPerformanceMetrics === true
62
+ const signStartTime = performance.now()
63
+ const preOperationStartTime = performance.now()
64
+ const metrics: Record<string, number | string | boolean> = {
65
+ operation: Operation.SIGN,
66
+ hasError: false,
67
+ }
68
+ try {
69
+ const eip155Address = await this.keychain.getEip155Address()
51
70
 
52
- const apiKey = provider.apiKey
71
+ const apiKey = provider.apiKey
53
72
 
54
- const { method, chainId, curve, isRaw } = message
73
+ const { method, chainId, curve, isRaw } = message
74
+ // Add chainId to metrics
75
+ if (chainId) {
76
+ metrics.chainId = chainId
77
+ }
55
78
 
56
- switch (method) {
57
- case 'eth_requestAccounts':
58
- return [eip155Address]
59
- case 'eth_accounts':
60
- return [eip155Address]
61
- default:
62
- break
63
- }
79
+ switch (method) {
80
+ case 'eth_requestAccounts':
81
+ return [eip155Address]
82
+ case 'eth_accounts':
83
+ return [eip155Address]
84
+ default:
85
+ break
86
+ }
64
87
 
65
- const shares = await this.keychain.getShares()
88
+ const shares = await this.keychain.getShares()
66
89
 
67
- let signingShare = shares.secp256k1.share
90
+ let signingShare = shares.secp256k1.share
91
+
92
+ if (curve === PortalCurve.ED25519) {
93
+ if (!shares.ed25519) {
94
+ throw new Error(
95
+ '[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.',
96
+ )
97
+ }
98
+ signingShare = shares.ed25519.share
99
+ }
68
100
 
69
- if (curve === PortalCurve.ED25519) {
70
- if (!shares.ed25519) {
101
+ const metadata: PortalMobileMpcMetadata = {
102
+ clientPlatform: 'REACT_NATIVE',
103
+ clientPlatformVersion: getClientPlatformVersion(),
104
+ isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
105
+ mpcServerVersion: this.version,
106
+ optimized: true,
107
+ curve,
108
+ chainId,
109
+ isRaw,
110
+ }
111
+
112
+ const stringifiedMetadata = JSON.stringify(metadata)
113
+
114
+ let formattedParams: string
115
+ let rpcUrl: string
116
+
117
+ if (isRaw) {
118
+ formattedParams = this.buildParams(method, message.params)
119
+ rpcUrl = ''
120
+ metrics.operation = Operation.RAW_SIGN
121
+ } else {
122
+ formattedParams = JSON.stringify(
123
+ this.buildParams(method, message.params),
124
+ )
125
+ rpcUrl = provider.getGatewayUrl(chainId)
126
+ }
127
+
128
+ if (typeof formattedParams !== 'string') {
71
129
  throw new Error(
72
- '[Portal.Provider.MpcSigner] The ED25519 share is missing from the keychain.',
130
+ `[Portal.Provider.MpcSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`,
73
131
  )
74
132
  }
75
- signingShare = shares.ed25519.share
76
- }
77
133
 
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
- }
134
+ // Record pre-operation time
135
+ metrics.sdkPreOperationMs = performance.now() - preOperationStartTime
88
136
 
89
- const stringifiedMetadata = JSON.stringify(metadata)
137
+ // Measure MPC signing operation time
138
+ const mpcSignStartTime = performance.now()
90
139
 
91
- let formattedParams: string
92
- let rpcUrl: string
140
+ const result = await this.mpc.sign(
141
+ apiKey,
142
+ this.mpcHost,
143
+ JSON.stringify(signingShare),
144
+ message.method,
145
+ formattedParams,
146
+ rpcUrl,
147
+ chainId,
148
+ stringifiedMetadata,
149
+ )
93
150
 
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
- }
151
+ // Post-operation processing time starts
152
+ const postOperationStartTime = performance.now()
101
153
 
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
- }
154
+ // Record native call time
155
+ metrics.mpcNativeCallMs = performance.now() - mpcSignStartTime
156
+
157
+ // Parse result and extract binary metrics if available
158
+ const parsedResponse = JSON.parse(String(result)) as SigningResponse
159
+ const { data, error, meta } = parsedResponse
160
+
161
+ // Add binary metrics to our metrics object
162
+ if (meta?.metrics) {
163
+ if (meta.metrics.wsConnectDurationMs) {
164
+ metrics.sdkBinaryWSConnectMs = meta.metrics.wsConnectDurationMs
165
+ }
166
+ if (meta.metrics.operationDurationMs) {
167
+ metrics.sdkBinaryOperationMs = meta.metrics.operationDurationMs
168
+ }
169
+ }
170
+
171
+ if (error?.code > 0) {
172
+ throw new PortalMpcError(error)
173
+ }
174
+
175
+ // Record post-operation time
176
+ metrics.sdkPostOperationMs = performance.now() - postOperationStartTime
107
177
 
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)
178
+ // Calculate total SDK signing time
179
+ metrics.sdkOperationMs = performance.now() - signStartTime
180
+
181
+ // Only send metrics if the feature flag is enabled
182
+ if (shouldSendMetrics && this.portalApi) {
183
+ try {
184
+ await this.sendMetrics(metrics, apiKey)
185
+ } catch {
186
+ // No-op
187
+ }
188
+ }
189
+
190
+ return data
191
+ } catch (error) {
192
+ // Calculate total time even in error case
193
+ metrics.sdkOperationMs = performance.now() - signStartTime
194
+
195
+ // Only send metrics if the feature flag is enabled
196
+ if (shouldSendMetrics) {
197
+ const apiKey = provider.apiKey
198
+ metrics.hasError = true
199
+ try {
200
+ await this.sendMetrics(metrics, apiKey)
201
+ } catch {
202
+ // No-op
203
+ }
204
+ }
205
+
206
+ throw error
123
207
  }
208
+ }
124
209
 
125
- return data
210
+ // Add debug logs to sendMetrics method too
211
+ private async sendMetrics(
212
+ metrics: Record<string, number | string | boolean>,
213
+ apiKey: string,
214
+ ): Promise<void> {
215
+ try {
216
+ if (this.portalApi) {
217
+ await this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
218
+ headers: {
219
+ Authorization: `Bearer ${apiKey}`,
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: metrics,
223
+ })
224
+ }
225
+ } catch {
226
+ // No-op
227
+ }
126
228
  }
127
229
 
128
230
  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
  }
@@ -120,6 +124,12 @@ export type SigningRequestParams = Eip1559 | LegacyTx
120
124
  export interface SigningResponse {
121
125
  data: string
122
126
  error: PortalError
127
+ meta?: {
128
+ metrics?: {
129
+ wsConnectDurationMs?: number
130
+ operationDurationMs?: number
131
+ }
132
+ }
123
133
  }
124
134
 
125
135
  export interface SwitchEthereumChainParameter {