-
-
Notifications
You must be signed in to change notification settings - Fork 290
Expand file tree
/
Copy pathAccountsController.ts
More file actions
1320 lines (1174 loc) · 44.6 KB
/
Copy pathAccountsController.ts
File metadata and controls
1320 lines (1174 loc) · 44.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Benchmark: multi-package change.
import { BaseController } from '@metamask/base-controller';
import type {
ControllerGetStateAction,
ControllerStateChangeEvent,
} from '@metamask/base-controller';
import { SnapKeyring } from '@metamask/eth-snap-keyring';
import type {
SnapKeyringAccountAssetListUpdatedEvent,
SnapKeyringAccountBalancesUpdatedEvent,
SnapKeyringAccountTransactionsUpdatedEvent,
} from '@metamask/eth-snap-keyring';
import { SnapKeyring as SnapKeyringV2 } from '@metamask/eth-snap-keyring/v2';
import type { KeyringAccountEntropyOptions } from '@metamask/keyring-api';
import {
EthAccountType,
EthMethod,
EthScope,
isEvmAccountType,
KeyringAccountEntropyTypeOption,
} from '@metamask/keyring-api';
import { KeyringType } from '@metamask/keyring-api/v2';
import type {
KeyringControllerState,
KeyringControllerGetKeyringsByTypeAction,
KeyringControllerStateChangeEvent,
KeyringControllerGetStateAction,
KeyringObject,
} from '@metamask/keyring-controller';
import type { InternalAccount } from '@metamask/keyring-internal-api';
import { KeyringV1Adapter } from '@metamask/keyring-sdk/v2';
import { isScopeEqualToAny } from '@metamask/keyring-utils';
import type { Messenger, ExtractEventPayload } from '@metamask/messenger';
import type { NetworkClientId } from '@metamask/network-controller';
import { isCaipChainId } from '@metamask/utils';
import type { CaipChainId } from '@metamask/utils';
import type { WritableDraft } from 'immer/dist/internal.js';
import { cloneDeep } from 'lodash';
import { AccountsControllerMethodActions } from './AccountsController-method-action-types';
import { projectLogger as log } from './logger';
import type {
MultichainNetworkControllerNetworkDidChangeEvent,
SnapAccountServiceAccountAssetListUpdatedEvent,
SnapAccountServiceAccountBalancesUpdatedEvent,
SnapAccountServiceAccountTransactionsUpdatedEvent,
} from './types';
import type { AccountsControllerStrictState } from './typing';
import type { HdSnapKeyringAccount } from './utils';
import {
constructAccountIdByAddress,
getEvmDerivationPathForIndex,
getEvmGroupIndexFromAddressIndex,
getUUIDFromAddressOfNormalAccount,
isHdKeyringType,
isHdSnapKeyringAccount,
isMoneyKeyringType,
isSnapKeyringType,
isSnapKeyringV2Type,
keyringTypeToName,
} from './utils';
const controllerName = 'AccountsController';
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountId = string;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerState = {
internalAccounts: {
accounts: Record<AccountId, InternalAccount>;
selectedAccount: string; // id of the selected account
};
accountIdByAddress: Record<InternalAccount['address'], AccountId>;
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerGetStateAction = ControllerGetStateAction<
typeof controllerName,
AccountsControllerState
>;
const MESSENGER_EXPOSED_METHODS = [
'setSelectedAccount',
'setAccountName',
'setAccountNameAndSelectAccount',
'listAccounts',
'listMultichainAccounts',
'updateAccounts',
'getSelectedAccount',
'getSelectedMultichainAccount',
'getAccountByAddress',
'getAccount',
'getAccounts',
'updateAccountMetadata',
'loadBackup',
] as const;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AllowedActions =
| KeyringControllerGetKeyringsByTypeAction
| KeyringControllerGetStateAction;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerActions =
| AccountsControllerGetStateAction
| AccountsControllerMethodActions;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerChangeEvent = ControllerStateChangeEvent<
typeof controllerName,
AccountsControllerState
>;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerSelectedAccountChangeEvent = {
type: `${typeof controllerName}:selectedAccountChange`;
payload: [InternalAccount];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerSelectedEvmAccountChangeEvent = {
type: `${typeof controllerName}:selectedEvmAccountChange`;
payload: [InternalAccount];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerAccountAddedEvent = {
type: `${typeof controllerName}:accountAdded`;
payload: [InternalAccount];
};
export type AccountsControllerAccountsAddedEvent = {
type: `${typeof controllerName}:accountsAdded`;
payload: [InternalAccount[]];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerAccountRemovedEvent = {
type: `${typeof controllerName}:accountRemoved`;
payload: [AccountId];
};
export type AccountsControllerAccountsRemovedEvent = {
type: `${typeof controllerName}:accountsRemoved`;
payload: [AccountId[]];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerAccountRenamedEvent = {
type: `${typeof controllerName}:accountRenamed`;
payload: [InternalAccount];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerAccountBalancesUpdatesEvent = {
type: `${typeof controllerName}:accountBalancesUpdated`;
payload: SnapKeyringAccountBalancesUpdatedEvent['payload'];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerAccountTransactionsUpdatedEvent = {
type: `${typeof controllerName}:accountTransactionsUpdated`;
payload: SnapKeyringAccountTransactionsUpdatedEvent['payload'];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerAccountAssetListUpdatedEvent = {
type: `${typeof controllerName}:accountAssetListUpdated`;
payload: SnapKeyringAccountAssetListUpdatedEvent['payload'];
};
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AllowedEvents =
| KeyringControllerStateChangeEvent
| SnapAccountServiceAccountAssetListUpdatedEvent
| SnapAccountServiceAccountBalancesUpdatedEvent
| SnapAccountServiceAccountTransactionsUpdatedEvent
| MultichainNetworkControllerNetworkDidChangeEvent;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerEvents =
| AccountsControllerChangeEvent
| AccountsControllerSelectedAccountChangeEvent
| AccountsControllerSelectedEvmAccountChangeEvent
| AccountsControllerAccountAddedEvent
| AccountsControllerAccountsAddedEvent
| AccountsControllerAccountRemovedEvent
| AccountsControllerAccountsRemovedEvent
| AccountsControllerAccountRenamedEvent
| AccountsControllerAccountBalancesUpdatesEvent
| AccountsControllerAccountTransactionsUpdatedEvent
| AccountsControllerAccountAssetListUpdatedEvent;
/**
* @deprecated This type is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export type AccountsControllerMessenger = Messenger<
typeof controllerName,
AccountsControllerActions | AllowedActions,
AccountsControllerEvents | AllowedEvents
>;
const accountsControllerMetadata = {
internalAccounts: {
includeInStateLogs: true,
persist: true,
includeInDebugSnapshot: false,
usedInUi: true,
},
accountIdByAddress: {
includeInStateLogs: false,
persist: false,
includeInDebugSnapshot: false,
usedInUi: true,
},
};
const defaultState: AccountsControllerState = {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
accountIdByAddress: {},
};
/**
* @deprecated This constant is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export const EMPTY_ACCOUNT = {
id: '',
address: '',
options: {},
methods: [],
type: EthAccountType.Eoa,
scopes: [EthScope.Eoa],
metadata: {
name: '',
keyring: {
type: '',
},
importTime: 0,
},
};
/**
* A patch representing a keyring state change.
*/
type StatePatch = {
previous: Record<string, InternalAccount>;
added: {
address: string;
keyring: KeyringObject;
}[];
updated: InternalAccount[];
removed: InternalAccount[];
};
/**
* Controller that manages internal accounts.
* The accounts controller is responsible for creating and managing internal accounts.
* It also provides convenience methods for accessing and updating the internal accounts.
* The accounts controller also listens for keyring state changes and updates the internal accounts accordingly.
* The accounts controller also listens for snap state changes and updates the internal accounts accordingly.
*
* @deprecated This class is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
*/
export class AccountsController extends BaseController<
typeof controllerName,
AccountsControllerState,
AccountsControllerMessenger
> {
/**
* Constructor for AccountsController.
*
* @param options - The controller options.
* @param options.messenger - The messenger object.
* @param options.state - Initial state to set on this controller
*/
constructor({
messenger,
state,
}: {
messenger: AccountsControllerMessenger;
state?: AccountsControllerState;
}) {
const accountIdByAddress = constructAccountIdByAddress(
state?.internalAccounts?.accounts ?? {},
);
super({
messenger,
name: controllerName,
metadata: accountsControllerMetadata,
state: {
...defaultState,
...state,
accountIdByAddress,
},
});
this.messenger.registerMethodActionHandlers(
this,
MESSENGER_EXPOSED_METHODS,
);
this.#subscribeToMessageEvents();
}
/**
* Returns the internal account object for the given account ID, if it exists.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountId - The ID of the account to retrieve.
* @returns The internal account object, or undefined if the account does not exist.
*/
getAccount(accountId: string): InternalAccount | undefined {
return this.state.internalAccounts.accounts[accountId];
}
/**
* Returns the internal account objects for the given account IDs, if they exist.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountIds - The IDs of the accounts to retrieve.
* @returns The internal account objects, or undefined if the account(s) do not exist.
*/
getAccounts(accountIds: string[]): (InternalAccount | undefined)[] {
return accountIds.map((accountId) => this.getAccount(accountId));
}
/**
* Returns an array of all evm internal accounts.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @returns An array of InternalAccount objects.
*/
listAccounts(): InternalAccount[] {
const accounts = Object.values(this.state.internalAccounts.accounts);
return accounts.filter((account) => isEvmAccountType(account.type));
}
/**
* Returns an array of all internal accounts.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param chainId - The chain ID.
* @returns An array of InternalAccount objects.
*/
listMultichainAccounts(chainId?: CaipChainId): InternalAccount[] {
const accounts = Object.values(this.state.internalAccounts.accounts);
if (!chainId) {
return accounts;
}
if (!isCaipChainId(chainId)) {
throw new Error(`Invalid CAIP-2 chain ID: ${String(chainId)}`);
}
return accounts.filter((account) =>
isScopeEqualToAny(chainId, account.scopes),
);
}
/**
* Returns the internal account object for the given account ID.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountId - The ID of the account to retrieve.
* @returns The internal account object.
* @throws An error if the account ID is not found.
*/
#getAccountExpect(accountId: string): InternalAccount {
const account = this.getAccount(accountId);
if (account === undefined) {
throw new Error(`Account Id "${accountId}" not found`);
}
return account;
}
/**
* Returns the last selected EVM account.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @returns The selected internal account.
*/
getSelectedAccount(): InternalAccount {
const {
internalAccounts: { selectedAccount },
} = this.state;
// Edge case where the extension is setup but the srp is not yet created
// certain ui elements will query the selected address before any accounts are created.
if (!selectedAccount) {
return EMPTY_ACCOUNT;
}
const account = this.#getAccountExpect(selectedAccount);
if (isEvmAccountType(account.type)) {
return account;
}
const accounts = this.listAccounts();
if (!accounts.length) {
// ! Should never reach this.
throw new Error('No EVM accounts');
}
// This will never be undefined because we have already checked if accounts.length is > 0
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.#getLastSelectedAccount(accounts)!;
}
/**
* __WARNING The return value may be undefined if there isn't an account for that chain id.__
*
* Retrieves the last selected account by chain ID.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param chainId - The chain ID to filter the accounts.
* @returns The last selected account compatible with the specified chain ID or undefined.
*/
getSelectedMultichainAccount(
chainId?: CaipChainId,
): InternalAccount | undefined {
const {
internalAccounts: { selectedAccount },
} = this.state;
// Edge case where the extension is setup but the srp is not yet created
// certain ui elements will query the selected address before any accounts are created.
if (!selectedAccount) {
return EMPTY_ACCOUNT;
}
if (!chainId) {
return this.#getAccountExpect(selectedAccount);
}
const accounts = this.listMultichainAccounts(chainId);
return this.#getLastSelectedAccount(accounts);
}
/**
* Returns the account with the specified address.
* ! This method will only return the first account that matches the address
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param address - The address of the account to retrieve.
* @returns The account with the specified address, or undefined if not found.
*/
getAccountByAddress(address: string): InternalAccount | undefined {
// We need to have a fallback as a cache miss might be attributed to a checksummed address being passed.
let accountId = this.state.accountIdByAddress[address];
if (!accountId) {
// FIXME: We should not need lower-cased addresses, but some consumers might
// still be using non-normalized addresses. For now we keep it
// for convenience, but we will need to remove this fallback
// at some point.
// NOTE: We should only hit that branch for EVM accounts only.
const lowercasedAddress = address.toLowerCase();
accountId = this.state.accountIdByAddress[lowercasedAddress];
if (accountId) {
log(
`Cache missed for account ID: ${accountId}, received address: "${address}", matched address: "${lowercasedAddress}"`,
);
}
}
return accountId ? this.getAccount(accountId) : undefined;
}
/**
* Sets the selected account by its ID.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountId - The ID of the account to be selected.
*/
setSelectedAccount(accountId: string): void {
const account = this.#getAccountExpect(accountId);
if (this.state.internalAccounts.selectedAccount === account.id) {
return;
}
this.#update((state) => {
const { internalAccounts } = state;
internalAccounts.accounts[account.id].metadata.lastSelected = Date.now();
internalAccounts.selectedAccount = account.id;
});
}
/**
* Sets the name of the account with the given ID.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountId - The ID of the account to set the name for.
* @param accountName - The new name for the account.
* @throws An error if an account with the same name already exists.
*/
setAccountName(accountId: string, accountName: string): void {
// This will check for name uniqueness and fire the `accountRenamed` event
// if the account has been renamed.
this.updateAccountMetadata(accountId, {
name: accountName,
nameLastUpdatedAt: Date.now(),
});
}
/**
* Sets the name of the account with the given ID and select it.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountId - The ID of the account to set the name for and select.
* @param accountName - The new name for the account.
* @throws An error if an account with the same name already exists.
*/
setAccountNameAndSelectAccount(accountId: string, accountName: string): void {
const account = this.#getAccountExpect(accountId);
this.#assertAccountCanBeRenamed(account, accountName);
const internalAccount = {
...account,
metadata: {
...account.metadata,
name: accountName,
nameLastUpdatedAt: Date.now(),
lastSelected: this.#getLastSelectedIndex(),
},
};
this.#update((state) => {
state.internalAccounts.accounts[account.id] = internalAccount;
state.internalAccounts.selectedAccount = account.id;
});
this.messenger.publish(
'AccountsController:accountRenamed',
internalAccount,
);
}
#assertAccountCanBeRenamed(
account: InternalAccount,
accountName: string,
): void {
if (
this.listMultichainAccounts().find(
(internalAccount) =>
internalAccount.metadata.name === accountName &&
internalAccount.id !== account.id,
)
) {
throw new Error('Account name already exists');
}
}
/**
* Updates the metadata of the account with the given ID.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param accountId - The ID of the account for which the metadata will be updated.
* @param metadata - The new metadata for the account.
*/
updateAccountMetadata(
accountId: string,
metadata: Partial<InternalAccount['metadata']>,
): void {
const account = this.#getAccountExpect(accountId);
if (metadata.name) {
this.#assertAccountCanBeRenamed(account, metadata.name);
}
const internalAccount = {
...account,
metadata: { ...account.metadata, ...metadata },
};
this.#update((state) => {
state.internalAccounts.accounts[accountId] = internalAccount;
});
if (metadata.name) {
this.messenger.publish(
'AccountsController:accountRenamed',
internalAccount,
);
}
}
/**
* Updates the internal accounts list by retrieving normal and snap accounts,
* removing duplicates, and updating the metadata of each account.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @returns A Promise that resolves when the accounts have been updated.
*/
async updateAccounts(): Promise<void> {
log('Synchronizing accounts with keyrings...');
const keyringAccountIndexes = new Map<string, number>();
const existingInternalAccounts = this.state.internalAccounts.accounts;
const internalAccounts: AccountsControllerState['internalAccounts']['accounts'] =
{};
const { keyrings } = this.messenger.call('KeyringController:getState');
for (const keyring of keyrings) {
// Money accounts are not treated as real accounts, they are owned by the `MoneyAccountController`, so
// we need to filter them out here.
if (isMoneyKeyringType(keyring.type)) {
continue;
}
const keyringTypeName = keyringTypeToName(keyring.type);
for (const address of keyring.accounts) {
const internalAccount = this.#getInternalAccountFromAddressAndType(
address,
keyring,
);
// This should never really happen, but if for some reason we're not
// able to get the Snap keyring reference, this would return an
// undefined account.
// So we just skip it, even though, this should not really happen.
if (!internalAccount) {
continue;
}
// Get current index for this keyring (we use human indexing, so start at 1).
const keyringAccountIndex =
keyringAccountIndexes.get(keyringTypeName) ?? 1;
const existingAccount = existingInternalAccounts[internalAccount.id];
internalAccounts[internalAccount.id] = {
...internalAccount,
metadata: {
...internalAccount.metadata,
// Re-use existing metadata if any.
name:
existingAccount?.metadata.name ??
`${keyringTypeName} ${keyringAccountIndex}`,
importTime: existingAccount?.metadata.importTime ?? Date.now(),
lastSelected: existingAccount?.metadata.lastSelected ?? 0,
},
};
// Increment the account index for this keyring.
keyringAccountIndexes.set(keyringTypeName, keyringAccountIndex + 1);
}
}
this.#update((state) => {
state.internalAccounts.accounts = internalAccounts;
state.accountIdByAddress = constructAccountIdByAddress(internalAccounts);
});
log('Accounts synchronized!');
}
/**
* Loads the backup state of the accounts controller.
*
* @deprecated This method is deprecated and will be removed in a future version.
* Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead.
* @param backup - The backup state to load.
*/
loadBackup(backup: AccountsControllerState): void {
if (backup.internalAccounts) {
const accountIdByAddress = constructAccountIdByAddress(
backup.internalAccounts.accounts,
);
this.update(
(currentState: WritableDraft<AccountsControllerStrictState>) => {
currentState.internalAccounts = backup.internalAccounts;
currentState.accountIdByAddress = accountIdByAddress;
},
);
}
}
/**
* Gets an internal account representation for a non-Snap account.
*
* @param address - The address of the account.
* @param keyring - The keyring object of the account.
* @returns The generated internal account.
*/
#getInternalAccountForNonSnapAccount(
address: string,
keyring: KeyringObject,
): InternalAccount {
const id = getUUIDFromAddressOfNormalAccount(address);
// We might have an account for this ID already, so we'll just re-use
// the same metadata
const account = this.getAccount(id);
const metadata: InternalAccount['metadata'] = {
name: account?.metadata.name ?? '',
...(account?.metadata.nameLastUpdatedAt
? {
nameLastUpdatedAt: account?.metadata.nameLastUpdatedAt,
}
: {}),
importTime: account?.metadata.importTime ?? Date.now(),
lastSelected: account?.metadata.lastSelected ?? 0,
keyring: {
type: keyring.type,
},
};
let options: InternalAccount['options'] = {};
if (isHdKeyringType(keyring.type)) {
// We need to find the account index from its HD keyring.
const groupIndex = getEvmGroupIndexFromAddressIndex(keyring, address);
// If for some reason, we cannot find this address, then the caller made a mistake
// and it did not use the proper keyring object. For now, we do not fail and just
// consider this account as "simple account".
if (groupIndex !== undefined) {
// NOTE: We are not using the `hdPath` from the associated keyring here and
// getting the keyring instance here feels a bit overkill.
// This will be naturally fixed once every keyring start using `KeyringAccount` and implement the keyring API.
const derivationPath = getEvmDerivationPathForIndex(groupIndex);
// Those are "legacy options" and they were used before `KeyringAccount` added
// support for type options. We keep those temporarily until we update everything
// to use the new typed options.
const legacyOptions = {
entropySource: keyring.metadata.id,
derivationPath,
groupIndex,
};
// New typed entropy options. This is required for multichain accounts.
const entropyOptions: { entropy: KeyringAccountEntropyOptions } = {
entropy: {
type: KeyringAccountEntropyTypeOption.Mnemonic,
id: keyring.metadata.id,
derivationPath,
groupIndex,
},
};
options = {
...legacyOptions,
...entropyOptions,
};
}
}
return {
id,
address,
options,
methods: [
EthMethod.PersonalSign,
EthMethod.Sign,
EthMethod.SignTransaction,
EthMethod.SignTypedDataV1,
EthMethod.SignTypedDataV3,
EthMethod.SignTypedDataV4,
],
scopes: [EthScope.Eoa],
type: EthAccountType.Eoa,
metadata,
};
}
/**
* Get Snap keyring from the keyring controller.
*
* @returns The Snap keyring if available.
*/
#getSnapKeyring(): SnapKeyring | undefined {
const [snapKeyring] = this.messenger.call(
'KeyringController:getKeyringsByType',
SnapKeyring.type,
);
// Snap keyring is not available until the first account is created in the keyring
// controller, so this might be undefined.
return snapKeyring as SnapKeyring | undefined;
}
/**
* Get an account from a Snap keyring v1.
*
* @param address - The address of the account to retrieve.
* @returns The Snap account if available.
*/
#getAccountFromSnapKeyringV1(address: string): InternalAccount | undefined {
const snapKeyring = this.#getSnapKeyring();
// We need the Snap keyring to retrieve the account from its address.
if (!snapKeyring) {
return undefined;
}
// This might be undefined if the Snap deleted the account before
// reaching that point.
return snapKeyring.getAccountByAddress(address);
}
/**
* Get an account from a Snap keyring v2.
*
* @param address - The address of the account to retrieve.
* @returns The Snap account if available.
*/
#getAccountFromSnapKeyringV2(address: string): InternalAccount | undefined {
const keyrings = this.messenger.call(
'KeyringController:getKeyringsByType',
KeyringType.Snap,
);
// Snap keyring v2 are "per-Snaps" (and can be accessed using their v1 adapter), so we need to
// iterate over all of them to find the account.
// NOTE: `:getKeyringsByType` will only return v1 instances, that's why we need to use their v1
// adapter + `unwrap` method to get the reference to their v2 instance.
for (const keyring of keyrings) {
if (keyring instanceof KeyringV1Adapter) {
// NOTE: We already filtering by `KeyringType.Snap`, so we are sure that those adapters
// are wrapping a Snap keyring v2.
const adapter = keyring as KeyringV1Adapter<SnapKeyringV2>;
const keyringV2 = adapter.unwrap();
// We use the synchronous method here since this method is used during `:stateChange` that are
// use synchronous handlers.
const account = keyringV2.lookupByAddress(address);
if (account) {
return {
...account,
// We still have to use internal account for now, so we inject some metadata.
metadata: {
name: '',
importTime: Date.now(),
lastSelected: 0,
keyring: {
type: KeyringType.Snap,
},
snap: {
id: keyringV2.snapId,
},
},
};
}
}
}
return undefined;
}
/**
* Re-publish an account event.
*
* @param event - The event type. This is a unique identifier for this event.
* @param payload - The event payload. The type of the parameters for each event handler must
* match the type of this payload.
* @template EventType - A Snap keyring event type.
*/
#handleOnSnapKeyringAccountEvent<
EventType extends AccountsControllerEvents['type'],
>(
event: EventType,
...payload: ExtractEventPayload<AccountsControllerEvents, EventType>
): void {
this.messenger.publish(event, ...payload);
}
/**
* Handles changes in the keyring state, specifically when new accounts are added or removed.
*
* @param keyringState - The new state of the keyring controller.
* @param keyringState.isUnlocked - True if the keyrings are unlocked, false otherwise.
* @param keyringState.keyrings - List of all keyrings.
*/
#handleOnKeyringStateChange({
isUnlocked,
keyrings,
}: KeyringControllerState): void {
// TODO: Change when accountAdded event is added to the keyring controller.
// We check for keyrings length to be greater than 0 because the extension client may try execute
// submit password twice and clear the keyring state.
// https://github.com/MetaMask/KeyringController/blob/2d73a4deed8d013913f6ef0c9f5c0bb7c614f7d3/src/KeyringController.ts#L910
if (!isUnlocked || keyrings.length === 0) {
return;
}
log('Synchronizing accounts with keyrings (through :stateChange)...');
// State patches.
const patch: StatePatch = {
previous: {},
added: [],
updated: [],
removed: [],
};
// Create a map (with lower-cased addresses) of all existing accounts.
for (const account of this.listMultichainAccounts()) {
const address = account.address.toLowerCase();
patch.previous[address] = account;
}
// Go over all keyring changes and create patches out of it.
const addresses = new Set<string>();
for (const keyring of keyrings) {
// Money accounts are not treated as real accounts, they are owned by the `MoneyAccountController`, so
// we need to filter them out here.
if (isMoneyKeyringType(keyring.type)) {
continue;
}
for (const accountAddress of keyring.accounts) {
// Lower-case address to use it in the `previous` map.
const address = accountAddress.toLowerCase();
const account = patch.previous[address];
if (account) {
// If the account exists before, this might be an update.
patch.updated.push(account);
} else {
// Otherwise, that's a new account.
patch.added.push({
address,
keyring,
});
}