Skip to content

feat: add multichain-account-service (readonly) #6141

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

Merged
merged 29 commits into from
Jul 21, 2025

Conversation

ccharly
Copy link
Contributor

@ccharly ccharly commented Jul 17, 2025

Explanation

Introducing a new service to support multichain accounts/wallets.

References

N/A

Changelog

N/A

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

@ccharly ccharly force-pushed the feat/multichain-accounts-controller-readonly branch from 8ffe4ea to 7c23474 Compare July 17, 2025 16:53
Copy link

socket-security bot commented Jul 17, 2025

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

@ccharly ccharly force-pushed the feat/multichain-accounts-controller-readonly branch from 9755a66 to 51f4404 Compare July 18, 2025 09:12
@ccharly ccharly marked this pull request as ready for review July 18, 2025 10:05
@ccharly ccharly requested a review from a team as a code owner July 18, 2025 10:05
const accounts: Bip44Account<InternalAccount>[] = [];

for (const account of this.messenger.call(
'AccountsController:listMultichainAccounts',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth adding a comment saying what this actually does (until such time as the action is changed)
i.e. it fetches all internal accounts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, some JS doc would be good here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added a small comment explaining what the action does, and also added a comment to explain that we should probably rename this at some point!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here: de60d19

Copy link
Contributor

@cryptodev-2s cryptodev-2s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The work looks good to me, I left some comments/suggestions

Co-authored-by: cryptodev-2s <[email protected]>
cursor[bot]

This comment was marked as outdated.

Comment on lines 59 to 64
const entropySources = [];
for (const keyring of keyrings) {
if (keyring.type === (KeyringTypes.hd as string)) {
entropySources.push(keyring.metadata.id);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think you can use a reduce here and condense this code block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I did refactor this a bit indeed 😅 I saw that when handling KeyringController:stateChange, so it should be simpler now.

@cryptodev-2s
Copy link
Contributor

@ccharly What do you think about renaming this to a Service instead of a Controller?

@ccharly
Copy link
Contributor Author

ccharly commented Jul 18, 2025

@ccharly What do you think about renaming this to a Service instead of a Controller?

Well, that was our initial idea with @danroc actually! 😄 I just thought that *Service were mostly used for external data providers.

But yes, this "controller" will remain stateless, so MultichainAccountService makes sense IMO. Would that match our naming convention @cryptodev-2s? If yes, I'd be happy to change the name 👍

Just to be clear, this controller is only using "internal data" (InternalAccount and other account-related data), but it does not interface with any remote/external API.

@cryptodev-2s
Copy link
Contributor

@ccharly What do you think about renaming this to a Service instead of a Controller?

Well, that was our initial idea with @danroc actually! 😄 I just thought that *Service were mostly used for external data providers.

But yes, this "controller" will remain stateless, so MultichainAccountService makes sense IMO. Would that match our naming convention @cryptodev-2s? If yes, I'd be happy to change the name 👍

Just to be clear, this controller is only using "internal data" (InternalAccount and other account-related data), but it does not interface with any remote/external API.

Yes, that aligns well with our current definition of what a Service could be. Glad we’re on the same page, feel free to proceed with the rename.

cursor[bot]

This comment was marked as outdated.

@ccharly ccharly changed the title feat: add multichain-account-controller (readonly) feat: add multichain-account-service (readonly) Jul 18, 2025
@ccharly
Copy link
Contributor Author

ccharly commented Jul 18, 2025

Bug: Bip44Account Function Error Handling

The isBip44Account function may throw a TypeError if account.options is undefined when attempting to access account.options.entropy. A defensive check for account.options?.entropy is recommended. Additionally, the function uses console.warn directly, which should be replaced with a proper logging mechanism.

packages/multichain-account-service/src/providers/BaseAccountProvider.ts#L26-L33

): account is Bip44Account<Account> {
if (
!account.options.entropy ||
account.options.entropy.type !== KeyringAccountEntropyTypeOption.Mnemonic
) {
console.warn(
"! Found an HD account with invalid entropy options: account won't be associated to its wallet.",
);

Fix in CursorFix in Web

Was this report helpful? Give feedback by reacting with 👍 or 👎

Doing this defensive check sounds a bit too much to me given that Account extends KeyringAccount and options should always be defined.

cryptodev-2s
cryptodev-2s previously approved these changes Jul 18, 2025
Copy link
Contributor

@cryptodev-2s cryptodev-2s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

hmalik88
hmalik88 previously approved these changes Jul 18, 2025
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Just some minor comments.

Comment on lines 65 to 70
// TODO: For now, we to every `KeyringController` state change to detect when
// new entropy sources/SRPs are being added. Having a dedicated event when
// new keyrings are added would make this more efficient.
this.#messenger.subscribe('KeyringController:stateChange', (state) => {
this.#setMultichainAccountWallets(state.keyrings);
});
Copy link
Contributor

@mcmire mcmire Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mention having a dedicated event, but there's already another way to do this that's more efficient.

The subscribe method takes a third argument, a function that works in spirit similar to input selectors in Redux (that is, they should be simple and only return a single property of the state object). This function should return the part of state that you are interested in observing.

What about this:

Suggested change
// TODO: For now, we to every `KeyringController` state change to detect when
// new entropy sources/SRPs are being added. Having a dedicated event when
// new keyrings are added would make this more efficient.
this.#messenger.subscribe('KeyringController:stateChange', (state) => {
this.#setMultichainAccountWallets(state.keyrings);
});
this.#messenger.subscribe(
'KeyringController:stateChange',
(keyringsFromState) => {
this.#setMultichainAccountWallets(keyringsFromState);
},
selectKeyringControllerKeyrings,
);

And then above the class MultichainAccountService line, you could add:

/**
 * Select keyrings from keyring controller state.
 *
 * @param state - The keyring controller state.
 * @returns The keyrings.
 */
function selectKeyringControllerKeyrings(state: KeyringControllerState) {
  return state.keyrings;
}

Because the entire keyrings array is changed if a new keyring is added or removed, it will detect that state.keyrings is a new object reference and your subscription function will be called. And if no keyrings are added or removed, your subscription function will not be called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good idea! I always forgot we have built-in selectors for controllers too! 🙈

Let me do this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here: 7399ee3

isAccountCompatible(account: InternalAccount): boolean {
return (
account.type === EthAccountType.Eoa &&
account.metadata.keyring.type === (KeyringTypes.hd as string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this type assertion needed?

Suggested change
account.metadata.keyring.type === (KeyringTypes.hd as string)
account.metadata.keyring.type === KeyringTypes.hd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar answer: #6141 (comment)

isAccountCompatible(account: InternalAccount): boolean {
return (
account.type === SolAccountType.DataAccount &&
account.metadata.keyring.type === (KeyringTypes.snap as string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this type assertion needed?

Suggested change
account.metadata.keyring.type === (KeyringTypes.snap as string)
account.metadata.keyring.type === KeyringTypes.snap

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it's not "truly needed" but we do get a warning if we're not doing it that way, cause account.metadata.keyring.type is actually a string and we compare it with an enum value... I'm also a bit surprised by this TBH, but I have seen this same warning in some AccountsController comparisons too:

The two values in this comparison do not have a shared enum type.eslint[@typescript-eslint/no-unsafe-enum-comparison](https://typescript-eslint.io/rules/no-unsafe-enum-comparison)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh weird. For some reason I wasn't getting that warning locally. But I do know about it. Never mind then :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Are these tests for tests? :)

Typically if a test helper needs tests it indicates it's too complex. Maybe we can rely on the fact it is being tested indirectly and so we don't need this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I can probably remove this yes. I had to add a tests because some MockAccountBuilder methods are not yet in use, but they will be used in a future PR. For the moment I guess I can just remove them, thus, they should be covered "implicitly" yes 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here: 18a6358

@ccharly ccharly dismissed stale reviews from hmalik88 and cryptodev-2s via d6dd7f0 July 18, 2025 21:33
cursor[bot]

This comment was marked as outdated.

Co-authored-by: Elliot Winkler <[email protected]>
cursor[bot]

This comment was marked as outdated.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Function Fails When Options Undefined

The isBip44Account function throws a TypeError if account.options is undefined, as it directly accesses account.options.entropy. This prevents the intended behavior of returning false and logging a warning. Use optional chaining (account.options?.entropy) to safely access the property.

packages/multichain-account-service/src/providers/BaseAccountProvider.ts#L26-L29

): account is Bip44Account<Account> {
if (
!account.options.entropy ||
account.options.entropy.type !== KeyringAccountEntropyTypeOption.Mnemonic

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

@ccharly ccharly enabled auto-merge (squash) July 21, 2025 08:07
Copy link
Contributor

@cryptodev-2s cryptodev-2s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@ccharly ccharly merged commit c3a0150 into main Jul 21, 2025
219 checks passed
@ccharly ccharly deleted the feat/multichain-accounts-controller-readonly branch July 21, 2025 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants