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: fetch networks with transaction activity by accounts #5551

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a038b1f
refactor: moved logic from networks controller to multichain networks…
vinnyhoward Mar 27, 2025
ed1e38b
feat: getNetworksWithActivityByAccounts fetches active accounts based…
vinnyhoward Mar 28, 2025
77e7d53
Merge branch 'main' of github.com:MetaMask/core into feat-4469-active…
vinnyhoward Mar 31, 2025
8305935
fix: added proper mocks and tests coverage
vinnyhoward Mar 31, 2025
570daeb
fix: increase test coverage
vinnyhoward Mar 31, 2025
33da755
fix: increased test coverage and removed abort controller in new util…
vinnyhoward Mar 31, 2025
515e8ca
Merge branch 'main' of github.com:MetaMask/core into feat-4469-active…
vinnyhoward Mar 31, 2025
f795988
feat: add multichain network activity support
vinnyhoward Apr 1, 2025
9e07ee4
Merge branch 'main' of github.com:MetaMask/core into feat-4469-active…
vinnyhoward Apr 1, 2025
c27c25f
feat: updated headers to track where API is in use
vinnyhoward Apr 1, 2025
c3c3549
feat: improved response validation and format, improved test coverage…
vinnyhoward Apr 2, 2025
ac8bf2e
Merge branch 'main' into feat-4469-active-networks-method
vinnyhoward Apr 2, 2025
11f780a
test: increased coverage
vinnyhoward Apr 2, 2025
0aeff1a
Merge branch 'feat-4469-active-networks-method' of github.com:MetaMas…
vinnyhoward Apr 2, 2025
e52518a
update: bump controller-utils version
vinnyhoward Apr 2, 2025
1fc89c1
fix: improved formatCaipAccountId tests
vinnyhoward Apr 2, 2025
a47f771
fix: improved types
vinnyhoward Apr 4, 2025
67e4a07
refactor: replaced some validation logic with helper functions from @…
vinnyhoward Apr 7, 2025
973a141
Update packages/multichain-network-controller/src/utils.ts
vinnyhoward Apr 7, 2025
afab1fd
refactor: extract network activity fetching into dedicated service an…
vinnyhoward Apr 8, 2025
c953452
Merge branch 'feat-4469-active-networks-method'
vinnyhoward Apr 8, 2025
7c51f40
Merge branch 'main' of github.com:MetaMask/core into feat-4469-active…
vinnyhoward Apr 8, 2025
c3f1043
fix: lint package issues, removed clear mocks, removed try/catch for …
vinnyhoward Apr 8, 2025
459cf6d
fix: removed unused import
vinnyhoward Apr 8, 2025
4821961
feat: Refactor and enhance type validation
vinnyhoward Apr 8, 2025
9eacbe4
Merge branch 'main' of github.com:MetaMask/core into feat-4469-active…
vinnyhoward Apr 8, 2025
7ed8669
fix: lint
vinnyhoward Apr 8, 2025
65ce16c
Update packages/multichain-network-controller/src/constants.ts
vinnyhoward Apr 9, 2025
2fe78bd
Update packages/multichain-network-controller/src/types.ts
vinnyhoward Apr 9, 2025
a63885f
Update packages/multichain-network-controller/src/types.ts
vinnyhoward Apr 9, 2025
2e3ce04
Update packages/multichain-network-controller/src/types.ts
vinnyhoward Apr 9, 2025
d78a816
Update packages/multichain-network-controller/src/utils.ts
vinnyhoward Apr 9, 2025
6c68265
refactor(tests): improve utils.test.ts maintainability and type safety
vinnyhoward Apr 9, 2025
91e210e
Merge branch 'main' into feat-4469-active-networks-method
vinnyhoward Apr 9, 2025
276f1b2
refactor: use abstract network service type in multichain network con…
vinnyhoward Apr 9, 2025
eb661b5
fix: lint
vinnyhoward Apr 9, 2025
8357ae7
refactor: improve CAIP validation using superstruct
vinnyhoward Apr 10, 2025
d05ea8e
refactor: reorganized folder structure
vinnyhoward Apr 10, 2025
3f9ea1c
fix: lint
vinnyhoward Apr 10, 2025
dc6f29e
refactor: moved all api related logic into its own file to mirror rep…
vinnyhoward Apr 10, 2025
2cc91f2
Merge branch 'main' of github.com:MetaMask/core into feat-4469-active…
vinnyhoward Apr 10, 2025
7acf9a5
refactor: moved all api related utlity functions and its respective t…
vinnyhoward Apr 10, 2025
5a1b297
Update packages/multichain-network-controller/src/MultichainNetworkSe…
vinnyhoward Apr 11, 2025
89f1f8b
Update packages/multichain-network-controller/src/api/accounts-api.te…
vinnyhoward Apr 11, 2025
ef2e717
Update packages/multichain-network-controller/src/api/accounts-api.te…
vinnyhoward Apr 11, 2025
8575f42
Update packages/multichain-network-controller/src/api/accounts-api.te…
vinnyhoward Apr 11, 2025
eedaca9
Update packages/multichain-network-controller/src/api/accounts-api.te…
vinnyhoward Apr 11, 2025
38d67de
Update packages/multichain-network-controller/src/types.ts
vinnyhoward Apr 11, 2025
ee22bdf
Update packages/multichain-network-controller/src/MultichainNetworkCo…
vinnyhoward Apr 11, 2025
67b43a0
Update packages/multichain-network-controller/src/MultichainNetworkCo…
vinnyhoward Apr 11, 2025
79b99de
Update packages/multichain-network-controller/src/MultichainNetworkCo…
vinnyhoward Apr 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/multichain-network-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- New method `getNetworksWithTransactionActivityByAccounts` to fetch active networks for multiple accounts in a single request ([#5551](https://github.com/MetaMask/core/pull/5551))
- New `MultichainNetworkService` for handling network activity fetching ([#5551](https://github.com/MetaMask/core/pull/5551))
- New types for network activity state and responses ([#5551](https://github.com/MetaMask/core/pull/5551))

### Changed

- Updated state management for network activity ([#5551](https://github.com/MetaMask/core/pull/5551))

## [0.3.0]

### Changed
Expand Down
6 changes: 5 additions & 1 deletion packages/multichain-network-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,19 @@
},
"dependencies": {
"@metamask/base-controller": "^8.0.0",
"@metamask/controller-utils": "^11.7.0",
"@metamask/keyring-api": "^17.4.0",
"@metamask/keyring-utils": "^3.0.0",
"@metamask/utils": "^11.2.0",
"@solana/addresses": "^2.0.0"
"@solana/addresses": "^2.0.0",
"loglevel": "^1.8.1"
},
"devDependencies": {
"@metamask/accounts-controller": "^27.0.0",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/keyring-controller": "^21.0.2",
"@metamask/network-controller": "^23.2.0",
"@metamask/superstruct": "^3.1.0",
"@types/jest": "^27.4.1",
"@types/uuid": "^8.3.0",
"deepmerge": "^4.2.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SolAccountType,
type KeyringAccountType,
type CaipChainId,
EthScope,
} from '@metamask/keyring-api';
import type {
NetworkControllerGetStateAction,
Expand All @@ -16,17 +17,34 @@ import type {
NetworkControllerRemoveNetworkAction,
NetworkControllerFindNetworkClientIdByChainIdAction,
} from '@metamask/network-controller';
import { KnownCaipNamespace } from '@metamask/utils';
import log from 'loglevel';

import { getDefaultMultichainNetworkControllerState } from './constants';
import { MultichainNetworkController } from './MultichainNetworkController';
import { createMockInternalAccount } from '../../tests/utils';
import { type ActiveNetworksResponse } from '../api/accounts-api';
import { getDefaultMultichainNetworkControllerState } from '../constants';
import type { AbstractMultichainNetworkService } from '../MultichainNetworkService/AbstractMultichainNetworkService';
import {
type AllowedActions,
type AllowedEvents,
type MultichainNetworkControllerAllowedActions,
type MultichainNetworkControllerAllowedEvents,
MULTICHAIN_NETWORK_CONTROLLER_NAME,
} from './types';
import { createMockInternalAccount } from '../tests/utils';
} from '../types';

/**
* Creates a mock network service for testing.
*
* @returns A mock network service that implements the MultichainNetworkService interface.
*/
function createMockNetworkService(): AbstractMultichainNetworkService {
return {
fetchNetworkActivity: jest
.fn()
.mockResolvedValue({ activeNetworks: [] } as ActiveNetworksResponse),
};
}

/**
* Setup a test controller instance.
Expand All @@ -38,6 +56,7 @@ import { createMockInternalAccount } from '../tests/utils';
* @param args.removeNetwork - Mock for NetworkController:removeNetwork action.
* @param args.getSelectedChainId - Mock for NetworkController:getSelectedChainId action.
* @param args.findNetworkClientIdByChainId - Mock for NetworkController:findNetworkClientIdByChainId action.
* @param args.mockNetworkService - Mock for MultichainNetworkService.
* @returns A collection of test controllers and mocks.
*/
function setupController({
Expand All @@ -47,6 +66,7 @@ function setupController({
removeNetwork,
getSelectedChainId,
findNetworkClientIdByChainId,
mockNetworkService,
}: {
options?: Partial<
ConstructorParameters<typeof MultichainNetworkController>[0]
Expand All @@ -71,6 +91,7 @@ function setupController({
ReturnType<NetworkControllerFindNetworkClientIdByChainIdAction['handler']>,
Parameters<NetworkControllerFindNetworkClientIdByChainIdAction['handler']>
>;
mockNetworkService?: AbstractMultichainNetworkService;
} = {}) {
const messenger = new Messenger<
MultichainNetworkControllerAllowedActions,
Expand Down Expand Up @@ -149,18 +170,21 @@ function setupController({
'NetworkController:removeNetwork',
'NetworkController:getSelectedChainId',
'NetworkController:findNetworkClientIdByChainId',
'AccountsController:listMultichainAccounts',
],
allowedEvents: ['AccountsController:selectedAccountChange'],
});

// Default state to use Solana network with EVM as active network
const defaultNetworkService = createMockNetworkService();

const controller = new MultichainNetworkController({
messenger: options.messenger || controllerMessenger,
messenger: options.messenger ?? controllerMessenger,
state: {
selectedMultichainNetworkChainId: SolScope.Mainnet,
isEvmSelected: true,
...options.state,
},
networkService: mockNetworkService ?? defaultNetworkService,
});

const triggerSelectedAccountChange = (accountType: KeyringAccountType) => {
Expand Down Expand Up @@ -191,6 +215,7 @@ function setupController({
mockFindNetworkClientIdByChainId,
publishSpy,
triggerSelectedAccountChange,
networkService: mockNetworkService ?? defaultNetworkService,
};
}

Expand Down Expand Up @@ -522,4 +547,123 @@ describe('MultichainNetworkController', () => {
);
});
});

describe('getNetworksWithTransactionActivityByAccounts', () => {
const MOCK_EVM_ADDRESS = '0x1234567890123456789012345678901234567890';
const MOCK_SOLANA_ADDRESS = 'solana123';
const MOCK_EVM_CHAIN_1 = '1';
const MOCK_EVM_CHAIN_137 = '137';
const MOCK_SOLANA_CHAIN = '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';

beforeEach(() => {
jest.spyOn(log, 'error').mockImplementation();
});

it('returns empty object when no accounts exist', async () => {
const { controller, messenger } = setupController({
getSelectedChainId: jest.fn().mockReturnValue('0x1'),
});

messenger.registerActionHandler(
'AccountsController:listMultichainAccounts',
() => [],
);

const result =
await controller.getNetworksWithTransactionActivityByAccounts();
expect(result).toStrictEqual({});
});

it('fetches and formats network activity for EVM accounts', async () => {
const mockResponse = {
activeNetworks: [
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_1}:${MOCK_EVM_ADDRESS}`,
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_137}:${MOCK_EVM_ADDRESS}`,
],
};

const mockNetworkService = createMockNetworkService();
mockNetworkService.fetchNetworkActivity.mockResolvedValue(mockResponse);

const { controller, messenger } = setupController({
mockNetworkService,
});

messenger.registerActionHandler(
'AccountsController:listMultichainAccounts',
() => [
createMockInternalAccount({
type: EthAccountType.Eoa,
address: MOCK_EVM_ADDRESS,
scopes: [EthScope.Eoa],
}),
],
);

const result =
await controller.getNetworksWithTransactionActivityByAccounts();

expect(mockNetworkService.fetchNetworkActivity).toHaveBeenCalledWith([
`${KnownCaipNamespace.Eip155}:0:${MOCK_EVM_ADDRESS}`,
]);

expect(result).toStrictEqual({
[MOCK_EVM_ADDRESS]: {
namespace: KnownCaipNamespace.Eip155,
activeChains: [MOCK_EVM_CHAIN_1, MOCK_EVM_CHAIN_137],
},
});
});

it('formats network activity for mixed EVM and non-EVM accounts', async () => {
const mockResponse = {
activeNetworks: [
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_1}:${MOCK_EVM_ADDRESS}`,
`${KnownCaipNamespace.Solana}:${MOCK_SOLANA_CHAIN}:${MOCK_SOLANA_ADDRESS}`,
],
};

const mockNetworkService = createMockNetworkService();
mockNetworkService.fetchNetworkActivity.mockResolvedValue(mockResponse);

const { controller, messenger } = setupController({
mockNetworkService,
});

messenger.registerActionHandler(
'AccountsController:listMultichainAccounts',
() => [
createMockInternalAccount({
type: EthAccountType.Eoa,
address: MOCK_EVM_ADDRESS,
scopes: [EthScope.Eoa],
}),
createMockInternalAccount({
type: SolAccountType.DataAccount,
address: MOCK_SOLANA_ADDRESS,
scopes: [SolScope.Mainnet],
}),
],
);

const result =
await controller.getNetworksWithTransactionActivityByAccounts();

expect(mockNetworkService.fetchNetworkActivity).toHaveBeenCalledWith([
`${KnownCaipNamespace.Eip155}:0:${MOCK_EVM_ADDRESS}`,
`${KnownCaipNamespace.Solana}:${MOCK_SOLANA_CHAIN}:${MOCK_SOLANA_ADDRESS}`,
]);

expect(result).toStrictEqual({
[MOCK_EVM_ADDRESS]: {
namespace: KnownCaipNamespace.Eip155,
activeChains: [MOCK_EVM_CHAIN_1],
},
[MOCK_SOLANA_ADDRESS]: {
namespace: KnownCaipNamespace.Solana,
activeChains: [MOCK_SOLANA_CHAIN],
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@ import type { InternalAccount } from '@metamask/keyring-internal-api';
import type { NetworkClientId } from '@metamask/network-controller';
import { type CaipChainId, isCaipChainId } from '@metamask/utils';

import {
type ActiveNetworksByAddress,
toAllowedCaipAccountIds,
toActiveNetworksByAddress,
} from '../api/accounts-api';
import {
MULTICHAIN_NETWORK_CONTROLLER_METADATA,
getDefaultMultichainNetworkControllerState,
} from './constants';
} from '../constants';
import type { AbstractMultichainNetworkService } from '../MultichainNetworkService/AbstractMultichainNetworkService';
import {
MULTICHAIN_NETWORK_CONTROLLER_NAME,
type MultichainNetworkControllerState,
type MultichainNetworkControllerMessenger,
type SupportedCaipChainId,
} from './types';
} from '../types';
import {
checkIfSupportedCaipChainId,
getChainIdForNonEvmAddress,
convertEvmCaipToHexChainId,
isEvmCaipChainId,
} from './utils';
} from '../utils';

/**
* The MultichainNetworkController is responsible for fetching and caching account
Expand All @@ -30,15 +36,19 @@ export class MultichainNetworkController extends BaseController<
MultichainNetworkControllerState,
MultichainNetworkControllerMessenger
> {
readonly #networkService: AbstractMultichainNetworkService;

constructor({
messenger,
state,
networkService,
}: {
messenger: MultichainNetworkControllerMessenger;
state?: Omit<
Partial<MultichainNetworkControllerState>,
'multichainNetworkConfigurationsByChainId'
>;
networkService: AbstractMultichainNetworkService;
}) {
super({
messenger,
Expand All @@ -50,6 +60,7 @@ export class MultichainNetworkController extends BaseController<
},
});

this.#networkService = networkService;
this.#subscribeToMessageEvents();
this.#registerMessageHandlers();
}
Expand Down Expand Up @@ -139,6 +150,35 @@ export class MultichainNetworkController extends BaseController<
return await this.#setActiveEvmNetwork(id);
}

/**
* Returns the active networks for the available EVM addresses (non-EVM networks will be supported in the future).
* Fetches the data from the API and caches it in state.
*
* @returns A promise that resolves to the active networks for the available addresses
*/
async getNetworksWithTransactionActivityByAccounts(): Promise<ActiveNetworksByAddress> {
const accounts = this.messagingSystem.call(
'AccountsController:listMultichainAccounts',
);
if (!accounts || accounts.length === 0) {
return this.state.networksWithTransactionActivity;
}

const formattedAccounts = accounts
.map((account: InternalAccount) => toAllowedCaipAccountIds(account))
.flat();

const activeNetworks =
await this.#networkService.fetchNetworkActivity(formattedAccounts);
const formattedNetworks = toActiveNetworksByAddress(activeNetworks);

this.update((state) => {
state.networksWithTransactionActivity = formattedNetworks;
});

return this.state.networksWithTransactionActivity;
}

/**
* Removes an EVM network from the list of networks.
* This method re-directs the request to the network-controller.
Expand Down Expand Up @@ -260,5 +300,9 @@ export class MultichainNetworkController extends BaseController<
'MultichainNetworkController:setActiveNetwork',
this.setActiveNetwork.bind(this),
);
this.messagingSystem.registerActionHandler(
'MultichainNetworkController:getNetworksWithTransactionActivityByAccounts',
this.getNetworksWithTransactionActivityByAccounts.bind(this),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PublicInterface } from '@metamask/utils';

import type { MultichainNetworkService } from './MultichainNetworkService';

/**
* A service object which is responsible for fetching network activity data.
*/
export type AbstractMultichainNetworkService =
PublicInterface<MultichainNetworkService>;
Loading
Loading