@portal-hq/provider 4.1.4 → 4.1.6

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.
@@ -88,10 +88,22 @@ class EnclaveSigner {
88
88
  break;
89
89
  }
90
90
  const shares = yield this.keychain.getShares();
91
+ // Validate shares exist
92
+ if (!shares.secp256k1) {
93
+ throw new utils_1.PortalMpcError({
94
+ code: utils_1.PortalErrorCodes.FailedToParseInputShareObject,
95
+ id: 'MissingShare',
96
+ message: '[Portal.Provider.EnclaveSigner] The SECP256K1 share is missing from the keychain.',
97
+ });
98
+ }
91
99
  let signingShare = shares.secp256k1.share;
92
100
  if (curve === core_1.PortalCurve.ED25519) {
93
101
  if (!shares.ed25519) {
94
- throw new Error('[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.');
102
+ throw new utils_1.PortalMpcError({
103
+ code: utils_1.PortalErrorCodes.FailedToParseInputShareObject,
104
+ id: 'MissingShare',
105
+ message: '[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.',
106
+ });
95
107
  }
96
108
  signingShare = shares.ed25519.share;
97
109
  }
@@ -107,17 +119,24 @@ class EnclaveSigner {
107
119
  reqId: traceId,
108
120
  connectionTracingEnabled: shouldSendMetrics,
109
121
  };
122
+ // Build params
123
+ // Avoid double JSON encoding: if params is already a string (e.g. a hex message), pass it directly; otherwise stringify objects/arrays.
124
+ const params = this.buildParams(method, message.params);
110
125
  let formattedParams;
111
- let rpcUrl;
112
126
  if (isRaw) {
113
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
114
- rpcUrl = '';
115
- metrics.operation = Operation.RAW_SIGN;
127
+ if (typeof params !== 'string') {
128
+ throw new Error('[Portal.Provider.EnclaveSigner] For raw signing, params must be a string (e.g., a hex-encoded message).');
129
+ }
130
+ formattedParams = params;
116
131
  }
117
132
  else {
118
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
119
- rpcUrl = provider.getGatewayUrl(chainId);
133
+ formattedParams =
134
+ typeof params === 'string' ? params : JSON.stringify(params);
120
135
  }
136
+ // Get RPC URL (getGatewayUrl handles undefined chainId)
137
+ const rpcUrl = isRaw ? '' : provider.getGatewayUrl(chainId);
138
+ // Set metrics operation
139
+ metrics.operation = isRaw ? Operation.RAW_SIGN : Operation.SIGN;
121
140
  if (typeof formattedParams !== 'string') {
122
141
  throw new Error(`[Portal.Provider.EnclaveSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
123
142
  }
@@ -125,9 +144,40 @@ class EnclaveSigner {
125
144
  metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
126
145
  // Measure enclave signing operation time
127
146
  const enclaveSignStartTime = performance.now();
128
- const result = isRaw
129
- ? yield this.enclaveRawSign(apiKey, JSON.stringify(signingShare), formattedParams, curve || 'SECP256K1')
130
- : yield this.enclaveSign(apiKey, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId || '', JSON.stringify(metadata));
147
+ // Build request based on operation type
148
+ let endpoint;
149
+ let requestBody;
150
+ if (isRaw) {
151
+ // Raw sign endpoint and body
152
+ endpoint = `/v1/raw/sign/${curve || 'SECP256K1'}`;
153
+ requestBody = {
154
+ params: formattedParams,
155
+ share: JSON.stringify(signingShare),
156
+ };
157
+ }
158
+ else {
159
+ // Standard sign endpoint and body
160
+ endpoint = '/v1/sign';
161
+ requestBody = {
162
+ method: message.method,
163
+ params: formattedParams,
164
+ share: JSON.stringify(signingShare),
165
+ chainId: chainId || '',
166
+ rpcUrl: rpcUrl,
167
+ metadataStr: JSON.stringify(metadata),
168
+ clientPlatform: 'REACT_NATIVE',
169
+ clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
170
+ };
171
+ }
172
+ // Make API request and process response
173
+ let result;
174
+ try {
175
+ const response = yield this.makeEnclaveRequest(endpoint, apiKey, requestBody);
176
+ result = this.processEnclaveResponse(response);
177
+ }
178
+ catch (error) {
179
+ this.handleEnclaveError(error);
180
+ }
131
181
  // Post-operation processing time starts
132
182
  const postOperationStartTime = performance.now();
133
183
  // Record HTTP call time
@@ -181,118 +231,117 @@ class EnclaveSigner {
181
231
  }
182
232
  });
183
233
  }
184
- enclaveRawSign(apiKey, signingShare, params, curve) {
185
- var _a;
186
- return __awaiter(this, void 0, void 0, function* () {
187
- if (!apiKey || !signingShare || !params || !curve) {
188
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided for raw signing');
189
- }
190
- const requestBody = {
191
- params: params,
192
- share: signingShare,
193
- };
194
- const endpoint = `/v1/raw/sign/${curve}`;
195
- try {
196
- const response = yield this.requests.post(endpoint, {
197
- headers: {
198
- Authorization: `Bearer ${apiKey}`,
199
- 'Content-Type': 'application/json',
200
- },
201
- body: requestBody,
202
- });
203
- return this.encodeSuccessResult(response.data);
204
- }
205
- catch (error) {
206
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
207
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
208
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
209
- }
210
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
211
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
212
- error.message || 'Network error occurred');
234
+ /**
235
+ * Parse API error from HttpError message
236
+ * HttpError format: "{status} - {statusText}: {responseBody}"
237
+ * API returns: {"id":"ERROR_ID","message":"error text","code":216}
238
+ */
239
+ parseApiError(errorMessage) {
240
+ // Extract response body after the colon
241
+ const colonIndex = errorMessage.indexOf(':');
242
+ if (colonIndex === -1) {
243
+ return null;
244
+ }
245
+ const responseBody = errorMessage.substring(colonIndex + 1).trim();
246
+ // Try to extract JSON object from response (handles prefixed JSON)
247
+ const jsonMatch = responseBody.match(/\{.*\}$/);
248
+ const jsonString = jsonMatch ? jsonMatch[0] : responseBody;
249
+ try {
250
+ const parsed = JSON.parse(jsonString);
251
+ // Ensure all required fields are present
252
+ if (parsed.id &&
253
+ parsed.message !== undefined &&
254
+ parsed.code !== undefined) {
255
+ return {
256
+ id: parsed.id,
257
+ message: parsed.message,
258
+ code: parsed.code,
259
+ };
213
260
  }
214
- });
261
+ }
262
+ catch (_a) {
263
+ // JSON parsing failed
264
+ }
265
+ return null;
215
266
  }
216
- enclaveSign(apiKey, signingShare, method, params, rpcURL, chainId, metadata) {
217
- var _a;
267
+ /**
268
+ * Make HTTP request to Enclave API
269
+ * Returns raw response without processing
270
+ */
271
+ makeEnclaveRequest(endpoint, apiKey, body) {
218
272
  return __awaiter(this, void 0, void 0, function* () {
219
- if (!apiKey ||
220
- !signingShare ||
221
- !method ||
222
- !params ||
223
- !chainId ||
224
- !metadata) {
225
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided');
226
- }
227
- const requestBody = {
228
- method: method,
229
- params: params,
230
- share: signingShare,
231
- chainId: chainId,
232
- rpcUrl: rpcURL,
233
- metadataStr: metadata,
234
- clientPlatform: 'REACT_NATIVE',
235
- clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
236
- };
237
- try {
238
- const response = yield this.requests.post('/v1/sign', {
239
- headers: {
240
- Authorization: `Bearer ${apiKey}`,
241
- 'Content-Type': 'application/json',
242
- },
243
- body: requestBody,
244
- });
245
- return this.encodeSuccessResult(response.data);
246
- }
247
- catch (error) {
248
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
249
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
250
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
251
- }
252
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
253
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
254
- error.message || 'Network error occurred');
255
- }
273
+ return yield this.requests.post(endpoint, {
274
+ headers: {
275
+ Authorization: `Bearer ${apiKey}`,
276
+ 'Content-Type': 'application/json',
277
+ },
278
+ body,
279
+ });
256
280
  });
257
281
  }
258
- // Helper function to encode success results
259
- encodeSuccessResult(data) {
260
- const successResult = { data: data, error: undefined };
261
- return this.encodeJSON(successResult);
262
- }
263
- // Helper function to decode Portal errors
264
- decodePortalError(errorStr) {
265
- if (!errorStr)
266
- return null;
267
- try {
268
- return JSON.parse(errorStr);
282
+ /**
283
+ * Process Enclave API response
284
+ * Checks for error format and extracts data
285
+ */
286
+ processEnclaveResponse(response) {
287
+ // Check for API-level errors first
288
+ // API returns errors at top level: {id, message, code}
289
+ if ('id' in response && 'message' in response && 'code' in response) {
290
+ const errorResponse = response;
291
+ throw new utils_1.PortalMpcError({
292
+ code: errorResponse.code,
293
+ id: errorResponse.id,
294
+ message: errorResponse.message,
295
+ });
269
296
  }
270
- catch (_a) {
271
- return null;
297
+ // API returns success as: {data: string}
298
+ const successResponse = response;
299
+ if (!successResponse.data) {
300
+ throw new utils_1.PortalMpcError({
301
+ code: utils_1.PortalErrorCodes.BadRequest,
302
+ id: 'InvalidResponse',
303
+ message: 'API response missing data field',
304
+ });
272
305
  }
306
+ return successResponse.data;
273
307
  }
274
- // Helper function to encode error results
275
- encodeErrorResult(id, message) {
276
- const errorResult = {
277
- data: undefined,
278
- error: { id, message },
279
- };
280
- return this.encodeJSON(errorResult);
281
- }
282
- // Helper function to encode any object to JSON string
283
- encodeJSON(value) {
284
- try {
285
- const jsonString = JSON.stringify(value);
286
- return jsonString;
308
+ /**
309
+ * Handle errors from Enclave API calls
310
+ * Parses and transforms to PortalMpcError
311
+ */
312
+ handleEnclaveError(error) {
313
+ // If it's already a PortalMpcError, re-throw it
314
+ if (error instanceof utils_1.PortalMpcError) {
315
+ throw error;
287
316
  }
288
- catch (error) {
289
- return JSON.stringify({
290
- error: {
291
- id: 'ENCODING_ERROR',
292
- message: `Failed to encode JSON: ${error.message}`,
293
- },
317
+ // Handle HTTP error responses
318
+ // HttpError format: "{status} - {statusText}: {responseBody}"
319
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
320
+ const errorMessage = typeof error.message === 'string' ? error.message : '';
321
+ // Try to parse API error from HttpError message
322
+ const apiError = this.parseApiError(errorMessage);
323
+ if (apiError) {
324
+ throw new utils_1.PortalMpcError({
325
+ code: apiError.code,
326
+ id: apiError.id,
327
+ message: apiError.message,
294
328
  });
295
329
  }
330
+ // Check for 401 Unauthorized
331
+ if (errorMessage.startsWith('401 -')) {
332
+ throw new utils_1.PortalMpcError({
333
+ code: utils_1.PortalErrorCodes.InvalidApiKey,
334
+ id: 'Unauthorized',
335
+ message: 'Unauthorized request',
336
+ });
337
+ }
338
+ // Handle network errors
339
+ throw new utils_1.PortalMpcError({
340
+ code: utils_1.PortalErrorCodes.SigningNetworkError,
341
+ id: 'SIGNING_NETWORK_ERROR',
342
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
343
+ message: errorMessage || 'Network error occurred',
344
+ });
296
345
  }
297
346
  sendMetrics(metrics, apiKey) {
298
347
  return __awaiter(this, void 0, void 0, function* () {
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { PortalCurve } from '@portal-hq/core';
11
- import { HttpRequester, getClientPlatformVersion, } from '@portal-hq/utils';
11
+ import { HttpRequester, PortalErrorCodes, PortalMpcError, getClientPlatformVersion, } from '@portal-hq/utils';
12
12
  import UUID from 'react-native-uuid';
13
13
  var Operation;
14
14
  (function (Operation) {
@@ -83,10 +83,22 @@ class EnclaveSigner {
83
83
  break;
84
84
  }
85
85
  const shares = yield this.keychain.getShares();
86
+ // Validate shares exist
87
+ if (!shares.secp256k1) {
88
+ throw new PortalMpcError({
89
+ code: PortalErrorCodes.FailedToParseInputShareObject,
90
+ id: 'MissingShare',
91
+ message: '[Portal.Provider.EnclaveSigner] The SECP256K1 share is missing from the keychain.',
92
+ });
93
+ }
86
94
  let signingShare = shares.secp256k1.share;
87
95
  if (curve === PortalCurve.ED25519) {
88
96
  if (!shares.ed25519) {
89
- throw new Error('[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.');
97
+ throw new PortalMpcError({
98
+ code: PortalErrorCodes.FailedToParseInputShareObject,
99
+ id: 'MissingShare',
100
+ message: '[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.',
101
+ });
90
102
  }
91
103
  signingShare = shares.ed25519.share;
92
104
  }
@@ -102,17 +114,24 @@ class EnclaveSigner {
102
114
  reqId: traceId,
103
115
  connectionTracingEnabled: shouldSendMetrics,
104
116
  };
117
+ // Build params
118
+ // Avoid double JSON encoding: if params is already a string (e.g. a hex message), pass it directly; otherwise stringify objects/arrays.
119
+ const params = this.buildParams(method, message.params);
105
120
  let formattedParams;
106
- let rpcUrl;
107
121
  if (isRaw) {
108
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
109
- rpcUrl = '';
110
- metrics.operation = Operation.RAW_SIGN;
122
+ if (typeof params !== 'string') {
123
+ throw new Error('[Portal.Provider.EnclaveSigner] For raw signing, params must be a string (e.g., a hex-encoded message).');
124
+ }
125
+ formattedParams = params;
111
126
  }
112
127
  else {
113
- formattedParams = JSON.stringify(this.buildParams(method, message.params));
114
- rpcUrl = provider.getGatewayUrl(chainId);
128
+ formattedParams =
129
+ typeof params === 'string' ? params : JSON.stringify(params);
115
130
  }
131
+ // Get RPC URL (getGatewayUrl handles undefined chainId)
132
+ const rpcUrl = isRaw ? '' : provider.getGatewayUrl(chainId);
133
+ // Set metrics operation
134
+ metrics.operation = isRaw ? Operation.RAW_SIGN : Operation.SIGN;
116
135
  if (typeof formattedParams !== 'string') {
117
136
  throw new Error(`[Portal.Provider.EnclaveSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
118
137
  }
@@ -120,9 +139,40 @@ class EnclaveSigner {
120
139
  metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
121
140
  // Measure enclave signing operation time
122
141
  const enclaveSignStartTime = performance.now();
123
- const result = isRaw
124
- ? yield this.enclaveRawSign(apiKey, JSON.stringify(signingShare), formattedParams, curve || 'SECP256K1')
125
- : yield this.enclaveSign(apiKey, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId || '', JSON.stringify(metadata));
142
+ // Build request based on operation type
143
+ let endpoint;
144
+ let requestBody;
145
+ if (isRaw) {
146
+ // Raw sign endpoint and body
147
+ endpoint = `/v1/raw/sign/${curve || 'SECP256K1'}`;
148
+ requestBody = {
149
+ params: formattedParams,
150
+ share: JSON.stringify(signingShare),
151
+ };
152
+ }
153
+ else {
154
+ // Standard sign endpoint and body
155
+ endpoint = '/v1/sign';
156
+ requestBody = {
157
+ method: message.method,
158
+ params: formattedParams,
159
+ share: JSON.stringify(signingShare),
160
+ chainId: chainId || '',
161
+ rpcUrl: rpcUrl,
162
+ metadataStr: JSON.stringify(metadata),
163
+ clientPlatform: 'REACT_NATIVE',
164
+ clientPlatformVersion: getClientPlatformVersion(),
165
+ };
166
+ }
167
+ // Make API request and process response
168
+ let result;
169
+ try {
170
+ const response = yield this.makeEnclaveRequest(endpoint, apiKey, requestBody);
171
+ result = this.processEnclaveResponse(response);
172
+ }
173
+ catch (error) {
174
+ this.handleEnclaveError(error);
175
+ }
126
176
  // Post-operation processing time starts
127
177
  const postOperationStartTime = performance.now();
128
178
  // Record HTTP call time
@@ -176,118 +226,117 @@ class EnclaveSigner {
176
226
  }
177
227
  });
178
228
  }
179
- enclaveRawSign(apiKey, signingShare, params, curve) {
180
- var _a;
181
- return __awaiter(this, void 0, void 0, function* () {
182
- if (!apiKey || !signingShare || !params || !curve) {
183
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided for raw signing');
184
- }
185
- const requestBody = {
186
- params: params,
187
- share: signingShare,
188
- };
189
- const endpoint = `/v1/raw/sign/${curve}`;
190
- try {
191
- const response = yield this.requests.post(endpoint, {
192
- headers: {
193
- Authorization: `Bearer ${apiKey}`,
194
- 'Content-Type': 'application/json',
195
- },
196
- body: requestBody,
197
- });
198
- return this.encodeSuccessResult(response.data);
199
- }
200
- catch (error) {
201
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
202
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
203
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
204
- }
205
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
206
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
207
- error.message || 'Network error occurred');
229
+ /**
230
+ * Parse API error from HttpError message
231
+ * HttpError format: "{status} - {statusText}: {responseBody}"
232
+ * API returns: {"id":"ERROR_ID","message":"error text","code":216}
233
+ */
234
+ parseApiError(errorMessage) {
235
+ // Extract response body after the colon
236
+ const colonIndex = errorMessage.indexOf(':');
237
+ if (colonIndex === -1) {
238
+ return null;
239
+ }
240
+ const responseBody = errorMessage.substring(colonIndex + 1).trim();
241
+ // Try to extract JSON object from response (handles prefixed JSON)
242
+ const jsonMatch = responseBody.match(/\{.*\}$/);
243
+ const jsonString = jsonMatch ? jsonMatch[0] : responseBody;
244
+ try {
245
+ const parsed = JSON.parse(jsonString);
246
+ // Ensure all required fields are present
247
+ if (parsed.id &&
248
+ parsed.message !== undefined &&
249
+ parsed.code !== undefined) {
250
+ return {
251
+ id: parsed.id,
252
+ message: parsed.message,
253
+ code: parsed.code,
254
+ };
208
255
  }
209
- });
256
+ }
257
+ catch (_a) {
258
+ // JSON parsing failed
259
+ }
260
+ return null;
210
261
  }
211
- enclaveSign(apiKey, signingShare, method, params, rpcURL, chainId, metadata) {
212
- var _a;
262
+ /**
263
+ * Make HTTP request to Enclave API
264
+ * Returns raw response without processing
265
+ */
266
+ makeEnclaveRequest(endpoint, apiKey, body) {
213
267
  return __awaiter(this, void 0, void 0, function* () {
214
- if (!apiKey ||
215
- !signingShare ||
216
- !method ||
217
- !params ||
218
- !chainId ||
219
- !metadata) {
220
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided');
221
- }
222
- const requestBody = {
223
- method: method,
224
- params: params,
225
- share: signingShare,
226
- chainId: chainId,
227
- rpcUrl: rpcURL,
228
- metadataStr: metadata,
229
- clientPlatform: 'REACT_NATIVE',
230
- clientPlatformVersion: getClientPlatformVersion(),
231
- };
232
- try {
233
- const response = yield this.requests.post('/v1/sign', {
234
- headers: {
235
- Authorization: `Bearer ${apiKey}`,
236
- 'Content-Type': 'application/json',
237
- },
238
- body: requestBody,
239
- });
240
- return this.encodeSuccessResult(response.data);
241
- }
242
- catch (error) {
243
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
244
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
245
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
246
- }
247
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
248
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
249
- error.message || 'Network error occurred');
250
- }
268
+ return yield this.requests.post(endpoint, {
269
+ headers: {
270
+ Authorization: `Bearer ${apiKey}`,
271
+ 'Content-Type': 'application/json',
272
+ },
273
+ body,
274
+ });
251
275
  });
252
276
  }
253
- // Helper function to encode success results
254
- encodeSuccessResult(data) {
255
- const successResult = { data: data, error: undefined };
256
- return this.encodeJSON(successResult);
257
- }
258
- // Helper function to decode Portal errors
259
- decodePortalError(errorStr) {
260
- if (!errorStr)
261
- return null;
262
- try {
263
- return JSON.parse(errorStr);
277
+ /**
278
+ * Process Enclave API response
279
+ * Checks for error format and extracts data
280
+ */
281
+ processEnclaveResponse(response) {
282
+ // Check for API-level errors first
283
+ // API returns errors at top level: {id, message, code}
284
+ if ('id' in response && 'message' in response && 'code' in response) {
285
+ const errorResponse = response;
286
+ throw new PortalMpcError({
287
+ code: errorResponse.code,
288
+ id: errorResponse.id,
289
+ message: errorResponse.message,
290
+ });
264
291
  }
265
- catch (_a) {
266
- return null;
292
+ // API returns success as: {data: string}
293
+ const successResponse = response;
294
+ if (!successResponse.data) {
295
+ throw new PortalMpcError({
296
+ code: PortalErrorCodes.BadRequest,
297
+ id: 'InvalidResponse',
298
+ message: 'API response missing data field',
299
+ });
267
300
  }
301
+ return successResponse.data;
268
302
  }
269
- // Helper function to encode error results
270
- encodeErrorResult(id, message) {
271
- const errorResult = {
272
- data: undefined,
273
- error: { id, message },
274
- };
275
- return this.encodeJSON(errorResult);
276
- }
277
- // Helper function to encode any object to JSON string
278
- encodeJSON(value) {
279
- try {
280
- const jsonString = JSON.stringify(value);
281
- return jsonString;
303
+ /**
304
+ * Handle errors from Enclave API calls
305
+ * Parses and transforms to PortalMpcError
306
+ */
307
+ handleEnclaveError(error) {
308
+ // If it's already a PortalMpcError, re-throw it
309
+ if (error instanceof PortalMpcError) {
310
+ throw error;
282
311
  }
283
- catch (error) {
284
- return JSON.stringify({
285
- error: {
286
- id: 'ENCODING_ERROR',
287
- message: `Failed to encode JSON: ${error.message}`,
288
- },
312
+ // Handle HTTP error responses
313
+ // HttpError format: "{status} - {statusText}: {responseBody}"
314
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
315
+ const errorMessage = typeof error.message === 'string' ? error.message : '';
316
+ // Try to parse API error from HttpError message
317
+ const apiError = this.parseApiError(errorMessage);
318
+ if (apiError) {
319
+ throw new PortalMpcError({
320
+ code: apiError.code,
321
+ id: apiError.id,
322
+ message: apiError.message,
289
323
  });
290
324
  }
325
+ // Check for 401 Unauthorized
326
+ if (errorMessage.startsWith('401 -')) {
327
+ throw new PortalMpcError({
328
+ code: PortalErrorCodes.InvalidApiKey,
329
+ id: 'Unauthorized',
330
+ message: 'Unauthorized request',
331
+ });
332
+ }
333
+ // Handle network errors
334
+ throw new PortalMpcError({
335
+ code: PortalErrorCodes.SigningNetworkError,
336
+ id: 'SIGNING_NETWORK_ERROR',
337
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
338
+ message: errorMessage || 'Network error occurred',
339
+ });
291
340
  }
292
341
  sendMetrics(metrics, apiKey) {
293
342
  return __awaiter(this, void 0, void 0, function* () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portal-hq/provider",
3
- "version": "4.1.4",
3
+ "version": "4.1.6",
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.4",
23
- "@portal-hq/utils": "^4.1.4",
22
+ "@portal-hq/connect": "^4.1.6",
23
+ "@portal-hq/utils": "^4.1.6",
24
24
  "@types/react-native-uuid": "^2.0.0",
25
25
  "react-native-uuid": "^2.0.3"
26
26
  },
@@ -32,5 +32,5 @@
32
32
  "ts-jest": "^29.0.3",
33
33
  "typescript": "^4.8.4"
34
34
  },
35
- "gitHead": "6507173bd858f430a4144bd9e700878a879cf286"
35
+ "gitHead": "6906f8cc1dbd92d87effa174b8fa73b74bf63532"
36
36
  }
@@ -4,6 +4,8 @@ import {
4
4
  HttpRequester,
5
5
  IPortalProvider,
6
6
  KeychainAdapter,
7
+ PortalErrorCodes,
8
+ PortalMpcError,
7
9
  type SigningRequestArguments,
8
10
  getClientPlatformVersion,
9
11
  } from '@portal-hq/utils'
@@ -12,9 +14,7 @@ import UUID from 'react-native-uuid'
12
14
  import {
13
15
  type MpcSignerOptions,
14
16
  PortalMobileMpcMetadata,
15
- type EnclaveSignRequest,
16
17
  type EnclaveSignResponse,
17
- type EnclaveSignResult,
18
18
  } from '../../types'
19
19
  import Signer from './abstract'
20
20
 
@@ -88,13 +88,27 @@ class EnclaveSigner implements Signer {
88
88
  }
89
89
 
90
90
  const shares = await this.keychain.getShares()
91
+
92
+ // Validate shares exist
93
+ if (!shares.secp256k1) {
94
+ throw new PortalMpcError({
95
+ code: PortalErrorCodes.FailedToParseInputShareObject,
96
+ id: 'MissingShare',
97
+ message:
98
+ '[Portal.Provider.EnclaveSigner] The SECP256K1 share is missing from the keychain.',
99
+ })
100
+ }
101
+
91
102
  let signingShare = shares.secp256k1.share
92
103
 
93
104
  if (curve === PortalCurve.ED25519) {
94
105
  if (!shares.ed25519) {
95
- throw new Error(
96
- '[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.',
97
- )
106
+ throw new PortalMpcError({
107
+ code: PortalErrorCodes.FailedToParseInputShareObject,
108
+ id: 'MissingShare',
109
+ message:
110
+ '[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.',
111
+ })
98
112
  }
99
113
  signingShare = shares.ed25519.share
100
114
  }
@@ -112,22 +126,29 @@ class EnclaveSigner implements Signer {
112
126
  connectionTracingEnabled: shouldSendMetrics,
113
127
  }
114
128
 
129
+ // Build params
130
+ // Avoid double JSON encoding: if params is already a string (e.g. a hex message), pass it directly; otherwise stringify objects/arrays.
131
+ const params = this.buildParams(method, message.params)
115
132
  let formattedParams: string
116
- let rpcUrl: string
117
133
 
118
134
  if (isRaw) {
119
- formattedParams = JSON.stringify(
120
- this.buildParams(method, message.params),
121
- )
122
- rpcUrl = ''
123
- metrics.operation = Operation.RAW_SIGN
135
+ if (typeof params !== 'string') {
136
+ throw new Error(
137
+ '[Portal.Provider.EnclaveSigner] For raw signing, params must be a string (e.g., a hex-encoded message).',
138
+ )
139
+ }
140
+ formattedParams = params
124
141
  } else {
125
- formattedParams = JSON.stringify(
126
- this.buildParams(method, message.params),
127
- )
128
- rpcUrl = provider.getGatewayUrl(chainId)
142
+ formattedParams =
143
+ typeof params === 'string' ? params : JSON.stringify(params)
129
144
  }
130
145
 
146
+ // Get RPC URL (getGatewayUrl handles undefined chainId)
147
+ const rpcUrl = isRaw ? '' : provider.getGatewayUrl(chainId)
148
+
149
+ // Set metrics operation
150
+ metrics.operation = isRaw ? Operation.RAW_SIGN : Operation.SIGN
151
+
131
152
  if (typeof formattedParams !== 'string') {
132
153
  throw new Error(
133
154
  `[Portal.Provider.EnclaveSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`,
@@ -140,22 +161,44 @@ class EnclaveSigner implements Signer {
140
161
  // Measure enclave signing operation time
141
162
  const enclaveSignStartTime = performance.now()
142
163
 
143
- const result = isRaw
144
- ? await this.enclaveRawSign(
145
- apiKey,
146
- JSON.stringify(signingShare),
147
- formattedParams,
148
- curve || 'SECP256K1',
149
- )
150
- : await this.enclaveSign(
151
- apiKey,
152
- JSON.stringify(signingShare),
153
- message.method,
154
- formattedParams,
155
- rpcUrl,
156
- chainId || '',
157
- JSON.stringify(metadata),
158
- )
164
+ // Build request based on operation type
165
+ let endpoint: string
166
+ let requestBody: Record<string, any>
167
+
168
+ if (isRaw) {
169
+ // Raw sign endpoint and body
170
+ endpoint = `/v1/raw/sign/${curve || 'SECP256K1'}`
171
+ requestBody = {
172
+ params: formattedParams,
173
+ share: JSON.stringify(signingShare),
174
+ }
175
+ } else {
176
+ // Standard sign endpoint and body
177
+ endpoint = '/v1/sign'
178
+ requestBody = {
179
+ method: message.method,
180
+ params: formattedParams,
181
+ share: JSON.stringify(signingShare),
182
+ chainId: chainId || '',
183
+ rpcUrl: rpcUrl,
184
+ metadataStr: JSON.stringify(metadata),
185
+ clientPlatform: 'REACT_NATIVE',
186
+ clientPlatformVersion: getClientPlatformVersion(),
187
+ }
188
+ }
189
+
190
+ // Make API request and process response
191
+ let result: string
192
+ try {
193
+ const response = await this.makeEnclaveRequest(
194
+ endpoint,
195
+ apiKey,
196
+ requestBody,
197
+ )
198
+ result = this.processEnclaveResponse(response)
199
+ } catch (error) {
200
+ this.handleEnclaveError(error)
201
+ }
159
202
 
160
203
  // Post-operation processing time starts
161
204
  const postOperationStartTime = performance.now()
@@ -216,153 +259,141 @@ class EnclaveSigner implements Signer {
216
259
  }
217
260
  }
218
261
 
219
- private async enclaveRawSign(
220
- apiKey: string,
221
- signingShare: string,
222
- params: string,
223
- curve: string,
224
- ): Promise<string> {
225
- if (!apiKey || !signingShare || !params || !curve) {
226
- return this.encodeErrorResult(
227
- 'INVALID_PARAMETERS',
228
- 'Invalid parameters provided for raw signing',
229
- )
262
+ /**
263
+ * Parse API error from HttpError message
264
+ * HttpError format: "{status} - {statusText}: {responseBody}"
265
+ * API returns: {"id":"ERROR_ID","message":"error text","code":216}
266
+ */
267
+ private parseApiError(
268
+ errorMessage: string,
269
+ ): { id: string; message: string; code: number } | null {
270
+ // Extract response body after the colon
271
+ const colonIndex = errorMessage.indexOf(':')
272
+ if (colonIndex === -1) {
273
+ return null
230
274
  }
231
275
 
232
- const requestBody = {
233
- params: params,
234
- share: signingShare,
235
- }
276
+ const responseBody = errorMessage.substring(colonIndex + 1).trim()
236
277
 
237
- const endpoint = `/v1/raw/sign/${curve}`
278
+ // Try to extract JSON object from response (handles prefixed JSON)
279
+ const jsonMatch = responseBody.match(/\{.*\}$/)
280
+ const jsonString = jsonMatch ? jsonMatch[0] : responseBody
238
281
 
239
282
  try {
240
- const response = await this.requests.post<EnclaveSignResponse>(endpoint, {
241
- headers: {
242
- Authorization: `Bearer ${apiKey}`,
243
- 'Content-Type': 'application/json',
244
- },
245
- body: requestBody,
246
- })
247
-
248
- return this.encodeSuccessResult(response.data)
249
- } catch (error: any) {
250
- if (error?.response?.data) {
251
- const portalError = this.decodePortalError(
252
- JSON.stringify(error.response.data),
253
- )
254
- return this.encodeErrorResult(portalError?.id, portalError?.message)
283
+ const parsed = JSON.parse(jsonString)
284
+
285
+ // Ensure all required fields are present
286
+ if (
287
+ parsed.id &&
288
+ parsed.message !== undefined &&
289
+ parsed.code !== undefined
290
+ ) {
291
+ return {
292
+ id: parsed.id,
293
+ message: parsed.message,
294
+ code: parsed.code,
295
+ }
255
296
  }
256
- return this.encodeErrorResult(
257
- 'SIGNING_NETWORK_ERROR',
258
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
259
- error.message || 'Network error occurred',
260
- )
297
+ } catch {
298
+ // JSON parsing failed
261
299
  }
300
+
301
+ return null
262
302
  }
263
303
 
264
- private async enclaveSign(
304
+ /**
305
+ * Make HTTP request to Enclave API
306
+ * Returns raw response without processing
307
+ */
308
+ private async makeEnclaveRequest(
309
+ endpoint: string,
265
310
  apiKey: string,
266
- signingShare: string,
267
- method: string,
268
- params: string,
269
- rpcURL: string,
270
- chainId: string,
271
- metadata: string,
272
- ): Promise<string> {
273
- if (
274
- !apiKey ||
275
- !signingShare ||
276
- !method ||
277
- !params ||
278
- !chainId ||
279
- !metadata
280
- ) {
281
- return this.encodeErrorResult(
282
- 'INVALID_PARAMETERS',
283
- 'Invalid parameters provided',
284
- )
285
- }
311
+ body: Record<string, any>,
312
+ ): Promise<EnclaveSignResponse> {
313
+ return await this.requests.post<EnclaveSignResponse>(endpoint, {
314
+ headers: {
315
+ Authorization: `Bearer ${apiKey}`,
316
+ 'Content-Type': 'application/json',
317
+ },
318
+ body,
319
+ })
320
+ }
286
321
 
287
- const requestBody: EnclaveSignRequest = {
288
- method: method,
289
- params: params,
290
- share: signingShare,
291
- chainId: chainId,
292
- rpcUrl: rpcURL,
293
- metadataStr: metadata,
294
- clientPlatform: 'REACT_NATIVE',
295
- clientPlatformVersion: getClientPlatformVersion(),
322
+ /**
323
+ * Process Enclave API response
324
+ * Checks for error format and extracts data
325
+ */
326
+ private processEnclaveResponse(response: EnclaveSignResponse): string {
327
+ // Check for API-level errors first
328
+ // API returns errors at top level: {id, message, code}
329
+ if ('id' in response && 'message' in response && 'code' in response) {
330
+ const errorResponse = response as {
331
+ id: string
332
+ message: string
333
+ code: number
334
+ }
335
+ throw new PortalMpcError({
336
+ code: errorResponse.code,
337
+ id: errorResponse.id,
338
+ message: errorResponse.message,
339
+ })
296
340
  }
297
341
 
298
- try {
299
- const response = await this.requests.post<EnclaveSignResponse>(
300
- '/v1/sign',
301
- {
302
- headers: {
303
- Authorization: `Bearer ${apiKey}`,
304
- 'Content-Type': 'application/json',
305
- },
306
- body: requestBody,
307
- },
308
- )
309
-
310
- return this.encodeSuccessResult(response.data)
311
- } catch (error: any) {
312
- if (error?.response?.data) {
313
- const portalError = this.decodePortalError(
314
- JSON.stringify(error.response.data),
315
- )
316
- return this.encodeErrorResult(portalError?.id, portalError?.message)
317
- }
318
- return this.encodeErrorResult(
319
- 'SIGNING_NETWORK_ERROR',
320
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
321
- error.message || 'Network error occurred',
322
- )
342
+ // API returns success as: {data: string}
343
+ const successResponse = response as { data: string }
344
+ if (!successResponse.data) {
345
+ throw new PortalMpcError({
346
+ code: PortalErrorCodes.BadRequest,
347
+ id: 'InvalidResponse',
348
+ message: 'API response missing data field',
349
+ })
323
350
  }
324
- }
325
351
 
326
- // Helper function to encode success results
327
- private encodeSuccessResult(data: string): string {
328
- const successResult: EnclaveSignResult = { data: data, error: undefined }
329
- return this.encodeJSON(successResult)
352
+ return successResponse.data
330
353
  }
331
354
 
332
- // Helper function to decode Portal errors
333
- private decodePortalError(
334
- errorStr?: string,
335
- ): { id?: string; message?: string } | null {
336
- if (!errorStr) return null
337
- try {
338
- return JSON.parse(errorStr)
339
- } catch {
340
- return null
355
+ /**
356
+ * Handle errors from Enclave API calls
357
+ * Parses and transforms to PortalMpcError
358
+ */
359
+ private handleEnclaveError(error: any): never {
360
+ // If it's already a PortalMpcError, re-throw it
361
+ if (error instanceof PortalMpcError) {
362
+ throw error
341
363
  }
342
- }
343
364
 
344
- // Helper function to encode error results
345
- private encodeErrorResult(id?: string, message?: string): string {
346
- const errorResult: EnclaveSignResult = {
347
- data: undefined,
348
- error: { id, message },
365
+ // Handle HTTP error responses
366
+ // HttpError format: "{status} - {statusText}: {responseBody}"
367
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
368
+ const errorMessage: string =
369
+ typeof error.message === 'string' ? error.message : ''
370
+
371
+ // Try to parse API error from HttpError message
372
+ const apiError = this.parseApiError(errorMessage)
373
+ if (apiError) {
374
+ throw new PortalMpcError({
375
+ code: apiError.code,
376
+ id: apiError.id,
377
+ message: apiError.message,
378
+ })
349
379
  }
350
- return this.encodeJSON(errorResult)
351
- }
352
380
 
353
- // Helper function to encode any object to JSON string
354
- private encodeJSON<T>(value: T): string {
355
- try {
356
- const jsonString = JSON.stringify(value)
357
- return jsonString
358
- } catch (error: any) {
359
- return JSON.stringify({
360
- error: {
361
- id: 'ENCODING_ERROR',
362
- message: `Failed to encode JSON: ${error.message}`,
363
- },
381
+ // Check for 401 Unauthorized
382
+ if (errorMessage.startsWith('401 -')) {
383
+ throw new PortalMpcError({
384
+ code: PortalErrorCodes.InvalidApiKey,
385
+ id: 'Unauthorized',
386
+ message: 'Unauthorized request',
364
387
  })
365
388
  }
389
+
390
+ // Handle network errors
391
+ throw new PortalMpcError({
392
+ code: PortalErrorCodes.SigningNetworkError,
393
+ id: 'SIGNING_NETWORK_ERROR',
394
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
395
+ message: errorMessage || 'Network error occurred',
396
+ })
366
397
  }
367
398
 
368
399
  private async sendMetrics(
package/types.d.ts CHANGED
@@ -179,10 +179,12 @@ export interface EnclaveSignRequest {
179
179
  clientPlatformVersion: string
180
180
  }
181
181
 
182
- export interface EnclaveSignResponse {
183
- data: string
184
- error?: {
185
- id: string
186
- message: string
187
- }
188
- }
182
+ export type EnclaveSignResponse =
183
+ | {
184
+ data: string
185
+ }
186
+ | {
187
+ id: string
188
+ message: string
189
+ code: number
190
+ }