@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.
@@ -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
- // TODO: Migrate this to `#withKeyringOrRollback` so that only the
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
- // TODO: Migrate this to `#withKeyringOrRollback` so that only the
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.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
411
+ return this.#persistOrRollback(async () => {
424
412
  assertIsValidPassword(password);
425
- await this.#createNewVaultWithKeyring(transaction, password, {
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.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
432
+ return this.#persistOrRollback(async () => {
445
433
  const accounts = await this.#getAccountsFromKeyrings();
446
434
  if (!accounts.length) {
447
- await this.#createNewVaultWithKeyring(transaction, password, {
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
- // TODO: Migrate this to `#withKeyringOrRollback` so that only the
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.#withTransaction(
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
- // TODO: Migrate this to `#withKeyringOrRollback` so that only the
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
- // TODO: Migrate this to `#withKeyringOrRollback` so that only the
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 entry = this.#keyrings[keyringIndex];
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
- await transaction.remove(entry);
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.#withTransaction(
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(transaction);
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.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async () => {
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.#withTransaction(
989
- // The unlock transaction is a rollback safety net: whether to
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.#withTransaction(
1004
- // The criterion is the restoration's change flag from the unlock
1005
- // above — not this transaction's own session diff.
1006
- () => this.#beginWalletTransaction(() => hasChanged), async () => undefined);
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.#withTransaction(
1035
- // The unlock transaction is a rollback safety net: whether to
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.#withTransaction(
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
- return true;
1026
+ await this.#updateVault();
1061
1027
  }
1062
- return false;
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.#withKeyringOrRollback({ v2: false }, () => this.#selectKeyringEntry({ v2: false, selector }), async () => {
1104
- if (!options.createIfMissing || !('type' in selector)) {
1105
- return;
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 newKeyring = (await this.#newKeyring(selector.type, options.createWithData));
1108
- return this.#keyrings.find(({ keyring }) => keyring === newKeyring);
1109
- }, async ({ keyring, metadata }) => this.#assertNoUnsafeDirectKeyringAccess(await operation({
1110
- keyring: keyring,
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.#withKeyringOrRollback({ v2: true }, () => this.#selectKeyringEntry({ v2: true, selector }), async () => undefined, async ({ keyringV2, metadata }) => this.#assertNoUnsafeDirectKeyringAccess(await operation({
1187
- keyring: keyringV2,
1188
- metadata,
1189
- }), keyringV2));
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.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
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((entry) => transaction.destroy(entry)));
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(transaction, password, keyring) {
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(transaction);
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
- serializedKeyrings.push(...this.#unsupportedKeyrings);
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(transaction, serializedKeyrings) {
1613
- await this.#clearKeyrings(transaction);
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(transaction, credentials) {
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(transaction, vault);
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
- return getAccountsFromKeyrings([
1725
- ...this.#keyrings.map(({ keyring }) => keyring),
1726
- ...additionalKeyrings,
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(transaction) {
1867
+ async #clearKeyrings() {
1823
1868
  this.#assertControllerMutexIsLocked();
1824
- for (const entry of this.#keyrings) {
1825
- await transaction.destroy(entry);
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 assertNoDuplicateAccounts([
1878
- ...this.#keyrings.map(({ keyring }) => keyring),
1879
- ...additionalKeyrings,
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 a transaction that operates on a single keyring entry, selected
1907
- * (and possibly created) before the transaction starts. Only that keyring
1908
- * is snapshotted, diffed, persisted, and rolled back; the other keyrings
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 options - Transaction options.
1919
- * @param options.v2 - Whether the transaction operates on the keyring's
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 #withKeyringOrRollback({ v2 }, select, create, run) {
1928
- return this.#withTransaction(() => this.#beginKeyringTransaction({ v2 }, select, create), async (transaction, entry) => {
1929
- // The drain cleanup below can only fire when the wallet holds
1930
- // other keyrings: the bracketing account reads are skipped
1931
- // entirely otherwise, so single-keyring wallets make no extra
1932
- // `getAccounts` calls. For a created keyring, the wallet holds it
1933
- // already, so even a `getAccounts` rejection before the operation
1934
- // runs must still be rolled back.
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
- // The vault is updated by the commit, only if the operated keyring
1946
- // changed.
1947
- return result;
1983
+ return callbackResult;
1948
1984
  });
1949
1985
  }
1950
1986
  /**
1951
- * Begin a keyring-scoped transaction: select, or create, the entry to
1952
- * operate on, validate it, and snapshot the operated keyring.
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
- * @returns The vault session.
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 #withTransaction(construct, run) {
2046
- return this.#withControllerLock(async () => {
2047
- const { transaction, setup } = await construct();
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
- const result = await run(transaction, setup);
2050
- await transaction.commit();
2051
- return result;
1998
+ return await callback({ releaseLock });
2052
1999
  }
2053
2000
  catch (error) {
2054
- // What the transaction owns is restored to its previous state.
2055
- await transaction.rollback();
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