@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.
- package/lib/commonjs/signers/enclave.js +148 -106
- package/lib/esm/signers/enclave.js +149 -107
- package/package.json +4 -4
- package/src/signers/enclave.ts +170 -146
- package/types.d.ts +9 -7
|
@@ -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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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.
|
|
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.
|
|
23
|
-
"@portal-hq/utils": "^4.1.
|
|
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": "
|
|
35
|
+
"gitHead": "6906f8cc1dbd92d87effa174b8fa73b74bf63532"
|
|
36
36
|
}
|
package/src/signers/enclave.ts
CHANGED
|
@@ -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
|
|
96
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
240
|
-
params: params,
|
|
241
|
-
share: signingShare,
|
|
242
|
-
}
|
|
276
|
+
const responseBody = errorMessage.substring(colonIndex + 1).trim()
|
|
243
277
|
|
|
244
|
-
|
|
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
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
error
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
export type EnclaveSignResponse =
|
|
183
|
+
| {
|
|
184
|
+
data: string
|
|
185
|
+
}
|
|
186
|
+
| {
|
|
187
|
+
id: string
|
|
188
|
+
message: string
|
|
189
|
+
code: number
|
|
190
|
+
}
|