@metamask-previews/keyring-controller 28.2.0-preview-66aa9ab6e → 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.
@@ -0,0 +1,483 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ /**
3
+ * The context of an in-flight vault transaction.
4
+ *
5
+ * Every mutation of the vault session runs inside a transaction, created
6
+ * before the mutation begins. The transaction states what it owns — its
7
+ * scope — and tracks what the mutation did to what it owns: `begin()`
8
+ * takes the snapshot that defines the restore point, `rollback()` restores
9
+ * exactly what the scope owns on failure, `hasChanged()` answers whether
10
+ * the scope changed, and `commit()` persists what it owns when its scope
11
+ * says so — the wallet scope's persist policy decides, the keyring scope
12
+ * persists iff the operated keyring changed.
13
+ *
14
+ * ## Contract
15
+ *
16
+ * 1. Every mutation runs inside a transaction. The transaction snapshots
17
+ * exactly what it owns, before any mutation.
18
+ * 2. On failure, the transaction restores exactly what it owns.
19
+ * Everything outside its scope is untouched — instances, references,
20
+ * and all.
21
+ * 3. Mutations made outside a transaction (e.g. through instances handed
22
+ * out by deprecated direct-access getters) are undefined behavior — a
23
+ * bug in the caller, not an input the transaction defends against. The
24
+ * transaction does not reconcile them, and must not amplify them.
25
+ * 4. On success, nothing is destroyed or rebuilt; instances survive in
26
+ * place and references remain valid.
27
+ * 5. When a rebuild is necessary (failure path only), the stale instance
28
+ * is always destroyed before its replacement is created, as it must
29
+ * release the resources shared with the replacement (e.g. the SDK
30
+ * singleton of hardware keyrings) before the replacement is
31
+ * initialized.
32
+ *
33
+ * The transaction is also the home of the data it accumulates about
34
+ * itself, which must not outlive it: the keyring instances it destroyed,
35
+ * including partially destroyed ones — destruction releases resources
36
+ * that `serialize()` does not capture (e.g. the bridge of a hardware
37
+ * keyring), so a rollback must rebuild a destroyed instance even when its
38
+ * serialized state still matches its snapshot.
39
+ */
40
+ export class VaultTransaction {
41
+ #scope;
42
+ #session;
43
+ #operations;
44
+ #destroyedKeyrings = new WeakSet();
45
+ #walletSnapshot;
46
+ #keyringSnapshot;
47
+ /**
48
+ * @param scope - What the transaction owns.
49
+ * @param session - The vault session state the transaction operates on.
50
+ * @param operations - The controller-owned primitives the transaction
51
+ * drives.
52
+ */
53
+ constructor(scope, session, operations) {
54
+ this.#scope = scope;
55
+ this.#session = session;
56
+ this.#operations = operations;
57
+ }
58
+ /**
59
+ * What the transaction owns — and therefore what it restores on failure.
60
+ *
61
+ * @returns The transaction scope.
62
+ */
63
+ get scope() {
64
+ return this.#scope;
65
+ }
66
+ /**
67
+ * Take the pre-mutation snapshot that defines the restore point: every
68
+ * keyring of the wallet for the wallet scope, the operated keyring for
69
+ * the keyring scope (skipped when the transaction created it), plus the
70
+ * wholesale-captured session state.
71
+ *
72
+ * Must be called once, before the transaction's first mutation; a
73
+ * failure leaves the session untouched.
74
+ */
75
+ async begin() {
76
+ const { type } = this.#scope;
77
+ if (type === 'wallet') {
78
+ const keyrings = this.#session.getKeyrings();
79
+ this.#walletSnapshot = {
80
+ keyrings: await Promise.all(keyrings.map((entry) => this.#snapshotKeyring(entry, keyrings))),
81
+ unsupportedKeyrings: cloneDeep(this.#session.getUnsupportedKeyrings()),
82
+ encryptionKey: cloneDeep(this.#session.getEncryptionKey()),
83
+ };
84
+ return;
85
+ }
86
+ if (!this.#scope.created) {
87
+ this.#keyringSnapshot = await this.#snapshotKeyring(this.#scope.entry, this.#session.getKeyrings());
88
+ }
89
+ }
90
+ /**
91
+ * Check whether what the scope owns has changed since `begin()`.
92
+ *
93
+ * For the keyring scope, the operated keyring is resolved by identity:
94
+ * another keyring sharing its id must not affect the decision. For the
95
+ * wallet scope, the whole session state — the serialized keyrings, the
96
+ * unsupported keyrings, and the encryption key — is compared against the
97
+ * state captured at `begin()`.
98
+ *
99
+ * @returns Whether the scope has changed.
100
+ */
101
+ async hasChanged() {
102
+ const scope = this.#scope;
103
+ if (scope.type === 'wallet') {
104
+ return this.#hasWalletChanged();
105
+ }
106
+ const { entry, created } = scope;
107
+ const exists = this.#session.getKeyrings().includes(entry);
108
+ // A keyring created by the transaction is a change — unless the
109
+ // transaction drained it right away, leaving the session as it was
110
+ // before the keyring existed.
111
+ if (created) {
112
+ return exists;
113
+ }
114
+ // The operated keyring was removed by the drained-keyring cleanup.
115
+ if (!exists) {
116
+ return true;
117
+ }
118
+ return !(await this.#isUnchanged(entry, this.#requireKeyringSnapshot()));
119
+ }
120
+ /**
121
+ * Persist what the scope owns, iff it changed: the keyring scope
122
+ * persists the vault when the operated keyring changed, and the wallet
123
+ * scope when its persist policy says so.
124
+ *
125
+ * Must be called while the controller mutex is held, after `begin()` and
126
+ * a successful operation.
127
+ */
128
+ async commit() {
129
+ const scope = this.#scope;
130
+ if (scope.type === 'keyring') {
131
+ if (await this.hasChanged()) {
132
+ await this.#operations.updateVault();
133
+ }
134
+ return;
135
+ }
136
+ if (await scope.persist(() => this.hasChanged())) {
137
+ await this.#operations.updateVault();
138
+ }
139
+ }
140
+ /**
141
+ * Restore what the scope owns from the snapshot taken at `begin()`:
142
+ * every keyring of the wallet, plus the unsupported keyrings and the
143
+ * encryption key, for the wallet scope; the single operated keyring for
144
+ * the keyring scope. Everything outside the scope is untouched.
145
+ *
146
+ * Must be called while the controller mutex is held, and only after
147
+ * `begin()`.
148
+ */
149
+ async rollback() {
150
+ const scope = this.#scope;
151
+ if (scope.type === 'wallet') {
152
+ await this.#rollbackWallet();
153
+ return;
154
+ }
155
+ await this.#rollbackKeyring(scope);
156
+ }
157
+ /**
158
+ * Destroy the given keyring entry's instances, marking the keyring as
159
+ * destroyed by this transaction — even when the destruction fails
160
+ * partway, as the rollback must rebuild it rather than keep it, whatever
161
+ * resources it already released.
162
+ *
163
+ * @param entry - The keyring entry to destroy.
164
+ * @param options - Destroy options.
165
+ * @param options.ignoreErrors - Whether to log destruction errors
166
+ * instead of throwing them: a rollback must not let them mask the
167
+ * transaction's own error.
168
+ */
169
+ async destroy(entry, { ignoreErrors } = { ignoreErrors: false }) {
170
+ try {
171
+ await entry.keyring.destroy?.();
172
+ await entry.keyringV2?.destroy?.();
173
+ }
174
+ catch (error) {
175
+ if (!ignoreErrors) {
176
+ throw error;
177
+ }
178
+ console.error(error);
179
+ }
180
+ finally {
181
+ this.#destroyedKeyrings.add(entry.keyring);
182
+ }
183
+ }
184
+ /**
185
+ * Check whether the given keyring entry's keyring was destroyed during
186
+ * this transaction, including partially: such a keyring must be treated
187
+ * as changed by the transaction, and rebuilt on rollback, even when its
188
+ * serialized state still matches its snapshot.
189
+ *
190
+ * @param entry - The keyring entry to check.
191
+ * @returns Whether the entry's keyring was destroyed.
192
+ */
193
+ isDestroyed(entry) {
194
+ return this.#destroyedKeyrings.has(entry.keyring);
195
+ }
196
+ /**
197
+ * Remove the given keyring entry from the session keyrings, destroying
198
+ * its instances. A no-op for an entry that is not registered: a missing
199
+ * entry must never fall back to another position.
200
+ *
201
+ * @param entry - The keyring entry to remove.
202
+ */
203
+ async remove(entry) {
204
+ if (this.#session.removeKeyring(entry)) {
205
+ await this.destroy(entry);
206
+ }
207
+ }
208
+ /**
209
+ * Take a snapshot of the given keyring entry, to roll back to.
210
+ *
211
+ * @param entry - The keyring entry.
212
+ * @param keyrings - The keyrings the entry is registered in, resolving
213
+ * the entry's position.
214
+ * @returns The keyring snapshot.
215
+ */
216
+ async #snapshotKeyring(entry, keyrings) {
217
+ // Cloned, as keyrings may return aliased internal state from
218
+ // `serialize()`.
219
+ return {
220
+ entry,
221
+ index: keyrings.indexOf(entry),
222
+ type: entry.keyring.type,
223
+ data: cloneDeep(await entry.keyring.serialize()),
224
+ metadata: { ...entry.metadata },
225
+ };
226
+ }
227
+ /**
228
+ * Reconcile the keyrings in memory with the pre-transaction snapshot,
229
+ * touching only the keyrings that the failed transaction changed.
230
+ *
231
+ * The snapshot is walked in order, preserving the keyring order:
232
+ * unchanged keyrings keep their instances, mutated keyrings are rebuilt
233
+ * in place, removed keyrings are recreated at their snapshot position,
234
+ * and keyrings created by the transaction are destroyed and dropped. A
235
+ * stale instance is always destroyed before its replacement is created,
236
+ * as it must release the resources shared with the replacement (e.g.
237
+ * the SDK singleton of hardware keyrings) before the replacement is
238
+ * initialized.
239
+ */
240
+ async #rollbackWallet() {
241
+ const snapshot = this.#requireWalletSnapshot();
242
+ // Keyrings and encryption credentials are restored to their previous
243
+ // state.
244
+ this.#session.setEncryptionKey(snapshot.encryptionKey);
245
+ // Restored wholesale before the walk: this undoes any parking
246
+ // performed by the failed transaction, while keyrings the walk fails
247
+ // to recreate are parked on top.
248
+ this.#session.setUnsupportedKeyrings(snapshot.unsupportedKeyrings);
249
+ // The keyrings live at the time of the rollback, before the walk
250
+ // replaces them below.
251
+ const oldKeyrings = this.#session.getKeyrings();
252
+ // Built locally, and assigned once after the reconciliation: readers
253
+ // never observe a partially reconciled list, while duplicate account
254
+ // checks during recreation only consider already reconciled keyrings.
255
+ const reconciled = [];
256
+ for (const keyringSnapshot of snapshot.keyrings) {
257
+ const old = keyringSnapshot.entry;
258
+ if (!oldKeyrings.includes(old)) {
259
+ // Removed and destroyed by the transaction: rebuilt from its
260
+ // snapshot, at its original position.
261
+ const entry = await this.#recreate(undefined, keyringSnapshot, {
262
+ duplicateCheckBase: reconciled,
263
+ });
264
+ if (entry) {
265
+ reconciled.push(entry);
266
+ }
267
+ continue;
268
+ }
269
+ if (await this.#isUnchanged(old, keyringSnapshot)) {
270
+ // The transaction did not change the keyring: keep the old
271
+ // instance.
272
+ reconciled.push(old);
273
+ continue;
274
+ }
275
+ // The transaction mutated the keyring: rebuilt from its snapshot,
276
+ // the stale instance destroyed by the recreation itself.
277
+ const entry = await this.#recreate(old, keyringSnapshot, {
278
+ duplicateCheckBase: reconciled,
279
+ });
280
+ if (entry) {
281
+ reconciled.push(entry);
282
+ }
283
+ }
284
+ // Whatever is left was created by the failed transaction: destroy and
285
+ // drop it.
286
+ const snapshotEntries = new Set(snapshot.keyrings.map((keyringSnapshot) => keyringSnapshot.entry));
287
+ for (const leftover of oldKeyrings.filter(
288
+ // Keyring entries that are not present in the snapshots, i.e.,
289
+ // created by the failed transaction, so we need to destroy and drop
290
+ // them.
291
+ (old) => !snapshotEntries.has(old))) {
292
+ // Destruction failures must not mask the transaction's error.
293
+ await this.destroy(leftover, { ignoreErrors: true });
294
+ }
295
+ // Assigned once, after the reconciliation: everything outside
296
+ // observed a complete list — the pre-rollback one until now, the
297
+ // reconciled one from here on.
298
+ this.#session.replaceKeyrings(reconciled);
299
+ }
300
+ /**
301
+ * Restore the keyring entry operated on by a failed keyring-scoped
302
+ * transaction from its snapshot, touching no other keyring: a keyring
303
+ * created by the transaction is destroyed and dropped, and an existing
304
+ * keyring is always rebuilt from its snapshot — whether or not the
305
+ * transaction changed or removed it — so that state `serialize()` does
306
+ * not capture is reset too, and the stale instance is detached from any
307
+ * of the operation's mutations still landing on it.
308
+ *
309
+ * @param scope - The keyring scope of the failed transaction, holding
310
+ * the operated entry and whether the transaction created it.
311
+ */
312
+ async #rollbackKeyring(scope) {
313
+ const { entry, created } = scope;
314
+ if (created) {
315
+ // The failed transaction created the keyring: destroy it and drop
316
+ // it.
317
+ this.#session.removeKeyring(entry);
318
+ await this.destroy(entry, { ignoreErrors: true });
319
+ return;
320
+ }
321
+ const snapshot = this.#requireKeyringSnapshot();
322
+ // `-1` when the transaction already removed the entry from the
323
+ // keyrings (e.g. the drained-keyring cleanup).
324
+ const keyrings = this.#session.getKeyrings();
325
+ const index = keyrings.indexOf(entry);
326
+ if (index === -1) {
327
+ // The transaction removed the keyring: rebuild it at its original
328
+ // position.
329
+ const newEntry = await this.#recreate(undefined, snapshot);
330
+ if (newEntry) {
331
+ this.#session.insertKeyringAt(snapshot.index, newEntry);
332
+ }
333
+ return;
334
+ }
335
+ // The keyring is always rebuilt on failure, even when its serialized
336
+ // state matches the snapshot: state that `serialize()` does not
337
+ // capture (e.g. the runtime-only state of hardware keyrings) is
338
+ // reset too, and the stale instance is detached from any of the
339
+ // operation's mutations still landing on it. The old entry is removed
340
+ // first, then destroyed by the recreation itself, before its
341
+ // replacement takes back its position.
342
+ this.#session.removeKeyring(entry);
343
+ const newEntry = await this.#recreate(entry, snapshot);
344
+ if (newEntry) {
345
+ this.#session.insertKeyringAt(index, newEntry);
346
+ }
347
+ }
348
+ /**
349
+ * Build a new keyring entry from a keyring snapshot, without updating
350
+ * the session keyrings. The old keyring entry, if any, is destroyed
351
+ * before the replacement is created, as it must release the resources
352
+ * shared with the replacement (e.g. the SDK singleton of hardware
353
+ * keyrings) before the replacement is initialized. On failure, mirrors
354
+ * the controller's keyring restoration: the error is logged and the
355
+ * serialized keyring is parked in the unsupported keyrings.
356
+ *
357
+ * @param old - The old keyring entry replaced by the recreated keyring,
358
+ * or `undefined` if the transaction already destroyed it.
359
+ * @param snapshot - The keyring snapshot to recreate.
360
+ * @param options - Recreation options.
361
+ * @param options.duplicateCheckBase - The already reconciled keyrings
362
+ * the recreated keyring is checked against for duplicate accounts;
363
+ * when omitted, the check is skipped: the wallet-scope rollback checks
364
+ * against the reconciled-so-far keyrings, while the keyring-scope
365
+ * rollback owns the only keyring that changed, so a duplicate would
366
+ * come from a mutation made outside a transaction.
367
+ * @returns The new keyring entry, or `undefined` if it could not be
368
+ * recreated.
369
+ */
370
+ async #recreate(old, snapshot, { duplicateCheckBase } = {}) {
371
+ try {
372
+ if (old) {
373
+ await this.destroy(old, { ignoreErrors: true });
374
+ }
375
+ const entry = await this.#operations.createKeyring(snapshot.type, snapshot.data, snapshot.metadata);
376
+ if (duplicateCheckBase) {
377
+ await this.#operations.assertNoDuplicateAccounts([
378
+ ...duplicateCheckBase.map(({ keyring }) => keyring),
379
+ entry.keyring,
380
+ ]);
381
+ }
382
+ return entry;
383
+ }
384
+ catch (error) {
385
+ console.error(error);
386
+ // Only the serialized keyring is parked: the snapshot position must
387
+ // not leak into the vault.
388
+ this.#session.parkUnsupportedKeyring({
389
+ type: snapshot.type,
390
+ data: snapshot.data,
391
+ metadata: snapshot.metadata,
392
+ });
393
+ return undefined;
394
+ }
395
+ }
396
+ /**
397
+ * Check whether the given keyring entry's serialized state is unchanged
398
+ * relative to the given snapshot. A keyring destroyed during this
399
+ * transaction — even one whose destruction failed partway — and a
400
+ * keyring that cannot be serialized anymore are treated as changed:
401
+ * destruction releases resources that `serialize()` does not capture.
402
+ *
403
+ * @param entry - The keyring entry to check.
404
+ * @param snapshot - The keyring snapshot to check against.
405
+ * @returns Whether the keyring is unchanged.
406
+ */
407
+ async #isUnchanged(entry, snapshot) {
408
+ if (this.isDestroyed(entry)) {
409
+ return false;
410
+ }
411
+ try {
412
+ return (JSON.stringify(await entry.keyring.serialize()) ===
413
+ JSON.stringify(snapshot.data));
414
+ }
415
+ catch {
416
+ return false;
417
+ }
418
+ }
419
+ /**
420
+ * Check whether the wallet session state changed since the snapshot
421
+ * taken at `begin()`: the serialized keyrings, the unsupported keyrings,
422
+ * and the encryption key are compared, as a whole, against the state
423
+ * captured at `begin()`.
424
+ *
425
+ * @returns Whether the session state changed.
426
+ */
427
+ async #hasWalletChanged() {
428
+ const snapshot = this.#requireWalletSnapshot();
429
+ // The session state at `begin()`, reconstructed from the snapshot.
430
+ const oldState = JSON.stringify(this.#sessionStateOf(snapshot.keyrings.map(({ type, data, metadata }) => ({
431
+ type,
432
+ data,
433
+ metadata,
434
+ })), snapshot.unsupportedKeyrings, snapshot.encryptionKey));
435
+ // The session state now, serialized live.
436
+ const keyrings = this.#session.getKeyrings();
437
+ const serializedKeyrings = await Promise.all(keyrings.map(async ({ keyring, metadata }) => ({
438
+ type: keyring.type,
439
+ data: await keyring.serialize(),
440
+ metadata,
441
+ })));
442
+ const newState = JSON.stringify(this.#sessionStateOf(serializedKeyrings, this.#session.getUnsupportedKeyrings(), this.#session.getEncryptionKey()));
443
+ return oldState !== newState;
444
+ }
445
+ /**
446
+ * Assemble the session state from its parts, so that the two sides of a
447
+ * change comparison always take the same shape.
448
+ *
449
+ * @param keyrings - The serialized keyrings.
450
+ * @param unsupportedKeyrings - The unsupported keyrings.
451
+ * @param encryptionKey - The cached encryption key, if any.
452
+ * @returns The session state.
453
+ */
454
+ #sessionStateOf(keyrings, unsupportedKeyrings, encryptionKey) {
455
+ return {
456
+ keyrings: [...keyrings, ...unsupportedKeyrings],
457
+ encryptionKey,
458
+ };
459
+ }
460
+ /**
461
+ * The snapshot of the wallet session, taken at `begin()`.
462
+ *
463
+ * @returns The wallet snapshot.
464
+ */
465
+ #requireWalletSnapshot() {
466
+ if (!this.#walletSnapshot) {
467
+ throw new Error('The wallet transaction was not begun');
468
+ }
469
+ return this.#walletSnapshot;
470
+ }
471
+ /**
472
+ * The snapshot of the operated keyring, taken at `begin()`.
473
+ *
474
+ * @returns The keyring snapshot.
475
+ */
476
+ #requireKeyringSnapshot() {
477
+ if (!this.#keyringSnapshot) {
478
+ throw new Error('The keyring transaction was not begun');
479
+ }
480
+ return this.#keyringSnapshot;
481
+ }
482
+ }
483
+ //# sourceMappingURL=VaultTransaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VaultTransaction.js","sourceRoot":"","sources":["../src/VaultTransaction.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EthKeyring } from '@metamask/keyring-internal-api';\nimport { cloneDeep } from 'lodash-es';\n\nimport type {\n CachedEncryptionKey,\n KeyringEntry,\n KeyringMetadata,\n SerializedKeyring,\n} from './KeyringController.js';\n\n/**\n * The scope of a vault transaction: what it owns — and therefore what it\n * restores on failure. Everything outside the scope is untouched, even on\n * failure: instances, references, and all.\n */\nexport type TransactionScope = WalletTransactionScope | KeyringTransactionScope;\n\n/**\n * The wallet transaction scope: the transaction owns the whole wallet —\n * all keyrings, the unsupported keyrings, and the encryption key.\n */\nexport type WalletTransactionScope = {\n type: 'wallet';\n /**\n * Decides, when the transaction commits, whether the vault is written —\n * possibly preparing the write first (e.g. re-deriving the encryption\n * key). The session-change status is given as a lazy check, so that\n * policies that ignore it never pay for it.\n */\n persist: VaultPersistPolicy;\n};\n\n/**\n * The wallet-scope persist policy: answers, when the transaction commits,\n * whether the vault is written.\n *\n * The standard policy persists only when the session state changed since\n * `begin()`; other flows force the write, decline it, or gate it on their\n * own criterion.\n */\nexport type VaultPersistPolicy = (\n hasChanged: () => Promise<boolean>,\n) => boolean | Promise<boolean>;\n\n/**\n * The keyring transaction scope: the transaction owns a single keyring\n * entry, selected or created before the transaction began.\n */\nexport type KeyringTransactionScope = {\n type: 'keyring';\n /**\n * The operated keyring entry.\n */\n entry: KeyringEntry;\n /**\n * Whether the transaction created the operated keyring entry.\n */\n created: boolean;\n};\n\n/**\n * The live vault session state the transactions operate on: what the\n * transaction snapshots at `begin()`, mutates through, and restores on\n * `rollback()`. The controller owns the real state and implements this\n * interface; every member reads or rewrites the state it owns.\n */\nexport type VaultSession = {\n /**\n * The keyrings currently registered, as a fresh array: mutations go\n * through the other members.\n */\n getKeyrings(): KeyringEntry[];\n /**\n * Replace the registered keyrings wholesale.\n */\n replaceKeyrings(keyrings: KeyringEntry[]): void;\n /**\n * Remove the given entry from the registered keyrings, without touching\n * the others. A no-op for an entry that is not registered: a missing\n * entry must never fall back to another position.\n *\n * @returns Whether the entry was registered.\n */\n removeKeyring(entry: KeyringEntry): boolean;\n /**\n * Insert the given entry at the given position in the registered\n * keyrings.\n */\n insertKeyringAt(index: number, entry: KeyringEntry): void;\n /**\n * The unsupported keyrings currently parked, as a fresh array.\n */\n getUnsupportedKeyrings(): SerializedKeyring[];\n /**\n * Replace the unsupported keyrings wholesale.\n */\n setUnsupportedKeyrings(keyrings: SerializedKeyring[]): void;\n /**\n * Park the given serialized keyring in the unsupported keyrings.\n */\n parkUnsupportedKeyring(keyring: SerializedKeyring): void;\n /**\n * The cached encryption key, if any.\n */\n getEncryptionKey(): CachedEncryptionKey | undefined;\n /**\n * Set, or clear, the cached encryption key.\n */\n setEncryptionKey(key: CachedEncryptionKey | undefined): void;\n};\n\n/**\n * The controller-owned primitives the transactions drive: building new\n * keyring entries, and enforcing the no-duplicate-accounts invariant on an\n * explicit list of keyrings.\n */\nexport type VaultOperations = {\n /**\n * Build a keyring entry from its serialized parts, without registering\n * it.\n */\n createKeyring(\n type: string,\n data?: unknown,\n metadata?: KeyringMetadata,\n ): Promise<KeyringEntry>;\n /**\n * Assert that the given keyrings hold no duplicate account.\n *\n * @throws If the keyrings hold a duplicate account.\n */\n assertNoDuplicateAccounts(keyrings: EthKeyring[]): Promise<void>;\n /**\n * Update the vault with the current session state.\n */\n updateVault(): Promise<boolean>;\n};\n\n/**\n * A serialized keyring snapshot, with the keyring's position in the\n * keyrings array at the time the snapshot was taken, and the keyring entry\n * itself, resolving the snapshot back to its keyring by identity — never\n * by metadata id, which corrupted vaults may duplicate.\n */\ntype KeyringSnapshot = SerializedKeyring & {\n entry: KeyringEntry;\n index: number;\n};\n\n/**\n * The wallet-scope restore point: pre-transaction snapshots of all keyrings\n * plus the wholesale-captured session state. Unsupported keyrings hold no\n * keyring instances, so they are captured and restored wholesale.\n */\ntype WalletSnapshot = {\n keyrings: KeyringSnapshot[];\n unsupportedKeyrings: SerializedKeyring[];\n encryptionKey?: CachedEncryptionKey;\n};\n\n/**\n * The context of an in-flight vault transaction.\n *\n * Every mutation of the vault session runs inside a transaction, created\n * before the mutation begins. The transaction states what it owns — its\n * scope — and tracks what the mutation did to what it owns: `begin()`\n * takes the snapshot that defines the restore point, `rollback()` restores\n * exactly what the scope owns on failure, `hasChanged()` answers whether\n * the scope changed, and `commit()` persists what it owns when its scope\n * says so — the wallet scope's persist policy decides, the keyring scope\n * persists iff the operated keyring changed.\n *\n * ## Contract\n *\n * 1. Every mutation runs inside a transaction. The transaction snapshots\n * exactly what it owns, before any mutation.\n * 2. On failure, the transaction restores exactly what it owns.\n * Everything outside its scope is untouched — instances, references,\n * and all.\n * 3. Mutations made outside a transaction (e.g. through instances handed\n * out by deprecated direct-access getters) are undefined behavior — a\n * bug in the caller, not an input the transaction defends against. The\n * transaction does not reconcile them, and must not amplify them.\n * 4. On success, nothing is destroyed or rebuilt; instances survive in\n * place and references remain valid.\n * 5. When a rebuild is necessary (failure path only), the stale instance\n * is always destroyed before its replacement is created, as it must\n * release the resources shared with the replacement (e.g. the SDK\n * singleton of hardware keyrings) before the replacement is\n * initialized.\n *\n * The transaction is also the home of the data it accumulates about\n * itself, which must not outlive it: the keyring instances it destroyed,\n * including partially destroyed ones — destruction releases resources\n * that `serialize()` does not capture (e.g. the bridge of a hardware\n * keyring), so a rollback must rebuild a destroyed instance even when its\n * serialized state still matches its snapshot.\n */\nexport class VaultTransaction {\n readonly #scope: TransactionScope;\n\n readonly #session: VaultSession;\n\n readonly #operations: VaultOperations;\n\n readonly #destroyedKeyrings = new WeakSet<EthKeyring>();\n\n #walletSnapshot?: WalletSnapshot;\n\n #keyringSnapshot?: KeyringSnapshot;\n\n /**\n * @param scope - What the transaction owns.\n * @param session - The vault session state the transaction operates on.\n * @param operations - The controller-owned primitives the transaction\n * drives.\n */\n constructor(\n scope: TransactionScope,\n session: VaultSession,\n operations: VaultOperations,\n ) {\n this.#scope = scope;\n this.#session = session;\n this.#operations = operations;\n }\n\n /**\n * What the transaction owns — and therefore what it restores on failure.\n *\n * @returns The transaction scope.\n */\n get scope(): TransactionScope {\n return this.#scope;\n }\n\n /**\n * Take the pre-mutation snapshot that defines the restore point: every\n * keyring of the wallet for the wallet scope, the operated keyring for\n * the keyring scope (skipped when the transaction created it), plus the\n * wholesale-captured session state.\n *\n * Must be called once, before the transaction's first mutation; a\n * failure leaves the session untouched.\n */\n async begin(): Promise<void> {\n const { type } = this.#scope;\n\n if (type === 'wallet') {\n const keyrings = this.#session.getKeyrings();\n this.#walletSnapshot = {\n keyrings: await Promise.all(\n keyrings.map((entry) => this.#snapshotKeyring(entry, keyrings)),\n ),\n unsupportedKeyrings: cloneDeep(this.#session.getUnsupportedKeyrings()),\n encryptionKey: cloneDeep(this.#session.getEncryptionKey()),\n };\n return;\n }\n\n if (!this.#scope.created) {\n this.#keyringSnapshot = await this.#snapshotKeyring(\n this.#scope.entry,\n this.#session.getKeyrings(),\n );\n }\n }\n\n /**\n * Check whether what the scope owns has changed since `begin()`.\n *\n * For the keyring scope, the operated keyring is resolved by identity:\n * another keyring sharing its id must not affect the decision. For the\n * wallet scope, the whole session state — the serialized keyrings, the\n * unsupported keyrings, and the encryption key — is compared against the\n * state captured at `begin()`.\n *\n * @returns Whether the scope has changed.\n */\n async hasChanged(): Promise<boolean> {\n const scope = this.#scope;\n\n if (scope.type === 'wallet') {\n return this.#hasWalletChanged();\n }\n\n const { entry, created } = scope;\n const exists = this.#session.getKeyrings().includes(entry);\n\n // A keyring created by the transaction is a change — unless the\n // transaction drained it right away, leaving the session as it was\n // before the keyring existed.\n if (created) {\n return exists;\n }\n\n // The operated keyring was removed by the drained-keyring cleanup.\n if (!exists) {\n return true;\n }\n\n return !(await this.#isUnchanged(entry, this.#requireKeyringSnapshot()));\n }\n\n /**\n * Persist what the scope owns, iff it changed: the keyring scope\n * persists the vault when the operated keyring changed, and the wallet\n * scope when its persist policy says so.\n *\n * Must be called while the controller mutex is held, after `begin()` and\n * a successful operation.\n */\n async commit(): Promise<void> {\n const scope = this.#scope;\n\n if (scope.type === 'keyring') {\n if (await this.hasChanged()) {\n await this.#operations.updateVault();\n }\n return;\n }\n\n if (await scope.persist(() => this.hasChanged())) {\n await this.#operations.updateVault();\n }\n }\n\n /**\n * Restore what the scope owns from the snapshot taken at `begin()`:\n * every keyring of the wallet, plus the unsupported keyrings and the\n * encryption key, for the wallet scope; the single operated keyring for\n * the keyring scope. Everything outside the scope is untouched.\n *\n * Must be called while the controller mutex is held, and only after\n * `begin()`.\n */\n async rollback(): Promise<void> {\n const scope = this.#scope;\n\n if (scope.type === 'wallet') {\n await this.#rollbackWallet();\n return;\n }\n\n await this.#rollbackKeyring(scope);\n }\n\n /**\n * Destroy the given keyring entry's instances, marking the keyring as\n * destroyed by this transaction — even when the destruction fails\n * partway, as the rollback must rebuild it rather than keep it, whatever\n * resources it already released.\n *\n * @param entry - The keyring entry to destroy.\n * @param options - Destroy options.\n * @param options.ignoreErrors - Whether to log destruction errors\n * instead of throwing them: a rollback must not let them mask the\n * transaction's own error.\n */\n async destroy(\n entry: KeyringEntry,\n { ignoreErrors }: { ignoreErrors: boolean } = { ignoreErrors: false },\n ): Promise<void> {\n try {\n await entry.keyring.destroy?.();\n await entry.keyringV2?.destroy?.();\n } catch (error) {\n if (!ignoreErrors) {\n throw error;\n }\n console.error(error);\n } finally {\n this.#destroyedKeyrings.add(entry.keyring);\n }\n }\n\n /**\n * Check whether the given keyring entry's keyring was destroyed during\n * this transaction, including partially: such a keyring must be treated\n * as changed by the transaction, and rebuilt on rollback, even when its\n * serialized state still matches its snapshot.\n *\n * @param entry - The keyring entry to check.\n * @returns Whether the entry's keyring was destroyed.\n */\n isDestroyed(entry: KeyringEntry): boolean {\n return this.#destroyedKeyrings.has(entry.keyring);\n }\n\n /**\n * Remove the given keyring entry from the session keyrings, destroying\n * its instances. A no-op for an entry that is not registered: a missing\n * entry must never fall back to another position.\n *\n * @param entry - The keyring entry to remove.\n */\n async remove(entry: KeyringEntry): Promise<void> {\n if (this.#session.removeKeyring(entry)) {\n await this.destroy(entry);\n }\n }\n\n /**\n * Take a snapshot of the given keyring entry, to roll back to.\n *\n * @param entry - The keyring entry.\n * @param keyrings - The keyrings the entry is registered in, resolving\n * the entry's position.\n * @returns The keyring snapshot.\n */\n async #snapshotKeyring(\n entry: KeyringEntry,\n keyrings: KeyringEntry[],\n ): Promise<KeyringSnapshot> {\n // Cloned, as keyrings may return aliased internal state from\n // `serialize()`.\n return {\n entry,\n index: keyrings.indexOf(entry),\n type: entry.keyring.type,\n data: cloneDeep(await entry.keyring.serialize()),\n metadata: { ...entry.metadata },\n };\n }\n\n /**\n * Reconcile the keyrings in memory with the pre-transaction snapshot,\n * touching only the keyrings that the failed transaction changed.\n *\n * The snapshot is walked in order, preserving the keyring order:\n * unchanged keyrings keep their instances, mutated keyrings are rebuilt\n * in place, removed keyrings are recreated at their snapshot position,\n * and keyrings created by the transaction are destroyed and dropped. A\n * stale instance is always destroyed before its replacement is created,\n * as it must release the resources shared with the replacement (e.g.\n * the SDK singleton of hardware keyrings) before the replacement is\n * initialized.\n */\n async #rollbackWallet(): Promise<void> {\n const snapshot = this.#requireWalletSnapshot();\n\n // Keyrings and encryption credentials are restored to their previous\n // state.\n this.#session.setEncryptionKey(snapshot.encryptionKey);\n\n // Restored wholesale before the walk: this undoes any parking\n // performed by the failed transaction, while keyrings the walk fails\n // to recreate are parked on top.\n this.#session.setUnsupportedKeyrings(snapshot.unsupportedKeyrings);\n\n // The keyrings live at the time of the rollback, before the walk\n // replaces them below.\n const oldKeyrings = this.#session.getKeyrings();\n\n // Built locally, and assigned once after the reconciliation: readers\n // never observe a partially reconciled list, while duplicate account\n // checks during recreation only consider already reconciled keyrings.\n const reconciled: KeyringEntry[] = [];\n\n for (const keyringSnapshot of snapshot.keyrings) {\n const old = keyringSnapshot.entry;\n\n if (!oldKeyrings.includes(old)) {\n // Removed and destroyed by the transaction: rebuilt from its\n // snapshot, at its original position.\n const entry = await this.#recreate(undefined, keyringSnapshot, {\n duplicateCheckBase: reconciled,\n });\n if (entry) {\n reconciled.push(entry);\n }\n continue;\n }\n\n if (await this.#isUnchanged(old, keyringSnapshot)) {\n // The transaction did not change the keyring: keep the old\n // instance.\n reconciled.push(old);\n continue;\n }\n\n // The transaction mutated the keyring: rebuilt from its snapshot,\n // the stale instance destroyed by the recreation itself.\n const entry = await this.#recreate(old, keyringSnapshot, {\n duplicateCheckBase: reconciled,\n });\n if (entry) {\n reconciled.push(entry);\n }\n }\n\n // Whatever is left was created by the failed transaction: destroy and\n // drop it.\n const snapshotEntries = new Set(\n snapshot.keyrings.map((keyringSnapshot) => keyringSnapshot.entry),\n );\n for (const leftover of oldKeyrings.filter(\n // Keyring entries that are not present in the snapshots, i.e.,\n // created by the failed transaction, so we need to destroy and drop\n // them.\n (old) => !snapshotEntries.has(old),\n )) {\n // Destruction failures must not mask the transaction's error.\n await this.destroy(leftover, { ignoreErrors: true });\n }\n\n // Assigned once, after the reconciliation: everything outside\n // observed a complete list — the pre-rollback one until now, the\n // reconciled one from here on.\n this.#session.replaceKeyrings(reconciled);\n }\n\n /**\n * Restore the keyring entry operated on by a failed keyring-scoped\n * transaction from its snapshot, touching no other keyring: a keyring\n * created by the transaction is destroyed and dropped, and an existing\n * keyring is always rebuilt from its snapshot — whether or not the\n * transaction changed or removed it — so that state `serialize()` does\n * not capture is reset too, and the stale instance is detached from any\n * of the operation's mutations still landing on it.\n *\n * @param scope - The keyring scope of the failed transaction, holding\n * the operated entry and whether the transaction created it.\n */\n async #rollbackKeyring(scope: KeyringTransactionScope): Promise<void> {\n const { entry, created } = scope;\n\n if (created) {\n // The failed transaction created the keyring: destroy it and drop\n // it.\n this.#session.removeKeyring(entry);\n await this.destroy(entry, { ignoreErrors: true });\n return;\n }\n\n const snapshot = this.#requireKeyringSnapshot();\n\n // `-1` when the transaction already removed the entry from the\n // keyrings (e.g. the drained-keyring cleanup).\n const keyrings = this.#session.getKeyrings();\n const index = keyrings.indexOf(entry);\n\n if (index === -1) {\n // The transaction removed the keyring: rebuild it at its original\n // position.\n const newEntry = await this.#recreate(undefined, snapshot);\n if (newEntry) {\n this.#session.insertKeyringAt(snapshot.index, newEntry);\n }\n return;\n }\n\n // The keyring is always rebuilt on failure, even when its serialized\n // state matches the snapshot: state that `serialize()` does not\n // capture (e.g. the runtime-only state of hardware keyrings) is\n // reset too, and the stale instance is detached from any of the\n // operation's mutations still landing on it. The old entry is removed\n // first, then destroyed by the recreation itself, before its\n // replacement takes back its position.\n this.#session.removeKeyring(entry);\n const newEntry = await this.#recreate(entry, snapshot);\n if (newEntry) {\n this.#session.insertKeyringAt(index, newEntry);\n }\n }\n\n /**\n * Build a new keyring entry from a keyring snapshot, without updating\n * the session keyrings. The old keyring entry, if any, is destroyed\n * before the replacement is created, as it must release the resources\n * shared with the replacement (e.g. the SDK singleton of hardware\n * keyrings) before the replacement is initialized. On failure, mirrors\n * the controller's keyring restoration: the error is logged and the\n * serialized keyring is parked in the unsupported keyrings.\n *\n * @param old - The old keyring entry replaced by the recreated keyring,\n * or `undefined` if the transaction already destroyed it.\n * @param snapshot - The keyring snapshot to recreate.\n * @param options - Recreation options.\n * @param options.duplicateCheckBase - The already reconciled keyrings\n * the recreated keyring is checked against for duplicate accounts;\n * when omitted, the check is skipped: the wallet-scope rollback checks\n * against the reconciled-so-far keyrings, while the keyring-scope\n * rollback owns the only keyring that changed, so a duplicate would\n * come from a mutation made outside a transaction.\n * @returns The new keyring entry, or `undefined` if it could not be\n * recreated.\n */\n async #recreate(\n old: KeyringEntry | undefined,\n snapshot: KeyringSnapshot,\n { duplicateCheckBase }: { duplicateCheckBase?: KeyringEntry[] } = {},\n ): Promise<KeyringEntry | undefined> {\n try {\n if (old) {\n await this.destroy(old, { ignoreErrors: true });\n }\n\n const entry = await this.#operations.createKeyring(\n snapshot.type,\n snapshot.data,\n snapshot.metadata,\n );\n\n if (duplicateCheckBase) {\n await this.#operations.assertNoDuplicateAccounts([\n ...duplicateCheckBase.map(({ keyring }) => keyring),\n entry.keyring,\n ]);\n }\n\n return entry;\n } catch (error) {\n console.error(error);\n // Only the serialized keyring is parked: the snapshot position must\n // not leak into the vault.\n this.#session.parkUnsupportedKeyring({\n type: snapshot.type,\n data: snapshot.data,\n metadata: snapshot.metadata,\n });\n return undefined;\n }\n }\n\n /**\n * Check whether the given keyring entry's serialized state is unchanged\n * relative to the given snapshot. A keyring destroyed during this\n * transaction — even one whose destruction failed partway — and a\n * keyring that cannot be serialized anymore are treated as changed:\n * destruction releases resources that `serialize()` does not capture.\n *\n * @param entry - The keyring entry to check.\n * @param snapshot - The keyring snapshot to check against.\n * @returns Whether the keyring is unchanged.\n */\n async #isUnchanged(\n entry: KeyringEntry,\n snapshot: KeyringSnapshot,\n ): Promise<boolean> {\n if (this.isDestroyed(entry)) {\n return false;\n }\n\n try {\n return (\n JSON.stringify(await entry.keyring.serialize()) ===\n JSON.stringify(snapshot.data)\n );\n } catch {\n return false;\n }\n }\n\n /**\n * Check whether the wallet session state changed since the snapshot\n * taken at `begin()`: the serialized keyrings, the unsupported keyrings,\n * and the encryption key are compared, as a whole, against the state\n * captured at `begin()`.\n *\n * @returns Whether the session state changed.\n */\n async #hasWalletChanged(): Promise<boolean> {\n const snapshot = this.#requireWalletSnapshot();\n\n // The session state at `begin()`, reconstructed from the snapshot.\n const oldState = JSON.stringify(\n this.#sessionStateOf(\n snapshot.keyrings.map(({ type, data, metadata }) => ({\n type,\n data,\n metadata,\n })),\n snapshot.unsupportedKeyrings,\n snapshot.encryptionKey,\n ),\n );\n\n // The session state now, serialized live.\n const keyrings = this.#session.getKeyrings();\n const serializedKeyrings = await Promise.all(\n keyrings.map(async ({ keyring, metadata }) => ({\n type: keyring.type,\n data: await keyring.serialize(),\n metadata,\n })),\n );\n\n const newState = JSON.stringify(\n this.#sessionStateOf(\n serializedKeyrings,\n this.#session.getUnsupportedKeyrings(),\n this.#session.getEncryptionKey(),\n ),\n );\n\n return oldState !== newState;\n }\n\n /**\n * Assemble the session state from its parts, so that the two sides of a\n * change comparison always take the same shape.\n *\n * @param keyrings - The serialized keyrings.\n * @param unsupportedKeyrings - The unsupported keyrings.\n * @param encryptionKey - The cached encryption key, if any.\n * @returns The session state.\n */\n #sessionStateOf(\n keyrings: SerializedKeyring[],\n unsupportedKeyrings: SerializedKeyring[],\n encryptionKey: CachedEncryptionKey | undefined,\n ): { keyrings: SerializedKeyring[]; encryptionKey?: CachedEncryptionKey } {\n return {\n keyrings: [...keyrings, ...unsupportedKeyrings],\n encryptionKey,\n };\n }\n\n /**\n * The snapshot of the wallet session, taken at `begin()`.\n *\n * @returns The wallet snapshot.\n */\n #requireWalletSnapshot(): WalletSnapshot {\n if (!this.#walletSnapshot) {\n throw new Error('The wallet transaction was not begun');\n }\n return this.#walletSnapshot;\n }\n\n /**\n * The snapshot of the operated keyring, taken at `begin()`.\n *\n * @returns The keyring snapshot.\n */\n #requireKeyringSnapshot(): KeyringSnapshot {\n if (!this.#keyringSnapshot) {\n throw new Error('The keyring transaction was not begun');\n }\n return this.#keyringSnapshot;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/keyring-controller",
3
- "version": "28.2.0-preview-66aa9ab6e",
3
+ "version": "28.2.0-preview-4a8d4a2",
4
4
  "description": "Stores identities seen in the wallet and manages interactions such as signing",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -80,7 +80,7 @@
80
80
  "typedoc": "^0.28.20",
81
81
  "typedoc-plugin-missing-exports": "^4.1.4",
82
82
  "typescript": "npm:@typescript/typescript6@^6.0.2",
83
- "uuid": "^14.0.0",
83
+ "uuid": "^11.1.1",
84
84
  "vitest": "^5.0.0"
85
85
  },
86
86
  "engines": {