Skip to content
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

feat: Add multichain-network-controller package #5209

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

tommasini
Copy link
Contributor

@tommasini tommasini commented Jan 29, 2025

Implementation of the new Multichain network controller, this is the first version of the multichain network controller and it will handle the source of truth for non evm networks.

Multichain network controller includes a state variable that helps the consumer knows which networks should be interacting with (evm or non-evm).

Multichain network controller will work as a proxy with the existing Network Controller, which currently handles the evm networks source of truth.

References

Related to MetaMask/metamask-mobile#13234

Changelog

@metamask/multichain-network-controller

  • : Proxy to NetworkController set current evm active network
  • : State variable nonEvmSelected that tells consumers which evm should they show (evm or non-evm)
  • : Setter to change nonEvmSelected variable to true
  • : Setter to change nonEvmSelected variable to false

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 highlighted breaking changes using the "BREAKING" category above as appropriate
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

@@ -0,0 +1,20 @@
MIT License

Copyright (c) 2024 MetaMask
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should this be 2025?

Comment on lines 12 to 23
/**
* Here we ensure that TypeScript resolves `@metamask/*` imports to the
* uncompiled source code for packages that live in this repo.
*
* NOTE: This must be synchronized with the `moduleNameMapper` option in
* `jest.config.packages.js`.
*
* NOTE 2: This is not necessary when copying this package to `packages/`.
*/
"paths": {
"@metamask/*": ["../../packages/*/src", "../*/src"]
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
/**
* Here we ensure that TypeScript resolves `@metamask/*` imports to the
* uncompiled source code for packages that live in this repo.
*
* NOTE: This must be synchronized with the `moduleNameMapper` option in
* `jest.config.packages.js`.
*
* NOTE 2: This is not necessary when copying this package to `packages/`.
*/
"paths": {
"@metamask/*": ["../../packages/*/src", "../*/src"]
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, we should not need these changes as they are provided by tsconfig.packages.json.

},
"references": [
{ "path": "../base-controller" },
{ "path": "../accounts-controller" },
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we remove this for now, since it's not interacting with the accounts controller yet?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would recommend doing so, yes, and adding it in a future PR.

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.

Hi @tommasini! I checked through this PR and made some suggestions on ways we could bring this controller in alignment with other controllers.

I don't know if you know about this but we have guidelines we've written for controllers her: https://github.com/MetaMask/core/blob/d158e46244c24e6426ac42e85f991fed34d71700/docs/writing-controllers.md. We also have example controllers you can browse here: https://github.com/MetaMask/core/tree/d158e46244c24e6426ac42e85f991fed34d71700/examples/example-controllers. A lot of my suggestions are based on these guidelines but you might find it helpful to take a look through yourself to make sure you're familiar.

Let me know if you have any questions on comments I've left.

@@ -0,0 +1,70 @@
{
"name": "@metamask/multichain-network-controller",
"version": "0.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's change this back to 0.0.0. We will bump this to the correct version when we make a new release. Otherwise, this package could get released accidentally:

Suggested change
"version": "0.0.1",
"version": "0.0.0",

"devDependencies": {
"@metamask/accounts-controller": "^21.0.1",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/network-controller": "^22.1.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this controller talks to NetworkController. Can we add this as a peer dependency as well to tell clients that they need to be using the right version?

blockExplorerUrls: string[];
defaultBlockExplorerUrlIndex?: number;
lastUpdated?: number;
isEvm?: false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why this property is optional? Could it be this instead?

Suggested change
isEvm?: false;
isEvm: boolean;

string,
MultichainNetworkConfiguration
>;
selectedMultichainNetworkChainId: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also be a CAIP-19 type?

Copy link
Contributor

Choose a reason for hiding this comment

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

Should also be CaipChainId here IMO yes (CAIP-2) from @metamask/utils

let messenger: ControllerMessenger<AllowedActions, AllowedEvents>;

beforeEach(() => {
messenger = buildMessenger();
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than setting up tests using beforeEach — which can make things complicated over time — what are your thoughts on using a helper method to set up the controller in each test? Unfortunately we don't have a great example of this in core, but NftController has one called setupController that works pretty well:

In your case I'm imagining something like:

/**
 * Constructs a new global messenger for use in tests.
 *
 * @returns A new instance of ControllerMessenger that supports the actions and
 * events MultichainNetworkController needs to access.
 */
function getGlobalMessenger() {
  return new ControllerMessenger<
    MultichainNetworkControllerActions | AllowedActions,
    MultichainNetworkControllerEvents | AllowedEvents
  >();
}

/**
 * Constructs the messenger which is restricted for
 * MultichainNetworkController.
 *
 * @param globalMessenger - The global messenger to base the messenger from.
 * @returns The restricted messenger for MultichainNetworkController.
 */
function getControllerMessenger(
  globalMessenger = getGlobalMessenger(),
): MultichainNetworkControllerMessenger {
  return globalMessenger.getRestricted({
    name: 'MultichainNetworkController',
    allowedActions: ['NetworkController:setActiveNetwork'],
    allowedEvents: [],
  });
}

/**
 * Constructs the MultichainNetworkController and the global messenger for use
 * in tests.
 *
 * @param args - Arguments to this function.
 * @param args.options - Controller options.
 * @param args.mockSetActiveNetwork - The handler to use for the
 * `NetworkController:setActiveNetwork` action.
 * @returns The MultichainNetworkController and the global messenger.
 */
function setupController({
  options,
  mockSetActiveNetwork = jest.fn(async () => {
    // do nothing
  }),
}: {
  options?: Partial<
    ConstructorParameters<typeof MultichainNetworkController>[0]
  >;
  mockSetActiveNetwork?: NetworkControllerSetActiveNetworkAction['handler'];
}) {
  const messenger = getGlobalMessenger();
  messenger.registerActionHandler(
    'NetworkController:setActiveNetwork',
    mockSetActiveNetwork,
  );

  const controllerMessenger = getControllerMessenger(messenger);

  const controller = new MultichainNetworkController({
    ...options,
    messenger: controllerMessenger,
  });

  return { controller, messenger };
}

Then you don't need to spy on the messenger, you can just pass in a function or mock function that lets you override the setActiveNetwork call.

},
"references": [
{ "path": "../base-controller" },
{ "path": "../accounts-controller" },
Copy link
Contributor

Choose a reason for hiding this comment

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

I would recommend doing so, yes, and adding it in a future PR.

export const bitcoinCaip2ChainId = 'bip122:000000000019d6689c085ae165831e93';
export const solanaCaip2ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';

export const multichainNetworkConfigurations: Record<string, MultichainNetworkConfiguration> = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Where are these constants used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think they were added here to import in the consumer (mobile/extension) and when we initialise the controller we can have this source of truth of data for non evm networks!
Do you think since it's not directly used in the multichain network controller doesn't make sense?

Copy link
Contributor

@mcmire mcmire Jan 30, 2025

Choose a reason for hiding this comment

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

Ah. For NetworkController we prepopulate the state with network configurations for all known supported Infura networks, so that a consumer can access information for each network without having to explicitly add it:

function getDefaultNetworkConfigurationsByChainId(): Record<

getDefaultNetworkConfigurationsByChainId();

Does it make sense to do a similar thing for the MultichainNetworkController? I know things work differently here so I'm not sure.

"@metamask/utils": "^11.0.1"
},
"devDependencies": {
"@metamask/accounts-controller": "^21.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly to your question in tsconfig.json, do we need this dependency yet?

Comment on lines 12 to 23
/**
* Here we ensure that TypeScript resolves `@metamask/*` imports to the
* uncompiled source code for packages that live in this repo.
*
* NOTE: This must be synchronized with the `moduleNameMapper` option in
* `jest.config.packages.js`.
*
* NOTE 2: This is not necessary when copying this package to `packages/`.
*/
"paths": {
"@metamask/*": ["../../packages/*/src", "../*/src"]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, we should not need these changes as they are provided by tsconfig.packages.json.

@mcmire
Copy link
Contributor

mcmire commented Jan 29, 2025

A couple more things:

  • It looks like there are some lint violations in these new files. Due to some recent changes yarn lint:fix doesn't work as before (and the CI output is not very helpful, sorry). I would advise turning on Prettier in your editor and then re-saving all of the files to see if that gets through a lot of the lint violations. I'll make some suggestions in the PR if I spot anything other than formatting issues.
  • You might want to add this new package under teams.json and CODEOWNERS. We've had a couple of new controllers added recently — you can see this PR for an example: https://github.com/MetaMask/core/pull/5133/files#diff-3d36a1bf06148bc6ba1ce2ed3d19de32ea708d955fed212c0d27c536f0bd4da7

Comment on lines 6 to 7
AllowedActions,
AllowedEvents,
Copy link
Contributor

@mcmire mcmire Jan 29, 2025

Choose a reason for hiding this comment

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

It is okay to export these from the file, but not from the package itself:

Suggested change
AllowedActions,
AllowedEvents,

See here for more: https://github.com/MetaMask/core/blob/main/docs/writing-controllers.md#define-but-do-not-export-a-type-union-for-external-action-types

};

export type MultichainNetworkConfiguration = {
chainId: string; // Should be Caip2 type
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use CaipChainId from @metamask/utils

export type MultichainNetworkConfiguration = {
chainId: string; // Should be Caip2 type
name: string;
nativeCurrency: string; // Should be Caip19 type
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use CaipAssetType from @metamask/utils

string,
MultichainNetworkConfiguration
>;
selectedMultichainNetworkChainId: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should also be CaipChainId here IMO yes (CAIP-2) from @metamask/utils

>;
selectedMultichainNetworkChainId: string;
multichainNetworksMetadata: Record<string, MultichainNetworkMetadata>;
nonEvmSelected: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, instead of "storing" a state like this, we could compute differently maybe.

Unless we really need this to be in the state?

If we assume that selectedMultichainNetworkChainId will also be set to a CAIP-2 chain ID for EVM, we could compute it like:

isNonEvmSelectedNetwork(): boolean {
   const { namespace } = parseCaipChainId(this.state.selectedMultichainNetworkChainId);
   
   // 'eip155' is the CAIP-2 namespace for EVMs.
   return namespace !== KnownCaipNamespace.Eip155;
}

IMO, that's "safer" to compute it dynamically to avoid any de-sync logic between the 2, WDYT?

Note

This is true only if selectedMultichainNetworkChainId also references EVM chain IDs.

});
}

async setActiveNetwork(clientId: string, chainId?: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should also be a CaipChainId rather than a string here.

I include a suggestion, but you will need to add the associated import { CaipChainId } from '@metamask/utils' yourself 😅

Suggested change
async setActiveNetwork(clientId: string, chainId?: string): Promise<void> {
async setActiveNetwork(clientId: string, chainId?: CaipChainId): Promise<void> {

Comment on lines 8 to 9
export const bitcoinCaip2ChainId = 'bip122:000000000019d6689c085ae165831e93';
export const solanaCaip2ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
Copy link
Contributor

Choose a reason for hiding this comment

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

Those are now exported by the keyring-api, but not sure we want to have an extra deps to this package here?

We keep repeating those constants here and there, so for now we could just depend on it until we move them somewhere else if need be, see:


export const multichainNetworkConfigurations: Record<string, MultichainNetworkConfiguration> = {
bitcoinCaip2ChainId: {
chainId: bitcoinCaip2ChainId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be a BtcScope for now if we decide to use the keyring-api but again... IDK about depending on this package in this context 😅

isEvm: false,
},
solanaCaip2ChainId: {
chainId: solanaCaip2ChainId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment, but with SolScope

…script documentation, and solve most of the review comments
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.

4 participants