Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ linkStyle default opacity:0.5
earn_controller --> transaction_controller;
eip_5792_middleware --> transaction_controller;
eip_5792_middleware --> keyring_controller;
eip_7702_internal_rpc_middleware --> controller_utils;
eip1193_permission_middleware --> chain_agnostic_permission;
eip1193_permission_middleware --> controller_utils;
eip1193_permission_middleware --> json_rpc_engine;
Expand Down Expand Up @@ -301,6 +302,7 @@ linkStyle default opacity:0.5
remote_feature_flag_controller --> base_controller;
remote_feature_flag_controller --> controller_utils;
sample_controllers --> base_controller;
sample_controllers --> messenger;
sample_controllers --> controller_utils;
sample_controllers --> network_controller;
seedless_onboarding_controller --> base_controller;
Expand Down
336 changes: 158 additions & 178 deletions docs/controller-guidelines.md

Large diffs are not rendered by default.

88 changes: 53 additions & 35 deletions docs/data-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

## What is a data service?

A **data service** is a pattern for making interactions with an external API (fetching token prices, storing accounts, etc.). It is implemented as a plain TypeScript class with methods that are exposed through the messaging system.
A **data service** is a pattern for making interactions with an external API (fetching token prices, storing accounts, etc.). It is implemented as a plain TypeScript class with methods that are exposed through a messenger.

## Why use this pattern?

If you want to talk to an API, it might be tempting to define a method in the controller or a function in a separate file. However, implementing the data service pattern is advantageous for the following reasons:

1. The pattern provides an abstraction that allows for implementing and reusing strategies that are common when working with external APIs, such as batching, automatic retries with exponential backoff, etc.
2. By integrating with the messaging system, other parts of the application can make use of the data service without needing to go through the controller, or in fact, without needing a reference to the data service at all.
2. By integrating with a messenger, other parts of the application can make use of the data service without needing to go through the controller, or in fact, without needing a reference to the data service at all.

## How to create a data service

Expand Down Expand Up @@ -78,7 +78,7 @@ Next we'll define the messenger. We give the messenger a namespace, and we expos
```typescript
// (top of file)

import type { RestrictedMessenger } from '@metamask/base-controller';
import type { Messenger } from '@metamask/messenger';

const SERVICE_NAME = 'GasPricesService';

Expand All @@ -95,21 +95,19 @@ export type GasPricesServiceEvents = never;

type AllowedEvents = never;

export type GasPricesServiceMessenger = RestrictedMessenger<
export type GasPricesServiceMessenger = Messenger<
typeof SERVICE_NAME,
GasPricesServiceActions | AllowedActions,
GasPricesServiceEvents | AllowedEvents,
AllowedActions['type'],
AllowedEvents['type']
GasPricesServiceEvents | AllowedEvents
>;

// ...
```

Note that we need to add `@metamask/base-controller` as a direct dependency of the package to bring in the `RestrictedMessenger` type (here we assume that our package is called `@metamask/gas-prices-controller`):
Note that we need to add `@metamask/messenger` as a direct dependency of the package to bring in the `Messenger` type (here we assume that our package is called `@metamask/gas-prices-controller`):

```shell
yarn workspace @metamask/gas-prices-controller add @metamask/base-controller
yarn workspace @metamask/gas-prices-controller add @metamask/messenger
```

Finally we will register the method as an action handler on the messenger:
Expand Down Expand Up @@ -145,7 +143,7 @@ export class GasPricesService {
<details><summary><b>View whole file</b></summary><br />

```typescript
import type { RestrictedMessenger } from '@metamask/base-controller';
import type { Messenger } from '@metamask/messenger';

const SERVICE_NAME = 'GasPricesService';

Expand All @@ -162,12 +160,10 @@ export type GasPricesServiceEvents = never;

type AllowedEvents = never;

export type GasPricesServiceMessenger = RestrictedMessenger<
export type GasPricesServiceMessenger = Messenger<
typeof SERVICE_NAME,
GasPricesServiceActions | AllowedActions,
GasPricesServiceEvents | AllowedEvents,
AllowedActions['type'],
AllowedEvents['type']
GasPricesServiceEvents | AllowedEvents
>;

type GasPricesResponse = {
Expand Down Expand Up @@ -272,10 +268,12 @@ import { Messenger } from '@metamask/base-controller';
// ...

function buildMessenger(): GasPricesServiceMessenger {
return new Messenger().getRestricted({
name: 'GasPricesService',
allowedActions: [],
allowedEvents: [],
return new Messenger<
'GasPricesService',
GasPricesServiceActions,
GasPricesServiceEvents
>({
namespace: 'GasPricesService',
});
}
```
Expand Down Expand Up @@ -321,7 +319,11 @@ describe('GasPricesService', () => {
```typescript
import nock from 'nock';

import type { GasPricesServiceMessenger } from './gas-prices-service';
import type {
GasPricesServiceMessenger,
GasPricesServiceActions,
GasPricesServiceEvents,
} from './gas-prices-service';
import { GasPricesService } from './gas-prices-service';

describe('GasPricesService', () => {
Expand Down Expand Up @@ -375,10 +377,12 @@ describe('GasPricesService', () => {
});

function buildMessenger(): GasPricesServiceMessenger {
return new Messenger().getRestricted({
name: 'GasPricesService',
allowedActions: [],
allowedEvents: [],
return new Messenger<
'GasPricesService',
GasPricesServiceActions,
GasPricesServiceEvents
>({
namespace: 'GasPricesService',
});
}
```
Expand All @@ -387,30 +391,37 @@ function buildMessenger(): GasPricesServiceMessenger {

## How to use a data service

Let's say that we wanted to use our data service that we built above. To do this, we will instantiate the messenger for the data service — which itself relies on a global messenger — and then the data service itself.
Let's say that we wanted to use our data service that we built above. To do this, we will instantiate the messenger for the data service — which itself relies on a root messenger — and then the data service itself.

First we need to import the data service:

```typescript
import { GasPricesService } from '@metamask/gas-prices-service';
```

Then we create a global messenger:
Then we create a root messenger:

```typescript
const globalMessenger = new Messenger();
const rootMessenger = new Messenger<'Root', AllActions, AllEvents>({
namespace: 'Root',
});
```

Then we create a messenger for the GasPricesService:

```typescript
const gasPricesServiceMessenger = globalMessenger.getRestricted({
allowedActions: [],
allowedEvents: [],
const gasPricesServiceMessenger = new Messenger<
'GasPricesService',
GasPricesServiceActions,
GasPricesServiceEvents,
typeof rootMessenger
>({
namespace: 'GasPricesService',
parent: rootMessenger,
});
```

Now we instantiate the data service to register the action handler on the global messenger. We assume we have a global `fetch` function available:
Now we instantiate the data service to register the action handler on the root messenger. We assume we have a global `fetch` function available:

```typescript
const gasPricesService = new GasPricesService({
Expand All @@ -421,7 +432,7 @@ const gasPricesService = new GasPricesService({

Great! Now that we've set up the data service and its messenger action, we can use it somewhere else.

Let's say we wanted to use it in a controller. We'd just need to allow that controller's messenger access to `GasPricesService:fetchGasPrices` by passing it via the `allowedActions` option.
Let's say we wanted to use `GasPricesService:fetchGasPrices` in a controller. First, that controller's messenger would need to include `GasPricesService:fetchGasPrices` in its type defintion.

This code would probably be in the controller package itself. For instance, if we had a file `packages/send-controller/send-controller.ts`, we might have:

Expand All @@ -436,23 +447,30 @@ type SendControllerEvents = ...;

type AllowedEvents = ...;

type SendControllerMessenger = RestrictedMessenger<
type SendControllerMessenger = Messenger<
'SendController',
SendControllerActions | AllowedActions,
SendControllerEvents | AllowedEvents,
AllowedActions['type'],
AllowedEvents['type']
>;
```

Then we'll need to allow that controller's messenger access to `GasPricesService:fetchGasPrices` by delegating it from the root messenger:

```typescript
rootMessenger.delegate({
actions: ['GasPricesService:fetchGasPrices'],
messenger: sendControllerMessenger,
});
```

Then, later on in our controller, we could say:

```typescript
class SendController extends BaseController {
// ...

await someMethodThatUsesGasPrices() {
const gasPrices = await this.#messagingSystem.call(
const gasPrices = await this.messenger.call(
'GasPricesService:fetchGasPrices',
);
// ... use gasPrices somehow ...
Expand Down
5 changes: 5 additions & 0 deletions packages/sample-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING:** Migrate to new `Messenger` class ([#6335](https://github.com/MetaMask/core/pull/6335))
- **BREAKING:** Rename metadata property `anonymous` to `includeInDebugSnapshot` ([#6335](https://github.com/MetaMask/core/pull/6335))

## [2.0.2]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/sample-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@metamask/base-controller": "^8.4.2",
"@metamask/messenger": "^0.3.0",
"@metamask/utils": "^11.8.1"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Messenger, deriveStateFromMetadata } from '@metamask/base-controller';
import { deriveStateFromMetadata } from '@metamask/base-controller/next';
import {
Messenger,
MOCK_ANY_NAMESPACE,
type MockAnyNamespace,
type MessengerActions,
type MessengerEvents,
} from '@metamask/messenger';
import { SampleGasPricesController } from '@metamask/sample-controllers';
import type { SampleGasPricesControllerMessenger } from '@metamask/sample-controllers';

import { flushPromises } from '../../../tests/helpers';
import type {
ExtractAvailableAction,
ExtractAvailableEvent,
} from '../../base-controller/tests/helpers';
import { buildMockGetNetworkClientById } from '../../network-controller/tests/helpers';

describe('SampleGasPricesController', () => {
Expand Down Expand Up @@ -301,7 +304,7 @@ describe('SampleGasPricesController', () => {
deriveStateFromMetadata(
controller.state,
controller.metadata,
'anonymous',
'includeInDebugSnapshot',
),
).toMatchInlineSnapshot(`Object {}`);
});
Expand Down Expand Up @@ -362,8 +365,9 @@ describe('SampleGasPricesController', () => {
* required by the controller under test.
*/
type RootMessenger = Messenger<
ExtractAvailableAction<SampleGasPricesControllerMessenger>,
ExtractAvailableEvent<SampleGasPricesControllerMessenger>
MockAnyNamespace,
MessengerActions<SampleGasPricesControllerMessenger>,
MessengerEvents<SampleGasPricesControllerMessenger>
>;

/**
Expand All @@ -389,7 +393,7 @@ type WithControllerOptions = {
* @returns The root messenger.
*/
function getRootMessenger(): RootMessenger {
return new Messenger();
return new Messenger({ namespace: MOCK_ANY_NAMESPACE });
}

/**
Expand All @@ -402,14 +406,19 @@ function getRootMessenger(): RootMessenger {
function getMessenger(
rootMessenger: RootMessenger,
): SampleGasPricesControllerMessenger {
return rootMessenger.getRestricted({
name: 'SampleGasPricesController',
allowedActions: [
'SampleGasPricesService:fetchGasPrices',
const messenger: SampleGasPricesControllerMessenger = new Messenger({
namespace: 'SampleGasPricesController',
parent: rootMessenger,
});
rootMessenger.delegate({
actions: [
'NetworkController:getNetworkClientById',
'SampleGasPricesService:fetchGasPrices',
],
allowedEvents: ['NetworkController:stateChange'],
events: ['NetworkController:stateChange'],
messenger,
});
return messenger;
}

/**
Expand Down
Loading
Loading