@metamask-previews/keyring-controller 28.2.0-preview-4a8d4a2 → 28.2.0-preview-5669a8ae0
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/CHANGELOG.md +0 -7
- package/dist/KeyringController-method-action-types.d.ts +0 -6
- package/dist/KeyringController-method-action-types.d.ts.map +1 -1
- package/dist/KeyringController-method-action-types.js.map +1 -1
- package/dist/KeyringController.d.ts +0 -22
- package/dist/KeyringController.d.ts.map +1 -1
- package/dist/KeyringController.js +198 -291
- package/dist/KeyringController.js.map +1 -1
- package/package.json +1 -1
- package/dist/VaultTransaction.d.ts +0 -247
- package/dist/VaultTransaction.d.ts.map +0 -1
- package/dist/VaultTransaction.js +0 -483
- package/dist/VaultTransaction.js.map +0 -1
|
@@ -8,11 +8,11 @@ import { SimpleKeyring as SimpleKeyringV2 } from '@metamask/eth-simple-keyring/v
|
|
|
8
8
|
import { add0x, assertIsStrictHexString, bytesToHex, hasProperty, hexToBytes, isObject, isStrictHexString, isValidHexAddress, isValidJson, remove0x, } from '@metamask/utils';
|
|
9
9
|
import { Mutex } from 'async-mutex';
|
|
10
10
|
import * as ethereumjsWallet from 'ethereumjs-wallet';
|
|
11
|
+
import { cloneDeep } from 'lodash-es';
|
|
11
12
|
// When generating a ULID within the same millisecond, monotonicFactory provides some guarantees regarding sort order.
|
|
12
13
|
import { ulid } from 'ulid';
|
|
13
14
|
import { KeyringControllerErrorMessage } from './constants.js';
|
|
14
15
|
import { KeyringControllerError } from './errors.js';
|
|
15
|
-
import { VaultTransaction } from './VaultTransaction.js';
|
|
16
16
|
/**
|
|
17
17
|
* `ethereumjs-wallet` is CommonJS, and Node cannot reliably detect its named
|
|
18
18
|
* exports, so importing `thirdparty` directly fails at run time. It also
|
|
@@ -257,14 +257,6 @@ function normalize(address) {
|
|
|
257
257
|
// address value!
|
|
258
258
|
return isEthAddress(address) ? ethNormalize(address) : address;
|
|
259
259
|
}
|
|
260
|
-
/**
|
|
261
|
-
* The standard wallet persist policy: the vault is written when the
|
|
262
|
-
* session state changed since the transaction began.
|
|
263
|
-
*
|
|
264
|
-
* @param hasChanged - Lazily checks whether the session state changed.
|
|
265
|
-
* @returns Whether the vault should be written.
|
|
266
|
-
*/
|
|
267
|
-
const persistWhenSessionChanged = (hasChanged) => hasChanged();
|
|
268
260
|
/**
|
|
269
261
|
* Controller responsible for establishing and managing user identity.
|
|
270
262
|
*
|
|
@@ -355,9 +347,7 @@ export class KeyringController extends BaseController {
|
|
|
355
347
|
*/
|
|
356
348
|
async addNewAccount(accountCount) {
|
|
357
349
|
this.#assertIsUnlocked();
|
|
358
|
-
|
|
359
|
-
// operated keyring is snapshotted and diffed.
|
|
360
|
-
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async () => {
|
|
350
|
+
return this.#persistOrRollback(async () => {
|
|
361
351
|
const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0];
|
|
362
352
|
if (!primaryKeyring) {
|
|
363
353
|
throw new KeyringControllerError('No HD keyring found');
|
|
@@ -392,9 +382,7 @@ export class KeyringController extends BaseController {
|
|
|
392
382
|
// and account using a "Snap Keyring". This function assume the `keyring` is
|
|
393
383
|
// ethereum compatible, but "Snap Keyring" might not be.
|
|
394
384
|
this.#assertIsUnlocked();
|
|
395
|
-
|
|
396
|
-
// operated keyring is snapshotted and diffed.
|
|
397
|
-
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async () => {
|
|
385
|
+
return this.#persistOrRollback(async () => {
|
|
398
386
|
const oldAccounts = await this.#getAccountsFromKeyrings();
|
|
399
387
|
if (accountCount && oldAccounts.length !== accountCount) {
|
|
400
388
|
if (accountCount > oldAccounts.length) {
|
|
@@ -420,9 +408,9 @@ export class KeyringController extends BaseController {
|
|
|
420
408
|
* @returns Promise resolving when the operation ends successfully.
|
|
421
409
|
*/
|
|
422
410
|
async createNewVaultAndRestore(password, seed) {
|
|
423
|
-
return this.#
|
|
411
|
+
return this.#persistOrRollback(async () => {
|
|
424
412
|
assertIsValidPassword(password);
|
|
425
|
-
await this.#createNewVaultWithKeyring(
|
|
413
|
+
await this.#createNewVaultWithKeyring(password, {
|
|
426
414
|
type: KeyringTypes.hd,
|
|
427
415
|
opts: {
|
|
428
416
|
mnemonic: seed,
|
|
@@ -441,10 +429,10 @@ export class KeyringController extends BaseController {
|
|
|
441
429
|
* @returns Promise resolving when the operation ends successfully.
|
|
442
430
|
*/
|
|
443
431
|
async createNewVaultAndKeychain(password) {
|
|
444
|
-
return this.#
|
|
432
|
+
return this.#persistOrRollback(async () => {
|
|
445
433
|
const accounts = await this.#getAccountsFromKeyrings();
|
|
446
434
|
if (!accounts.length) {
|
|
447
|
-
await this.#createNewVaultWithKeyring(
|
|
435
|
+
await this.#createNewVaultWithKeyring(password, {
|
|
448
436
|
type: KeyringTypes.hd,
|
|
449
437
|
});
|
|
450
438
|
}
|
|
@@ -460,9 +448,7 @@ export class KeyringController extends BaseController {
|
|
|
460
448
|
*/
|
|
461
449
|
async addNewKeyring(type, opts) {
|
|
462
450
|
this.#assertIsUnlocked();
|
|
463
|
-
|
|
464
|
-
// operated keyring is snapshotted and diffed.
|
|
465
|
-
return this.#getKeyringMetadata(await this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async () => this.#newKeyring(type, opts)));
|
|
451
|
+
return this.#getKeyringMetadata(await this.#persistOrRollback(async () => this.#newKeyring(type, opts)));
|
|
466
452
|
}
|
|
467
453
|
/**
|
|
468
454
|
* Method to verify a given password validity. Throws an
|
|
@@ -663,10 +649,9 @@ export class KeyringController extends BaseController {
|
|
|
663
649
|
* operation completes.
|
|
664
650
|
*/
|
|
665
651
|
async persistAllKeyrings() {
|
|
666
|
-
return this.#
|
|
667
|
-
// The deprecated method's contract is an unconditional vault write.
|
|
668
|
-
() => this.#beginWalletTransaction(() => true), async () => {
|
|
652
|
+
return this.#withRollback(async () => {
|
|
669
653
|
this.#assertIsUnlocked();
|
|
654
|
+
await this.#updateVault();
|
|
670
655
|
return true;
|
|
671
656
|
});
|
|
672
657
|
}
|
|
@@ -683,9 +668,7 @@ export class KeyringController extends BaseController {
|
|
|
683
668
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
684
669
|
args) {
|
|
685
670
|
this.#assertIsUnlocked();
|
|
686
|
-
|
|
687
|
-
// operated keyring is snapshotted and diffed.
|
|
688
|
-
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async () => {
|
|
671
|
+
return this.#persistOrRollback(async () => {
|
|
689
672
|
let privateKey;
|
|
690
673
|
switch (strategy) {
|
|
691
674
|
case AccountImportStrategy.privateKey: {
|
|
@@ -741,15 +724,12 @@ export class KeyringController extends BaseController {
|
|
|
741
724
|
*/
|
|
742
725
|
async removeAccount(address) {
|
|
743
726
|
this.#assertIsUnlocked();
|
|
744
|
-
|
|
745
|
-
// operated keyring is snapshotted and diffed.
|
|
746
|
-
await this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
|
|
727
|
+
await this.#persistOrRollback(async () => {
|
|
747
728
|
const keyringIndex = await this.#findKeyringIndexForAccount(address);
|
|
748
729
|
if (keyringIndex === -1) {
|
|
749
730
|
throw new KeyringControllerError(KeyringControllerErrorMessage.NoKeyring);
|
|
750
731
|
}
|
|
751
|
-
const
|
|
752
|
-
const { keyring } = entry;
|
|
732
|
+
const { keyring, keyringV2 } = this.#keyrings[keyringIndex];
|
|
753
733
|
const isPrimaryKeyring = keyringIndex === 0;
|
|
754
734
|
const shouldRemoveKeyring = (await keyring.getAccounts()).length === 1;
|
|
755
735
|
// Primary keyring should never be removed, so we need to keep at least one account in it
|
|
@@ -769,7 +749,8 @@ export class KeyringController extends BaseController {
|
|
|
769
749
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
770
750
|
await keyring.removeAccount(address);
|
|
771
751
|
if (shouldRemoveKeyring) {
|
|
772
|
-
|
|
752
|
+
this.#keyrings.splice(keyringIndex, 1);
|
|
753
|
+
await this.#destroyKeyring(keyring, keyringV2);
|
|
773
754
|
}
|
|
774
755
|
});
|
|
775
756
|
this.messenger.publish(`${name}:accountRemoved`, address);
|
|
@@ -781,12 +762,9 @@ export class KeyringController extends BaseController {
|
|
|
781
762
|
*/
|
|
782
763
|
async setLocked() {
|
|
783
764
|
this.#assertIsUnlocked();
|
|
784
|
-
return this.#
|
|
785
|
-
// Locking clears the session without rewriting the vault, and the
|
|
786
|
-
// encryption key is gone by commit time anyway.
|
|
787
|
-
() => this.#beginWalletTransaction(() => false), async (transaction) => {
|
|
765
|
+
return this.#withRollback(async () => {
|
|
788
766
|
this.#encryptionKey = undefined;
|
|
789
|
-
await this.#clearKeyrings(
|
|
767
|
+
await this.#clearKeyrings();
|
|
790
768
|
this.update((state) => {
|
|
791
769
|
state.isUnlocked = false;
|
|
792
770
|
state.keyrings = [];
|
|
@@ -968,7 +946,7 @@ export class KeyringController extends BaseController {
|
|
|
968
946
|
*/
|
|
969
947
|
changePassword(password) {
|
|
970
948
|
this.#assertIsUnlocked();
|
|
971
|
-
return this.#
|
|
949
|
+
return this.#persistOrRollback(async () => {
|
|
972
950
|
assertIsValidPassword(password);
|
|
973
951
|
await this.#deriveAndSetEncryptionKey(password, {
|
|
974
952
|
ignoreExistingVault: true,
|
|
@@ -985,12 +963,8 @@ export class KeyringController extends BaseController {
|
|
|
985
963
|
* @returns Promise resolving when the operation completes.
|
|
986
964
|
*/
|
|
987
965
|
async submitEncryptionKey(encryptionKey, encryptionSalt) {
|
|
988
|
-
const { hasChanged } = await this.#
|
|
989
|
-
|
|
990
|
-
// upgrade the vault is the next transaction's decision, on the
|
|
991
|
-
// restoration's own change flag.
|
|
992
|
-
() => this.#beginWalletTransaction(() => false), async (transaction) => {
|
|
993
|
-
const result = await this.#unlockKeyrings(transaction, {
|
|
966
|
+
const { hasChanged } = await this.#withRollback(async () => {
|
|
967
|
+
const result = await this.#unlockKeyrings({
|
|
994
968
|
encryptionKey,
|
|
995
969
|
encryptionSalt,
|
|
996
970
|
});
|
|
@@ -1000,10 +974,11 @@ export class KeyringController extends BaseController {
|
|
|
1000
974
|
try {
|
|
1001
975
|
// if new metadata has been generated during login, we
|
|
1002
976
|
// can attempt to upgrade the vault.
|
|
1003
|
-
await this.#
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
977
|
+
await this.#withRollback(async () => {
|
|
978
|
+
if (hasChanged) {
|
|
979
|
+
await this.#updateVault();
|
|
980
|
+
}
|
|
981
|
+
});
|
|
1007
982
|
}
|
|
1008
983
|
catch (error) {
|
|
1009
984
|
// We don't want to throw an error if the upgrade fails
|
|
@@ -1031,11 +1006,8 @@ export class KeyringController extends BaseController {
|
|
|
1031
1006
|
* @returns Promise resolving when the operation completes.
|
|
1032
1007
|
*/
|
|
1033
1008
|
async submitPassword(password) {
|
|
1034
|
-
const { hasChanged } = await this.#
|
|
1035
|
-
|
|
1036
|
-
// upgrade the vault is the next transaction's decision.
|
|
1037
|
-
() => this.#beginWalletTransaction(() => false), async (transaction) => {
|
|
1038
|
-
const result = await this.#unlockKeyrings(transaction, { password });
|
|
1009
|
+
const { hasChanged } = await this.#withRollback(async () => {
|
|
1010
|
+
const result = await this.#unlockKeyrings({ password });
|
|
1039
1011
|
this.#setUnlocked();
|
|
1040
1012
|
return result;
|
|
1041
1013
|
});
|
|
@@ -1043,24 +1015,17 @@ export class KeyringController extends BaseController {
|
|
|
1043
1015
|
// If there are stronger encryption params available, or
|
|
1044
1016
|
// if the keyring state has changed during deserialization, we
|
|
1045
1017
|
// can attempt to upgrade the vault.
|
|
1046
|
-
await this.#
|
|
1047
|
-
// The criterion combines the restoration's change flag from the
|
|
1048
|
-
// unlock above — not this transaction's own session diff — with
|
|
1049
|
-
// the available encryption params, and gates the re-derivation
|
|
1050
|
-
// of the encryption key on it.
|
|
1051
|
-
() => this.#beginWalletTransaction(async () => {
|
|
1018
|
+
await this.#withRollback(async () => {
|
|
1052
1019
|
if (hasChanged || this.#isNewEncryptionAvailable()) {
|
|
1053
|
-
// If the vault is being upgraded, we want to ignore the
|
|
1054
|
-
// metadata that is already in the vault, so we can
|
|
1055
|
-
// effectively re-encrypt the vault with the new encryption
|
|
1056
|
-
// config.
|
|
1057
1020
|
await this.#deriveAndSetEncryptionKey(password, {
|
|
1021
|
+
// If the vault is being upgraded, we want to ignore the metadata
|
|
1022
|
+
// that is already in the vault, so we can effectively
|
|
1023
|
+
// re-encrypt the vault with the new encryption config.
|
|
1058
1024
|
ignoreExistingVault: true,
|
|
1059
1025
|
});
|
|
1060
|
-
|
|
1026
|
+
await this.#updateVault();
|
|
1061
1027
|
}
|
|
1062
|
-
|
|
1063
|
-
}), async () => undefined);
|
|
1028
|
+
});
|
|
1064
1029
|
}
|
|
1065
1030
|
catch (error) {
|
|
1066
1031
|
// We don't want to throw an error if the upgrade fails
|
|
@@ -1100,16 +1065,22 @@ export class KeyringController extends BaseController {
|
|
|
1100
1065
|
createIfMissing: false,
|
|
1101
1066
|
}) {
|
|
1102
1067
|
this.#assertIsUnlocked();
|
|
1103
|
-
return this.#
|
|
1104
|
-
|
|
1105
|
-
|
|
1068
|
+
return this.#persistOrRollback(async () => {
|
|
1069
|
+
let entry = await this.#selectKeyringEntry({
|
|
1070
|
+
v2: false,
|
|
1071
|
+
selector,
|
|
1072
|
+
});
|
|
1073
|
+
if (!entry && 'type' in selector && options.createIfMissing) {
|
|
1074
|
+
const newKeyring = (await this.#newKeyring(selector.type, options.createWithData));
|
|
1075
|
+
entry = this.#keyrings.find(({ keyring }) => keyring === newKeyring);
|
|
1076
|
+
}
|
|
1077
|
+
if (!entry) {
|
|
1078
|
+
throw new KeyringControllerError(KeyringControllerErrorMessage.KeyringNotFound);
|
|
1106
1079
|
}
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
metadata,
|
|
1112
|
-
}), keyring));
|
|
1080
|
+
const { metadata } = entry;
|
|
1081
|
+
const keyring = entry.keyring;
|
|
1082
|
+
return this.#assertNoUnsafeDirectKeyringAccess(await this.#cleanUpEmptiedKeyringsAfter(async () => operation({ keyring, metadata })), keyring);
|
|
1083
|
+
});
|
|
1113
1084
|
}
|
|
1114
1085
|
/**
|
|
1115
1086
|
* Select a keyring and execute the given operation with the selected
|
|
@@ -1173,9 +1144,6 @@ export class KeyringController extends BaseController {
|
|
|
1173
1144
|
* function execution, or rolls back the changes if an error
|
|
1174
1145
|
* is thrown.
|
|
1175
1146
|
*
|
|
1176
|
-
* Only the selected keyring is snapshotted, persisted, and rolled back:
|
|
1177
|
-
* the operation must not mutate other keyrings.
|
|
1178
|
-
*
|
|
1179
1147
|
* @param selector - Keyring selector object.
|
|
1180
1148
|
* @param operation - Function to execute with the wrapped V2 keyring.
|
|
1181
1149
|
* @returns Promise resolving to the result of the function execution.
|
|
@@ -1183,10 +1151,24 @@ export class KeyringController extends BaseController {
|
|
|
1183
1151
|
*/
|
|
1184
1152
|
async withKeyringV2(selector, operation) {
|
|
1185
1153
|
this.#assertIsUnlocked();
|
|
1186
|
-
return this.#
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1154
|
+
return this.#persistOrRollback(async () => {
|
|
1155
|
+
const entry = await this.#selectKeyringEntry({
|
|
1156
|
+
v2: true,
|
|
1157
|
+
selector,
|
|
1158
|
+
});
|
|
1159
|
+
if (!entry) {
|
|
1160
|
+
throw new KeyringControllerError(KeyringControllerErrorMessage.KeyringNotFound);
|
|
1161
|
+
}
|
|
1162
|
+
if (!entry.keyringV2) {
|
|
1163
|
+
throw new KeyringControllerError(KeyringControllerErrorMessage.KeyringV2NotSupported);
|
|
1164
|
+
}
|
|
1165
|
+
const { metadata } = entry;
|
|
1166
|
+
const keyring = entry.keyringV2;
|
|
1167
|
+
return this.#assertNoUnsafeDirectKeyringAccess(await this.#cleanUpEmptiedKeyringsAfter(async () => operation({
|
|
1168
|
+
keyring,
|
|
1169
|
+
metadata,
|
|
1170
|
+
})), keyring);
|
|
1171
|
+
});
|
|
1190
1172
|
}
|
|
1191
1173
|
/**
|
|
1192
1174
|
* Select a keyring, wrap it in a `KeyringV2` adapter, and execute
|
|
@@ -1255,7 +1237,7 @@ export class KeyringController extends BaseController {
|
|
|
1255
1237
|
*/
|
|
1256
1238
|
async withController(operation) {
|
|
1257
1239
|
this.#assertIsUnlocked();
|
|
1258
|
-
return this.#
|
|
1240
|
+
return this.#persistOrRollback(async () => {
|
|
1259
1241
|
// Track created and removed keyrings during the operation execution.
|
|
1260
1242
|
const createdEntries = new Set();
|
|
1261
1243
|
const removedEntries = new Set();
|
|
@@ -1291,7 +1273,7 @@ export class KeyringController extends BaseController {
|
|
|
1291
1273
|
},
|
|
1292
1274
|
};
|
|
1293
1275
|
const destroyKeyrings = async (entries) => {
|
|
1294
|
-
await Promise.all([...entries].map((
|
|
1276
|
+
await Promise.all([...entries].map(({ keyring, keyringV2 }) => this.#destroyKeyring(keyring, keyringV2)));
|
|
1295
1277
|
};
|
|
1296
1278
|
let result;
|
|
1297
1279
|
try {
|
|
@@ -1441,14 +1423,13 @@ export class KeyringController extends BaseController {
|
|
|
1441
1423
|
* creates a new wallet with 1 account.
|
|
1442
1424
|
*
|
|
1443
1425
|
* @fires KeyringController:unlock
|
|
1444
|
-
* @param transaction - The transaction creating the new vault.
|
|
1445
1426
|
* @param password - The password to encrypt the vault with.
|
|
1446
1427
|
* @param keyring - A object containing the params to instantiate a new keyring.
|
|
1447
1428
|
* @param keyring.type - The keyring type.
|
|
1448
1429
|
* @param keyring.opts - Optional parameters required to instantiate the keyring.
|
|
1449
1430
|
* @returns A promise that resolves to the state.
|
|
1450
1431
|
*/
|
|
1451
|
-
async #createNewVaultWithKeyring(
|
|
1432
|
+
async #createNewVaultWithKeyring(password, keyring) {
|
|
1452
1433
|
this.#assertControllerMutexIsLocked();
|
|
1453
1434
|
if (typeof password !== 'string') {
|
|
1454
1435
|
throw new TypeError(KeyringControllerErrorMessage.WrongPasswordType);
|
|
@@ -1460,7 +1441,7 @@ export class KeyringController extends BaseController {
|
|
|
1460
1441
|
await this.#deriveAndSetEncryptionKey(password, {
|
|
1461
1442
|
ignoreExistingVault: true,
|
|
1462
1443
|
});
|
|
1463
|
-
await this.#clearKeyrings(
|
|
1444
|
+
await this.#clearKeyrings();
|
|
1464
1445
|
await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);
|
|
1465
1446
|
this.#setUnlocked();
|
|
1466
1447
|
}
|
|
@@ -1587,11 +1568,15 @@ export class KeyringController extends BaseController {
|
|
|
1587
1568
|
}
|
|
1588
1569
|
/**
|
|
1589
1570
|
* Serialize the current array of keyring instances,
|
|
1590
|
-
* including unsupported keyrings.
|
|
1571
|
+
* including unsupported keyrings by default.
|
|
1591
1572
|
*
|
|
1573
|
+
* @param options - Method options.
|
|
1574
|
+
* @param options.includeUnsupported - Whether to include unsupported keyrings.
|
|
1592
1575
|
* @returns The serialized keyrings.
|
|
1593
1576
|
*/
|
|
1594
|
-
async #getSerializedKeyrings(
|
|
1577
|
+
async #getSerializedKeyrings({ includeUnsupported } = {
|
|
1578
|
+
includeUnsupported: true,
|
|
1579
|
+
}) {
|
|
1595
1580
|
const serializedKeyrings = await Promise.all(this.#keyrings.map(async ({ keyring, metadata }) => {
|
|
1596
1581
|
return {
|
|
1597
1582
|
type: keyring.type,
|
|
@@ -1599,18 +1584,31 @@ export class KeyringController extends BaseController {
|
|
|
1599
1584
|
metadata,
|
|
1600
1585
|
};
|
|
1601
1586
|
}));
|
|
1602
|
-
|
|
1587
|
+
if (includeUnsupported) {
|
|
1588
|
+
serializedKeyrings.push(...this.#unsupportedKeyrings);
|
|
1589
|
+
}
|
|
1603
1590
|
return serializedKeyrings;
|
|
1604
1591
|
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Get a snapshot of session data held by instance variables.
|
|
1594
|
+
*
|
|
1595
|
+
* @returns An object with serialized keyrings, keyrings metadata,
|
|
1596
|
+
* and the user password.
|
|
1597
|
+
*/
|
|
1598
|
+
async #getSessionState() {
|
|
1599
|
+
return {
|
|
1600
|
+
keyrings: await this.#getSerializedKeyrings(),
|
|
1601
|
+
encryptionKey: this.#encryptionKey,
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1605
1604
|
/**
|
|
1606
1605
|
* Restore a serialized keyrings array.
|
|
1607
1606
|
*
|
|
1608
|
-
* @param transaction - The transaction restoring the keyrings.
|
|
1609
1607
|
* @param serializedKeyrings - The serialized keyrings array.
|
|
1610
1608
|
* @returns The restored keyrings.
|
|
1611
1609
|
*/
|
|
1612
|
-
async #restoreSerializedKeyrings(
|
|
1613
|
-
await this.#clearKeyrings(
|
|
1610
|
+
async #restoreSerializedKeyrings(serializedKeyrings) {
|
|
1611
|
+
await this.#clearKeyrings();
|
|
1614
1612
|
const keyrings = [];
|
|
1615
1613
|
let hasChanged = false;
|
|
1616
1614
|
for (const serializedKeyring of serializedKeyrings) {
|
|
@@ -1629,11 +1627,10 @@ export class KeyringController extends BaseController {
|
|
|
1629
1627
|
* Unlock Keyrings, decrypting the vault and deserializing all
|
|
1630
1628
|
* keyrings contained in it, using a password or an encryption key with salt.
|
|
1631
1629
|
*
|
|
1632
|
-
* @param transaction - The transaction unlocking the keyrings.
|
|
1633
1630
|
* @param credentials - The credentials to unlock the keyrings.
|
|
1634
1631
|
* @returns A promise resolving to the deserialized keyrings array.
|
|
1635
1632
|
*/
|
|
1636
|
-
async #unlockKeyrings(
|
|
1633
|
+
async #unlockKeyrings(credentials) {
|
|
1637
1634
|
return this.#withVaultLock(async () => {
|
|
1638
1635
|
if (!this.state.vault) {
|
|
1639
1636
|
throw new KeyringControllerError(KeyringControllerErrorMessage.VaultError);
|
|
@@ -1654,7 +1651,7 @@ export class KeyringController extends BaseController {
|
|
|
1654
1651
|
if (!isSerializedKeyringsArray(vault)) {
|
|
1655
1652
|
throw new KeyringControllerError(KeyringControllerErrorMessage.VaultDataError);
|
|
1656
1653
|
}
|
|
1657
|
-
const { keyrings, hasChanged } = await this.#restoreSerializedKeyrings(
|
|
1654
|
+
const { keyrings, hasChanged } = await this.#restoreSerializedKeyrings(vault);
|
|
1658
1655
|
const updatedKeyrings = await this.#getUpdatedKeyrings();
|
|
1659
1656
|
this.update((state) => {
|
|
1660
1657
|
state.keyrings = updatedKeyrings;
|
|
@@ -1721,10 +1718,14 @@ export class KeyringController extends BaseController {
|
|
|
1721
1718
|
* @returns A promise resolving to an array of accounts.
|
|
1722
1719
|
*/
|
|
1723
1720
|
async #getAccountsFromKeyrings(additionalKeyrings = []) {
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1721
|
+
const keyrings = this.#keyrings.map(({ keyring }) => keyring);
|
|
1722
|
+
const keyringArrays = await Promise.all([...keyrings, ...additionalKeyrings].map(async (keyring) => keyring.getAccounts()));
|
|
1723
|
+
const addresses = keyringArrays.reduce((res, arr) => {
|
|
1724
|
+
return res.concat(arr);
|
|
1725
|
+
}, []);
|
|
1726
|
+
// Cast to `string[]` here is safe here because `addresses` has no nullish
|
|
1727
|
+
// values, and `normalize` returns `string` unless given a nullish value
|
|
1728
|
+
return addresses.map(normalize);
|
|
1728
1729
|
}
|
|
1729
1730
|
/**
|
|
1730
1731
|
* Create a new keyring, ensuring that the first account is
|
|
@@ -1813,16 +1814,60 @@ export class KeyringController extends BaseController {
|
|
|
1813
1814
|
}
|
|
1814
1815
|
return { keyring, keyringV2, metadata: keyringMetadata };
|
|
1815
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Run the given operation and afterwards clean up any keyring whose
|
|
1819
|
+
* account list transitioned from non-empty to empty during the operation.
|
|
1820
|
+
*
|
|
1821
|
+
* This mirrors the cleanup behavior of {@link KeyringController.removeAccount}
|
|
1822
|
+
* for code paths where the consumer mutates a keyring directly via
|
|
1823
|
+
* {@link KeyringController.withKeyring} or
|
|
1824
|
+
* {@link KeyringController.withKeyringV2}: if the consumer drains the last
|
|
1825
|
+
* account from a keyring, the now-empty keyring is removed from
|
|
1826
|
+
* {@link KeyringController.#keyrings} and destroyed before persistence runs.
|
|
1827
|
+
*
|
|
1828
|
+
* Pre-existing empty keyrings (e.g. those created intentionally via
|
|
1829
|
+
* {@link KeyringController.addNewKeyring} without subsequent account
|
|
1830
|
+
* creation) are left alone, as are keyrings created within the operation
|
|
1831
|
+
* itself (they are not part of the pre-operation snapshot). The primary
|
|
1832
|
+
* keyring (see {@link KeyringController.#isPrimaryKeyring}) is also preserved
|
|
1833
|
+
* unconditionally to keep `removeAccount`'s primary-keyring invariant intact.
|
|
1834
|
+
*
|
|
1835
|
+
* @param operation - The operation to execute.
|
|
1836
|
+
* @returns The result of the operation.
|
|
1837
|
+
* @template Result - The type of the value resolved by the operation.
|
|
1838
|
+
*/
|
|
1839
|
+
async #cleanUpEmptiedKeyringsAfter(operation) {
|
|
1840
|
+
// Only the primary keyring exists, which is never auto-removed, so there
|
|
1841
|
+
// is nothing to clean up regardless of what the operation does.
|
|
1842
|
+
if (this.#keyrings.length <= 1) {
|
|
1843
|
+
return operation();
|
|
1844
|
+
}
|
|
1845
|
+
const wasNonEmpty = new WeakSet();
|
|
1846
|
+
await Promise.all(this.#keyrings.map(async ({ keyring }) => {
|
|
1847
|
+
if ((await keyring.getAccounts()).length > 0) {
|
|
1848
|
+
wasNonEmpty.add(keyring);
|
|
1849
|
+
}
|
|
1850
|
+
}));
|
|
1851
|
+
const result = await operation();
|
|
1852
|
+
const isNowEmpty = await Promise.all(this.#keyrings.map(async ({ keyring }) => (await keyring.getAccounts()).length === 0));
|
|
1853
|
+
const emptied = this.#keyrings.filter((entry, index) => !this.#isPrimaryKeyring(entry, this.#keyrings) &&
|
|
1854
|
+
wasNonEmpty.has(entry.keyring) &&
|
|
1855
|
+
isNowEmpty[index]);
|
|
1856
|
+
if (emptied.length > 0) {
|
|
1857
|
+
const removed = new Set(emptied);
|
|
1858
|
+
this.#keyrings = this.#keyrings.filter((entry) => !removed.has(entry));
|
|
1859
|
+
await Promise.all(emptied.map(({ keyring, keyringV2 }) => this.#destroyKeyring(keyring, keyringV2)));
|
|
1860
|
+
}
|
|
1861
|
+
return result;
|
|
1862
|
+
}
|
|
1816
1863
|
/**
|
|
1817
1864
|
* Remove all managed keyrings, destroying all their
|
|
1818
1865
|
* instances in memory.
|
|
1819
|
-
*
|
|
1820
|
-
* @param transaction - The transaction clearing the keyrings.
|
|
1821
1866
|
*/
|
|
1822
|
-
async #clearKeyrings(
|
|
1867
|
+
async #clearKeyrings() {
|
|
1823
1868
|
this.#assertControllerMutexIsLocked();
|
|
1824
|
-
for (const
|
|
1825
|
-
await
|
|
1869
|
+
for (const { keyring, keyringV2 } of this.#keyrings) {
|
|
1870
|
+
await this.#destroyKeyring(keyring, keyringV2);
|
|
1826
1871
|
}
|
|
1827
1872
|
this.#keyrings = [];
|
|
1828
1873
|
this.#unsupportedKeyrings = [];
|
|
@@ -1867,6 +1912,22 @@ export class KeyringController extends BaseController {
|
|
|
1867
1912
|
return undefined;
|
|
1868
1913
|
}
|
|
1869
1914
|
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Destroy Keyring
|
|
1917
|
+
*
|
|
1918
|
+
* Some keyrings support a method called `destroy`, that destroys the
|
|
1919
|
+
* keyring along with removing all its event listeners and, in some cases,
|
|
1920
|
+
* clears the keyring bridge iframe from the DOM.
|
|
1921
|
+
*
|
|
1922
|
+
* @param keyring - The keyring to destroy.
|
|
1923
|
+
* @param keyringV2 - The keyring v2 to destroy (if any).
|
|
1924
|
+
*/
|
|
1925
|
+
async #destroyKeyring(keyring, keyringV2) {
|
|
1926
|
+
await keyring.destroy?.();
|
|
1927
|
+
if (keyringV2) {
|
|
1928
|
+
await keyringV2.destroy?.();
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1870
1931
|
/**
|
|
1871
1932
|
* Assert that there are no duplicate accounts in the keyrings.
|
|
1872
1933
|
*
|
|
@@ -1874,10 +1935,10 @@ export class KeyringController extends BaseController {
|
|
|
1874
1935
|
* @throws If there are duplicate accounts.
|
|
1875
1936
|
*/
|
|
1876
1937
|
async #assertNoDuplicateAccounts(additionalKeyrings = []) {
|
|
1877
|
-
await
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1938
|
+
const accounts = await this.#getAccountsFromKeyrings(additionalKeyrings);
|
|
1939
|
+
if (new Set(accounts).size !== accounts.length) {
|
|
1940
|
+
throw new KeyringControllerError(KeyringControllerErrorMessage.DuplicatedAccount);
|
|
1941
|
+
}
|
|
1881
1942
|
}
|
|
1882
1943
|
/**
|
|
1883
1944
|
* Set the `isUnlocked` to true and notify listeners
|
|
@@ -1903,174 +1964,47 @@ export class KeyringController extends BaseController {
|
|
|
1903
1964
|
}
|
|
1904
1965
|
}
|
|
1905
1966
|
/**
|
|
1906
|
-
* Execute
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
1909
|
-
* keep their instances even if the transaction fails. This relies on the
|
|
1910
|
-
* `withKeyring` family only mutating the keyring it is given.
|
|
1911
|
-
*
|
|
1912
|
-
* If the operation drains the operated keyring of all its accounts, the
|
|
1913
|
-
* keyring is removed and destroyed (mirroring `removeAccount`), unless it
|
|
1914
|
-
* is the primary keyring. A setup failure — selection, creation, or
|
|
1915
|
-
* validation — never triggers a rollback; see
|
|
1916
|
-
* `#beginKeyringTransaction`.
|
|
1967
|
+
* Execute the given function after acquiring the controller lock
|
|
1968
|
+
* and save the vault to state after it (only if needed), or rollback to their
|
|
1969
|
+
* previous state in case of error.
|
|
1917
1970
|
*
|
|
1918
|
-
* @param
|
|
1919
|
-
* @
|
|
1920
|
-
* V2 adapter. The selected or created entry must then have one.
|
|
1921
|
-
* @param select - Selects the keyring entry to operate on, if any.
|
|
1922
|
-
* @param create - Creates the keyring entry to operate on when selection
|
|
1923
|
-
* found nothing, if needed.
|
|
1924
|
-
* @param run - Runs the operation with the selected or created entry.
|
|
1925
|
-
* @returns The result of the operation.
|
|
1971
|
+
* @param callback - The function to execute.
|
|
1972
|
+
* @returns The result of the function.
|
|
1926
1973
|
*/
|
|
1927
|
-
async #
|
|
1928
|
-
return this.#
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
//
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
const canDrain = this.#keyrings.length > 1;
|
|
1936
|
-
const wasNonEmpty = canDrain && (await entry.keyring.getAccounts()).length > 0;
|
|
1937
|
-
const result = await run(entry);
|
|
1938
|
-
const isEmpty = canDrain && (await entry.keyring.getAccounts()).length === 0;
|
|
1939
|
-
// Only the operated keyring can have transitioned to empty.
|
|
1940
|
-
if (wasNonEmpty &&
|
|
1941
|
-
isEmpty &&
|
|
1942
|
-
!this.#isPrimaryKeyring(entry, this.#keyrings)) {
|
|
1943
|
-
await transaction.remove(entry);
|
|
1974
|
+
async #persistOrRollback(callback) {
|
|
1975
|
+
return this.#withRollback(async ({ releaseLock }) => {
|
|
1976
|
+
const oldState = JSON.stringify(await this.#getSessionState());
|
|
1977
|
+
const callbackResult = await callback({ releaseLock });
|
|
1978
|
+
const newState = JSON.stringify(await this.#getSessionState());
|
|
1979
|
+
// State is committed only if the operation is successful and need to trigger a vault update.
|
|
1980
|
+
if (oldState !== newState) {
|
|
1981
|
+
await this.#updateVault();
|
|
1944
1982
|
}
|
|
1945
|
-
|
|
1946
|
-
// changed.
|
|
1947
|
-
return result;
|
|
1983
|
+
return callbackResult;
|
|
1948
1984
|
});
|
|
1949
1985
|
}
|
|
1950
1986
|
/**
|
|
1951
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
* Everything here runs before the transaction starts, so a failure
|
|
1955
|
-
* leaves the controller untouched and never triggers a rollback: the
|
|
1956
|
-
* rollback of an entry without a snapshot would misread it as created
|
|
1957
|
-
* by the transaction, destroying a pre-existing keyring. The V2
|
|
1958
|
-
* validation happens before any of the keyring's accounts are read, so
|
|
1959
|
-
* that the missing adapter takes precedence over keyring-level errors.
|
|
1960
|
-
*
|
|
1961
|
-
* @param options - Transaction options.
|
|
1962
|
-
* @param options.v2 - Whether the transaction operates on the keyring's
|
|
1963
|
-
* V2 adapter.
|
|
1964
|
-
* @param select - Selects the keyring entry to operate on, if any.
|
|
1965
|
-
* @param create - Creates the keyring entry to operate on when selection
|
|
1966
|
-
* found nothing, if needed.
|
|
1967
|
-
* @returns The transaction, and the operated entry.
|
|
1968
|
-
*/
|
|
1969
|
-
async #beginKeyringTransaction({ v2 }, select, create) {
|
|
1970
|
-
const selected = await select();
|
|
1971
|
-
const entry = selected ?? (await create());
|
|
1972
|
-
if (!entry) {
|
|
1973
|
-
throw new KeyringControllerError(KeyringControllerErrorMessage.KeyringNotFound);
|
|
1974
|
-
}
|
|
1975
|
-
if (v2 && !entry.keyringV2) {
|
|
1976
|
-
throw new KeyringControllerError(KeyringControllerErrorMessage.KeyringV2NotSupported);
|
|
1977
|
-
}
|
|
1978
|
-
const transaction = new VaultTransaction({ type: 'keyring', entry, created: !selected }, this.#vaultSession(), this.#vaultOperations());
|
|
1979
|
-
await transaction.begin();
|
|
1980
|
-
return { transaction, setup: entry };
|
|
1981
|
-
}
|
|
1982
|
-
/**
|
|
1983
|
-
* The vault session state, as the transactions see it: what they
|
|
1984
|
-
* snapshot at `begin()`, mutate through, and restore on `rollback()`.
|
|
1987
|
+
* Execute the given function after acquiring the controller lock
|
|
1988
|
+
* and rollback keyrings and password states in case of error.
|
|
1985
1989
|
*
|
|
1986
|
-
* @
|
|
1987
|
-
|
|
1988
|
-
#vaultSession() {
|
|
1989
|
-
return {
|
|
1990
|
-
getKeyrings: () => [...this.#keyrings],
|
|
1991
|
-
replaceKeyrings: (keyrings) => {
|
|
1992
|
-
this.#keyrings = keyrings;
|
|
1993
|
-
},
|
|
1994
|
-
removeKeyring: (entry) => {
|
|
1995
|
-
const index = this.#keyrings.indexOf(entry);
|
|
1996
|
-
if (index !== -1) {
|
|
1997
|
-
this.#keyrings.splice(index, 1);
|
|
1998
|
-
return true;
|
|
1999
|
-
}
|
|
2000
|
-
return false;
|
|
2001
|
-
},
|
|
2002
|
-
insertKeyringAt: (index, entry) => {
|
|
2003
|
-
this.#keyrings.splice(index, 0, entry);
|
|
2004
|
-
},
|
|
2005
|
-
getUnsupportedKeyrings: () => [...this.#unsupportedKeyrings],
|
|
2006
|
-
setUnsupportedKeyrings: (keyrings) => {
|
|
2007
|
-
this.#unsupportedKeyrings = keyrings;
|
|
2008
|
-
},
|
|
2009
|
-
parkUnsupportedKeyring: (keyring) => {
|
|
2010
|
-
this.#unsupportedKeyrings.push(keyring);
|
|
2011
|
-
},
|
|
2012
|
-
getEncryptionKey: () => this.#encryptionKey,
|
|
2013
|
-
setEncryptionKey: (key) => {
|
|
2014
|
-
this.#encryptionKey = key;
|
|
2015
|
-
},
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
|
-
/**
|
|
2019
|
-
* The controller-owned primitives the transactions drive: building new
|
|
2020
|
-
* keyring entries, and enforcing the no-duplicate-accounts invariant on
|
|
2021
|
-
* an explicit list of keyrings.
|
|
2022
|
-
*
|
|
2023
|
-
* @returns The vault operations.
|
|
2024
|
-
*/
|
|
2025
|
-
#vaultOperations() {
|
|
2026
|
-
return {
|
|
2027
|
-
createKeyring: async (type, data, metadata) => this.#createKeyring(type, data, metadata),
|
|
2028
|
-
assertNoDuplicateAccounts: async (keyrings) => assertNoDuplicateAccounts(keyrings),
|
|
2029
|
-
updateVault: async () => this.#updateVault(),
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
/**
|
|
2033
|
-
* Execute the given function inside a vault transaction, after
|
|
2034
|
-
* acquiring the controller lock. The transaction is constructed and
|
|
2035
|
-
* begun before the function runs — a failure there leaves the
|
|
2036
|
-
* controller untouched — and committed after the function succeeds, or
|
|
2037
|
-
* rolled back if it throws.
|
|
2038
|
-
*
|
|
2039
|
-
* @param construct - Constructs and begins the transaction, plus
|
|
2040
|
-
* whatever the operation needs from the construction.
|
|
2041
|
-
* @param run - Runs the operation, with the transaction and what its
|
|
2042
|
-
* construction produced.
|
|
2043
|
-
* @returns The result of the operation.
|
|
1990
|
+
* @param callback - The function to execute atomically.
|
|
1991
|
+
* @returns The result of the function.
|
|
2044
1992
|
*/
|
|
2045
|
-
async #
|
|
2046
|
-
return this.#withControllerLock(async () => {
|
|
2047
|
-
const
|
|
1993
|
+
async #withRollback(callback) {
|
|
1994
|
+
return this.#withControllerLock(async ({ releaseLock }) => {
|
|
1995
|
+
const currentSerializedKeyrings = await this.#getSerializedKeyrings();
|
|
1996
|
+
const currentEncryptionKey = cloneDeep(this.#encryptionKey);
|
|
2048
1997
|
try {
|
|
2049
|
-
|
|
2050
|
-
await transaction.commit();
|
|
2051
|
-
return result;
|
|
1998
|
+
return await callback({ releaseLock });
|
|
2052
1999
|
}
|
|
2053
2000
|
catch (error) {
|
|
2054
|
-
//
|
|
2055
|
-
|
|
2001
|
+
// Keyrings and encryption credentials are restored to their previous state
|
|
2002
|
+
this.#encryptionKey = currentEncryptionKey;
|
|
2003
|
+
await this.#restoreSerializedKeyrings(currentSerializedKeyrings);
|
|
2056
2004
|
throw error;
|
|
2057
2005
|
}
|
|
2058
2006
|
});
|
|
2059
2007
|
}
|
|
2060
|
-
/**
|
|
2061
|
-
* Begin a wallet-scoped transaction, snapshotting the whole session.
|
|
2062
|
-
*
|
|
2063
|
-
* @param persist - Decides, when the transaction commits, whether the
|
|
2064
|
-
* vault is written.
|
|
2065
|
-
* @returns The transaction, and nothing else for the operation.
|
|
2066
|
-
*/
|
|
2067
|
-
async #beginWalletTransaction(persist) {
|
|
2068
|
-
const transaction = new VaultTransaction({ type: 'wallet', persist }, this.#vaultSession(), this.#vaultOperations());
|
|
2069
|
-
// The snapshot is taken before the transaction starts: a failure
|
|
2070
|
-
// here leaves the controller untouched.
|
|
2071
|
-
await transaction.begin();
|
|
2072
|
-
return { transaction, setup: undefined };
|
|
2073
|
-
}
|
|
2074
2008
|
/**
|
|
2075
2009
|
* Assert that the controller mutex is locked.
|
|
2076
2010
|
*
|
|
@@ -2166,32 +2100,5 @@ async function withLock(mutex, callback) {
|
|
|
2166
2100
|
function getDefaultKeyringMetadata() {
|
|
2167
2101
|
return { id: ulid(), name: '' };
|
|
2168
2102
|
}
|
|
2169
|
-
/**
|
|
2170
|
-
* Retrieves all the accounts held by the given keyrings.
|
|
2171
|
-
*
|
|
2172
|
-
* @param keyrings - The keyrings to read the accounts from.
|
|
2173
|
-
* @returns A promise resolving to an array of accounts.
|
|
2174
|
-
*/
|
|
2175
|
-
async function getAccountsFromKeyrings(keyrings) {
|
|
2176
|
-
const keyringArrays = await Promise.all(keyrings.map(async (keyring) => keyring.getAccounts()));
|
|
2177
|
-
const addresses = keyringArrays.reduce((res, arr) => {
|
|
2178
|
-
return res.concat(arr);
|
|
2179
|
-
}, []);
|
|
2180
|
-
// Cast to `string[]` here is safe here because `addresses` has no nullish
|
|
2181
|
-
// values, and `normalize` returns `string` unless given a nullish value
|
|
2182
|
-
return addresses.map(normalize);
|
|
2183
|
-
}
|
|
2184
|
-
/**
|
|
2185
|
-
* Assert that there are no duplicate accounts across the given keyrings.
|
|
2186
|
-
*
|
|
2187
|
-
* @param keyrings - The keyrings to check.
|
|
2188
|
-
* @throws If there are duplicate accounts.
|
|
2189
|
-
*/
|
|
2190
|
-
async function assertNoDuplicateAccounts(keyrings) {
|
|
2191
|
-
const accounts = await getAccountsFromKeyrings(keyrings);
|
|
2192
|
-
if (new Set(accounts).size !== accounts.length) {
|
|
2193
|
-
throw new KeyringControllerError(KeyringControllerErrorMessage.DuplicatedAccount);
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
2103
|
export default KeyringController;
|
|
2197
2104
|
//# sourceMappingURL=KeyringController.js.map
|