@portal-hq/provider 4.1.5 → 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
  }
@@ -121,7 +133,7 @@ class EnclaveSigner {
121
133
  formattedParams =
122
134
  typeof params === 'string' ? params : JSON.stringify(params);
123
135
  }
124
- // Get RPC URL
136
+ // Get RPC URL (getGatewayUrl handles undefined chainId)
125
137
  const rpcUrl = isRaw ? '' : provider.getGatewayUrl(chainId);
126
138
  // Set metrics operation
127
139
  metrics.operation = isRaw ? Operation.RAW_SIGN : Operation.SIGN;
@@ -132,9 +144,40 @@ class EnclaveSigner {
132
144
  metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
133
145
  // Measure enclave signing operation time
134
146
  const enclaveSignStartTime = performance.now();
135
- const result = isRaw
136
- ? yield this.enclaveRawSign(apiKey, JSON.stringify(signingShare), formattedParams, curve || 'SECP256K1')
137
- : 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
+ }
138
181
  // Post-operation processing time starts
139
182
  const postOperationStartTime = performance.now();
140
183
  // Record HTTP call time
@@ -188,118 +231,117 @@ class EnclaveSigner {
188
231
  }
189
232
  });
190
233
  }
191
- enclaveRawSign(apiKey, signingShare, params, curve) {
192
- var _a;
193
- return __awaiter(this, void 0, void 0, function* () {
194
- if (!apiKey || !signingShare || !params || !curve) {
195
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided for raw signing');
196
- }
197
- const requestBody = {
198
- params: params,
199
- share: signingShare,
200
- };
201
- const endpoint = `/v1/raw/sign/${curve}`;
202
- try {
203
- const response = yield this.requests.post(endpoint, {
204
- headers: {
205
- Authorization: `Bearer ${apiKey}`,
206
- 'Content-Type': 'application/json',
207
- },
208
- body: requestBody,
209
- });
210
- return this.encodeSuccessResult(response.data);
211
- }
212
- catch (error) {
213
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
214
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
215
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
216
- }
217
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
218
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
219
- 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
+ };
220
260
  }
221
- });
261
+ }
262
+ catch (_a) {
263
+ // JSON parsing failed
264
+ }
265
+ return null;
222
266
  }
223
- enclaveSign(apiKey, signingShare, method, params, rpcURL, chainId, metadata) {
224
- var _a;
267
+ /**
268
+ * Make HTTP request to Enclave API
269
+ * Returns raw response without processing
270
+ */
271
+ makeEnclaveRequest(endpoint, apiKey, body) {
225
272
  return __awaiter(this, void 0, void 0, function* () {
226
- if (!apiKey ||
227
- !signingShare ||
228
- !method ||
229
- !params ||
230
- !chainId ||
231
- !metadata) {
232
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided');
233
- }
234
- const requestBody = {
235
- method: method,
236
- params: params,
237
- share: signingShare,
238
- chainId: chainId,
239
- rpcUrl: rpcURL,
240
- metadataStr: metadata,
241
- clientPlatform: 'REACT_NATIVE',
242
- clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
243
- };
244
- try {
245
- const response = yield this.requests.post('/v1/sign', {
246
- headers: {
247
- Authorization: `Bearer ${apiKey}`,
248
- 'Content-Type': 'application/json',
249
- },
250
- body: requestBody,
251
- });
252
- return this.encodeSuccessResult(response.data);
253
- }
254
- catch (error) {
255
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
256
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
257
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
258
- }
259
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
260
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
261
- error.message || 'Network error occurred');
262
- }
273
+ return yield this.requests.post(endpoint, {
274
+ headers: {
275
+ Authorization: `Bearer ${apiKey}`,
276
+ 'Content-Type': 'application/json',
277
+ },
278
+ body,
279
+ });
263
280
  });
264
281
  }
265
- // Helper function to encode success results
266
- encodeSuccessResult(data) {
267
- const successResult = { data: data, error: undefined };
268
- return this.encodeJSON(successResult);
269
- }
270
- // Helper function to decode Portal errors
271
- decodePortalError(errorStr) {
272
- if (!errorStr)
273
- return null;
274
- try {
275
- 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
+ });
276
296
  }
277
- catch (_a) {
278
- 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
+ });
279
305
  }
306
+ return successResponse.data;
280
307
  }
281
- // Helper function to encode error results
282
- encodeErrorResult(id, message) {
283
- const errorResult = {
284
- data: undefined,
285
- error: { id, message },
286
- };
287
- return this.encodeJSON(errorResult);
288
- }
289
- // Helper function to encode any object to JSON string
290
- encodeJSON(value) {
291
- try {
292
- const jsonString = JSON.stringify(value);
293
- 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;
294
316
  }
295
- catch (error) {
296
- return JSON.stringify({
297
- error: {
298
- id: 'ENCODING_ERROR',
299
- message: `Failed to encode JSON: ${error.message}`,
300
- },
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,
301
328
  });
302
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
+ });
303
345
  }
304
346
  sendMetrics(metrics, apiKey) {
305
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
  }
@@ -116,7 +128,7 @@ class EnclaveSigner {
116
128
  formattedParams =
117
129
  typeof params === 'string' ? params : JSON.stringify(params);
118
130
  }
119
- // Get RPC URL
131
+ // Get RPC URL (getGatewayUrl handles undefined chainId)
120
132
  const rpcUrl = isRaw ? '' : provider.getGatewayUrl(chainId);
121
133
  // Set metrics operation
122
134
  metrics.operation = isRaw ? Operation.RAW_SIGN : Operation.SIGN;
@@ -127,9 +139,40 @@ class EnclaveSigner {
127
139
  metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
128
140
  // Measure enclave signing operation time
129
141
  const enclaveSignStartTime = performance.now();
130
- const result = isRaw
131
- ? yield this.enclaveRawSign(apiKey, JSON.stringify(signingShare), formattedParams, curve || 'SECP256K1')
132
- : 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
+ }
133
176
  // Post-operation processing time starts
134
177
  const postOperationStartTime = performance.now();
135
178
  // Record HTTP call time
@@ -183,118 +226,117 @@ class EnclaveSigner {
183
226
  }
184
227
  });
185
228
  }
186
- enclaveRawSign(apiKey, signingShare, params, curve) {
187
- var _a;
188
- return __awaiter(this, void 0, void 0, function* () {
189
- if (!apiKey || !signingShare || !params || !curve) {
190
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided for raw signing');
191
- }
192
- const requestBody = {
193
- params: params,
194
- share: signingShare,
195
- };
196
- const endpoint = `/v1/raw/sign/${curve}`;
197
- try {
198
- const response = yield this.requests.post(endpoint, {
199
- headers: {
200
- Authorization: `Bearer ${apiKey}`,
201
- 'Content-Type': 'application/json',
202
- },
203
- body: requestBody,
204
- });
205
- return this.encodeSuccessResult(response.data);
206
- }
207
- catch (error) {
208
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
209
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
210
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
211
- }
212
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
213
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
214
- 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
+ };
215
255
  }
216
- });
256
+ }
257
+ catch (_a) {
258
+ // JSON parsing failed
259
+ }
260
+ return null;
217
261
  }
218
- enclaveSign(apiKey, signingShare, method, params, rpcURL, chainId, metadata) {
219
- var _a;
262
+ /**
263
+ * Make HTTP request to Enclave API
264
+ * Returns raw response without processing
265
+ */
266
+ makeEnclaveRequest(endpoint, apiKey, body) {
220
267
  return __awaiter(this, void 0, void 0, function* () {
221
- if (!apiKey ||
222
- !signingShare ||
223
- !method ||
224
- !params ||
225
- !chainId ||
226
- !metadata) {
227
- return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided');
228
- }
229
- const requestBody = {
230
- method: method,
231
- params: params,
232
- share: signingShare,
233
- chainId: chainId,
234
- rpcUrl: rpcURL,
235
- metadataStr: metadata,
236
- clientPlatform: 'REACT_NATIVE',
237
- clientPlatformVersion: getClientPlatformVersion(),
238
- };
239
- try {
240
- const response = yield this.requests.post('/v1/sign', {
241
- headers: {
242
- Authorization: `Bearer ${apiKey}`,
243
- 'Content-Type': 'application/json',
244
- },
245
- body: requestBody,
246
- });
247
- return this.encodeSuccessResult(response.data);
248
- }
249
- catch (error) {
250
- if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
251
- const portalError = this.decodePortalError(JSON.stringify(error.response.data));
252
- return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
253
- }
254
- return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
255
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
256
- error.message || 'Network error occurred');
257
- }
268
+ return yield this.requests.post(endpoint, {
269
+ headers: {
270
+ Authorization: `Bearer ${apiKey}`,
271
+ 'Content-Type': 'application/json',
272
+ },
273
+ body,
274
+ });
258
275
  });
259
276
  }
260
- // Helper function to encode success results
261
- encodeSuccessResult(data) {
262
- const successResult = { data: data, error: undefined };
263
- return this.encodeJSON(successResult);
264
- }
265
- // Helper function to decode Portal errors
266
- decodePortalError(errorStr) {
267
- if (!errorStr)
268
- return null;
269
- try {
270
- 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
+ });
271
291
  }
272
- catch (_a) {
273
- 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
+ });
274
300
  }
301
+ return successResponse.data;
275
302
  }
276
- // Helper function to encode error results
277
- encodeErrorResult(id, message) {
278
- const errorResult = {
279
- data: undefined,
280
- error: { id, message },
281
- };
282
- return this.encodeJSON(errorResult);
283
- }
284
- // Helper function to encode any object to JSON string
285
- encodeJSON(value) {
286
- try {
287
- const jsonString = JSON.stringify(value);
288
- 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;
289
311
  }
290
- catch (error) {
291
- return JSON.stringify({
292
- error: {
293
- id: 'ENCODING_ERROR',
294
- message: `Failed to encode JSON: ${error.message}`,
295
- },
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,
296
323
  });
297
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
+ });
298
340
  }
299
341
  sendMetrics(metrics, apiKey) {
300
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.5",
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.5",
23
- "@portal-hq/utils": "^4.1.5",
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": "5ba19390f12935fc74b6235a4b2655a1e1c7a07f"
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
  }
@@ -129,7 +143,7 @@ class EnclaveSigner implements Signer {
129
143
  typeof params === 'string' ? params : JSON.stringify(params)
130
144
  }
131
145
 
132
- // Get RPC URL
146
+ // Get RPC URL (getGatewayUrl handles undefined chainId)
133
147
  const rpcUrl = isRaw ? '' : provider.getGatewayUrl(chainId)
134
148
 
135
149
  // Set metrics operation
@@ -147,22 +161,44 @@ class EnclaveSigner implements Signer {
147
161
  // Measure enclave signing operation time
148
162
  const enclaveSignStartTime = performance.now()
149
163
 
150
- const result = isRaw
151
- ? await this.enclaveRawSign(
152
- apiKey,
153
- JSON.stringify(signingShare),
154
- formattedParams,
155
- curve || 'SECP256K1',
156
- )
157
- : await this.enclaveSign(
158
- apiKey,
159
- JSON.stringify(signingShare),
160
- message.method,
161
- formattedParams,
162
- rpcUrl,
163
- chainId || '',
164
- JSON.stringify(metadata),
165
- )
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
+ }
166
202
 
167
203
  // Post-operation processing time starts
168
204
  const postOperationStartTime = performance.now()
@@ -223,153 +259,141 @@ class EnclaveSigner implements Signer {
223
259
  }
224
260
  }
225
261
 
226
- private async enclaveRawSign(
227
- apiKey: string,
228
- signingShare: string,
229
- params: string,
230
- curve: string,
231
- ): Promise<string> {
232
- if (!apiKey || !signingShare || !params || !curve) {
233
- return this.encodeErrorResult(
234
- 'INVALID_PARAMETERS',
235
- 'Invalid parameters provided for raw signing',
236
- )
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
237
274
  }
238
275
 
239
- const requestBody = {
240
- params: params,
241
- share: signingShare,
242
- }
276
+ const responseBody = errorMessage.substring(colonIndex + 1).trim()
243
277
 
244
- 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
245
281
 
246
282
  try {
247
- const response = await this.requests.post<EnclaveSignResponse>(endpoint, {
248
- headers: {
249
- Authorization: `Bearer ${apiKey}`,
250
- 'Content-Type': 'application/json',
251
- },
252
- body: requestBody,
253
- })
254
-
255
- return this.encodeSuccessResult(response.data)
256
- } catch (error: any) {
257
- if (error?.response?.data) {
258
- const portalError = this.decodePortalError(
259
- JSON.stringify(error.response.data),
260
- )
261
- 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
+ }
262
296
  }
263
- return this.encodeErrorResult(
264
- 'SIGNING_NETWORK_ERROR',
265
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
266
- error.message || 'Network error occurred',
267
- )
297
+ } catch {
298
+ // JSON parsing failed
268
299
  }
300
+
301
+ return null
269
302
  }
270
303
 
271
- 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,
272
310
  apiKey: string,
273
- signingShare: string,
274
- method: string,
275
- params: string,
276
- rpcURL: string,
277
- chainId: string,
278
- metadata: string,
279
- ): Promise<string> {
280
- if (
281
- !apiKey ||
282
- !signingShare ||
283
- !method ||
284
- !params ||
285
- !chainId ||
286
- !metadata
287
- ) {
288
- return this.encodeErrorResult(
289
- 'INVALID_PARAMETERS',
290
- 'Invalid parameters provided',
291
- )
292
- }
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
+ }
293
321
 
294
- const requestBody: EnclaveSignRequest = {
295
- method: method,
296
- params: params,
297
- share: signingShare,
298
- chainId: chainId,
299
- rpcUrl: rpcURL,
300
- metadataStr: metadata,
301
- clientPlatform: 'REACT_NATIVE',
302
- 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
+ })
303
340
  }
304
341
 
305
- try {
306
- const response = await this.requests.post<EnclaveSignResponse>(
307
- '/v1/sign',
308
- {
309
- headers: {
310
- Authorization: `Bearer ${apiKey}`,
311
- 'Content-Type': 'application/json',
312
- },
313
- body: requestBody,
314
- },
315
- )
316
-
317
- return this.encodeSuccessResult(response.data)
318
- } catch (error: any) {
319
- if (error?.response?.data) {
320
- const portalError = this.decodePortalError(
321
- JSON.stringify(error.response.data),
322
- )
323
- return this.encodeErrorResult(portalError?.id, portalError?.message)
324
- }
325
- return this.encodeErrorResult(
326
- 'SIGNING_NETWORK_ERROR',
327
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
328
- error.message || 'Network error occurred',
329
- )
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
+ })
330
350
  }
331
- }
332
351
 
333
- // Helper function to encode success results
334
- private encodeSuccessResult(data: string): string {
335
- const successResult: EnclaveSignResult = { data: data, error: undefined }
336
- return this.encodeJSON(successResult)
352
+ return successResponse.data
337
353
  }
338
354
 
339
- // Helper function to decode Portal errors
340
- private decodePortalError(
341
- errorStr?: string,
342
- ): { id?: string; message?: string } | null {
343
- if (!errorStr) return null
344
- try {
345
- return JSON.parse(errorStr)
346
- } catch {
347
- 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
348
363
  }
349
- }
350
364
 
351
- // Helper function to encode error results
352
- private encodeErrorResult(id?: string, message?: string): string {
353
- const errorResult: EnclaveSignResult = {
354
- data: undefined,
355
- 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
+ })
356
379
  }
357
- return this.encodeJSON(errorResult)
358
- }
359
380
 
360
- // Helper function to encode any object to JSON string
361
- private encodeJSON<T>(value: T): string {
362
- try {
363
- const jsonString = JSON.stringify(value)
364
- return jsonString
365
- } catch (error: any) {
366
- return JSON.stringify({
367
- error: {
368
- id: 'ENCODING_ERROR',
369
- message: `Failed to encode JSON: ${error.message}`,
370
- },
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',
371
387
  })
372
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
+ })
373
397
  }
374
398
 
375
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
+ }