@talismn/keyring 0.0.0-pr2277-20251211071316 → 0.0.0-pr2295-20260110031023

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.
@@ -1,451 +0,0 @@
1
- 'use strict';
2
-
3
- var crypto$1 = require('@talismn/crypto');
4
-
5
- const isAccountOfType = (account, type) => {
6
- return account?.type === type;
7
- };
8
- const isAccountInTypes = (account, types) => {
9
- return !!account && types.includes(account.type);
10
- };
11
- const ACCOUNT_TYPES_OWNED = ["keypair", "ledger-ethereum", "ledger-polkadot", "ledger-solana", "polkadot-vault"];
12
- const ACCOUNT_TYPES_EXTERNAL = ["contact", "watch-only", "ledger-ethereum", "ledger-polkadot", "ledger-solana", "polkadot-vault", "signet"];
13
- const isAccountExternal = account => {
14
- return isAccountInTypes(account, ACCOUNT_TYPES_EXTERNAL);
15
- };
16
- const isAccountOwned = account => {
17
- return isAccountInTypes(account, ACCOUNT_TYPES_OWNED);
18
- };
19
- const isAccountPortfolio = account => {
20
- return isAccountOwned(account) || isAccountOfType(account, "watch-only") && account.isPortfolio;
21
- };
22
- const isAccountNotContact = acc => acc.type !== "contact";
23
- const isAccountAddressEthereum = account => {
24
- return !!account && crypto$1.isEthereumAddress(account.address);
25
- };
26
- const isAccountPlatformEthereum = account => {
27
- return !!account && account.type !== "ledger-polkadot" && crypto$1.isEthereumAddress(account.address);
28
- };
29
- const isAccountPlatformSolana = account => {
30
- return !!account && crypto$1.isSolanaAddress(account.address);
31
- };
32
- const isAccountPlatformPolkadot = account => {
33
- return !!account && account.type !== "ledger-ethereum" && (isAccountAddressEthereum(account) || isAccountAddressSs58(account));
34
- };
35
- const isAccountAddressSs58 = account => {
36
- return !!account && crypto$1.detectAddressEncoding(account.address) === "ss58";
37
- };
38
- const isAccountLedgerPolkadot = account => {
39
- return isAccountOfType(account, "ledger-polkadot");
40
- };
41
- const isAccountLedgerPolkadotGeneric = account => {
42
- return isAccountOfType(account, "ledger-polkadot") && !account.genesisHash;
43
- };
44
- const isAccountLedgerPolkadotLegacy = account => {
45
- return isAccountOfType(account, "ledger-polkadot") && !!account.genesisHash;
46
- };
47
- const isAccountBitcoin = account => {
48
- return !!account && crypto$1.isBitcoinAddress(account.address);
49
- };
50
- const getAccountGenesisHash = account => {
51
- if (!account) return undefined;
52
- return "genesisHash" in account ? account.genesisHash || undefined : undefined;
53
- };
54
- const getAccountSignetUrl = account => {
55
- return isAccountOfType(account, "signet") ? account.url : undefined;
56
- };
57
- const getAccountPlatform = account => {
58
- if (!account) return undefined;
59
- return "curve" in account ? crypto$1.getAccountPlatformFromCurve(account.curve) : crypto$1.getAccountPlatformFromAddress(account.address);
60
- };
61
-
62
- // Derive a key generated with PBKDF2 that will be used for AES-GCM encryption
63
- const deriveKey = async (password, salt) =>
64
- // Deriving 32-byte key using PBKDF2 with 100,000 iterations and SHA-256
65
- await crypto.subtle.importKey("raw", await crypto$1.pbkdf2("SHA-256", new TextEncoder().encode(password), salt, 100_000,
66
- // 100,000 iterations
67
- 32 // 32 bytes (32 * 8 == 256 bits)
68
- ), {
69
- name: "AES-GCM",
70
- length: 256
71
- }, false, ["encrypt", "decrypt"]);
72
- const encryptData = async (data, password) => {
73
- try {
74
- const salt = crypto.getRandomValues(new Uint8Array(16)); // 16 bytes of salt
75
- const iv = crypto.getRandomValues(new Uint8Array(12)); // 12-byte IV for AES-GCM
76
- const key = await deriveKey(password, salt);
77
-
78
- // encrypt
79
- const encryptedSeed = await crypto.subtle.encrypt({
80
- name: "AES-GCM",
81
- iv
82
- }, key, data);
83
-
84
- // Combine salt, IV, and encrypted seed
85
- const combined = new Uint8Array(salt.length + iv.length + encryptedSeed.byteLength);
86
- combined.set(salt, 0);
87
- combined.set(iv, salt.length);
88
- combined.set(new Uint8Array(encryptedSeed), salt.length + iv.length);
89
-
90
- // Base64 encode the combined data
91
- return btoa(String.fromCharCode(...combined));
92
- } catch (cause) {
93
- throw new Error("Failed to encrypt data", {
94
- cause
95
- });
96
- }
97
- };
98
- const decryptData = async (encryptedData, password) => {
99
- try {
100
- // Decode Base64 and parse the combined data
101
- const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
102
-
103
- // Extract salt, IV, and encrypted seed
104
- const salt = combined.slice(0, 16); // First 16 bytes
105
- const iv = combined.slice(16, 28); // Next 12 bytes
106
- const encryptedSeed = combined.slice(28); // Remaining bytes
107
-
108
- const key = await deriveKey(password, salt);
109
-
110
- // Decrypt the seed
111
- const decryptedSeed = await crypto.subtle.decrypt({
112
- name: "AES-GCM",
113
- iv
114
- }, key, encryptedSeed);
115
- return new Uint8Array(decryptedSeed);
116
- } catch (cause) {
117
- throw new Error("Failed to decrypt data", {
118
- cause
119
- });
120
- }
121
- };
122
- const changeEncryptedDataPassword = async (encryptedData, oldPassword, newPassword) => {
123
- try {
124
- const decrypted = await decryptData(encryptedData, oldPassword);
125
- return await encryptData(decrypted, newPassword);
126
- } catch (cause) {
127
- throw new Error("Failed to change password on encrypted data", {
128
- cause
129
- });
130
- }
131
- };
132
-
133
- // we dont want to reference @talismn/util in the keyring, so we copy this here
134
-
135
- const REGEX_HEX_STRING = /^0x[0-9a-fA-F]*$/;
136
- const isHexString = value => {
137
- return typeof value === "string" && REGEX_HEX_STRING.test(value);
138
- };
139
-
140
- class Keyring {
141
- #data;
142
- constructor(data) {
143
- this.#data = structuredClone(data);
144
- }
145
- static create() {
146
- return new Keyring({
147
- passwordCheck: null,
148
- // well-known data encrypted using the password, used to ensure all secrets of the keyring are encrypted with the same password
149
- mnemonics: [],
150
- accounts: []
151
- });
152
- }
153
- static load(data) {
154
- if (!data.accounts || !data.mnemonics) throw new Error("Invalid data");
155
-
156
- // automatic upgrade : set default values for newly introduced properties
157
- for (const account of data.accounts) {
158
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
159
- // @ts-expect-error
160
- if (account.type === "ledger-polkadot" && !account.curve) account.curve = "ed25519";
161
- }
162
- return new Keyring(data);
163
- }
164
- async checkPassword(password, reset = false) {
165
- if (typeof password !== "string" || !password) throw new Error("password is required");
166
- const passwordHash = oneWayHash(password);
167
- const PASSWORD_CHECK_PHRASE = "PASSWORD_CHECK_PHRASE";
168
-
169
- // run through same complexity as for other secrets, to make it so it s not easier to brute force passwordCheck than other secrets
170
- if (!this.#data.passwordCheck || reset) {
171
- const bytes = crypto$1.utf8.decode(PASSWORD_CHECK_PHRASE);
172
- this.#data.passwordCheck = await encryptData(bytes, passwordHash);
173
- } else {
174
- try {
175
- const bytes = await decryptData(this.#data.passwordCheck, passwordHash);
176
- const text = crypto$1.utf8.encode(bytes);
177
- if (text !== PASSWORD_CHECK_PHRASE) throw new Error("Invalid password");
178
- } catch {
179
- throw new Error("Invalid password");
180
- }
181
- }
182
- }
183
- toJson() {
184
- return structuredClone(this.#data);
185
- }
186
- async export(password, jsonPassword) {
187
- const keyring = new Keyring(structuredClone(this.#data));
188
- for (const mnemonic of keyring.#data.mnemonics) mnemonic.entropy = await changeEncryptedDataPassword(mnemonic.entropy, password, jsonPassword);
189
- for (const account of keyring.#data.accounts) if (account.type === "keypair") account.secretKey = await changeEncryptedDataPassword(account.secretKey, password, jsonPassword);
190
-
191
- // reset password check
192
- await keyring.checkPassword(jsonPassword, true);
193
- return keyring.toJson();
194
- }
195
- getMnemonics() {
196
- return this.#data.mnemonics.map(mnemonicFromStorage);
197
- }
198
- async addMnemonic({
199
- name,
200
- mnemonic,
201
- confirmed
202
- }, password) {
203
- if (typeof name !== "string" || !name) throw new Error("name is required");
204
- if (typeof mnemonic !== "string") throw new Error("mnemonic is required");
205
- if (typeof confirmed !== "boolean") throw new Error("confirmed is required");
206
- if (!crypto$1.isValidMnemonic(mnemonic)) throw new Error("Invalid mnemonic");
207
- await this.checkPassword(password);
208
- const entropy = crypto$1.mnemonicToEntropy(mnemonic);
209
-
210
- // id is a hash of the entropy, helps us prevent having duplicates and allows automatic remapping of accounts/mnemonics if mnemonics are deleted then re-added
211
- const id = oneWayHash(entropy);
212
- if (this.#data.mnemonics.find(s => s.id === id)) throw new Error("Mnemonic already exists");
213
- const storage = {
214
- id,
215
- name,
216
- entropy: await encryptData(entropy, password),
217
- confirmed,
218
- createdAt: Date.now()
219
- };
220
- this.#data.mnemonics.push(storage);
221
- return mnemonicFromStorage(storage);
222
- }
223
- getMnemonic(id) {
224
- const mnemonic = this.#data.mnemonics.find(s => s.id === id);
225
- return mnemonic ? mnemonicFromStorage(mnemonic) : null;
226
- }
227
- updateMnemonic(id, {
228
- name,
229
- confirmed
230
- }) {
231
- const mnemonic = this.#data.mnemonics.find(s => s.id === id);
232
- if (!mnemonic) throw new Error("Mnemonic not found");
233
- if (name !== undefined) {
234
- if (typeof name !== "string" || !name) throw new Error("name must be a string");
235
- mnemonic.name = name;
236
- }
237
- if (confirmed !== undefined) {
238
- if (typeof confirmed !== "boolean") throw new Error("confirmed must be a boolean");
239
- mnemonic.confirmed = confirmed;
240
- }
241
- return mnemonicFromStorage(mnemonic);
242
- }
243
- removeMnemonic(id) {
244
- const index = this.#data.mnemonics.findIndex(mnemonic => mnemonic.id == id);
245
- if (index === -1) throw new Error("Mnemonic not found");
246
- this.#data.mnemonics.splice(index, 1);
247
- }
248
- async getMnemonicText(id, password) {
249
- const mnemonic = this.#data.mnemonics.find(s => s.id === id);
250
- if (!mnemonic) throw new Error("Mnemonic not found");
251
- const entropy = await decryptData(mnemonic.entropy, password);
252
- return crypto$1.entropyToMnemonic(entropy);
253
- }
254
- getExistingMnemonicId(mnemonic) {
255
- const entropy = crypto$1.mnemonicToEntropy(mnemonic);
256
- const mnemonicId = oneWayHash(entropy);
257
- return this.#data.mnemonics.some(s => s.id === mnemonicId) ? mnemonicId : null;
258
- }
259
- getAccounts() {
260
- return this.#data.accounts.map(accountFromStorage);
261
- }
262
- getAccount(address) {
263
- const account = this.#data.accounts.find(s => crypto$1.isAddressEqual(s.address, address));
264
- return account ? accountFromStorage(account) : null;
265
- }
266
- updateAccount(address, {
267
- name,
268
- isPortfolio,
269
- genesisHash
270
- }) {
271
- const account = this.#data.accounts.find(s => s.address === address);
272
- if (!account) throw new Error("Account not found");
273
- if (name) {
274
- if (typeof name !== "string" || !name) throw new Error("name is required");
275
- account.name = name;
276
- }
277
- if (account.type === "watch-only" && isPortfolio !== undefined) {
278
- if (typeof isPortfolio !== "boolean") throw new Error("isPortfolio must be a boolean");
279
- account.isPortfolio = isPortfolio;
280
- }
281
- // allow updating genesisHash only for contacts
282
- if (account.type === "contact") {
283
- if (genesisHash) {
284
- if (!isHexString(genesisHash)) throw new Error("genesisHash must be a hex string");
285
- account.genesisHash = genesisHash;
286
- } else delete account.genesisHash;
287
- }
288
- return accountFromStorage(account);
289
- }
290
- removeAccount(address) {
291
- const index = this.#data.accounts.findIndex(s => crypto$1.isAddressEqual(s.address, address));
292
- if (index === -1) throw new Error("Account not found");
293
- this.#data.accounts.splice(index, 1);
294
- }
295
- addAccountExternal(options) {
296
- const address = crypto$1.normalizeAddress(options.address); // breaks if invalid address
297
-
298
- if (this.getAccount(address)) throw new Error("Account already exists");
299
- const account = {
300
- ...options,
301
- address,
302
- createdAt: Date.now()
303
- };
304
- if (!isAccountExternal(account)) throw new Error("Invalid account type");
305
- this.#data.accounts.push(account);
306
- return accountFromStorage(account);
307
- }
308
-
309
- /**
310
- * Needs to be called before deriving an account from a mnemonic.
311
- *
312
- * This will ensure that it is present (or add it if possible) in the keyring before actually creating the account.
313
- *
314
- * @param options
315
- * @param password
316
- * @returns the id of the mnemonic
317
- */
318
- async ensureMnemonic(options, password) {
319
- await this.checkPassword(password);
320
- switch (options.type) {
321
- case "new-mnemonic":
322
- {
323
- const {
324
- mnemonic,
325
- mnemonicName: name,
326
- confirmed
327
- } = options;
328
- if (typeof name !== "string" || !name) throw new Error("mnemonicName is required");
329
- if (typeof confirmed !== "boolean") throw new Error("confirmed is required");
330
- const mnemonicId = this.getExistingMnemonicId(mnemonic);
331
- if (mnemonicId) return mnemonicId;
332
- const {
333
- id
334
- } = await this.addMnemonic({
335
- name,
336
- mnemonic,
337
- confirmed
338
- }, password);
339
- return id;
340
- }
341
- case "existing-mnemonic":
342
- {
343
- if (typeof options.mnemonicId !== "string" || !options.mnemonicId) throw new Error("mnemonicId must be a string");
344
- return options.mnemonicId;
345
- }
346
- }
347
- }
348
- async addAccountDerive(options, password) {
349
- await this.checkPassword(password);
350
- const {
351
- curve,
352
- derivationPath,
353
- name
354
- } = options;
355
- const mnemonicId = await this.ensureMnemonic(options, password);
356
- const mnemonic = this.#data.mnemonics.find(s => s.id === mnemonicId);
357
- if (!mnemonic) throw new Error("Mnemonic not found");
358
- const entropy = await decryptData(mnemonic.entropy, password);
359
- const seed = await crypto$1.entropyToSeed(entropy, curve);
360
- const pair = crypto$1.deriveKeypair(seed, derivationPath, curve);
361
- if (this.getAccount(pair.address)) throw new Error("Account already exists");
362
- const account = {
363
- type: "keypair",
364
- curve,
365
- name,
366
- address: crypto$1.normalizeAddress(pair.address),
367
- secretKey: await encryptData(pair.secretKey, password),
368
- mnemonicId,
369
- derivationPath,
370
- createdAt: Date.now()
371
- };
372
- this.#data.accounts.push(account);
373
- return accountFromStorage(account);
374
- }
375
- async addAccountKeypair({
376
- curve,
377
- name,
378
- secretKey
379
- }, password) {
380
- await this.checkPassword(password);
381
- const publicKey = crypto$1.getPublicKeyFromSecret(secretKey, curve);
382
- const encoding = crypto$1.addressEncodingFromCurve(curve);
383
- const address = crypto$1.addressFromPublicKey(publicKey, encoding);
384
- if (this.getAccount(address)) throw new Error("Account already exists");
385
- const account = {
386
- type: "keypair",
387
- curve,
388
- name,
389
- address: crypto$1.normalizeAddress(address),
390
- secretKey: await encryptData(secretKey, password),
391
- createdAt: Date.now()
392
- };
393
- this.#data.accounts.push(account);
394
- return accountFromStorage(account);
395
- }
396
- getAccountSecretKey(address, password) {
397
- if (typeof address !== "string" || !address) throw new Error("address is required");
398
- if (typeof password !== "string" || !password) throw new Error("password is required");
399
- const account = this.#data.accounts.find(a => a.address === crypto$1.normalizeAddress(address));
400
- if (!account) throw new Error("Account not found");
401
- if (account.type !== "keypair") throw new Error("Secret key unavailable");
402
- return decryptData(account.secretKey, password);
403
- }
404
- async getDerivedAddress(mnemonicId, derivationPath, curve, password) {
405
- if (typeof mnemonicId !== "string" || !mnemonicId) throw new Error("mnemonicId is required");
406
- if (typeof password !== "string" || !password) throw new Error("password is required");
407
- const mnemonic = this.#data.mnemonics.find(s => s.id === mnemonicId);
408
- if (!mnemonic) throw new Error("Mnemonic not found");
409
- const entropy = await decryptData(mnemonic.entropy, password);
410
- const seed = await crypto$1.entropyToSeed(entropy, curve);
411
- const pair = crypto$1.deriveKeypair(seed, derivationPath, curve);
412
- return pair.address;
413
- }
414
- }
415
- const oneWayHash = bytes => {
416
- if (typeof bytes === "string") bytes = crypto$1.utf8.decode(bytes);
417
-
418
- // cryptographically secure one way hash
419
- // outputs 44 characters without special characters
420
- return crypto$1.base58.encode(crypto$1.blake3(bytes));
421
- };
422
- const mnemonicFromStorage = data => {
423
- const copy = structuredClone(data);
424
- if ("entropy" in copy) delete copy.entropy;
425
- return Object.freeze(copy);
426
- };
427
- const accountFromStorage = data => {
428
- const copy = structuredClone(data);
429
- if ("secretKey" in copy) delete copy.secretKey;
430
- return Object.freeze(copy);
431
- };
432
-
433
- exports.Keyring = Keyring;
434
- exports.getAccountGenesisHash = getAccountGenesisHash;
435
- exports.getAccountPlatform = getAccountPlatform;
436
- exports.getAccountSignetUrl = getAccountSignetUrl;
437
- exports.isAccountAddressEthereum = isAccountAddressEthereum;
438
- exports.isAccountAddressSs58 = isAccountAddressSs58;
439
- exports.isAccountBitcoin = isAccountBitcoin;
440
- exports.isAccountExternal = isAccountExternal;
441
- exports.isAccountInTypes = isAccountInTypes;
442
- exports.isAccountLedgerPolkadot = isAccountLedgerPolkadot;
443
- exports.isAccountLedgerPolkadotGeneric = isAccountLedgerPolkadotGeneric;
444
- exports.isAccountLedgerPolkadotLegacy = isAccountLedgerPolkadotLegacy;
445
- exports.isAccountNotContact = isAccountNotContact;
446
- exports.isAccountOfType = isAccountOfType;
447
- exports.isAccountOwned = isAccountOwned;
448
- exports.isAccountPlatformEthereum = isAccountPlatformEthereum;
449
- exports.isAccountPlatformPolkadot = isAccountPlatformPolkadot;
450
- exports.isAccountPlatformSolana = isAccountPlatformSolana;
451
- exports.isAccountPortfolio = isAccountPortfolio;