Skip to content

Commit c081c89

Browse files
vinnyhowardccharly
andauthored
feat: fetch networks with transaction activity by accounts (#5551)
## Explanation Currently, our client manually adds networks and checks user activity one network at a time. This is inefficient and doesn't scale well. This PR introduces a more efficient approach by: 1. Adding a new API integration that fetches active networks for multiple accounts in a single request 2. Managing network activity state through the controller 3. Adding type-safe methods to handle both EVM and non-EVM accounts 4. Extracting network fetching logic into a dedicated service layer Key additions: - `getNetworksWithTransactionActivityByAccounts`: Fetches active networks for accounts - `MultichainNetworkService`: New service layer handling network activity fetching - Enhanced state management for network configurations and activity - Improved error handling and fallback mechanisms The new implementation: - Reduces API calls through batch fetching - Improves separation of concerns with dedicated service layer - Enhances type safety and error handling - Provides better state management with fallbacks PRs for Client Integration [Extension](MetaMask/metamask-extension#31414) [Mobile](MetaMask/metamask-mobile#14348) ## References Related to [#4469 ](MetaMask/MetaMask-planning#4469) ## Changelog <!-- If you're making any consumer-facing changes, list those changes here as if you were updating a changelog, using the template below as a guide. (CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or FIXED. For security-related issues, follow the Security Advisory process.) Please take care to name the exact pieces of the API you've added or changed (e.g. types, interfaces, functions, or methods). If there are any breaking changes, make sure to offer a solution for consumers to follow once they upgrade to the changes. Finally, if you're only making changes to development scripts or tests, you may replace the template below with "None". --> ### `@metamask/multichain-network-controller` - **ADDED**: New method `getNetworksWithTransactionActivityByAccounts` to fetch active networks for accounts - **ADDED**: New `MultichainNetworkServiceController` for handling network activity fetching - **ADDED**: New types for network activity state and responses - **CHANGED**: Enhanced error handling for network requests - **CHANGED**: Improved type safety for messenger actions - **CHANGED**: Updated state management for network activity ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Charly Chevalier <[email protected]>
1 parent 374722f commit c081c89

18 files changed

+858
-22
lines changed

packages/multichain-network-controller/CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- New method `getNetworksWithTransactionActivityByAccounts` to fetch active networks for multiple accounts in a single request ([#5551](https://github.com/MetaMask/core/pull/5551))
13+
- New `MultichainNetworkService` for handling network activity fetching ([#5551](https://github.com/MetaMask/core/pull/5551))
14+
- New types for network activity state and responses ([#5551](https://github.com/MetaMask/core/pull/5551))
15+
16+
### Changed
17+
18+
- Updated state management for network activity ([#5551](https://github.com/MetaMask/core/pull/5551))
19+
1020
## [0.4.0]
1121

1222
### Added

packages/multichain-network-controller/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@
4848
},
4949
"dependencies": {
5050
"@metamask/base-controller": "^8.0.0",
51+
"@metamask/controller-utils": "^11.7.0",
5152
"@metamask/keyring-api": "^17.4.0",
53+
"@metamask/keyring-internal-api": "^6.0.1",
54+
"@metamask/superstruct": "^3.1.0",
5255
"@metamask/utils": "^11.2.0",
5356
"@solana/addresses": "^2.0.0"
5457
},

packages/multichain-network-controller/src/MultichainNetworkController.test.ts renamed to packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.test.ts

+152-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
SolAccountType,
99
type KeyringAccountType,
1010
type CaipChainId,
11+
EthScope,
1112
} from '@metamask/keyring-api';
1213
import type {
1314
NetworkControllerGetStateAction,
@@ -16,17 +17,36 @@ import type {
1617
NetworkControllerRemoveNetworkAction,
1718
NetworkControllerFindNetworkClientIdByChainIdAction,
1819
} from '@metamask/network-controller';
20+
import { KnownCaipNamespace, type CaipAccountId } from '@metamask/utils';
1921

20-
import { getDefaultMultichainNetworkControllerState } from './constants';
2122
import { MultichainNetworkController } from './MultichainNetworkController';
23+
import { createMockInternalAccount } from '../../tests/utils';
24+
import { type ActiveNetworksResponse } from '../api/accounts-api';
25+
import { getDefaultMultichainNetworkControllerState } from '../constants';
26+
import type { AbstractMultichainNetworkService } from '../MultichainNetworkService/AbstractMultichainNetworkService';
2227
import {
2328
type AllowedActions,
2429
type AllowedEvents,
2530
type MultichainNetworkControllerAllowedActions,
2631
type MultichainNetworkControllerAllowedEvents,
2732
MULTICHAIN_NETWORK_CONTROLLER_NAME,
28-
} from './types';
29-
import { createMockInternalAccount } from '../tests/utils';
33+
} from '../types';
34+
35+
/**
36+
* Creates a mock network service for testing.
37+
*
38+
* @param mockResponse - The mock response to return from fetchNetworkActivity
39+
* @returns A mock network service that implements the MultichainNetworkService interface.
40+
*/
41+
function createMockNetworkService(
42+
mockResponse: ActiveNetworksResponse = { activeNetworks: [] },
43+
): AbstractMultichainNetworkService {
44+
return {
45+
fetchNetworkActivity: jest
46+
.fn<Promise<ActiveNetworksResponse>, [CaipAccountId[]]>()
47+
.mockResolvedValue(mockResponse),
48+
};
49+
}
3050

3151
/**
3252
* Setup a test controller instance.
@@ -38,6 +58,7 @@ import { createMockInternalAccount } from '../tests/utils';
3858
* @param args.removeNetwork - Mock for NetworkController:removeNetwork action.
3959
* @param args.getSelectedChainId - Mock for NetworkController:getSelectedChainId action.
4060
* @param args.findNetworkClientIdByChainId - Mock for NetworkController:findNetworkClientIdByChainId action.
61+
* @param args.mockNetworkService - Mock for MultichainNetworkService.
4162
* @returns A collection of test controllers and mocks.
4263
*/
4364
function setupController({
@@ -47,6 +68,7 @@ function setupController({
4768
removeNetwork,
4869
getSelectedChainId,
4970
findNetworkClientIdByChainId,
71+
mockNetworkService,
5072
}: {
5173
options?: Partial<
5274
ConstructorParameters<typeof MultichainNetworkController>[0]
@@ -71,6 +93,7 @@ function setupController({
7193
ReturnType<NetworkControllerFindNetworkClientIdByChainIdAction['handler']>,
7294
Parameters<NetworkControllerFindNetworkClientIdByChainIdAction['handler']>
7395
>;
96+
mockNetworkService?: AbstractMultichainNetworkService;
7497
} = {}) {
7598
const messenger = new Messenger<
7699
MultichainNetworkControllerAllowedActions,
@@ -149,18 +172,21 @@ function setupController({
149172
'NetworkController:removeNetwork',
150173
'NetworkController:getSelectedChainId',
151174
'NetworkController:findNetworkClientIdByChainId',
175+
'AccountsController:listMultichainAccounts',
152176
],
153177
allowedEvents: ['AccountsController:selectedAccountChange'],
154178
});
155179

156-
// Default state to use Solana network with EVM as active network
180+
const defaultNetworkService = createMockNetworkService();
181+
157182
const controller = new MultichainNetworkController({
158-
messenger: options.messenger || controllerMessenger,
183+
messenger: options.messenger ?? controllerMessenger,
159184
state: {
160185
selectedMultichainNetworkChainId: SolScope.Mainnet,
161186
isEvmSelected: true,
162187
...options.state,
163188
},
189+
networkService: mockNetworkService ?? defaultNetworkService,
164190
});
165191

166192
const triggerSelectedAccountChange = (accountType: KeyringAccountType) => {
@@ -191,6 +217,7 @@ function setupController({
191217
mockFindNetworkClientIdByChainId,
192218
publishSpy,
193219
triggerSelectedAccountChange,
220+
networkService: mockNetworkService ?? defaultNetworkService,
194221
};
195222
}
196223

@@ -545,4 +572,124 @@ describe('MultichainNetworkController', () => {
545572
);
546573
});
547574
});
575+
576+
describe('getNetworksWithTransactionActivityByAccounts', () => {
577+
const MOCK_EVM_ADDRESS = '0x1234567890123456789012345678901234567890';
578+
const MOCK_SOLANA_ADDRESS = 'solana123';
579+
const MOCK_EVM_CHAIN_1 = '1';
580+
const MOCK_EVM_CHAIN_137 = '137';
581+
const MOCK_SOLANA_CHAIN = '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
582+
583+
it('returns empty object when no accounts exist', async () => {
584+
const { controller, messenger } = setupController({
585+
getSelectedChainId: jest.fn().mockReturnValue('0x1'),
586+
});
587+
588+
messenger.registerActionHandler(
589+
'AccountsController:listMultichainAccounts',
590+
() => [],
591+
);
592+
593+
const result =
594+
await controller.getNetworksWithTransactionActivityByAccounts();
595+
expect(result).toStrictEqual({});
596+
});
597+
598+
it('fetches and formats network activity for EVM accounts', async () => {
599+
const mockResponse: ActiveNetworksResponse = {
600+
activeNetworks: [
601+
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_1}:${MOCK_EVM_ADDRESS}`,
602+
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_137}:${MOCK_EVM_ADDRESS}`,
603+
],
604+
};
605+
606+
const mockNetworkService = createMockNetworkService(mockResponse);
607+
await mockNetworkService.fetchNetworkActivity([
608+
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_1}:${MOCK_EVM_ADDRESS}`,
609+
]);
610+
611+
const { controller, messenger } = setupController({
612+
mockNetworkService,
613+
});
614+
615+
messenger.registerActionHandler(
616+
'AccountsController:listMultichainAccounts',
617+
() => [
618+
createMockInternalAccount({
619+
type: EthAccountType.Eoa,
620+
address: MOCK_EVM_ADDRESS,
621+
scopes: [EthScope.Eoa],
622+
}),
623+
],
624+
);
625+
626+
const result =
627+
await controller.getNetworksWithTransactionActivityByAccounts();
628+
629+
expect(mockNetworkService.fetchNetworkActivity).toHaveBeenCalledWith([
630+
`${KnownCaipNamespace.Eip155}:0:${MOCK_EVM_ADDRESS}`,
631+
]);
632+
633+
expect(result).toStrictEqual({
634+
[MOCK_EVM_ADDRESS]: {
635+
namespace: KnownCaipNamespace.Eip155,
636+
activeChains: [MOCK_EVM_CHAIN_1, MOCK_EVM_CHAIN_137],
637+
},
638+
});
639+
});
640+
641+
it('formats network activity for mixed EVM and non-EVM accounts', async () => {
642+
const mockResponse: ActiveNetworksResponse = {
643+
activeNetworks: [
644+
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_1}:${MOCK_EVM_ADDRESS}`,
645+
`${KnownCaipNamespace.Solana}:${MOCK_SOLANA_CHAIN}:${MOCK_SOLANA_ADDRESS}`,
646+
],
647+
};
648+
649+
const mockNetworkService = createMockNetworkService(mockResponse);
650+
await mockNetworkService.fetchNetworkActivity([
651+
`${KnownCaipNamespace.Eip155}:${MOCK_EVM_CHAIN_1}:${MOCK_EVM_ADDRESS}`,
652+
`${KnownCaipNamespace.Solana}:${MOCK_SOLANA_CHAIN}:${MOCK_SOLANA_ADDRESS}`,
653+
]);
654+
655+
const { controller, messenger } = setupController({
656+
mockNetworkService,
657+
});
658+
659+
messenger.registerActionHandler(
660+
'AccountsController:listMultichainAccounts',
661+
() => [
662+
createMockInternalAccount({
663+
type: EthAccountType.Eoa,
664+
address: MOCK_EVM_ADDRESS,
665+
scopes: [EthScope.Eoa],
666+
}),
667+
createMockInternalAccount({
668+
type: SolAccountType.DataAccount,
669+
address: MOCK_SOLANA_ADDRESS,
670+
scopes: [SolScope.Mainnet],
671+
}),
672+
],
673+
);
674+
675+
const result =
676+
await controller.getNetworksWithTransactionActivityByAccounts();
677+
678+
expect(mockNetworkService.fetchNetworkActivity).toHaveBeenCalledWith([
679+
`${KnownCaipNamespace.Eip155}:0:${MOCK_EVM_ADDRESS}`,
680+
`${KnownCaipNamespace.Solana}:${MOCK_SOLANA_CHAIN}:${MOCK_SOLANA_ADDRESS}`,
681+
]);
682+
683+
expect(result).toStrictEqual({
684+
[MOCK_EVM_ADDRESS]: {
685+
namespace: KnownCaipNamespace.Eip155,
686+
activeChains: [MOCK_EVM_CHAIN_1],
687+
},
688+
[MOCK_SOLANA_ADDRESS]: {
689+
namespace: KnownCaipNamespace.Solana,
690+
activeChains: [MOCK_SOLANA_CHAIN],
691+
},
692+
});
693+
});
694+
});
548695
});

packages/multichain-network-controller/src/MultichainNetworkController.ts renamed to packages/multichain-network-controller/src/MultichainNetworkController/MultichainNetworkController.ts

+47-3
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,29 @@ import type { InternalAccount } from '@metamask/keyring-internal-api';
44
import type { NetworkClientId } from '@metamask/network-controller';
55
import { type CaipChainId, isCaipChainId } from '@metamask/utils';
66

7+
import {
8+
type ActiveNetworksByAddress,
9+
toAllowedCaipAccountIds,
10+
toActiveNetworksByAddress,
11+
} from '../api/accounts-api';
712
import {
813
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
914
MULTICHAIN_NETWORK_CONTROLLER_METADATA,
1015
getDefaultMultichainNetworkControllerState,
11-
} from './constants';
16+
} from '../constants';
17+
import type { AbstractMultichainNetworkService } from '../MultichainNetworkService/AbstractMultichainNetworkService';
1218
import {
1319
MULTICHAIN_NETWORK_CONTROLLER_NAME,
1420
type MultichainNetworkControllerState,
1521
type MultichainNetworkControllerMessenger,
1622
type SupportedCaipChainId,
17-
} from './types';
23+
} from '../types';
1824
import {
1925
checkIfSupportedCaipChainId,
2026
getChainIdForNonEvmAddress,
2127
convertEvmCaipToHexChainId,
2228
isEvmCaipChainId,
23-
} from './utils';
29+
} from '../utils';
2430

2531
/**
2632
* The MultichainNetworkController is responsible for fetching and caching account
@@ -31,15 +37,19 @@ export class MultichainNetworkController extends BaseController<
3137
MultichainNetworkControllerState,
3238
MultichainNetworkControllerMessenger
3339
> {
40+
readonly #networkService: AbstractMultichainNetworkService;
41+
3442
constructor({
3543
messenger,
3644
state,
45+
networkService,
3746
}: {
3847
messenger: MultichainNetworkControllerMessenger;
3948
state?: Omit<
4049
Partial<MultichainNetworkControllerState>,
4150
'multichainNetworkConfigurationsByChainId'
4251
>;
52+
networkService: AbstractMultichainNetworkService;
4353
}) {
4454
super({
4555
messenger,
@@ -55,6 +65,7 @@ export class MultichainNetworkController extends BaseController<
5565
},
5666
});
5767

68+
this.#networkService = networkService;
5869
this.#subscribeToMessageEvents();
5970
this.#registerMessageHandlers();
6071
}
@@ -144,6 +155,35 @@ export class MultichainNetworkController extends BaseController<
144155
return await this.#setActiveEvmNetwork(id);
145156
}
146157

158+
/**
159+
* Returns the active networks for the available EVM addresses (non-EVM networks will be supported in the future).
160+
* Fetches the data from the API and caches it in state.
161+
*
162+
* @returns A promise that resolves to the active networks for the available addresses
163+
*/
164+
async getNetworksWithTransactionActivityByAccounts(): Promise<ActiveNetworksByAddress> {
165+
const accounts = this.messagingSystem.call(
166+
'AccountsController:listMultichainAccounts',
167+
);
168+
if (!accounts || accounts.length === 0) {
169+
return this.state.networksWithTransactionActivity;
170+
}
171+
172+
const formattedAccounts = accounts
173+
.map((account: InternalAccount) => toAllowedCaipAccountIds(account))
174+
.flat();
175+
176+
const activeNetworks =
177+
await this.#networkService.fetchNetworkActivity(formattedAccounts);
178+
const formattedNetworks = toActiveNetworksByAddress(activeNetworks);
179+
180+
this.update((state) => {
181+
state.networksWithTransactionActivity = formattedNetworks;
182+
});
183+
184+
return this.state.networksWithTransactionActivity;
185+
}
186+
147187
/**
148188
* Removes an EVM network from the list of networks.
149189
* This method re-directs the request to the network-controller.
@@ -268,5 +308,9 @@ export class MultichainNetworkController extends BaseController<
268308
'MultichainNetworkController:setActiveNetwork',
269309
this.setActiveNetwork.bind(this),
270310
);
311+
this.messagingSystem.registerActionHandler(
312+
'MultichainNetworkController:getNetworksWithTransactionActivityByAccounts',
313+
this.getNetworksWithTransactionActivityByAccounts.bind(this),
314+
);
271315
}
272316
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { PublicInterface } from '@metamask/utils';
2+
3+
import type { MultichainNetworkService } from './MultichainNetworkService';
4+
5+
/**
6+
* A service object which is responsible for fetching network activity data.
7+
*/
8+
export type AbstractMultichainNetworkService =
9+
PublicInterface<MultichainNetworkService>;

0 commit comments

Comments
 (0)