pribado-seed-proxy-sdk 2.0.0
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/README.md +397 -0
- package/dist/index.d.mts +489 -0
- package/dist/index.d.ts +489 -0
- package/dist/index.js +829 -0
- package/dist/index.mjs +780 -0
- package/package.json +51 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { ethers as ethers2 } from "ethers";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var SAPPHIRE_MAINNET_CHAIN_ID = 23294;
|
|
6
|
+
var CONTRACT_ADDRESS = "0xabE5D482AceFED95E0D37dA89bC63F941f02f9A0";
|
|
7
|
+
var KEY_TYPE_SEED = 0;
|
|
8
|
+
var KEY_TYPE_PRIVATE_KEY = 1;
|
|
9
|
+
var SeedProxyErrorCode = /* @__PURE__ */ ((SeedProxyErrorCode2) => {
|
|
10
|
+
SeedProxyErrorCode2["INVALID_CONFIG"] = "INVALID_CONFIG";
|
|
11
|
+
SeedProxyErrorCode2["MISSING_API_KEY"] = "MISSING_API_KEY";
|
|
12
|
+
SeedProxyErrorCode2["INVALID_PASSWORD"] = "INVALID_PASSWORD";
|
|
13
|
+
SeedProxyErrorCode2["INVALID_KEY"] = "INVALID_KEY";
|
|
14
|
+
SeedProxyErrorCode2["KEY_NOT_FOUND"] = "KEY_NOT_FOUND";
|
|
15
|
+
SeedProxyErrorCode2["KEY_INACTIVE"] = "KEY_INACTIVE";
|
|
16
|
+
SeedProxyErrorCode2["AUTHENTICATION_FAILED"] = "AUTHENTICATION_FAILED";
|
|
17
|
+
SeedProxyErrorCode2["INVALID_SEED_PHRASE"] = "INVALID_SEED_PHRASE";
|
|
18
|
+
SeedProxyErrorCode2["INVALID_PRIVATE_KEY"] = "INVALID_PRIVATE_KEY";
|
|
19
|
+
SeedProxyErrorCode2["INVALID_ADDRESS"] = "INVALID_ADDRESS";
|
|
20
|
+
SeedProxyErrorCode2["INVALID_TRANSACTION"] = "INVALID_TRANSACTION";
|
|
21
|
+
SeedProxyErrorCode2["REGISTRATION_FAILED"] = "REGISTRATION_FAILED";
|
|
22
|
+
SeedProxyErrorCode2["SIGNING_FAILED"] = "SIGNING_FAILED";
|
|
23
|
+
SeedProxyErrorCode2["ROTATION_FAILED"] = "ROTATION_FAILED";
|
|
24
|
+
SeedProxyErrorCode2["REVOCATION_FAILED"] = "REVOCATION_FAILED";
|
|
25
|
+
SeedProxyErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
26
|
+
SeedProxyErrorCode2["TIMEOUT"] = "TIMEOUT";
|
|
27
|
+
SeedProxyErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
|
|
28
|
+
SeedProxyErrorCode2["GAS_LIMIT_EXCEEDED"] = "GAS_LIMIT_EXCEEDED";
|
|
29
|
+
return SeedProxyErrorCode2;
|
|
30
|
+
})(SeedProxyErrorCode || {});
|
|
31
|
+
var SeedProxyError = class extends Error {
|
|
32
|
+
constructor(message, code, cause) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.cause = cause;
|
|
36
|
+
this.name = "SeedProxyError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/crypto.ts
|
|
41
|
+
import { ethers } from "ethers";
|
|
42
|
+
function validateSeedPhrase(phrase) {
|
|
43
|
+
if (!phrase || typeof phrase !== "string") return false;
|
|
44
|
+
const words = phrase.trim().toLowerCase().split(/\s+/);
|
|
45
|
+
if (words.length !== 12 && words.length !== 24) return false;
|
|
46
|
+
return words.every((word) => /^[a-z]+$/.test(word));
|
|
47
|
+
}
|
|
48
|
+
function validatePrivateKey(key) {
|
|
49
|
+
if (!key || typeof key !== "string") return false;
|
|
50
|
+
const cleanKey = key.startsWith("0x") ? key.slice(2) : key;
|
|
51
|
+
return /^[0-9a-fA-F]{64}$/.test(cleanKey);
|
|
52
|
+
}
|
|
53
|
+
function validateAddress(address) {
|
|
54
|
+
try {
|
|
55
|
+
return ethers.isAddress(address);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function generateProxyKeyId(isL2S = false) {
|
|
61
|
+
const randomBytes = ethers.randomBytes(32);
|
|
62
|
+
const hex = ethers.hexlify(randomBytes).slice(2);
|
|
63
|
+
return isL2S ? `priv_secret${hex}` : `priv_${hex}`;
|
|
64
|
+
}
|
|
65
|
+
function hashProxyKey(proxyKeyId) {
|
|
66
|
+
let keyBytes;
|
|
67
|
+
if (proxyKeyId.startsWith("priv_secret")) {
|
|
68
|
+
keyBytes = "0x" + proxyKeyId.slice(11);
|
|
69
|
+
} else if (proxyKeyId.startsWith("priv_")) {
|
|
70
|
+
keyBytes = "0x" + proxyKeyId.slice(5);
|
|
71
|
+
} else {
|
|
72
|
+
keyBytes = proxyKeyId;
|
|
73
|
+
}
|
|
74
|
+
return ethers.keccak256(ethers.solidityPacked(["bytes32"], [keyBytes]));
|
|
75
|
+
}
|
|
76
|
+
function parseProxyKey(proxyKeyId) {
|
|
77
|
+
if (proxyKeyId.startsWith("priv_secret")) {
|
|
78
|
+
return "0x" + proxyKeyId.slice(11);
|
|
79
|
+
} else if (proxyKeyId.startsWith("priv_")) {
|
|
80
|
+
return "0x" + proxyKeyId.slice(5);
|
|
81
|
+
}
|
|
82
|
+
return proxyKeyId;
|
|
83
|
+
}
|
|
84
|
+
async function deriveKey(password, salt) {
|
|
85
|
+
const encoder = new TextEncoder();
|
|
86
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
87
|
+
"raw",
|
|
88
|
+
encoder.encode(password),
|
|
89
|
+
"PBKDF2",
|
|
90
|
+
false,
|
|
91
|
+
["deriveBits", "deriveKey"]
|
|
92
|
+
);
|
|
93
|
+
return crypto.subtle.deriveKey(
|
|
94
|
+
{
|
|
95
|
+
name: "PBKDF2",
|
|
96
|
+
salt: salt.buffer,
|
|
97
|
+
iterations: 1e5,
|
|
98
|
+
hash: "SHA-256"
|
|
99
|
+
},
|
|
100
|
+
passwordKey,
|
|
101
|
+
{ name: "AES-GCM", length: 256 },
|
|
102
|
+
false,
|
|
103
|
+
["encrypt", "decrypt"]
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
async function encryptSeedPhrase(seedPhrase, password) {
|
|
107
|
+
const encoder = new TextEncoder();
|
|
108
|
+
const data = encoder.encode(seedPhrase);
|
|
109
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
110
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
111
|
+
const key = await deriveKey(password, salt);
|
|
112
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
113
|
+
{ name: "AES-GCM", iv },
|
|
114
|
+
key,
|
|
115
|
+
data
|
|
116
|
+
);
|
|
117
|
+
const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
|
118
|
+
combined.set(salt, 0);
|
|
119
|
+
combined.set(iv, salt.length);
|
|
120
|
+
combined.set(new Uint8Array(encrypted), salt.length + iv.length);
|
|
121
|
+
return btoa(String.fromCharCode(...combined));
|
|
122
|
+
}
|
|
123
|
+
async function decryptSeedPhrase(encryptedBlob, password) {
|
|
124
|
+
const combined = Uint8Array.from(atob(encryptedBlob), (c) => c.charCodeAt(0));
|
|
125
|
+
const salt = combined.slice(0, 16);
|
|
126
|
+
const iv = combined.slice(16, 28);
|
|
127
|
+
const ciphertext = combined.slice(28);
|
|
128
|
+
const key = await deriveKey(password, salt);
|
|
129
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
130
|
+
{ name: "AES-GCM", iv },
|
|
131
|
+
key,
|
|
132
|
+
ciphertext
|
|
133
|
+
);
|
|
134
|
+
return new TextDecoder().decode(decrypted);
|
|
135
|
+
}
|
|
136
|
+
function wordToBytes32(word) {
|
|
137
|
+
const bytes = ethers.toUtf8Bytes(word);
|
|
138
|
+
const padded = ethers.zeroPadBytes(bytes, 32);
|
|
139
|
+
return padded;
|
|
140
|
+
}
|
|
141
|
+
function bytes32ToWord(bytes32) {
|
|
142
|
+
try {
|
|
143
|
+
const hex = bytes32.replace(/0+$/, "");
|
|
144
|
+
if (hex.length <= 2) return "";
|
|
145
|
+
return ethers.toUtf8String(hex);
|
|
146
|
+
} catch {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function encodeSeedWords(seedPhrase) {
|
|
151
|
+
const wordList = seedPhrase.trim().split(/\s+/);
|
|
152
|
+
const words = [];
|
|
153
|
+
for (let i = 0; i < 12; i++) {
|
|
154
|
+
if (i < wordList.length) {
|
|
155
|
+
words.push(wordToBytes32(wordList[i]));
|
|
156
|
+
} else {
|
|
157
|
+
words.push(ethers.zeroPadBytes("0x", 32));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { words, count: wordList.length };
|
|
161
|
+
}
|
|
162
|
+
function decodeSeedWords(words, count) {
|
|
163
|
+
const wordList = [];
|
|
164
|
+
for (let i = 0; i < count; i++) {
|
|
165
|
+
const word = bytes32ToWord(words[i]);
|
|
166
|
+
if (word) wordList.push(word);
|
|
167
|
+
}
|
|
168
|
+
return wordList.join(" ");
|
|
169
|
+
}
|
|
170
|
+
function hashOwner(address) {
|
|
171
|
+
return ethers.keccak256(ethers.solidityPacked(["address"], [address]));
|
|
172
|
+
}
|
|
173
|
+
function labelToBytes32(label) {
|
|
174
|
+
const bytes = ethers.toUtf8Bytes(label.slice(0, 31));
|
|
175
|
+
return ethers.zeroPadBytes(bytes, 32);
|
|
176
|
+
}
|
|
177
|
+
function bytes32ToLabel(bytes32) {
|
|
178
|
+
try {
|
|
179
|
+
const hex = bytes32.replace(/0+$/, "");
|
|
180
|
+
if (hex.length <= 2) return "";
|
|
181
|
+
return ethers.toUtf8String(hex);
|
|
182
|
+
} catch {
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function deriveAddress(seedPhrase) {
|
|
187
|
+
try {
|
|
188
|
+
const wallet = ethers.Wallet.fromPhrase(seedPhrase);
|
|
189
|
+
return wallet.address;
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error("Invalid seed phrase");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/client.ts
|
|
196
|
+
var SeedProxyClient = class {
|
|
197
|
+
constructor(config) {
|
|
198
|
+
this.abortController = null;
|
|
199
|
+
if (!config.baseUrl) {
|
|
200
|
+
throw new SeedProxyError(
|
|
201
|
+
"baseUrl is required",
|
|
202
|
+
"INVALID_CONFIG" /* INVALID_CONFIG */
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
this.config = {
|
|
206
|
+
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
207
|
+
// Remove trailing slash
|
|
208
|
+
apiKey: config.apiKey || "",
|
|
209
|
+
defaultStorage: config.defaultStorage || "l2s",
|
|
210
|
+
timeout: config.timeout || 3e4
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// =========================================================================
|
|
214
|
+
// Vault Registration
|
|
215
|
+
// =========================================================================
|
|
216
|
+
/**
|
|
217
|
+
* Register a new seed phrase vault
|
|
218
|
+
*
|
|
219
|
+
* The seed phrase is encrypted client-side before being sent to the server.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const result = await client.registerVault({
|
|
224
|
+
* seedPhrase: 'word1 word2 ... word12',
|
|
225
|
+
* password: 'secure-password',
|
|
226
|
+
* label: 'My Main Wallet',
|
|
227
|
+
* ownerAddress: '0x...',
|
|
228
|
+
* storageType: 'l2s' // or 'sapphire'
|
|
229
|
+
* });
|
|
230
|
+
*
|
|
231
|
+
* console.log('Proxy Key:', result.proxyKeyId);
|
|
232
|
+
* // Save this key securely - it's needed for authentication
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
async registerVault(params) {
|
|
236
|
+
const secret = params.secret || params.seedPhrase || "";
|
|
237
|
+
const isPrivateKey = secret.startsWith("0x") && secret.length === 66;
|
|
238
|
+
const keyType = params.keyType || (isPrivateKey ? "privateKey" : "seed");
|
|
239
|
+
if (keyType === "seed") {
|
|
240
|
+
if (!validateSeedPhrase(secret)) {
|
|
241
|
+
throw new SeedProxyError(
|
|
242
|
+
"Invalid seed phrase. Must be 12 or 24 words.",
|
|
243
|
+
"INVALID_SEED_PHRASE" /* INVALID_SEED_PHRASE */
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
} else if (keyType === "privateKey") {
|
|
247
|
+
if (!validatePrivateKey(secret)) {
|
|
248
|
+
throw new SeedProxyError(
|
|
249
|
+
"Invalid private key. Must be 0x + 64 hex characters.",
|
|
250
|
+
"INVALID_PRIVATE_KEY" /* INVALID_PRIVATE_KEY */
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!params.password || params.password.length < 6) {
|
|
255
|
+
throw new SeedProxyError(
|
|
256
|
+
"Password must be at least 6 characters",
|
|
257
|
+
"INVALID_PASSWORD" /* INVALID_PASSWORD */
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (!validateAddress(params.ownerAddress)) {
|
|
261
|
+
throw new SeedProxyError(
|
|
262
|
+
"Invalid owner address",
|
|
263
|
+
"INVALID_ADDRESS" /* INVALID_ADDRESS */
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const storageType = params.storageType || this.config.defaultStorage;
|
|
267
|
+
try {
|
|
268
|
+
const encryptedSecret = await encryptSeedPhrase(secret, params.password);
|
|
269
|
+
const proxyKeyId = generateProxyKeyId(storageType === "l2s");
|
|
270
|
+
const keyHash = hashProxyKey(proxyKeyId);
|
|
271
|
+
const response = await this.request("/api/sdk/register", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: {
|
|
274
|
+
proxyKeyId,
|
|
275
|
+
keyHash,
|
|
276
|
+
encryptedSeed: encryptedSecret,
|
|
277
|
+
keyType,
|
|
278
|
+
// 'seed' or 'privateKey'
|
|
279
|
+
label: params.label || "Unnamed Vault",
|
|
280
|
+
ownerAddress: params.ownerAddress,
|
|
281
|
+
storageType
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (!response.success) {
|
|
285
|
+
throw new SeedProxyError(
|
|
286
|
+
response.error || "Registration failed",
|
|
287
|
+
"REGISTRATION_FAILED" /* REGISTRATION_FAILED */
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
proxyKeyId,
|
|
293
|
+
vaultIndex: response.data?.vaultIndex,
|
|
294
|
+
txHash: response.data?.txHash
|
|
295
|
+
};
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof SeedProxyError) throw error;
|
|
298
|
+
throw new SeedProxyError(
|
|
299
|
+
"Failed to register vault",
|
|
300
|
+
"REGISTRATION_FAILED" /* REGISTRATION_FAILED */,
|
|
301
|
+
error
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// =========================================================================
|
|
306
|
+
// Authentication (Login with Proxy Key)
|
|
307
|
+
// =========================================================================
|
|
308
|
+
/**
|
|
309
|
+
* Authenticate using a proxy key to recover the seed phrase
|
|
310
|
+
*
|
|
311
|
+
* This also rotates the key for security (one-time use).
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* const result = await client.authenticate({
|
|
316
|
+
* proxyKeyId: 'priv_abc123...',
|
|
317
|
+
* password: 'your-password'
|
|
318
|
+
* });
|
|
319
|
+
*
|
|
320
|
+
* if (result.success) {
|
|
321
|
+
* console.log('Seed:', result.seedPhrase);
|
|
322
|
+
* console.log('New Key:', result.newProxyKeyId);
|
|
323
|
+
* // Store the new key for next login
|
|
324
|
+
* }
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
async authenticate(params) {
|
|
328
|
+
if (!params.proxyKeyId || !params.proxyKeyId.startsWith("priv_")) {
|
|
329
|
+
throw new SeedProxyError(
|
|
330
|
+
"Invalid proxy key ID format",
|
|
331
|
+
"INVALID_KEY" /* INVALID_KEY */
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (!params.password) {
|
|
335
|
+
throw new SeedProxyError(
|
|
336
|
+
"Password is required",
|
|
337
|
+
"INVALID_PASSWORD" /* INVALID_PASSWORD */
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const verification = await this.verifyKey({ proxyKeyId: params.proxyKeyId });
|
|
342
|
+
if (!verification.exists || !verification.valid) {
|
|
343
|
+
throw new SeedProxyError(
|
|
344
|
+
"Key not found or invalid",
|
|
345
|
+
"KEY_NOT_FOUND" /* KEY_NOT_FOUND */
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (!verification.isActive) {
|
|
349
|
+
throw new SeedProxyError(
|
|
350
|
+
"Key has been revoked or already used",
|
|
351
|
+
"KEY_INACTIVE" /* KEY_INACTIVE */
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const response = await this.request("/api/sdk/getSeed", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: {
|
|
357
|
+
proxyKeyId: params.proxyKeyId,
|
|
358
|
+
keyHash: hashProxyKey(params.proxyKeyId)
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
if (!response.success || !response.data?.encryptedSeed) {
|
|
362
|
+
throw new SeedProxyError(
|
|
363
|
+
response.error || "Failed to retrieve seed",
|
|
364
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
let seedPhrase;
|
|
368
|
+
try {
|
|
369
|
+
seedPhrase = await decryptSeedPhrase(response.data.encryptedSeed, params.password);
|
|
370
|
+
} catch {
|
|
371
|
+
throw new SeedProxyError(
|
|
372
|
+
"Incorrect password",
|
|
373
|
+
"INVALID_PASSWORD" /* INVALID_PASSWORD */
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const rotation = await this.rotateKey({
|
|
377
|
+
proxyKeyId: params.proxyKeyId,
|
|
378
|
+
password: params.password
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
success: true,
|
|
382
|
+
seedPhrase,
|
|
383
|
+
newProxyKeyId: rotation.newProxyKeyId
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof SeedProxyError) throw error;
|
|
387
|
+
throw new SeedProxyError(
|
|
388
|
+
"Authentication failed",
|
|
389
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */,
|
|
390
|
+
error
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// =========================================================================
|
|
395
|
+
// Key Verification
|
|
396
|
+
// =========================================================================
|
|
397
|
+
/**
|
|
398
|
+
* Verify if a proxy key exists and is active
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* const result = await client.verifyKey({ proxyKeyId: 'priv_abc123...' });
|
|
403
|
+
*
|
|
404
|
+
* if (result.valid && result.isActive) {
|
|
405
|
+
* console.log('Key is valid and can be used');
|
|
406
|
+
* }
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
async verifyKey(params) {
|
|
410
|
+
try {
|
|
411
|
+
const response = await this.request("/api/keybridge", {
|
|
412
|
+
method: "GET",
|
|
413
|
+
query: { proxyId: params.proxyKeyId }
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
exists: response.data?.exists || false,
|
|
417
|
+
valid: response.data?.valid || response.data?.exists || false,
|
|
418
|
+
isActive: response.data?.isActive || false,
|
|
419
|
+
vaultIndex: response.data?.vaultIndex,
|
|
420
|
+
type: response.data?.type
|
|
421
|
+
};
|
|
422
|
+
} catch {
|
|
423
|
+
return {
|
|
424
|
+
exists: false,
|
|
425
|
+
valid: false,
|
|
426
|
+
isActive: false
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// =========================================================================
|
|
431
|
+
// Key Rotation
|
|
432
|
+
// =========================================================================
|
|
433
|
+
/**
|
|
434
|
+
* Rotate a proxy key (invalidates old key, generates new one)
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```typescript
|
|
438
|
+
* const result = await client.rotateKey({
|
|
439
|
+
* proxyKeyId: 'priv_old...',
|
|
440
|
+
* password: 'your-password'
|
|
441
|
+
* });
|
|
442
|
+
*
|
|
443
|
+
* console.log('New key:', result.newProxyKeyId);
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
async rotateKey(params) {
|
|
447
|
+
const isL2S = params.proxyKeyId.startsWith("priv_secret");
|
|
448
|
+
const newProxyKeyId = generateProxyKeyId(isL2S);
|
|
449
|
+
const oldKeyHash = hashProxyKey(params.proxyKeyId);
|
|
450
|
+
const newKeyHash = hashProxyKey(newProxyKeyId);
|
|
451
|
+
try {
|
|
452
|
+
const response = await this.request("/api/sdk/rotate", {
|
|
453
|
+
method: "POST",
|
|
454
|
+
body: {
|
|
455
|
+
oldProxyKeyId: params.proxyKeyId,
|
|
456
|
+
oldKeyHash,
|
|
457
|
+
newKeyHash,
|
|
458
|
+
isL2S
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
if (!response.success) {
|
|
462
|
+
throw new SeedProxyError(
|
|
463
|
+
response.error || "Rotation failed",
|
|
464
|
+
"ROTATION_FAILED" /* ROTATION_FAILED */
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
newProxyKeyId
|
|
470
|
+
};
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (error instanceof SeedProxyError) throw error;
|
|
473
|
+
throw new SeedProxyError(
|
|
474
|
+
"Key rotation failed",
|
|
475
|
+
"ROTATION_FAILED" /* ROTATION_FAILED */,
|
|
476
|
+
error
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// =========================================================================
|
|
481
|
+
// Key Revocation
|
|
482
|
+
// =========================================================================
|
|
483
|
+
/**
|
|
484
|
+
* Permanently revoke a proxy key
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* const result = await client.revokeKey({
|
|
489
|
+
* proxyKeyId: 'priv_abc123...',
|
|
490
|
+
* password: 'your-password'
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
async revokeKey(params) {
|
|
495
|
+
try {
|
|
496
|
+
const response = await this.request("/api/sdk/revoke", {
|
|
497
|
+
method: "POST",
|
|
498
|
+
body: {
|
|
499
|
+
proxyKeyId: params.proxyKeyId,
|
|
500
|
+
keyHash: hashProxyKey(params.proxyKeyId)
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
success: response.success,
|
|
505
|
+
txHash: response.data?.txHash,
|
|
506
|
+
error: response.error
|
|
507
|
+
};
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error instanceof SeedProxyError) throw error;
|
|
510
|
+
throw new SeedProxyError(
|
|
511
|
+
"Revocation failed",
|
|
512
|
+
"REVOCATION_FAILED" /* REVOCATION_FAILED */,
|
|
513
|
+
error
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// =========================================================================
|
|
518
|
+
// List Vaults
|
|
519
|
+
// =========================================================================
|
|
520
|
+
/**
|
|
521
|
+
* List all vaults for an owner address
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* ```typescript
|
|
525
|
+
* const vaults = await client.listVaults('0x...');
|
|
526
|
+
*
|
|
527
|
+
* for (const vault of vaults) {
|
|
528
|
+
* console.log(vault.label, vault.type, vault.isActive);
|
|
529
|
+
* }
|
|
530
|
+
* ```
|
|
531
|
+
*/
|
|
532
|
+
async listVaults(ownerAddress) {
|
|
533
|
+
if (!validateAddress(ownerAddress)) {
|
|
534
|
+
throw new SeedProxyError(
|
|
535
|
+
"Invalid owner address",
|
|
536
|
+
"INVALID_ADDRESS" /* INVALID_ADDRESS */
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const response = await this.request("/api/sdk/vaults", {
|
|
541
|
+
method: "GET",
|
|
542
|
+
query: { owner: ownerAddress }
|
|
543
|
+
});
|
|
544
|
+
return response.data?.vaults || [];
|
|
545
|
+
} catch {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// =========================================================================
|
|
550
|
+
// Message Signing (Advanced)
|
|
551
|
+
// =========================================================================
|
|
552
|
+
/**
|
|
553
|
+
* Sign a message using the seed stored in a vault
|
|
554
|
+
*
|
|
555
|
+
* This decrypts the seed temporarily to sign, then clears it from memory.
|
|
556
|
+
*
|
|
557
|
+
* @example
|
|
558
|
+
* ```typescript
|
|
559
|
+
* const result = await client.signMessage({
|
|
560
|
+
* proxyKeyId: 'priv_abc123...',
|
|
561
|
+
* password: 'your-password',
|
|
562
|
+
* message: 'Hello, World!'
|
|
563
|
+
* });
|
|
564
|
+
*
|
|
565
|
+
* console.log('Signature:', result.signature);
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
async signMessage(params) {
|
|
569
|
+
try {
|
|
570
|
+
const authResult = await this.authenticate({
|
|
571
|
+
proxyKeyId: params.proxyKeyId,
|
|
572
|
+
password: params.password
|
|
573
|
+
});
|
|
574
|
+
if (!authResult.success || !authResult.seedPhrase) {
|
|
575
|
+
throw new SeedProxyError(
|
|
576
|
+
"Authentication failed",
|
|
577
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
const wallet = ethers2.Wallet.fromPhrase(authResult.seedPhrase);
|
|
581
|
+
const signature = await wallet.signMessage(params.message);
|
|
582
|
+
const sig = ethers2.Signature.from(signature);
|
|
583
|
+
return {
|
|
584
|
+
success: true,
|
|
585
|
+
signature,
|
|
586
|
+
v: sig.v,
|
|
587
|
+
r: sig.r,
|
|
588
|
+
s: sig.s,
|
|
589
|
+
newProxyKeyId: authResult.newProxyKeyId,
|
|
590
|
+
signerAddress: wallet.address
|
|
591
|
+
};
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (error instanceof SeedProxyError) throw error;
|
|
594
|
+
throw new SeedProxyError(
|
|
595
|
+
"Signing failed",
|
|
596
|
+
"SIGNING_FAILED" /* SIGNING_FAILED */,
|
|
597
|
+
error
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// =========================================================================
|
|
602
|
+
// Transaction Signing (Advanced)
|
|
603
|
+
// =========================================================================
|
|
604
|
+
/**
|
|
605
|
+
* Sign a transaction using the seed stored in a vault
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* const result = await client.signTransaction({
|
|
610
|
+
* proxyKeyId: 'priv_abc123...',
|
|
611
|
+
* password: 'your-password',
|
|
612
|
+
* transaction: {
|
|
613
|
+
* to: '0x...',
|
|
614
|
+
* value: '1000000000000000000', // 1 ETH in wei
|
|
615
|
+
* chainId: 1
|
|
616
|
+
* }
|
|
617
|
+
* });
|
|
618
|
+
*
|
|
619
|
+
* // Broadcast the signed transaction
|
|
620
|
+
* const txHash = await provider.sendTransaction(result.signedTransaction);
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
async signTransaction(params) {
|
|
624
|
+
try {
|
|
625
|
+
const authResult = await this.authenticate({
|
|
626
|
+
proxyKeyId: params.proxyKeyId,
|
|
627
|
+
password: params.password
|
|
628
|
+
});
|
|
629
|
+
if (!authResult.success || !authResult.seedPhrase) {
|
|
630
|
+
throw new SeedProxyError(
|
|
631
|
+
"Authentication failed",
|
|
632
|
+
"AUTHENTICATION_FAILED" /* AUTHENTICATION_FAILED */
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const wallet = ethers2.Wallet.fromPhrase(authResult.seedPhrase);
|
|
636
|
+
const tx = {
|
|
637
|
+
to: params.transaction.to,
|
|
638
|
+
value: params.transaction.value ? BigInt(params.transaction.value) : void 0,
|
|
639
|
+
data: params.transaction.data,
|
|
640
|
+
nonce: params.transaction.nonce,
|
|
641
|
+
gasLimit: params.transaction.gasLimit ? BigInt(params.transaction.gasLimit) : void 0,
|
|
642
|
+
gasPrice: params.transaction.gasPrice ? BigInt(params.transaction.gasPrice) : void 0,
|
|
643
|
+
maxFeePerGas: params.transaction.maxFeePerGas ? BigInt(params.transaction.maxFeePerGas) : void 0,
|
|
644
|
+
maxPriorityFeePerGas: params.transaction.maxPriorityFeePerGas ? BigInt(params.transaction.maxPriorityFeePerGas) : void 0,
|
|
645
|
+
chainId: params.transaction.chainId
|
|
646
|
+
};
|
|
647
|
+
const signedTx = await wallet.signTransaction(tx);
|
|
648
|
+
return {
|
|
649
|
+
success: true,
|
|
650
|
+
signedTransaction: signedTx
|
|
651
|
+
};
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (error instanceof SeedProxyError) throw error;
|
|
654
|
+
throw new SeedProxyError(
|
|
655
|
+
"Transaction signing failed",
|
|
656
|
+
"SIGNING_FAILED" /* SIGNING_FAILED */,
|
|
657
|
+
error
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// =========================================================================
|
|
662
|
+
// Utility Methods
|
|
663
|
+
// =========================================================================
|
|
664
|
+
/**
|
|
665
|
+
* Get the configured base URL
|
|
666
|
+
*/
|
|
667
|
+
getBaseUrl() {
|
|
668
|
+
return this.config.baseUrl;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Get the default storage type
|
|
672
|
+
*/
|
|
673
|
+
getDefaultStorage() {
|
|
674
|
+
return this.config.defaultStorage;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Cancel any pending requests
|
|
678
|
+
*/
|
|
679
|
+
cancelPendingRequests() {
|
|
680
|
+
if (this.abortController) {
|
|
681
|
+
this.abortController.abort();
|
|
682
|
+
this.abortController = null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// =========================================================================
|
|
686
|
+
// Private Methods
|
|
687
|
+
// =========================================================================
|
|
688
|
+
async request(endpoint, options) {
|
|
689
|
+
this.abortController = new AbortController();
|
|
690
|
+
let url = `${this.config.baseUrl}${endpoint}`;
|
|
691
|
+
if (options.query) {
|
|
692
|
+
const params = new URLSearchParams(options.query);
|
|
693
|
+
url += `?${params.toString()}`;
|
|
694
|
+
}
|
|
695
|
+
const headers = {
|
|
696
|
+
"Content-Type": "application/json"
|
|
697
|
+
};
|
|
698
|
+
if (this.config.apiKey) {
|
|
699
|
+
headers["X-API-Key"] = this.config.apiKey;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const response = await fetch(url, {
|
|
703
|
+
method: options.method,
|
|
704
|
+
headers,
|
|
705
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
706
|
+
signal: this.abortController.signal
|
|
707
|
+
});
|
|
708
|
+
const data = await response.json();
|
|
709
|
+
if (!response.ok) {
|
|
710
|
+
throw new SeedProxyError(
|
|
711
|
+
data.error || `HTTP ${response.status}`,
|
|
712
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return data;
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (error instanceof SeedProxyError) throw error;
|
|
718
|
+
if (error.name === "AbortError") {
|
|
719
|
+
throw new SeedProxyError(
|
|
720
|
+
"Request cancelled",
|
|
721
|
+
"TIMEOUT" /* TIMEOUT */
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
throw new SeedProxyError(
|
|
725
|
+
"Network error",
|
|
726
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
727
|
+
error
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
function createSeedProxyClient(config) {
|
|
733
|
+
return new SeedProxyClient(config);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/index.ts
|
|
737
|
+
var VERSION = "2.0.0";
|
|
738
|
+
export {
|
|
739
|
+
CONTRACT_ADDRESS,
|
|
740
|
+
KEY_TYPE_PRIVATE_KEY,
|
|
741
|
+
KEY_TYPE_SEED,
|
|
742
|
+
SAPPHIRE_MAINNET_CHAIN_ID,
|
|
743
|
+
SeedProxyClient,
|
|
744
|
+
SeedProxyError,
|
|
745
|
+
SeedProxyErrorCode,
|
|
746
|
+
VERSION,
|
|
747
|
+
bytes32ToLabel,
|
|
748
|
+
createSeedProxyClient,
|
|
749
|
+
decodeSeedWords,
|
|
750
|
+
decryptSeedPhrase,
|
|
751
|
+
createSeedProxyClient as default,
|
|
752
|
+
deriveAddress,
|
|
753
|
+
encodeSeedWords,
|
|
754
|
+
encryptSeedPhrase,
|
|
755
|
+
generateProxyKeyId,
|
|
756
|
+
hashOwner,
|
|
757
|
+
hashProxyKey,
|
|
758
|
+
labelToBytes32,
|
|
759
|
+
parseProxyKey,
|
|
760
|
+
validateAddress,
|
|
761
|
+
validatePrivateKey,
|
|
762
|
+
validateSeedPhrase
|
|
763
|
+
};
|
|
764
|
+
/**
|
|
765
|
+
* Pribado Seed Proxy SDK
|
|
766
|
+
*
|
|
767
|
+
* Main Client Class - HTTP-based integration for secure seed management
|
|
768
|
+
*
|
|
769
|
+
* @author Ralph Lawrence Pecayo
|
|
770
|
+
* @license MIT
|
|
771
|
+
*/
|
|
772
|
+
/**
|
|
773
|
+
* Pribado Seed Proxy SDK
|
|
774
|
+
*
|
|
775
|
+
* Secure encrypted endpoints for Web3 wallets
|
|
776
|
+
*
|
|
777
|
+
* @packageDocumentation
|
|
778
|
+
* @author Ralph Lawrence Pecayo
|
|
779
|
+
* @license MIT
|
|
780
|
+
*/
|