@metamask-previews/keyring-controller 28.2.0-preview-56cdd31f1 → 28.2.0-preview-4a8d4a2
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 +7 -0
- package/dist/KeyringController-method-action-types.d.ts +6 -0
- 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 +22 -0
- package/dist/KeyringController.d.ts.map +1 -1
- package/dist/KeyringController.js +291 -198
- package/dist/KeyringController.js.map +1 -1
- package/dist/VaultTransaction.d.ts +247 -0
- package/dist/VaultTransaction.d.ts.map +1 -0
- package/dist/VaultTransaction.js +483 -0
- package/dist/VaultTransaction.js.map +1 -0
- package/package.json +1 -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';
|
|
12
11
|
// When generating a ULID within the same millisecond, monotonicFactory provides some guarantees regarding sort order.
|
|
13
12
|
import { ulid } from 'ulid';
|
|
14
13
|
import { KeyringControllerErrorMessage } from './constants.js';
|
|
15
14
|
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,6 +257,14 @@ 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();
|
|
260
268
|
/**
|
|
261
269
|
* Controller responsible for establishing and managing user identity.
|
|
262
270
|
*
|
|
@@ -347,7 +355,9 @@ export class KeyringController extends BaseController {
|
|
|
347
355
|
*/
|
|
348
356
|
async addNewAccount(accountCount) {
|
|
349
357
|
this.#assertIsUnlocked();
|
|
350
|
-
|
|
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 () => {
|
|
351
361
|
const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0];
|
|
352
362
|
if (!primaryKeyring) {
|
|
353
363
|
throw new KeyringControllerError('No HD keyring found');
|
|
@@ -382,7 +392,9 @@ export class KeyringController extends BaseController {
|
|
|
382
392
|
// and account using a "Snap Keyring". This function assume the `keyring` is
|
|
383
393
|
// ethereum compatible, but "Snap Keyring" might not be.
|
|
384
394
|
this.#assertIsUnlocked();
|
|
385
|
-
|
|
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 () => {
|
|
386
398
|
const oldAccounts = await this.#getAccountsFromKeyrings();
|
|
387
399
|
if (accountCount && oldAccounts.length !== accountCount) {
|
|
388
400
|
if (accountCount > oldAccounts.length) {
|
|
@@ -408,9 +420,9 @@ export class KeyringController extends BaseController {
|
|
|
408
420
|
* @returns Promise resolving when the operation ends successfully.
|
|
409
421
|
*/
|
|
410
422
|
async createNewVaultAndRestore(password, seed) {
|
|
411
|
-
return this.#
|
|
423
|
+
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
|
|
412
424
|
assertIsValidPassword(password);
|
|
413
|
-
await this.#createNewVaultWithKeyring(password, {
|
|
425
|
+
await this.#createNewVaultWithKeyring(transaction, password, {
|
|
414
426
|
type: KeyringTypes.hd,
|
|
415
427
|
opts: {
|
|
416
428
|
mnemonic: seed,
|
|
@@ -429,10 +441,10 @@ export class KeyringController extends BaseController {
|
|
|
429
441
|
* @returns Promise resolving when the operation ends successfully.
|
|
430
442
|
*/
|
|
431
443
|
async createNewVaultAndKeychain(password) {
|
|
432
|
-
return this.#
|
|
444
|
+
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
|
|
433
445
|
const accounts = await this.#getAccountsFromKeyrings();
|
|
434
446
|
if (!accounts.length) {
|
|
435
|
-
await this.#createNewVaultWithKeyring(password, {
|
|
447
|
+
await this.#createNewVaultWithKeyring(transaction, password, {
|
|
436
448
|
type: KeyringTypes.hd,
|
|
437
449
|
});
|
|
438
450
|
}
|
|
@@ -448,7 +460,9 @@ export class KeyringController extends BaseController {
|
|
|
448
460
|
*/
|
|
449
461
|
async addNewKeyring(type, opts) {
|
|
450
462
|
this.#assertIsUnlocked();
|
|
451
|
-
|
|
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)));
|
|
452
466
|
}
|
|
453
467
|
/**
|
|
454
468
|
* Method to verify a given password validity. Throws an
|
|
@@ -649,9 +663,10 @@ export class KeyringController extends BaseController {
|
|
|
649
663
|
* operation completes.
|
|
650
664
|
*/
|
|
651
665
|
async persistAllKeyrings() {
|
|
652
|
-
return this.#
|
|
666
|
+
return this.#withTransaction(
|
|
667
|
+
// The deprecated method's contract is an unconditional vault write.
|
|
668
|
+
() => this.#beginWalletTransaction(() => true), async () => {
|
|
653
669
|
this.#assertIsUnlocked();
|
|
654
|
-
await this.#updateVault();
|
|
655
670
|
return true;
|
|
656
671
|
});
|
|
657
672
|
}
|
|
@@ -668,7 +683,9 @@ export class KeyringController extends BaseController {
|
|
|
668
683
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
669
684
|
args) {
|
|
670
685
|
this.#assertIsUnlocked();
|
|
671
|
-
|
|
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 () => {
|
|
672
689
|
let privateKey;
|
|
673
690
|
switch (strategy) {
|
|
674
691
|
case AccountImportStrategy.privateKey: {
|
|
@@ -724,12 +741,15 @@ export class KeyringController extends BaseController {
|
|
|
724
741
|
*/
|
|
725
742
|
async removeAccount(address) {
|
|
726
743
|
this.#assertIsUnlocked();
|
|
727
|
-
|
|
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) => {
|
|
728
747
|
const keyringIndex = await this.#findKeyringIndexForAccount(address);
|
|
729
748
|
if (keyringIndex === -1) {
|
|
730
749
|
throw new KeyringControllerError(KeyringControllerErrorMessage.NoKeyring);
|
|
731
750
|
}
|
|
732
|
-
const
|
|
751
|
+
const entry = this.#keyrings[keyringIndex];
|
|
752
|
+
const { keyring } = entry;
|
|
733
753
|
const isPrimaryKeyring = keyringIndex === 0;
|
|
734
754
|
const shouldRemoveKeyring = (await keyring.getAccounts()).length === 1;
|
|
735
755
|
// Primary keyring should never be removed, so we need to keep at least one account in it
|
|
@@ -749,8 +769,7 @@ export class KeyringController extends BaseController {
|
|
|
749
769
|
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
750
770
|
await keyring.removeAccount(address);
|
|
751
771
|
if (shouldRemoveKeyring) {
|
|
752
|
-
|
|
753
|
-
await this.#destroyKeyring(keyring, keyringV2);
|
|
772
|
+
await transaction.remove(entry);
|
|
754
773
|
}
|
|
755
774
|
});
|
|
756
775
|
this.messenger.publish(`${name}:accountRemoved`, address);
|
|
@@ -762,9 +781,12 @@ export class KeyringController extends BaseController {
|
|
|
762
781
|
*/
|
|
763
782
|
async setLocked() {
|
|
764
783
|
this.#assertIsUnlocked();
|
|
765
|
-
return this.#
|
|
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) => {
|
|
766
788
|
this.#encryptionKey = undefined;
|
|
767
|
-
await this.#clearKeyrings();
|
|
789
|
+
await this.#clearKeyrings(transaction);
|
|
768
790
|
this.update((state) => {
|
|
769
791
|
state.isUnlocked = false;
|
|
770
792
|
state.keyrings = [];
|
|
@@ -946,7 +968,7 @@ export class KeyringController extends BaseController {
|
|
|
946
968
|
*/
|
|
947
969
|
changePassword(password) {
|
|
948
970
|
this.#assertIsUnlocked();
|
|
949
|
-
return this.#
|
|
971
|
+
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async () => {
|
|
950
972
|
assertIsValidPassword(password);
|
|
951
973
|
await this.#deriveAndSetEncryptionKey(password, {
|
|
952
974
|
ignoreExistingVault: true,
|
|
@@ -963,8 +985,12 @@ export class KeyringController extends BaseController {
|
|
|
963
985
|
* @returns Promise resolving when the operation completes.
|
|
964
986
|
*/
|
|
965
987
|
async submitEncryptionKey(encryptionKey, encryptionSalt) {
|
|
966
|
-
const { hasChanged } = await this.#
|
|
967
|
-
|
|
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, {
|
|
968
994
|
encryptionKey,
|
|
969
995
|
encryptionSalt,
|
|
970
996
|
});
|
|
@@ -974,11 +1000,10 @@ export class KeyringController extends BaseController {
|
|
|
974
1000
|
try {
|
|
975
1001
|
// if new metadata has been generated during login, we
|
|
976
1002
|
// can attempt to upgrade the vault.
|
|
977
|
-
await this.#
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
});
|
|
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);
|
|
982
1007
|
}
|
|
983
1008
|
catch (error) {
|
|
984
1009
|
// We don't want to throw an error if the upgrade fails
|
|
@@ -1006,8 +1031,11 @@ export class KeyringController extends BaseController {
|
|
|
1006
1031
|
* @returns Promise resolving when the operation completes.
|
|
1007
1032
|
*/
|
|
1008
1033
|
async submitPassword(password) {
|
|
1009
|
-
const { hasChanged } = await this.#
|
|
1010
|
-
|
|
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 });
|
|
1011
1039
|
this.#setUnlocked();
|
|
1012
1040
|
return result;
|
|
1013
1041
|
});
|
|
@@ -1015,17 +1043,24 @@ export class KeyringController extends BaseController {
|
|
|
1015
1043
|
// If there are stronger encryption params available, or
|
|
1016
1044
|
// if the keyring state has changed during deserialization, we
|
|
1017
1045
|
// can attempt to upgrade the vault.
|
|
1018
|
-
await this.#
|
|
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 () => {
|
|
1019
1052
|
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.
|
|
1020
1057
|
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.
|
|
1024
1058
|
ignoreExistingVault: true,
|
|
1025
1059
|
});
|
|
1026
|
-
|
|
1060
|
+
return true;
|
|
1027
1061
|
}
|
|
1028
|
-
|
|
1062
|
+
return false;
|
|
1063
|
+
}), async () => undefined);
|
|
1029
1064
|
}
|
|
1030
1065
|
catch (error) {
|
|
1031
1066
|
// We don't want to throw an error if the upgrade fails
|
|
@@ -1065,22 +1100,16 @@ export class KeyringController extends BaseController {
|
|
|
1065
1100
|
createIfMissing: false,
|
|
1066
1101
|
}) {
|
|
1067
1102
|
this.#assertIsUnlocked();
|
|
1068
|
-
return this.#
|
|
1069
|
-
|
|
1070
|
-
|
|
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);
|
|
1103
|
+
return this.#withKeyringOrRollback({ v2: false }, () => this.#selectKeyringEntry({ v2: false, selector }), async () => {
|
|
1104
|
+
if (!options.createIfMissing || !('type' in selector)) {
|
|
1105
|
+
return;
|
|
1079
1106
|
}
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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));
|
|
1084
1113
|
}
|
|
1085
1114
|
/**
|
|
1086
1115
|
* Select a keyring and execute the given operation with the selected
|
|
@@ -1144,6 +1173,9 @@ export class KeyringController extends BaseController {
|
|
|
1144
1173
|
* function execution, or rolls back the changes if an error
|
|
1145
1174
|
* is thrown.
|
|
1146
1175
|
*
|
|
1176
|
+
* Only the selected keyring is snapshotted, persisted, and rolled back:
|
|
1177
|
+
* the operation must not mutate other keyrings.
|
|
1178
|
+
*
|
|
1147
1179
|
* @param selector - Keyring selector object.
|
|
1148
1180
|
* @param operation - Function to execute with the wrapped V2 keyring.
|
|
1149
1181
|
* @returns Promise resolving to the result of the function execution.
|
|
@@ -1151,24 +1183,10 @@ export class KeyringController extends BaseController {
|
|
|
1151
1183
|
*/
|
|
1152
1184
|
async withKeyringV2(selector, operation) {
|
|
1153
1185
|
this.#assertIsUnlocked();
|
|
1154
|
-
return this.#
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
});
|
|
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));
|
|
1172
1190
|
}
|
|
1173
1191
|
/**
|
|
1174
1192
|
* Select a keyring, wrap it in a `KeyringV2` adapter, and execute
|
|
@@ -1237,7 +1255,7 @@ export class KeyringController extends BaseController {
|
|
|
1237
1255
|
*/
|
|
1238
1256
|
async withController(operation) {
|
|
1239
1257
|
this.#assertIsUnlocked();
|
|
1240
|
-
return this.#
|
|
1258
|
+
return this.#withTransaction(() => this.#beginWalletTransaction(persistWhenSessionChanged), async (transaction) => {
|
|
1241
1259
|
// Track created and removed keyrings during the operation execution.
|
|
1242
1260
|
const createdEntries = new Set();
|
|
1243
1261
|
const removedEntries = new Set();
|
|
@@ -1273,7 +1291,7 @@ export class KeyringController extends BaseController {
|
|
|
1273
1291
|
},
|
|
1274
1292
|
};
|
|
1275
1293
|
const destroyKeyrings = async (entries) => {
|
|
1276
|
-
await Promise.all([...entries].map((
|
|
1294
|
+
await Promise.all([...entries].map((entry) => transaction.destroy(entry)));
|
|
1277
1295
|
};
|
|
1278
1296
|
let result;
|
|
1279
1297
|
try {
|
|
@@ -1423,13 +1441,14 @@ export class KeyringController extends BaseController {
|
|
|
1423
1441
|
* creates a new wallet with 1 account.
|
|
1424
1442
|
*
|
|
1425
1443
|
* @fires KeyringController:unlock
|
|
1444
|
+
* @param transaction - The transaction creating the new vault.
|
|
1426
1445
|
* @param password - The password to encrypt the vault with.
|
|
1427
1446
|
* @param keyring - A object containing the params to instantiate a new keyring.
|
|
1428
1447
|
* @param keyring.type - The keyring type.
|
|
1429
1448
|
* @param keyring.opts - Optional parameters required to instantiate the keyring.
|
|
1430
1449
|
* @returns A promise that resolves to the state.
|
|
1431
1450
|
*/
|
|
1432
|
-
async #createNewVaultWithKeyring(password, keyring) {
|
|
1451
|
+
async #createNewVaultWithKeyring(transaction, password, keyring) {
|
|
1433
1452
|
this.#assertControllerMutexIsLocked();
|
|
1434
1453
|
if (typeof password !== 'string') {
|
|
1435
1454
|
throw new TypeError(KeyringControllerErrorMessage.WrongPasswordType);
|
|
@@ -1441,7 +1460,7 @@ export class KeyringController extends BaseController {
|
|
|
1441
1460
|
await this.#deriveAndSetEncryptionKey(password, {
|
|
1442
1461
|
ignoreExistingVault: true,
|
|
1443
1462
|
});
|
|
1444
|
-
await this.#clearKeyrings();
|
|
1463
|
+
await this.#clearKeyrings(transaction);
|
|
1445
1464
|
await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);
|
|
1446
1465
|
this.#setUnlocked();
|
|
1447
1466
|
}
|
|
@@ -1568,15 +1587,11 @@ export class KeyringController extends BaseController {
|
|
|
1568
1587
|
}
|
|
1569
1588
|
/**
|
|
1570
1589
|
* Serialize the current array of keyring instances,
|
|
1571
|
-
* including unsupported keyrings
|
|
1590
|
+
* including unsupported keyrings.
|
|
1572
1591
|
*
|
|
1573
|
-
* @param options - Method options.
|
|
1574
|
-
* @param options.includeUnsupported - Whether to include unsupported keyrings.
|
|
1575
1592
|
* @returns The serialized keyrings.
|
|
1576
1593
|
*/
|
|
1577
|
-
async #getSerializedKeyrings(
|
|
1578
|
-
includeUnsupported: true,
|
|
1579
|
-
}) {
|
|
1594
|
+
async #getSerializedKeyrings() {
|
|
1580
1595
|
const serializedKeyrings = await Promise.all(this.#keyrings.map(async ({ keyring, metadata }) => {
|
|
1581
1596
|
return {
|
|
1582
1597
|
type: keyring.type,
|
|
@@ -1584,31 +1599,18 @@ export class KeyringController extends BaseController {
|
|
|
1584
1599
|
metadata,
|
|
1585
1600
|
};
|
|
1586
1601
|
}));
|
|
1587
|
-
|
|
1588
|
-
serializedKeyrings.push(...this.#unsupportedKeyrings);
|
|
1589
|
-
}
|
|
1602
|
+
serializedKeyrings.push(...this.#unsupportedKeyrings);
|
|
1590
1603
|
return serializedKeyrings;
|
|
1591
1604
|
}
|
|
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
|
-
}
|
|
1604
1605
|
/**
|
|
1605
1606
|
* Restore a serialized keyrings array.
|
|
1606
1607
|
*
|
|
1608
|
+
* @param transaction - The transaction restoring the keyrings.
|
|
1607
1609
|
* @param serializedKeyrings - The serialized keyrings array.
|
|
1608
1610
|
* @returns The restored keyrings.
|
|
1609
1611
|
*/
|
|
1610
|
-
async #restoreSerializedKeyrings(serializedKeyrings) {
|
|
1611
|
-
await this.#clearKeyrings();
|
|
1612
|
+
async #restoreSerializedKeyrings(transaction, serializedKeyrings) {
|
|
1613
|
+
await this.#clearKeyrings(transaction);
|
|
1612
1614
|
const keyrings = [];
|
|
1613
1615
|
let hasChanged = false;
|
|
1614
1616
|
for (const serializedKeyring of serializedKeyrings) {
|
|
@@ -1627,10 +1629,11 @@ export class KeyringController extends BaseController {
|
|
|
1627
1629
|
* Unlock Keyrings, decrypting the vault and deserializing all
|
|
1628
1630
|
* keyrings contained in it, using a password or an encryption key with salt.
|
|
1629
1631
|
*
|
|
1632
|
+
* @param transaction - The transaction unlocking the keyrings.
|
|
1630
1633
|
* @param credentials - The credentials to unlock the keyrings.
|
|
1631
1634
|
* @returns A promise resolving to the deserialized keyrings array.
|
|
1632
1635
|
*/
|
|
1633
|
-
async #unlockKeyrings(credentials) {
|
|
1636
|
+
async #unlockKeyrings(transaction, credentials) {
|
|
1634
1637
|
return this.#withVaultLock(async () => {
|
|
1635
1638
|
if (!this.state.vault) {
|
|
1636
1639
|
throw new KeyringControllerError(KeyringControllerErrorMessage.VaultError);
|
|
@@ -1651,7 +1654,7 @@ export class KeyringController extends BaseController {
|
|
|
1651
1654
|
if (!isSerializedKeyringsArray(vault)) {
|
|
1652
1655
|
throw new KeyringControllerError(KeyringControllerErrorMessage.VaultDataError);
|
|
1653
1656
|
}
|
|
1654
|
-
const { keyrings, hasChanged } = await this.#restoreSerializedKeyrings(vault);
|
|
1657
|
+
const { keyrings, hasChanged } = await this.#restoreSerializedKeyrings(transaction, vault);
|
|
1655
1658
|
const updatedKeyrings = await this.#getUpdatedKeyrings();
|
|
1656
1659
|
this.update((state) => {
|
|
1657
1660
|
state.keyrings = updatedKeyrings;
|
|
@@ -1718,14 +1721,10 @@ export class KeyringController extends BaseController {
|
|
|
1718
1721
|
* @returns A promise resolving to an array of accounts.
|
|
1719
1722
|
*/
|
|
1720
1723
|
async #getAccountsFromKeyrings(additionalKeyrings = []) {
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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);
|
|
1724
|
+
return getAccountsFromKeyrings([
|
|
1725
|
+
...this.#keyrings.map(({ keyring }) => keyring),
|
|
1726
|
+
...additionalKeyrings,
|
|
1727
|
+
]);
|
|
1729
1728
|
}
|
|
1730
1729
|
/**
|
|
1731
1730
|
* Create a new keyring, ensuring that the first account is
|
|
@@ -1814,60 +1813,16 @@ export class KeyringController extends BaseController {
|
|
|
1814
1813
|
}
|
|
1815
1814
|
return { keyring, keyringV2, metadata: keyringMetadata };
|
|
1816
1815
|
}
|
|
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
|
-
}
|
|
1863
1816
|
/**
|
|
1864
1817
|
* Remove all managed keyrings, destroying all their
|
|
1865
1818
|
* instances in memory.
|
|
1819
|
+
*
|
|
1820
|
+
* @param transaction - The transaction clearing the keyrings.
|
|
1866
1821
|
*/
|
|
1867
|
-
async #clearKeyrings() {
|
|
1822
|
+
async #clearKeyrings(transaction) {
|
|
1868
1823
|
this.#assertControllerMutexIsLocked();
|
|
1869
|
-
for (const
|
|
1870
|
-
await
|
|
1824
|
+
for (const entry of this.#keyrings) {
|
|
1825
|
+
await transaction.destroy(entry);
|
|
1871
1826
|
}
|
|
1872
1827
|
this.#keyrings = [];
|
|
1873
1828
|
this.#unsupportedKeyrings = [];
|
|
@@ -1912,22 +1867,6 @@ export class KeyringController extends BaseController {
|
|
|
1912
1867
|
return undefined;
|
|
1913
1868
|
}
|
|
1914
1869
|
}
|
|
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
|
-
}
|
|
1931
1870
|
/**
|
|
1932
1871
|
* Assert that there are no duplicate accounts in the keyrings.
|
|
1933
1872
|
*
|
|
@@ -1935,10 +1874,10 @@ export class KeyringController extends BaseController {
|
|
|
1935
1874
|
* @throws If there are duplicate accounts.
|
|
1936
1875
|
*/
|
|
1937
1876
|
async #assertNoDuplicateAccounts(additionalKeyrings = []) {
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1877
|
+
await assertNoDuplicateAccounts([
|
|
1878
|
+
...this.#keyrings.map(({ keyring }) => keyring),
|
|
1879
|
+
...additionalKeyrings,
|
|
1880
|
+
]);
|
|
1942
1881
|
}
|
|
1943
1882
|
/**
|
|
1944
1883
|
* Set the `isUnlocked` to true and notify listeners
|
|
@@ -1964,47 +1903,174 @@ export class KeyringController extends BaseController {
|
|
|
1964
1903
|
}
|
|
1965
1904
|
}
|
|
1966
1905
|
/**
|
|
1967
|
-
* Execute
|
|
1968
|
-
* and
|
|
1969
|
-
*
|
|
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.
|
|
1970
1911
|
*
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
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`.
|
|
1917
|
+
*
|
|
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.
|
|
1973
1926
|
*/
|
|
1974
|
-
async #
|
|
1975
|
-
return this.#
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
//
|
|
1980
|
-
|
|
1981
|
-
|
|
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);
|
|
1982
1944
|
}
|
|
1983
|
-
|
|
1945
|
+
// The vault is updated by the commit, only if the operated keyring
|
|
1946
|
+
// changed.
|
|
1947
|
+
return result;
|
|
1984
1948
|
});
|
|
1985
1949
|
}
|
|
1986
1950
|
/**
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1951
|
+
* Begin a keyring-scoped transaction: select, or create, the entry to
|
|
1952
|
+
* operate on, validate it, and snapshot the operated keyring.
|
|
1989
1953
|
*
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
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()`.
|
|
1985
|
+
*
|
|
1986
|
+
* @returns The vault session.
|
|
1992
1987
|
*/
|
|
1993
|
-
|
|
1994
|
-
return
|
|
1995
|
-
|
|
1996
|
-
|
|
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.
|
|
2044
|
+
*/
|
|
2045
|
+
async #withTransaction(construct, run) {
|
|
2046
|
+
return this.#withControllerLock(async () => {
|
|
2047
|
+
const { transaction, setup } = await construct();
|
|
1997
2048
|
try {
|
|
1998
|
-
|
|
2049
|
+
const result = await run(transaction, setup);
|
|
2050
|
+
await transaction.commit();
|
|
2051
|
+
return result;
|
|
1999
2052
|
}
|
|
2000
2053
|
catch (error) {
|
|
2001
|
-
//
|
|
2002
|
-
|
|
2003
|
-
await this.#restoreSerializedKeyrings(currentSerializedKeyrings);
|
|
2054
|
+
// What the transaction owns is restored to its previous state.
|
|
2055
|
+
await transaction.rollback();
|
|
2004
2056
|
throw error;
|
|
2005
2057
|
}
|
|
2006
2058
|
});
|
|
2007
2059
|
}
|
|
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
|
+
}
|
|
2008
2074
|
/**
|
|
2009
2075
|
* Assert that the controller mutex is locked.
|
|
2010
2076
|
*
|
|
@@ -2100,5 +2166,32 @@ async function withLock(mutex, callback) {
|
|
|
2100
2166
|
function getDefaultKeyringMetadata() {
|
|
2101
2167
|
return { id: ulid(), name: '' };
|
|
2102
2168
|
}
|
|
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
|
+
}
|
|
2103
2196
|
export default KeyringController;
|
|
2104
2197
|
//# sourceMappingURL=KeyringController.js.map
|