@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
|
@@ -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