-
-
Notifications
You must be signed in to change notification settings - Fork 279
feat(accounts-controller): add support for Snap keyring v2 #8513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
235c68e
6dedac2
f4f0bc0
e485873
aa8bb9f
9837e1c
c9d2191
3caea20
c0e42d7
4782f28
a6bafff
c49b5a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,13 @@ import type { | |
| ControllerStateChangeEvent, | ||
| } from '@metamask/base-controller'; | ||
| import { SnapKeyring } from '@metamask/eth-snap-keyring'; | ||
| import { SnapKeyring as SnapKeyringV2 } from '@metamask/eth-snap-keyring/v2'; | ||
| import type { | ||
| SnapKeyringAccountAssetListUpdatedEvent, | ||
| SnapKeyringAccountBalancesUpdatedEvent, | ||
| SnapKeyringAccountTransactionsUpdatedEvent, | ||
| } from '@metamask/eth-snap-keyring'; | ||
| import type { KeyringAccountEntropyOptions } from '@metamask/keyring-api'; | ||
| import type { KeyringAccount, KeyringAccountEntropyOptions } from '@metamask/keyring-api'; | ||
| import { | ||
| EthAccountType, | ||
| EthMethod, | ||
|
|
@@ -23,6 +24,7 @@ import type { | |
| KeyringControllerStateChangeEvent, | ||
| KeyringControllerGetStateAction, | ||
| KeyringObject, | ||
| KeyringMetadata, | ||
| } from '@metamask/keyring-controller'; | ||
| import type { InternalAccount } from '@metamask/keyring-internal-api'; | ||
| import { isScopeEqualToAny } from '@metamask/keyring-utils'; | ||
|
|
@@ -52,8 +54,11 @@ import { | |
| isHdSnapKeyringAccount, | ||
| isMoneyKeyringType, | ||
| isSnapKeyringType, | ||
| isSnapKeyringV2Type, | ||
| keyringTypeToName, | ||
| } from './utils'; | ||
| import { KeyringType } from '@metamask/keyring-api/v2'; | ||
| import { is } from '@metamask/superstruct'; | ||
|
|
||
| const controllerName = 'AccountsController'; | ||
|
|
||
|
|
@@ -847,6 +852,49 @@ export class AccountsController extends BaseController< | |
| return snapKeyring as SnapKeyring | undefined; | ||
| } | ||
|
|
||
| /** | ||
| * 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", so we need to iterate over all of them to find the account. | ||
| for (const keyring of keyrings) { | ||
| if (keyring instanceof SnapKeyringV2) { | ||
| // We use the synchronous method here since this method is used during `:stateChange` that are | ||
| // use synchronous handlers. | ||
| const account = keyring.lookupByAddress(address); | ||
| if (account) { | ||
| return { | ||
| ...account, | ||
| // We still have to use internal account for now, so we inject some metadata. | ||
| metadata: { | ||
| name: '', | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| importTime: Date.now(), | ||
| lastSelected: 0, | ||
| keyring: { | ||
| type: KeyringType.Snap, | ||
| }, | ||
| snap: { | ||
| name: keyring.snapId, | ||
| enabled: true, | ||
| id: keyring.snapId, | ||
| }, | ||
|
ccharly marked this conversation as resolved.
|
||
| }, | ||
|
Comment on lines
+888
to
+898
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new Snap keyring v2 only use |
||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Re-publish an account event. | ||
| * | ||
|
|
@@ -887,32 +935,16 @@ export class AccountsController extends BaseController< | |
| log('Synchronizing accounts with keyrings (through :stateChange)...'); | ||
|
|
||
| // State patches. | ||
| const generatePatch = (): StatePatch => { | ||
| return { | ||
| previous: {}, | ||
| added: [], | ||
| updated: [], | ||
| removed: [], | ||
| }; | ||
| }; | ||
| const patches = { | ||
| snap: generatePatch(), | ||
| normal: generatePatch(), | ||
| }; | ||
|
|
||
| // Gets the patch object based on the keyring type (since Snap accounts and other accounts | ||
| // are handled differently). | ||
| const patchOf = (type: string): StatePatch => { | ||
| if (isSnapKeyringType(type)) { | ||
| return patches.snap; | ||
| } | ||
| return patches.normal; | ||
|
Comment on lines
-884
to
-903
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just removed all this since we don't need that separation at all! |
||
| 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(); | ||
| const patch = patchOf(account.metadata.keyring.type); | ||
|
|
||
| patch.previous[address] = account; | ||
| } | ||
|
|
@@ -926,8 +958,6 @@ export class AccountsController extends BaseController< | |
| continue; | ||
| } | ||
|
|
||
| const patch = patchOf(keyring.type); | ||
|
|
||
| for (const accountAddress of keyring.accounts) { | ||
| // Lower-case address to use it in the `previous` map. | ||
| const address = accountAddress.toLowerCase(); | ||
|
|
@@ -951,12 +981,10 @@ export class AccountsController extends BaseController< | |
|
|
||
| // We might have accounts associated with removed keyrings, so we iterate | ||
| // over all previous known accounts and check against the keyring addresses. | ||
| for (const patch of [patches.snap, patches.normal]) { | ||
| for (const [address, account] of Object.entries(patch.previous)) { | ||
| // If a previous address is not part of the new addesses, then it got removed. | ||
| if (!addresses.has(address)) { | ||
| patch.removed.push(account); | ||
| } | ||
| for (const [address, account] of Object.entries(patch.previous)) { | ||
| // If a previous address is not part of the new addesses, then it got removed. | ||
| if (!addresses.has(address)) { | ||
| patch.removed.push(account); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -970,42 +998,40 @@ export class AccountsController extends BaseController< | |
| (state) => { | ||
| const { internalAccounts, accountIdByAddress } = state; | ||
|
|
||
| for (const patch of [patches.snap, patches.normal]) { | ||
| for (const account of patch.removed) { | ||
| delete internalAccounts.accounts[account.id]; | ||
| delete accountIdByAddress[account.address]; | ||
| for (const account of patch.removed) { | ||
| delete internalAccounts.accounts[account.id]; | ||
| delete accountIdByAddress[account.address]; | ||
|
|
||
| diff.removed.push(account.id); | ||
| } | ||
| diff.removed.push(account.id); | ||
| } | ||
|
|
||
| for (const added of patch.added) { | ||
| const account = this.#getInternalAccountFromAddressAndType( | ||
| added.address, | ||
| added.keyring, | ||
| ); | ||
|
|
||
| if (account) { | ||
| const accounts = Object.values( | ||
| internalAccounts.accounts, | ||
| ) as InternalAccount[]; | ||
|
|
||
| // If it's the first account, we need to select it. | ||
| const lastSelected = | ||
| accounts.length === 0 ? this.#getLastSelectedIndex() : 0; | ||
|
|
||
| internalAccounts.accounts[account.id] = { | ||
| ...account, | ||
| metadata: { | ||
| ...account.metadata, | ||
| importTime: Date.now(), | ||
| lastSelected, | ||
| }, | ||
| }; | ||
|
|
||
| accountIdByAddress[account.address] = account.id; | ||
|
|
||
| for (const added of patch.added) { | ||
| const account = this.#getInternalAccountFromAddressAndType( | ||
| added.address, | ||
| added.keyring, | ||
| ); | ||
|
|
||
| if (account) { | ||
| const accounts = Object.values( | ||
| internalAccounts.accounts, | ||
| ) as InternalAccount[]; | ||
|
|
||
| // If it's the first account, we need to select it. | ||
| const lastSelected = | ||
| accounts.length === 0 ? this.#getLastSelectedIndex() : 0; | ||
|
|
||
| internalAccounts.accounts[account.id] = { | ||
| ...account, | ||
| metadata: { | ||
| ...account.metadata, | ||
| importTime: Date.now(), | ||
| lastSelected, | ||
| }, | ||
| }; | ||
|
|
||
| accountIdByAddress[account.address] = account.id; | ||
|
|
||
| diff.added.push(internalAccounts.accounts[account.id]); | ||
| } | ||
| diff.added.push(internalAccounts.accounts[account.id]); | ||
| } | ||
| } | ||
| }, | ||
|
|
@@ -1192,17 +1218,27 @@ export class AccountsController extends BaseController< | |
| address: string, | ||
| keyring: KeyringObject, | ||
| ): InternalAccount | undefined { | ||
| if (isSnapKeyringType(keyring.type)) { | ||
| const snapKeyring = this.#getSnapKeyring(); | ||
| const isSnapKeyringV1 = isSnapKeyringType(keyring.type); | ||
| const isSnapKeyringV2 = isSnapKeyringV2Type(keyring.type); | ||
|
|
||
| if (isSnapKeyringV1 || isSnapKeyringV2) { | ||
| let account: InternalAccount | undefined; | ||
|
|
||
| if (isSnapKeyringV1) { | ||
| const snapKeyring = this.#getSnapKeyring(); | ||
|
|
||
| // We need the Snap keyring to retrieve the account from its address. | ||
| if (!snapKeyring) { | ||
| return undefined; | ||
| } | ||
|
|
||
| // 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. | ||
| account = snapKeyring.getAccountByAddress(address); | ||
| } else { | ||
| account = this.#getAccountFromSnapKeyringV2(address); | ||
| } | ||
|
|
||
| // This might be undefined if the Snap deleted the account before | ||
| // reaching that point. | ||
| let account = snapKeyring.getAccountByAddress(address); | ||
| if (account) { | ||
| // We force the copy here, to avoid mutating the reference returned by the Snap keyring. | ||
| account = cloneDeep(account); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems inefficient, can we keep a map in memory for faster lookups?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe thats an overkill given that there won't be hundreds of snap keyrings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No actually I added that a safe-guard (with a small runtime cost yep) but
:getKeyringsByTypeshould """guarantee""" us that we getSnapKeyringV2instances.Also, I couldn't use
:withKeyring*here because this code has to be synchronous 😬 Ultimately, account storage will get delegated to theKeyringControllerthat will keep allKeyringAccounts in memory (for synchronous access).We won't need that trick anymore once everything will be migrated to v2!