@portal-hq/provider 4.1.3 → 4.1.4
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/providers/index.js +22 -11
- package/lib/commonjs/signers/enclave.js +316 -0
- package/lib/commonjs/signers/index.js +3 -1
- package/lib/commonjs/signers/mpc.js +1 -1
- package/lib/esm/providers/index.js +23 -12
- package/lib/esm/signers/enclave.js +311 -0
- package/lib/esm/signers/index.js +1 -0
- package/lib/esm/signers/mpc.js +1 -1
- package/package.json +4 -4
- package/src/providers/index.ts +22 -12
- package/src/signers/enclave.ts +414 -0
- package/src/signers/index.ts +1 -0
- package/src/signers/mpc.ts +1 -1
- package/types.d.ts +45 -0
|
@@ -44,7 +44,7 @@ class Provider {
|
|
|
44
44
|
// Required
|
|
45
45
|
apiKey, keychain, gatewayConfig,
|
|
46
46
|
// Optional
|
|
47
|
-
autoApprove = false, apiHost = 'api.portalhq.io', mpcHost = 'mpc.portalhq.io', version = 'v6', chainId = 'eip155:11155111', featureFlags = {}, }) {
|
|
47
|
+
autoApprove = false, apiHost = 'api.portalhq.io', mpcHost = 'mpc.portalhq.io', enclaveMPCHost = 'mpc-client.portalhq.io', version = 'v6', chainId = 'eip155:11155111', featureFlags = {}, }) {
|
|
48
48
|
// Handle required fields
|
|
49
49
|
if (!apiKey || apiKey.length === 0) {
|
|
50
50
|
throw new utils_1.InvalidApiKeyError();
|
|
@@ -69,14 +69,26 @@ class Provider {
|
|
|
69
69
|
});
|
|
70
70
|
// Initialize Gateway HttpRequester
|
|
71
71
|
this.gateway = new utils_1.PortalRequests();
|
|
72
|
-
// Initialize
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
// Initialize the appropriate signer based on feature flags
|
|
73
|
+
if (featureFlags.useEnclaveMPCApi) {
|
|
74
|
+
this.signer = new signers_1.EnclaveSigner({
|
|
75
|
+
mpcHost,
|
|
76
|
+
enclaveMPCHost,
|
|
77
|
+
keychain,
|
|
78
|
+
version,
|
|
79
|
+
portalApi: this.portalApi,
|
|
80
|
+
featureFlags,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.signer = new signers_1.MpcSigner({
|
|
85
|
+
mpcHost,
|
|
86
|
+
keychain,
|
|
87
|
+
version,
|
|
88
|
+
portalApi: this.portalApi,
|
|
89
|
+
featureFlags,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
/**
|
|
82
94
|
* Invokes all registered event handlers with the data provided
|
|
@@ -282,8 +294,7 @@ class Provider {
|
|
|
282
294
|
});
|
|
283
295
|
}
|
|
284
296
|
/**
|
|
285
|
-
* Updates the chainId of this instance
|
|
286
|
-
* the gateway used for the new chain
|
|
297
|
+
* Updates the chainId of this instance
|
|
287
298
|
*
|
|
288
299
|
* @param chainId The numerical ID of the chain to switch to
|
|
289
300
|
* @returns BaseProvider
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const core_1 = require("@portal-hq/core");
|
|
16
|
+
const utils_1 = require("@portal-hq/utils");
|
|
17
|
+
const react_native_uuid_1 = __importDefault(require("react-native-uuid"));
|
|
18
|
+
var Operation;
|
|
19
|
+
(function (Operation) {
|
|
20
|
+
Operation["SIGN"] = "sign";
|
|
21
|
+
Operation["RAW_SIGN"] = "raw_sign";
|
|
22
|
+
})(Operation || (Operation = {}));
|
|
23
|
+
class EnclaveSigner {
|
|
24
|
+
constructor({ keychain, enclaveMPCHost = 'mpc-client.portalhq.io', version = 'v6', portalApi, featureFlags = {}, }) {
|
|
25
|
+
this.version = 'v6';
|
|
26
|
+
this.buildParams = (method, txParams) => {
|
|
27
|
+
let params = txParams;
|
|
28
|
+
switch (method) {
|
|
29
|
+
case 'eth_sign':
|
|
30
|
+
case 'personal_sign':
|
|
31
|
+
case 'eth_signTypedData_v3':
|
|
32
|
+
case 'eth_signTypedData_v4':
|
|
33
|
+
case 'sol_signMessage':
|
|
34
|
+
case 'sol_signTransaction':
|
|
35
|
+
case 'sol_signAndSendTransaction':
|
|
36
|
+
case 'sol_signAndConfirmTransaction':
|
|
37
|
+
if (!Array.isArray(txParams)) {
|
|
38
|
+
params = [txParams];
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
if (Array.isArray(txParams)) {
|
|
43
|
+
if (txParams.length === 1) {
|
|
44
|
+
params = txParams[0];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return params;
|
|
49
|
+
};
|
|
50
|
+
this.featureFlags = featureFlags;
|
|
51
|
+
this.keychain = keychain;
|
|
52
|
+
this.enclaveMPCHost = enclaveMPCHost;
|
|
53
|
+
this.version = version;
|
|
54
|
+
this.portalApi = portalApi;
|
|
55
|
+
this.requests = new utils_1.HttpRequester({
|
|
56
|
+
baseUrl: `https://${this.enclaveMPCHost}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
sign(message, provider) {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
// Always track metrics, but only send if feature flag is enabled
|
|
62
|
+
const shouldSendMetrics = this.featureFlags.enableSdkPerformanceMetrics === true;
|
|
63
|
+
const signStartTime = performance.now();
|
|
64
|
+
const preOperationStartTime = performance.now();
|
|
65
|
+
// Generate a traceId for this operation
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
67
|
+
const traceId = react_native_uuid_1.default.v4();
|
|
68
|
+
const metrics = {
|
|
69
|
+
hasError: false,
|
|
70
|
+
operation: Operation.SIGN,
|
|
71
|
+
signingMethod: message.method,
|
|
72
|
+
traceId,
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
const eip155Address = yield this.keychain.getEip155Address();
|
|
76
|
+
const apiKey = provider.apiKey;
|
|
77
|
+
const { method, chainId, curve, isRaw } = message;
|
|
78
|
+
// Add chainId to metrics
|
|
79
|
+
if (chainId) {
|
|
80
|
+
metrics.chainId = chainId;
|
|
81
|
+
}
|
|
82
|
+
switch (method) {
|
|
83
|
+
case 'eth_requestAccounts':
|
|
84
|
+
return [eip155Address];
|
|
85
|
+
case 'eth_accounts':
|
|
86
|
+
return [eip155Address];
|
|
87
|
+
default:
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const shares = yield this.keychain.getShares();
|
|
91
|
+
let signingShare = shares.secp256k1.share;
|
|
92
|
+
if (curve === core_1.PortalCurve.ED25519) {
|
|
93
|
+
if (!shares.ed25519) {
|
|
94
|
+
throw new Error('[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.');
|
|
95
|
+
}
|
|
96
|
+
signingShare = shares.ed25519.share;
|
|
97
|
+
}
|
|
98
|
+
const metadata = {
|
|
99
|
+
clientPlatform: 'REACT_NATIVE',
|
|
100
|
+
clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
|
|
101
|
+
isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
|
|
102
|
+
mpcServerVersion: this.version,
|
|
103
|
+
optimized: true,
|
|
104
|
+
curve,
|
|
105
|
+
chainId,
|
|
106
|
+
isRaw,
|
|
107
|
+
reqId: traceId,
|
|
108
|
+
connectionTracingEnabled: shouldSendMetrics,
|
|
109
|
+
};
|
|
110
|
+
let formattedParams;
|
|
111
|
+
let rpcUrl;
|
|
112
|
+
if (isRaw) {
|
|
113
|
+
formattedParams = JSON.stringify(this.buildParams(method, message.params));
|
|
114
|
+
rpcUrl = '';
|
|
115
|
+
metrics.operation = Operation.RAW_SIGN;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
formattedParams = JSON.stringify(this.buildParams(method, message.params));
|
|
119
|
+
rpcUrl = provider.getGatewayUrl(chainId);
|
|
120
|
+
}
|
|
121
|
+
if (typeof formattedParams !== 'string') {
|
|
122
|
+
throw new Error(`[Portal.Provider.EnclaveSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
|
|
123
|
+
}
|
|
124
|
+
// Record pre-operation time
|
|
125
|
+
metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
|
|
126
|
+
// Measure enclave signing operation time
|
|
127
|
+
const enclaveSignStartTime = performance.now();
|
|
128
|
+
const result = isRaw
|
|
129
|
+
? yield this.enclaveRawSign(apiKey, JSON.stringify(signingShare), formattedParams, curve || 'SECP256K1')
|
|
130
|
+
: yield this.enclaveSign(apiKey, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId || '', JSON.stringify(metadata));
|
|
131
|
+
// Post-operation processing time starts
|
|
132
|
+
const postOperationStartTime = performance.now();
|
|
133
|
+
// Record HTTP call time
|
|
134
|
+
metrics.enclaveHttpCallMs = performance.now() - enclaveSignStartTime;
|
|
135
|
+
// Record post-operation time
|
|
136
|
+
metrics.sdkPostOperationMs = performance.now() - postOperationStartTime;
|
|
137
|
+
// Calculate total SDK signing time
|
|
138
|
+
metrics.sdkOperationMs = performance.now() - signStartTime;
|
|
139
|
+
// Log performance timing to console
|
|
140
|
+
const timingMetrics = {};
|
|
141
|
+
if (typeof metrics.sdkPreOperationMs === 'number') {
|
|
142
|
+
timingMetrics['Pre-operation'] = metrics.sdkPreOperationMs;
|
|
143
|
+
}
|
|
144
|
+
if (typeof metrics.enclaveHttpCallMs === 'number') {
|
|
145
|
+
timingMetrics['Enclave HTTP Call'] = metrics.enclaveHttpCallMs;
|
|
146
|
+
}
|
|
147
|
+
if (typeof metrics.sdkPostOperationMs === 'number') {
|
|
148
|
+
timingMetrics['Post-operation'] = metrics.sdkPostOperationMs;
|
|
149
|
+
}
|
|
150
|
+
// Only send metrics if the feature flag is enabled
|
|
151
|
+
if (shouldSendMetrics && this.portalApi) {
|
|
152
|
+
try {
|
|
153
|
+
yield this.sendMetrics(metrics, apiKey);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
// No-op
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
// Calculate total time even in error case
|
|
163
|
+
metrics.sdkOperationMs = performance.now() - signStartTime;
|
|
164
|
+
// Log performance timing to console even in error case
|
|
165
|
+
const errorTimingMetrics = {};
|
|
166
|
+
if (typeof metrics.sdkPreOperationMs === 'number') {
|
|
167
|
+
errorTimingMetrics['Pre-operation'] = metrics.sdkPreOperationMs;
|
|
168
|
+
}
|
|
169
|
+
// Only send metrics if the feature flag is enabled
|
|
170
|
+
if (shouldSendMetrics) {
|
|
171
|
+
const apiKey = provider.apiKey;
|
|
172
|
+
metrics.hasError = true;
|
|
173
|
+
try {
|
|
174
|
+
yield this.sendMetrics(metrics, apiKey);
|
|
175
|
+
}
|
|
176
|
+
catch (_a) {
|
|
177
|
+
// No-op
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
enclaveRawSign(apiKey, signingShare, params, curve) {
|
|
185
|
+
var _a;
|
|
186
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
187
|
+
if (!apiKey || !signingShare || !params || !curve) {
|
|
188
|
+
return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided for raw signing');
|
|
189
|
+
}
|
|
190
|
+
const requestBody = {
|
|
191
|
+
params: params,
|
|
192
|
+
share: signingShare,
|
|
193
|
+
};
|
|
194
|
+
const endpoint = `/v1/raw/sign/${curve}`;
|
|
195
|
+
try {
|
|
196
|
+
const response = yield this.requests.post(endpoint, {
|
|
197
|
+
headers: {
|
|
198
|
+
Authorization: `Bearer ${apiKey}`,
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
},
|
|
201
|
+
body: requestBody,
|
|
202
|
+
});
|
|
203
|
+
return this.encodeSuccessResult(response.data);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
207
|
+
const portalError = this.decodePortalError(JSON.stringify(error.response.data));
|
|
208
|
+
return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
|
|
209
|
+
}
|
|
210
|
+
return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
212
|
+
error.message || 'Network error occurred');
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
enclaveSign(apiKey, signingShare, method, params, rpcURL, chainId, metadata) {
|
|
217
|
+
var _a;
|
|
218
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
219
|
+
if (!apiKey ||
|
|
220
|
+
!signingShare ||
|
|
221
|
+
!method ||
|
|
222
|
+
!params ||
|
|
223
|
+
!chainId ||
|
|
224
|
+
!metadata) {
|
|
225
|
+
return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided');
|
|
226
|
+
}
|
|
227
|
+
const requestBody = {
|
|
228
|
+
method: method,
|
|
229
|
+
params: params,
|
|
230
|
+
share: signingShare,
|
|
231
|
+
chainId: chainId,
|
|
232
|
+
rpcUrl: rpcURL,
|
|
233
|
+
metadataStr: metadata,
|
|
234
|
+
clientPlatform: 'REACT_NATIVE',
|
|
235
|
+
clientPlatformVersion: (0, utils_1.getClientPlatformVersion)(),
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
const response = yield this.requests.post('/v1/sign', {
|
|
239
|
+
headers: {
|
|
240
|
+
Authorization: `Bearer ${apiKey}`,
|
|
241
|
+
'Content-Type': 'application/json',
|
|
242
|
+
},
|
|
243
|
+
body: requestBody,
|
|
244
|
+
});
|
|
245
|
+
return this.encodeSuccessResult(response.data);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
249
|
+
const portalError = this.decodePortalError(JSON.stringify(error.response.data));
|
|
250
|
+
return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
|
|
251
|
+
}
|
|
252
|
+
return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
254
|
+
error.message || 'Network error occurred');
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Helper function to encode success results
|
|
259
|
+
encodeSuccessResult(data) {
|
|
260
|
+
const successResult = { data: data, error: undefined };
|
|
261
|
+
return this.encodeJSON(successResult);
|
|
262
|
+
}
|
|
263
|
+
// Helper function to decode Portal errors
|
|
264
|
+
decodePortalError(errorStr) {
|
|
265
|
+
if (!errorStr)
|
|
266
|
+
return null;
|
|
267
|
+
try {
|
|
268
|
+
return JSON.parse(errorStr);
|
|
269
|
+
}
|
|
270
|
+
catch (_a) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Helper function to encode error results
|
|
275
|
+
encodeErrorResult(id, message) {
|
|
276
|
+
const errorResult = {
|
|
277
|
+
data: undefined,
|
|
278
|
+
error: { id, message },
|
|
279
|
+
};
|
|
280
|
+
return this.encodeJSON(errorResult);
|
|
281
|
+
}
|
|
282
|
+
// Helper function to encode any object to JSON string
|
|
283
|
+
encodeJSON(value) {
|
|
284
|
+
try {
|
|
285
|
+
const jsonString = JSON.stringify(value);
|
|
286
|
+
return jsonString;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return JSON.stringify({
|
|
290
|
+
error: {
|
|
291
|
+
id: 'ENCODING_ERROR',
|
|
292
|
+
message: `Failed to encode JSON: ${error.message}`,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
sendMetrics(metrics, apiKey) {
|
|
298
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
299
|
+
try {
|
|
300
|
+
if (this.portalApi) {
|
|
301
|
+
yield this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
|
|
302
|
+
headers: {
|
|
303
|
+
Authorization: `Bearer ${apiKey}`,
|
|
304
|
+
'Content-Type': 'application/json',
|
|
305
|
+
},
|
|
306
|
+
body: metrics,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (_a) {
|
|
311
|
+
// No-op
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
exports.default = EnclaveSigner;
|
|
@@ -3,8 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MpcSigner = exports.Signer = void 0;
|
|
6
|
+
exports.EnclaveSigner = exports.MpcSigner = exports.Signer = void 0;
|
|
7
7
|
var abstract_1 = require("./abstract");
|
|
8
8
|
Object.defineProperty(exports, "Signer", { enumerable: true, get: function () { return __importDefault(abstract_1).default; } });
|
|
9
9
|
var mpc_1 = require("./mpc");
|
|
10
10
|
Object.defineProperty(exports, "MpcSigner", { enumerable: true, get: function () { return __importDefault(mpc_1).default; } });
|
|
11
|
+
var enclave_1 = require("./enclave");
|
|
12
|
+
Object.defineProperty(exports, "EnclaveSigner", { enumerable: true, get: function () { return __importDefault(enclave_1).default; } });
|
|
@@ -158,7 +158,7 @@ class MpcSigner {
|
|
|
158
158
|
metrics.sdkBinaryConnectMs = binaryMetrics.connectMs;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
if (
|
|
161
|
+
if (error === null || error === void 0 ? void 0 : error.id) {
|
|
162
162
|
throw new utils_1.PortalMpcError(error);
|
|
163
163
|
}
|
|
164
164
|
// Record post-operation time
|
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { PortalCurve } from '@portal-hq/core';
|
|
11
11
|
import { Events, HttpRequester, InvalidApiKeyError, InvalidGatewayConfigError, PortalRequests, ProviderRpcError, RpcErrorCodes, } from '@portal-hq/utils';
|
|
12
|
-
import { MpcSigner } from '../signers';
|
|
12
|
+
import { MpcSigner, EnclaveSigner } from '../signers';
|
|
13
13
|
const passiveSignerMethods = [
|
|
14
14
|
'eth_accounts',
|
|
15
15
|
'eth_chainId',
|
|
@@ -42,7 +42,7 @@ class Provider {
|
|
|
42
42
|
// Required
|
|
43
43
|
apiKey, keychain, gatewayConfig,
|
|
44
44
|
// Optional
|
|
45
|
-
autoApprove = false, apiHost = 'api.portalhq.io', mpcHost = 'mpc.portalhq.io', version = 'v6', chainId = 'eip155:11155111', featureFlags = {}, }) {
|
|
45
|
+
autoApprove = false, apiHost = 'api.portalhq.io', mpcHost = 'mpc.portalhq.io', enclaveMPCHost = 'mpc-client.portalhq.io', version = 'v6', chainId = 'eip155:11155111', featureFlags = {}, }) {
|
|
46
46
|
// Handle required fields
|
|
47
47
|
if (!apiKey || apiKey.length === 0) {
|
|
48
48
|
throw new InvalidApiKeyError();
|
|
@@ -67,14 +67,26 @@ class Provider {
|
|
|
67
67
|
});
|
|
68
68
|
// Initialize Gateway HttpRequester
|
|
69
69
|
this.gateway = new PortalRequests();
|
|
70
|
-
// Initialize
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
// Initialize the appropriate signer based on feature flags
|
|
71
|
+
if (featureFlags.useEnclaveMPCApi) {
|
|
72
|
+
this.signer = new EnclaveSigner({
|
|
73
|
+
mpcHost,
|
|
74
|
+
enclaveMPCHost,
|
|
75
|
+
keychain,
|
|
76
|
+
version,
|
|
77
|
+
portalApi: this.portalApi,
|
|
78
|
+
featureFlags,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.signer = new MpcSigner({
|
|
83
|
+
mpcHost,
|
|
84
|
+
keychain,
|
|
85
|
+
version,
|
|
86
|
+
portalApi: this.portalApi,
|
|
87
|
+
featureFlags,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
/**
|
|
80
92
|
* Invokes all registered event handlers with the data provided
|
|
@@ -280,8 +292,7 @@ class Provider {
|
|
|
280
292
|
});
|
|
281
293
|
}
|
|
282
294
|
/**
|
|
283
|
-
* Updates the chainId of this instance
|
|
284
|
-
* the gateway used for the new chain
|
|
295
|
+
* Updates the chainId of this instance
|
|
285
296
|
*
|
|
286
297
|
* @param chainId The numerical ID of the chain to switch to
|
|
287
298
|
* @returns BaseProvider
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { PortalCurve } from '@portal-hq/core';
|
|
11
|
+
import { HttpRequester, getClientPlatformVersion, } from '@portal-hq/utils';
|
|
12
|
+
import UUID from 'react-native-uuid';
|
|
13
|
+
var Operation;
|
|
14
|
+
(function (Operation) {
|
|
15
|
+
Operation["SIGN"] = "sign";
|
|
16
|
+
Operation["RAW_SIGN"] = "raw_sign";
|
|
17
|
+
})(Operation || (Operation = {}));
|
|
18
|
+
class EnclaveSigner {
|
|
19
|
+
constructor({ keychain, enclaveMPCHost = 'mpc-client.portalhq.io', version = 'v6', portalApi, featureFlags = {}, }) {
|
|
20
|
+
this.version = 'v6';
|
|
21
|
+
this.buildParams = (method, txParams) => {
|
|
22
|
+
let params = txParams;
|
|
23
|
+
switch (method) {
|
|
24
|
+
case 'eth_sign':
|
|
25
|
+
case 'personal_sign':
|
|
26
|
+
case 'eth_signTypedData_v3':
|
|
27
|
+
case 'eth_signTypedData_v4':
|
|
28
|
+
case 'sol_signMessage':
|
|
29
|
+
case 'sol_signTransaction':
|
|
30
|
+
case 'sol_signAndSendTransaction':
|
|
31
|
+
case 'sol_signAndConfirmTransaction':
|
|
32
|
+
if (!Array.isArray(txParams)) {
|
|
33
|
+
params = [txParams];
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
if (Array.isArray(txParams)) {
|
|
38
|
+
if (txParams.length === 1) {
|
|
39
|
+
params = txParams[0];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return params;
|
|
44
|
+
};
|
|
45
|
+
this.featureFlags = featureFlags;
|
|
46
|
+
this.keychain = keychain;
|
|
47
|
+
this.enclaveMPCHost = enclaveMPCHost;
|
|
48
|
+
this.version = version;
|
|
49
|
+
this.portalApi = portalApi;
|
|
50
|
+
this.requests = new HttpRequester({
|
|
51
|
+
baseUrl: `https://${this.enclaveMPCHost}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
sign(message, provider) {
|
|
55
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
// Always track metrics, but only send if feature flag is enabled
|
|
57
|
+
const shouldSendMetrics = this.featureFlags.enableSdkPerformanceMetrics === true;
|
|
58
|
+
const signStartTime = performance.now();
|
|
59
|
+
const preOperationStartTime = performance.now();
|
|
60
|
+
// Generate a traceId for this operation
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
62
|
+
const traceId = UUID.v4();
|
|
63
|
+
const metrics = {
|
|
64
|
+
hasError: false,
|
|
65
|
+
operation: Operation.SIGN,
|
|
66
|
+
signingMethod: message.method,
|
|
67
|
+
traceId,
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
const eip155Address = yield this.keychain.getEip155Address();
|
|
71
|
+
const apiKey = provider.apiKey;
|
|
72
|
+
const { method, chainId, curve, isRaw } = message;
|
|
73
|
+
// Add chainId to metrics
|
|
74
|
+
if (chainId) {
|
|
75
|
+
metrics.chainId = chainId;
|
|
76
|
+
}
|
|
77
|
+
switch (method) {
|
|
78
|
+
case 'eth_requestAccounts':
|
|
79
|
+
return [eip155Address];
|
|
80
|
+
case 'eth_accounts':
|
|
81
|
+
return [eip155Address];
|
|
82
|
+
default:
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
const shares = yield this.keychain.getShares();
|
|
86
|
+
let signingShare = shares.secp256k1.share;
|
|
87
|
+
if (curve === PortalCurve.ED25519) {
|
|
88
|
+
if (!shares.ed25519) {
|
|
89
|
+
throw new Error('[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.');
|
|
90
|
+
}
|
|
91
|
+
signingShare = shares.ed25519.share;
|
|
92
|
+
}
|
|
93
|
+
const metadata = {
|
|
94
|
+
clientPlatform: 'REACT_NATIVE',
|
|
95
|
+
clientPlatformVersion: getClientPlatformVersion(),
|
|
96
|
+
isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
|
|
97
|
+
mpcServerVersion: this.version,
|
|
98
|
+
optimized: true,
|
|
99
|
+
curve,
|
|
100
|
+
chainId,
|
|
101
|
+
isRaw,
|
|
102
|
+
reqId: traceId,
|
|
103
|
+
connectionTracingEnabled: shouldSendMetrics,
|
|
104
|
+
};
|
|
105
|
+
let formattedParams;
|
|
106
|
+
let rpcUrl;
|
|
107
|
+
if (isRaw) {
|
|
108
|
+
formattedParams = JSON.stringify(this.buildParams(method, message.params));
|
|
109
|
+
rpcUrl = '';
|
|
110
|
+
metrics.operation = Operation.RAW_SIGN;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
formattedParams = JSON.stringify(this.buildParams(method, message.params));
|
|
114
|
+
rpcUrl = provider.getGatewayUrl(chainId);
|
|
115
|
+
}
|
|
116
|
+
if (typeof formattedParams !== 'string') {
|
|
117
|
+
throw new Error(`[Portal.Provider.EnclaveSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`);
|
|
118
|
+
}
|
|
119
|
+
// Record pre-operation time
|
|
120
|
+
metrics.sdkPreOperationMs = performance.now() - preOperationStartTime;
|
|
121
|
+
// Measure enclave signing operation time
|
|
122
|
+
const enclaveSignStartTime = performance.now();
|
|
123
|
+
const result = isRaw
|
|
124
|
+
? yield this.enclaveRawSign(apiKey, JSON.stringify(signingShare), formattedParams, curve || 'SECP256K1')
|
|
125
|
+
: yield this.enclaveSign(apiKey, JSON.stringify(signingShare), message.method, formattedParams, rpcUrl, chainId || '', JSON.stringify(metadata));
|
|
126
|
+
// Post-operation processing time starts
|
|
127
|
+
const postOperationStartTime = performance.now();
|
|
128
|
+
// Record HTTP call time
|
|
129
|
+
metrics.enclaveHttpCallMs = performance.now() - enclaveSignStartTime;
|
|
130
|
+
// Record post-operation time
|
|
131
|
+
metrics.sdkPostOperationMs = performance.now() - postOperationStartTime;
|
|
132
|
+
// Calculate total SDK signing time
|
|
133
|
+
metrics.sdkOperationMs = performance.now() - signStartTime;
|
|
134
|
+
// Log performance timing to console
|
|
135
|
+
const timingMetrics = {};
|
|
136
|
+
if (typeof metrics.sdkPreOperationMs === 'number') {
|
|
137
|
+
timingMetrics['Pre-operation'] = metrics.sdkPreOperationMs;
|
|
138
|
+
}
|
|
139
|
+
if (typeof metrics.enclaveHttpCallMs === 'number') {
|
|
140
|
+
timingMetrics['Enclave HTTP Call'] = metrics.enclaveHttpCallMs;
|
|
141
|
+
}
|
|
142
|
+
if (typeof metrics.sdkPostOperationMs === 'number') {
|
|
143
|
+
timingMetrics['Post-operation'] = metrics.sdkPostOperationMs;
|
|
144
|
+
}
|
|
145
|
+
// Only send metrics if the feature flag is enabled
|
|
146
|
+
if (shouldSendMetrics && this.portalApi) {
|
|
147
|
+
try {
|
|
148
|
+
yield this.sendMetrics(metrics, apiKey);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
// No-op
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
// Calculate total time even in error case
|
|
158
|
+
metrics.sdkOperationMs = performance.now() - signStartTime;
|
|
159
|
+
// Log performance timing to console even in error case
|
|
160
|
+
const errorTimingMetrics = {};
|
|
161
|
+
if (typeof metrics.sdkPreOperationMs === 'number') {
|
|
162
|
+
errorTimingMetrics['Pre-operation'] = metrics.sdkPreOperationMs;
|
|
163
|
+
}
|
|
164
|
+
// Only send metrics if the feature flag is enabled
|
|
165
|
+
if (shouldSendMetrics) {
|
|
166
|
+
const apiKey = provider.apiKey;
|
|
167
|
+
metrics.hasError = true;
|
|
168
|
+
try {
|
|
169
|
+
yield this.sendMetrics(metrics, apiKey);
|
|
170
|
+
}
|
|
171
|
+
catch (_a) {
|
|
172
|
+
// No-op
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
enclaveRawSign(apiKey, signingShare, params, curve) {
|
|
180
|
+
var _a;
|
|
181
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
182
|
+
if (!apiKey || !signingShare || !params || !curve) {
|
|
183
|
+
return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided for raw signing');
|
|
184
|
+
}
|
|
185
|
+
const requestBody = {
|
|
186
|
+
params: params,
|
|
187
|
+
share: signingShare,
|
|
188
|
+
};
|
|
189
|
+
const endpoint = `/v1/raw/sign/${curve}`;
|
|
190
|
+
try {
|
|
191
|
+
const response = yield this.requests.post(endpoint, {
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${apiKey}`,
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
},
|
|
196
|
+
body: requestBody,
|
|
197
|
+
});
|
|
198
|
+
return this.encodeSuccessResult(response.data);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
202
|
+
const portalError = this.decodePortalError(JSON.stringify(error.response.data));
|
|
203
|
+
return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
|
|
204
|
+
}
|
|
205
|
+
return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
207
|
+
error.message || 'Network error occurred');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
enclaveSign(apiKey, signingShare, method, params, rpcURL, chainId, metadata) {
|
|
212
|
+
var _a;
|
|
213
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
214
|
+
if (!apiKey ||
|
|
215
|
+
!signingShare ||
|
|
216
|
+
!method ||
|
|
217
|
+
!params ||
|
|
218
|
+
!chainId ||
|
|
219
|
+
!metadata) {
|
|
220
|
+
return this.encodeErrorResult('INVALID_PARAMETERS', 'Invalid parameters provided');
|
|
221
|
+
}
|
|
222
|
+
const requestBody = {
|
|
223
|
+
method: method,
|
|
224
|
+
params: params,
|
|
225
|
+
share: signingShare,
|
|
226
|
+
chainId: chainId,
|
|
227
|
+
rpcUrl: rpcURL,
|
|
228
|
+
metadataStr: metadata,
|
|
229
|
+
clientPlatform: 'REACT_NATIVE',
|
|
230
|
+
clientPlatformVersion: getClientPlatformVersion(),
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
const response = yield this.requests.post('/v1/sign', {
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${apiKey}`,
|
|
236
|
+
'Content-Type': 'application/json',
|
|
237
|
+
},
|
|
238
|
+
body: requestBody,
|
|
239
|
+
});
|
|
240
|
+
return this.encodeSuccessResult(response.data);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
244
|
+
const portalError = this.decodePortalError(JSON.stringify(error.response.data));
|
|
245
|
+
return this.encodeErrorResult(portalError === null || portalError === void 0 ? void 0 : portalError.id, portalError === null || portalError === void 0 ? void 0 : portalError.message);
|
|
246
|
+
}
|
|
247
|
+
return this.encodeErrorResult('SIGNING_NETWORK_ERROR',
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
249
|
+
error.message || 'Network error occurred');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// Helper function to encode success results
|
|
254
|
+
encodeSuccessResult(data) {
|
|
255
|
+
const successResult = { data: data, error: undefined };
|
|
256
|
+
return this.encodeJSON(successResult);
|
|
257
|
+
}
|
|
258
|
+
// Helper function to decode Portal errors
|
|
259
|
+
decodePortalError(errorStr) {
|
|
260
|
+
if (!errorStr)
|
|
261
|
+
return null;
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(errorStr);
|
|
264
|
+
}
|
|
265
|
+
catch (_a) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Helper function to encode error results
|
|
270
|
+
encodeErrorResult(id, message) {
|
|
271
|
+
const errorResult = {
|
|
272
|
+
data: undefined,
|
|
273
|
+
error: { id, message },
|
|
274
|
+
};
|
|
275
|
+
return this.encodeJSON(errorResult);
|
|
276
|
+
}
|
|
277
|
+
// Helper function to encode any object to JSON string
|
|
278
|
+
encodeJSON(value) {
|
|
279
|
+
try {
|
|
280
|
+
const jsonString = JSON.stringify(value);
|
|
281
|
+
return jsonString;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return JSON.stringify({
|
|
285
|
+
error: {
|
|
286
|
+
id: 'ENCODING_ERROR',
|
|
287
|
+
message: `Failed to encode JSON: ${error.message}`,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
sendMetrics(metrics, apiKey) {
|
|
293
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
294
|
+
try {
|
|
295
|
+
if (this.portalApi) {
|
|
296
|
+
yield this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
|
|
297
|
+
headers: {
|
|
298
|
+
Authorization: `Bearer ${apiKey}`,
|
|
299
|
+
'Content-Type': 'application/json',
|
|
300
|
+
},
|
|
301
|
+
body: metrics,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch (_a) {
|
|
306
|
+
// No-op
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export default EnclaveSigner;
|
package/lib/esm/signers/index.js
CHANGED
package/lib/esm/signers/mpc.js
CHANGED
|
@@ -153,7 +153,7 @@ class MpcSigner {
|
|
|
153
153
|
metrics.sdkBinaryConnectMs = binaryMetrics.connectMs;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
if (
|
|
156
|
+
if (error === null || error === void 0 ? void 0 : error.id) {
|
|
157
157
|
throw new PortalMpcError(error);
|
|
158
158
|
}
|
|
159
159
|
// Record post-operation time
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portal-hq/provider",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.4",
|
|
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.4",
|
|
23
|
+
"@portal-hq/utils": "^4.1.4",
|
|
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": "6507173bd858f430a4144bd9e700878a879cf286"
|
|
36
36
|
}
|
package/src/providers/index.ts
CHANGED
|
@@ -21,8 +21,7 @@ import {
|
|
|
21
21
|
type RegisteredEventHandler,
|
|
22
22
|
SwitchEthereumChainParameter,
|
|
23
23
|
} from '../../types'
|
|
24
|
-
import { MpcSigner, Signer } from '../signers'
|
|
25
|
-
|
|
24
|
+
import { MpcSigner, Signer, EnclaveSigner } from '../signers'
|
|
26
25
|
const passiveSignerMethods = [
|
|
27
26
|
'eth_accounts',
|
|
28
27
|
'eth_chainId',
|
|
@@ -77,6 +76,7 @@ class Provider implements IPortalProvider {
|
|
|
77
76
|
autoApprove = false,
|
|
78
77
|
apiHost = 'api.portalhq.io',
|
|
79
78
|
mpcHost = 'mpc.portalhq.io',
|
|
79
|
+
enclaveMPCHost = 'mpc-client.portalhq.io',
|
|
80
80
|
version = 'v6',
|
|
81
81
|
chainId = 'eip155:11155111',
|
|
82
82
|
featureFlags = {},
|
|
@@ -110,14 +110,25 @@ class Provider implements IPortalProvider {
|
|
|
110
110
|
// Initialize Gateway HttpRequester
|
|
111
111
|
this.gateway = new PortalRequests()
|
|
112
112
|
|
|
113
|
-
// Initialize
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
// Initialize the appropriate signer based on feature flags
|
|
114
|
+
if (featureFlags.useEnclaveMPCApi) {
|
|
115
|
+
this.signer = new EnclaveSigner({
|
|
116
|
+
mpcHost,
|
|
117
|
+
enclaveMPCHost,
|
|
118
|
+
keychain,
|
|
119
|
+
version,
|
|
120
|
+
portalApi: this.portalApi,
|
|
121
|
+
featureFlags,
|
|
122
|
+
})
|
|
123
|
+
} else {
|
|
124
|
+
this.signer = new MpcSigner({
|
|
125
|
+
mpcHost,
|
|
126
|
+
keychain,
|
|
127
|
+
version,
|
|
128
|
+
portalApi: this.portalApi,
|
|
129
|
+
featureFlags,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
/**
|
|
@@ -362,8 +373,7 @@ class Provider implements IPortalProvider {
|
|
|
362
373
|
return result
|
|
363
374
|
}
|
|
364
375
|
/**
|
|
365
|
-
* Updates the chainId of this instance
|
|
366
|
-
* the gateway used for the new chain
|
|
376
|
+
* Updates the chainId of this instance
|
|
367
377
|
*
|
|
368
378
|
* @param chainId The numerical ID of the chain to switch to
|
|
369
379
|
* @returns BaseProvider
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { PortalCurve } from '@portal-hq/core'
|
|
2
|
+
import { FeatureFlags } from '@portal-hq/core/types'
|
|
3
|
+
import {
|
|
4
|
+
HttpRequester,
|
|
5
|
+
IPortalProvider,
|
|
6
|
+
KeychainAdapter,
|
|
7
|
+
type SigningRequestArguments,
|
|
8
|
+
getClientPlatformVersion,
|
|
9
|
+
} from '@portal-hq/utils'
|
|
10
|
+
import UUID from 'react-native-uuid'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type MpcSignerOptions,
|
|
14
|
+
PortalMobileMpcMetadata,
|
|
15
|
+
type EnclaveSignRequest,
|
|
16
|
+
type EnclaveSignResponse,
|
|
17
|
+
type EnclaveSignResult,
|
|
18
|
+
} from '../../types'
|
|
19
|
+
import Signer from './abstract'
|
|
20
|
+
|
|
21
|
+
enum Operation {
|
|
22
|
+
SIGN = 'sign',
|
|
23
|
+
RAW_SIGN = 'raw_sign',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class EnclaveSigner implements Signer {
|
|
27
|
+
private featureFlags: FeatureFlags
|
|
28
|
+
private keychain: KeychainAdapter
|
|
29
|
+
private enclaveMPCHost: string
|
|
30
|
+
private version = 'v6'
|
|
31
|
+
private portalApi?: HttpRequester
|
|
32
|
+
private requests: HttpRequester
|
|
33
|
+
|
|
34
|
+
constructor({
|
|
35
|
+
keychain,
|
|
36
|
+
enclaveMPCHost = 'mpc-client.portalhq.io',
|
|
37
|
+
version = 'v6',
|
|
38
|
+
portalApi,
|
|
39
|
+
featureFlags = {},
|
|
40
|
+
}: MpcSignerOptions & { enclaveMPCHost?: string }) {
|
|
41
|
+
this.featureFlags = featureFlags
|
|
42
|
+
this.keychain = keychain
|
|
43
|
+
this.enclaveMPCHost = enclaveMPCHost
|
|
44
|
+
this.version = version
|
|
45
|
+
this.portalApi = portalApi
|
|
46
|
+
this.requests = new HttpRequester({
|
|
47
|
+
baseUrl: `https://${this.enclaveMPCHost}`,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async sign(
|
|
52
|
+
message: SigningRequestArguments,
|
|
53
|
+
provider: IPortalProvider,
|
|
54
|
+
): Promise<any> {
|
|
55
|
+
// Always track metrics, but only send if feature flag is enabled
|
|
56
|
+
const shouldSendMetrics =
|
|
57
|
+
this.featureFlags.enableSdkPerformanceMetrics === true
|
|
58
|
+
const signStartTime = performance.now()
|
|
59
|
+
const preOperationStartTime = performance.now()
|
|
60
|
+
|
|
61
|
+
// Generate a traceId for this operation
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
63
|
+
const traceId = (UUID as any).v4() as string
|
|
64
|
+
const metrics: Record<string, number | string | boolean> = {
|
|
65
|
+
hasError: false,
|
|
66
|
+
operation: Operation.SIGN,
|
|
67
|
+
signingMethod: message.method,
|
|
68
|
+
traceId,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const eip155Address = await this.keychain.getEip155Address()
|
|
73
|
+
const apiKey = provider.apiKey
|
|
74
|
+
const { method, chainId, curve, isRaw } = message
|
|
75
|
+
|
|
76
|
+
// Add chainId to metrics
|
|
77
|
+
if (chainId) {
|
|
78
|
+
metrics.chainId = chainId
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (method) {
|
|
82
|
+
case 'eth_requestAccounts':
|
|
83
|
+
return [eip155Address]
|
|
84
|
+
case 'eth_accounts':
|
|
85
|
+
return [eip155Address]
|
|
86
|
+
default:
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const shares = await this.keychain.getShares()
|
|
91
|
+
let signingShare = shares.secp256k1.share
|
|
92
|
+
|
|
93
|
+
if (curve === PortalCurve.ED25519) {
|
|
94
|
+
if (!shares.ed25519) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'[Portal.Provider.EnclaveSigner] The ED25519 share is missing from the keychain.',
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
signingShare = shares.ed25519.share
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const metadata: PortalMobileMpcMetadata = {
|
|
103
|
+
clientPlatform: 'REACT_NATIVE',
|
|
104
|
+
clientPlatformVersion: getClientPlatformVersion(),
|
|
105
|
+
isMultiBackupEnabled: this.featureFlags.isMultiBackupEnabled,
|
|
106
|
+
mpcServerVersion: this.version,
|
|
107
|
+
optimized: true,
|
|
108
|
+
curve,
|
|
109
|
+
chainId,
|
|
110
|
+
isRaw,
|
|
111
|
+
reqId: traceId,
|
|
112
|
+
connectionTracingEnabled: shouldSendMetrics,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let formattedParams: string
|
|
116
|
+
let rpcUrl: string
|
|
117
|
+
|
|
118
|
+
if (isRaw) {
|
|
119
|
+
formattedParams = JSON.stringify(
|
|
120
|
+
this.buildParams(method, message.params),
|
|
121
|
+
)
|
|
122
|
+
rpcUrl = ''
|
|
123
|
+
metrics.operation = Operation.RAW_SIGN
|
|
124
|
+
} else {
|
|
125
|
+
formattedParams = JSON.stringify(
|
|
126
|
+
this.buildParams(method, message.params),
|
|
127
|
+
)
|
|
128
|
+
rpcUrl = provider.getGatewayUrl(chainId)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof formattedParams !== 'string') {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`[Portal.Provider.EnclaveSigner] The formatted params for the signing request could not be converted to a string. The params were: ${formattedParams}`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Record pre-operation time
|
|
138
|
+
metrics.sdkPreOperationMs = performance.now() - preOperationStartTime
|
|
139
|
+
|
|
140
|
+
// Measure enclave signing operation time
|
|
141
|
+
const enclaveSignStartTime = performance.now()
|
|
142
|
+
|
|
143
|
+
const result = isRaw
|
|
144
|
+
? await this.enclaveRawSign(
|
|
145
|
+
apiKey,
|
|
146
|
+
JSON.stringify(signingShare),
|
|
147
|
+
formattedParams,
|
|
148
|
+
curve || 'SECP256K1',
|
|
149
|
+
)
|
|
150
|
+
: await this.enclaveSign(
|
|
151
|
+
apiKey,
|
|
152
|
+
JSON.stringify(signingShare),
|
|
153
|
+
message.method,
|
|
154
|
+
formattedParams,
|
|
155
|
+
rpcUrl,
|
|
156
|
+
chainId || '',
|
|
157
|
+
JSON.stringify(metadata),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Post-operation processing time starts
|
|
161
|
+
const postOperationStartTime = performance.now()
|
|
162
|
+
|
|
163
|
+
// Record HTTP call time
|
|
164
|
+
metrics.enclaveHttpCallMs = performance.now() - enclaveSignStartTime
|
|
165
|
+
|
|
166
|
+
// Record post-operation time
|
|
167
|
+
metrics.sdkPostOperationMs = performance.now() - postOperationStartTime
|
|
168
|
+
|
|
169
|
+
// Calculate total SDK signing time
|
|
170
|
+
metrics.sdkOperationMs = performance.now() - signStartTime
|
|
171
|
+
|
|
172
|
+
// Log performance timing to console
|
|
173
|
+
const timingMetrics: Record<string, number> = {}
|
|
174
|
+
if (typeof metrics.sdkPreOperationMs === 'number') {
|
|
175
|
+
timingMetrics['Pre-operation'] = metrics.sdkPreOperationMs
|
|
176
|
+
}
|
|
177
|
+
if (typeof metrics.enclaveHttpCallMs === 'number') {
|
|
178
|
+
timingMetrics['Enclave HTTP Call'] = metrics.enclaveHttpCallMs
|
|
179
|
+
}
|
|
180
|
+
if (typeof metrics.sdkPostOperationMs === 'number') {
|
|
181
|
+
timingMetrics['Post-operation'] = metrics.sdkPostOperationMs
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Only send metrics if the feature flag is enabled
|
|
185
|
+
if (shouldSendMetrics && this.portalApi) {
|
|
186
|
+
try {
|
|
187
|
+
await this.sendMetrics(metrics, apiKey)
|
|
188
|
+
} catch (err) {
|
|
189
|
+
// No-op
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Calculate total time even in error case
|
|
196
|
+
metrics.sdkOperationMs = performance.now() - signStartTime
|
|
197
|
+
|
|
198
|
+
// Log performance timing to console even in error case
|
|
199
|
+
const errorTimingMetrics: Record<string, number> = {}
|
|
200
|
+
if (typeof metrics.sdkPreOperationMs === 'number') {
|
|
201
|
+
errorTimingMetrics['Pre-operation'] = metrics.sdkPreOperationMs
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Only send metrics if the feature flag is enabled
|
|
205
|
+
if (shouldSendMetrics) {
|
|
206
|
+
const apiKey = provider.apiKey
|
|
207
|
+
metrics.hasError = true
|
|
208
|
+
try {
|
|
209
|
+
await this.sendMetrics(metrics, apiKey)
|
|
210
|
+
} catch {
|
|
211
|
+
// No-op
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async enclaveRawSign(
|
|
220
|
+
apiKey: string,
|
|
221
|
+
signingShare: string,
|
|
222
|
+
params: string,
|
|
223
|
+
curve: string,
|
|
224
|
+
): Promise<string> {
|
|
225
|
+
if (!apiKey || !signingShare || !params || !curve) {
|
|
226
|
+
return this.encodeErrorResult(
|
|
227
|
+
'INVALID_PARAMETERS',
|
|
228
|
+
'Invalid parameters provided for raw signing',
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const requestBody = {
|
|
233
|
+
params: params,
|
|
234
|
+
share: signingShare,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const endpoint = `/v1/raw/sign/${curve}`
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const response = await this.requests.post<EnclaveSignResponse>(endpoint, {
|
|
241
|
+
headers: {
|
|
242
|
+
Authorization: `Bearer ${apiKey}`,
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
},
|
|
245
|
+
body: requestBody,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
return this.encodeSuccessResult(response.data)
|
|
249
|
+
} catch (error: any) {
|
|
250
|
+
if (error?.response?.data) {
|
|
251
|
+
const portalError = this.decodePortalError(
|
|
252
|
+
JSON.stringify(error.response.data),
|
|
253
|
+
)
|
|
254
|
+
return this.encodeErrorResult(portalError?.id, portalError?.message)
|
|
255
|
+
}
|
|
256
|
+
return this.encodeErrorResult(
|
|
257
|
+
'SIGNING_NETWORK_ERROR',
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
259
|
+
error.message || 'Network error occurred',
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async enclaveSign(
|
|
265
|
+
apiKey: string,
|
|
266
|
+
signingShare: string,
|
|
267
|
+
method: string,
|
|
268
|
+
params: string,
|
|
269
|
+
rpcURL: string,
|
|
270
|
+
chainId: string,
|
|
271
|
+
metadata: string,
|
|
272
|
+
): Promise<string> {
|
|
273
|
+
if (
|
|
274
|
+
!apiKey ||
|
|
275
|
+
!signingShare ||
|
|
276
|
+
!method ||
|
|
277
|
+
!params ||
|
|
278
|
+
!chainId ||
|
|
279
|
+
!metadata
|
|
280
|
+
) {
|
|
281
|
+
return this.encodeErrorResult(
|
|
282
|
+
'INVALID_PARAMETERS',
|
|
283
|
+
'Invalid parameters provided',
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const requestBody: EnclaveSignRequest = {
|
|
288
|
+
method: method,
|
|
289
|
+
params: params,
|
|
290
|
+
share: signingShare,
|
|
291
|
+
chainId: chainId,
|
|
292
|
+
rpcUrl: rpcURL,
|
|
293
|
+
metadataStr: metadata,
|
|
294
|
+
clientPlatform: 'REACT_NATIVE',
|
|
295
|
+
clientPlatformVersion: getClientPlatformVersion(),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const response = await this.requests.post<EnclaveSignResponse>(
|
|
300
|
+
'/v1/sign',
|
|
301
|
+
{
|
|
302
|
+
headers: {
|
|
303
|
+
Authorization: `Bearer ${apiKey}`,
|
|
304
|
+
'Content-Type': 'application/json',
|
|
305
|
+
},
|
|
306
|
+
body: requestBody,
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return this.encodeSuccessResult(response.data)
|
|
311
|
+
} catch (error: any) {
|
|
312
|
+
if (error?.response?.data) {
|
|
313
|
+
const portalError = this.decodePortalError(
|
|
314
|
+
JSON.stringify(error.response.data),
|
|
315
|
+
)
|
|
316
|
+
return this.encodeErrorResult(portalError?.id, portalError?.message)
|
|
317
|
+
}
|
|
318
|
+
return this.encodeErrorResult(
|
|
319
|
+
'SIGNING_NETWORK_ERROR',
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
|
321
|
+
error.message || 'Network error occurred',
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Helper function to encode success results
|
|
327
|
+
private encodeSuccessResult(data: string): string {
|
|
328
|
+
const successResult: EnclaveSignResult = { data: data, error: undefined }
|
|
329
|
+
return this.encodeJSON(successResult)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Helper function to decode Portal errors
|
|
333
|
+
private decodePortalError(
|
|
334
|
+
errorStr?: string,
|
|
335
|
+
): { id?: string; message?: string } | null {
|
|
336
|
+
if (!errorStr) return null
|
|
337
|
+
try {
|
|
338
|
+
return JSON.parse(errorStr)
|
|
339
|
+
} catch {
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Helper function to encode error results
|
|
345
|
+
private encodeErrorResult(id?: string, message?: string): string {
|
|
346
|
+
const errorResult: EnclaveSignResult = {
|
|
347
|
+
data: undefined,
|
|
348
|
+
error: { id, message },
|
|
349
|
+
}
|
|
350
|
+
return this.encodeJSON(errorResult)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Helper function to encode any object to JSON string
|
|
354
|
+
private encodeJSON<T>(value: T): string {
|
|
355
|
+
try {
|
|
356
|
+
const jsonString = JSON.stringify(value)
|
|
357
|
+
return jsonString
|
|
358
|
+
} catch (error: any) {
|
|
359
|
+
return JSON.stringify({
|
|
360
|
+
error: {
|
|
361
|
+
id: 'ENCODING_ERROR',
|
|
362
|
+
message: `Failed to encode JSON: ${error.message}`,
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async sendMetrics(
|
|
369
|
+
metrics: Record<string, number | string | boolean>,
|
|
370
|
+
apiKey: string,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
try {
|
|
373
|
+
if (this.portalApi) {
|
|
374
|
+
await this.portalApi.post('/api/v3/clients/me/sdk/metrics', {
|
|
375
|
+
headers: {
|
|
376
|
+
Authorization: `Bearer ${apiKey}`,
|
|
377
|
+
'Content-Type': 'application/json',
|
|
378
|
+
},
|
|
379
|
+
body: metrics,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
// No-op
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private buildParams = (method: string, txParams: any) => {
|
|
388
|
+
let params = txParams
|
|
389
|
+
|
|
390
|
+
switch (method) {
|
|
391
|
+
case 'eth_sign':
|
|
392
|
+
case 'personal_sign':
|
|
393
|
+
case 'eth_signTypedData_v3':
|
|
394
|
+
case 'eth_signTypedData_v4':
|
|
395
|
+
case 'sol_signMessage':
|
|
396
|
+
case 'sol_signTransaction':
|
|
397
|
+
case 'sol_signAndSendTransaction':
|
|
398
|
+
case 'sol_signAndConfirmTransaction':
|
|
399
|
+
if (!Array.isArray(txParams)) {
|
|
400
|
+
params = [txParams]
|
|
401
|
+
}
|
|
402
|
+
break
|
|
403
|
+
default:
|
|
404
|
+
if (Array.isArray(txParams)) {
|
|
405
|
+
if (txParams.length === 1) {
|
|
406
|
+
params = txParams[0]
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return params
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export default EnclaveSigner
|
package/src/signers/index.ts
CHANGED
package/src/signers/mpc.ts
CHANGED
package/types.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface MpcSignerOptions extends SignerOptions {
|
|
|
25
25
|
isSimulator?: boolean
|
|
26
26
|
mpcHost?: string
|
|
27
27
|
portalApi?: HttpRequester
|
|
28
|
+
enclaveMPCHost?: string
|
|
28
29
|
version?: string
|
|
29
30
|
featureFlags?: FeatureFlags
|
|
30
31
|
}
|
|
@@ -86,6 +87,7 @@ export interface ProviderOptions {
|
|
|
86
87
|
autoApprove?: boolean
|
|
87
88
|
apiHost?: string
|
|
88
89
|
mpcHost?: string
|
|
90
|
+
enclaveMPCHost?: string
|
|
89
91
|
version?: string
|
|
90
92
|
featureFlags?: FeatureFlags
|
|
91
93
|
}
|
|
@@ -106,6 +108,30 @@ export interface SignResult {
|
|
|
106
108
|
S: string
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
export interface EnclaveSignResult {
|
|
112
|
+
data?: string
|
|
113
|
+
error?: {
|
|
114
|
+
id?: string
|
|
115
|
+
message?: string
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ProviderOptions {
|
|
120
|
+
// Required
|
|
121
|
+
apiKey: string
|
|
122
|
+
keychain: KeychainAdapter
|
|
123
|
+
gatewayConfig: GatewayLike
|
|
124
|
+
|
|
125
|
+
// Optional
|
|
126
|
+
autoApprove?: boolean
|
|
127
|
+
apiHost?: string
|
|
128
|
+
mpcHost?: string
|
|
129
|
+
enclaveMPCHost?: string
|
|
130
|
+
version?: string
|
|
131
|
+
chainId?: string
|
|
132
|
+
featureFlags?: FeatureFlags
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
export interface SignerOptions {}
|
|
110
136
|
|
|
111
137
|
export interface Eip1559 {
|
|
@@ -141,3 +167,22 @@ export interface SigningResponse {
|
|
|
141
167
|
export interface SwitchEthereumChainParameter {
|
|
142
168
|
chainId: number
|
|
143
169
|
}
|
|
170
|
+
|
|
171
|
+
export interface EnclaveSignRequest {
|
|
172
|
+
method: string
|
|
173
|
+
params: string
|
|
174
|
+
share: string
|
|
175
|
+
chainId: string
|
|
176
|
+
rpcUrl: string
|
|
177
|
+
metadataStr: string
|
|
178
|
+
clientPlatform: string
|
|
179
|
+
clientPlatformVersion: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface EnclaveSignResponse {
|
|
183
|
+
data: string
|
|
184
|
+
error?: {
|
|
185
|
+
id: string
|
|
186
|
+
message: string
|
|
187
|
+
}
|
|
188
|
+
}
|