@portal-hq/provider 4.1.5 → 4.1.7

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.
@@ -38,7 +38,7 @@ class Provider {
38
38
  return this.keychain.getAddresses();
39
39
  }
40
40
  get address() {
41
- return this.keychain.getAddress();
41
+ return this.keychain.getAddresses().then((addrs) => addrs === null || addrs === void 0 ? void 0 : addrs.eip155);
42
42
  }
43
43
  constructor({
44
44
  // Required
@@ -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,130 @@ 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
+ // Handle case where API returns signature directly as a string
288
+ // This can happen if JSON parsing fails in HttpRequest.buildResponse()
289
+ if (typeof response === 'string') {
290
+ return response;
276
291
  }
277
- catch (_a) {
278
- return null;
292
+ // Ensure response is an object before using 'in' operator
293
+ if (!response || typeof response !== 'object') {
294
+ throw new utils_1.PortalMpcError({
295
+ code: utils_1.PortalErrorCodes.BadRequest,
296
+ id: 'InvalidResponse',
297
+ message: `Invalid API response format: expected object, got ${typeof response}`,
298
+ });
279
299
  }
300
+ // Check for API-level errors first
301
+ // API returns errors at top level: {id, message, code}
302
+ if ('id' in response && 'message' in response && 'code' in response) {
303
+ const errorResponse = response;
304
+ throw new utils_1.PortalMpcError({
305
+ code: errorResponse.code,
306
+ id: errorResponse.id,
307
+ message: errorResponse.message,
308
+ });
309
+ }
310
+ // API returns success as: {data: string}
311
+ const successResponse = response;
312
+ if (!successResponse.data) {
313
+ throw new utils_1.PortalMpcError({
314
+ code: utils_1.PortalErrorCodes.BadRequest,
315
+ id: 'InvalidResponse',
316
+ message: 'API response missing data field',
317
+ });
318
+ }
319
+ return successResponse.data;
280
320
  }
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;
321
+ /**
322
+ * Handle errors from Enclave API calls
323
+ * Parses and transforms to PortalMpcError
324
+ */
325
+ handleEnclaveError(error) {
326
+ // If it's already a PortalMpcError, re-throw it
327
+ if (error instanceof utils_1.PortalMpcError) {
328
+ throw error;
294
329
  }
295
- catch (error) {
296
- return JSON.stringify({
297
- error: {
298
- id: 'ENCODING_ERROR',
299
- message: `Failed to encode JSON: ${error.message}`,
300
- },
330
+ // Handle HTTP error responses
331
+ // HttpError format: "{status} - {statusText}: {responseBody}"
332
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
333
+ const errorMessage = typeof error.message === 'string' ? error.message : '';
334
+ // Try to parse API error from HttpError message
335
+ const apiError = this.parseApiError(errorMessage);
336
+ if (apiError) {
337
+ throw new utils_1.PortalMpcError({
338
+ code: apiError.code,
339
+ id: apiError.id,
340
+ message: apiError.message,
301
341
  });
302
342
  }
343
+ // Check for 401 Unauthorized
344
+ if (errorMessage.startsWith('401 -')) {
345
+ throw new utils_1.PortalMpcError({
346
+ code: utils_1.PortalErrorCodes.InvalidApiKey,
347
+ id: 'Unauthorized',
348
+ message: 'Unauthorized request',
349
+ });
350
+ }
351
+ // Handle network errors
352
+ throw new utils_1.PortalMpcError({
353
+ code: utils_1.PortalErrorCodes.SigningNetworkError,
354
+ id: 'SIGNING_NETWORK_ERROR',
355
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
356
+ message: errorMessage || 'Network error occurred',
357
+ });
303
358
  }
304
359
  sendMetrics(metrics, apiKey) {
305
360
  return __awaiter(this, void 0, void 0, function* () {
@@ -36,7 +36,7 @@ class Provider {
36
36
  return this.keychain.getAddresses();
37
37
  }
38
38
  get address() {
39
- return this.keychain.getAddress();
39
+ return this.keychain.getAddresses().then((addrs) => addrs === null || addrs === void 0 ? void 0 : addrs.eip155);
40
40
  }
41
41
  constructor({
42
42
  // Required
@@ -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,130 @@ 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
+ // Handle case where API returns signature directly as a string
283
+ // This can happen if JSON parsing fails in HttpRequest.buildResponse()
284
+ if (typeof response === 'string') {
285
+ return response;
271
286
  }
272
- catch (_a) {
273
- return null;
287
+ // Ensure response is an object before using 'in' operator
288
+ if (!response || typeof response !== 'object') {
289
+ throw new PortalMpcError({
290
+ code: PortalErrorCodes.BadRequest,
291
+ id: 'InvalidResponse',
292
+ message: `Invalid API response format: expected object, got ${typeof response}`,
293
+ });
274
294
  }
295
+ // Check for API-level errors first
296
+ // API returns errors at top level: {id, message, code}
297
+ if ('id' in response && 'message' in response && 'code' in response) {
298
+ const errorResponse = response;
299
+ throw new PortalMpcError({
300
+ code: errorResponse.code,
301
+ id: errorResponse.id,
302
+ message: errorResponse.message,
303
+ });
304
+ }
305
+ // API returns success as: {data: string}
306
+ const successResponse = response;
307
+ if (!successResponse.data) {
308
+ throw new PortalMpcError({
309
+ code: PortalErrorCodes.BadRequest,
310
+ id: 'InvalidResponse',
311
+ message: 'API response missing data field',
312
+ });
313
+ }
314
+ return successResponse.data;
275
315
  }
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;
316
+ /**
317
+ * Handle errors from Enclave API calls
318
+ * Parses and transforms to PortalMpcError
319
+ */
320
+ handleEnclaveError(error) {
321
+ // If it's already a PortalMpcError, re-throw it
322
+ if (error instanceof PortalMpcError) {
323
+ throw error;
289
324
  }
290
- catch (error) {
291
- return JSON.stringify({
292
- error: {
293
- id: 'ENCODING_ERROR',
294
- message: `Failed to encode JSON: ${error.message}`,
295
- },
325
+ // Handle HTTP error responses
326
+ // HttpError format: "{status} - {statusText}: {responseBody}"
327
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
328
+ const errorMessage = typeof error.message === 'string' ? error.message : '';
329
+ // Try to parse API error from HttpError message
330
+ const apiError = this.parseApiError(errorMessage);
331
+ if (apiError) {
332
+ throw new PortalMpcError({
333
+ code: apiError.code,
334
+ id: apiError.id,
335
+ message: apiError.message,
296
336
  });
297
337
  }
338
+ // Check for 401 Unauthorized
339
+ if (errorMessage.startsWith('401 -')) {
340
+ throw new PortalMpcError({
341
+ code: PortalErrorCodes.InvalidApiKey,
342
+ id: 'Unauthorized',
343
+ message: 'Unauthorized request',
344
+ });
345
+ }
346
+ // Handle network errors
347
+ throw new PortalMpcError({
348
+ code: PortalErrorCodes.SigningNetworkError,
349
+ id: 'SIGNING_NETWORK_ERROR',
350
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
351
+ message: errorMessage || 'Network error occurred',
352
+ });
298
353
  }
299
354
  sendMetrics(metrics, apiKey) {
300
355
  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.7",
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.7",
23
+ "@portal-hq/utils": "^4.1.7",
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": "d8be728d9b04f0c3e2577fd98b1e38282cc950ba"
36
36
  }
@@ -63,7 +63,7 @@ class Provider implements IPortalProvider {
63
63
  }
64
64
 
65
65
  public get address(): Promise<string | undefined> {
66
- return this.keychain.getAddress()
66
+ return this.keychain.getAddresses().then((addrs) => addrs?.eip155)
67
67
  }
68
68
 
69
69
  constructor({
@@ -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,156 @@ 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
- )
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
+ }
321
+
322
+ /**
323
+ * Process Enclave API response
324
+ * Checks for error format and extracts data
325
+ */
326
+ private processEnclaveResponse(response: EnclaveSignResponse): string {
327
+ // Handle case where API returns signature directly as a string
328
+ // This can happen if JSON parsing fails in HttpRequest.buildResponse()
329
+ if (typeof response === 'string') {
330
+ return response
292
331
  }
293
332
 
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(),
333
+ // Ensure response is an object before using 'in' operator
334
+ if (!response || typeof response !== 'object') {
335
+ throw new PortalMpcError({
336
+ code: PortalErrorCodes.BadRequest,
337
+ id: 'InvalidResponse',
338
+ message: `Invalid API response format: expected object, got ${typeof response}`,
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)
342
+ // Check for API-level errors first
343
+ // API returns errors at top level: {id, message, code}
344
+ if ('id' in response && 'message' in response && 'code' in response) {
345
+ const errorResponse = response as {
346
+ id: string
347
+ message: string
348
+ code: number
324
349
  }
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
- )
350
+ throw new PortalMpcError({
351
+ code: errorResponse.code,
352
+ id: errorResponse.id,
353
+ message: errorResponse.message,
354
+ })
330
355
  }
331
- }
332
356
 
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)
357
+ // API returns success as: {data: string}
358
+ const successResponse = response as { data: string }
359
+ if (!successResponse.data) {
360
+ throw new PortalMpcError({
361
+ code: PortalErrorCodes.BadRequest,
362
+ id: 'InvalidResponse',
363
+ message: 'API response missing data field',
364
+ })
365
+ }
366
+
367
+ return successResponse.data
337
368
  }
338
369
 
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
370
+ /**
371
+ * Handle errors from Enclave API calls
372
+ * Parses and transforms to PortalMpcError
373
+ */
374
+ private handleEnclaveError(error: any): never {
375
+ // If it's already a PortalMpcError, re-throw it
376
+ if (error instanceof PortalMpcError) {
377
+ throw error
348
378
  }
349
- }
350
379
 
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 },
380
+ // Handle HTTP error responses
381
+ // HttpError format: "{status} - {statusText}: {responseBody}"
382
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
383
+ const errorMessage: string =
384
+ typeof error.message === 'string' ? error.message : ''
385
+
386
+ // Try to parse API error from HttpError message
387
+ const apiError = this.parseApiError(errorMessage)
388
+ if (apiError) {
389
+ throw new PortalMpcError({
390
+ code: apiError.code,
391
+ id: apiError.id,
392
+ message: apiError.message,
393
+ })
356
394
  }
357
- return this.encodeJSON(errorResult)
358
- }
359
395
 
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
- },
396
+ // Check for 401 Unauthorized
397
+ if (errorMessage.startsWith('401 -')) {
398
+ throw new PortalMpcError({
399
+ code: PortalErrorCodes.InvalidApiKey,
400
+ id: 'Unauthorized',
401
+ message: 'Unauthorized request',
371
402
  })
372
403
  }
404
+
405
+ // Handle network errors
406
+ throw new PortalMpcError({
407
+ code: PortalErrorCodes.SigningNetworkError,
408
+ id: 'SIGNING_NETWORK_ERROR',
409
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
410
+ message: errorMessage || 'Network error occurred',
411
+ })
373
412
  }
374
413
 
375
414
  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
+ }