Skip to content

Commit 8159614

Browse files
committed
feat(multichain-account-service): add transient state support
1 parent e8820d2 commit 8159614

File tree

6 files changed

+138
-10
lines changed

6 files changed

+138
-10
lines changed

packages/multichain-account-service/src/MultichainAccountGroup.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ function setup({
4444
const wallet = new MultichainAccountWallet<Bip44Account<InternalAccount>>({
4545
providers,
4646
entropySource: MOCK_WALLET_1_ENTROPY_SOURCE,
47+
transientState: {
48+
isAlignmentInProgress: false,
49+
},
4750
});
4851

4952
const group = new MultichainAccountGroup({

packages/multichain-account-service/src/MultichainAccountService.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,36 @@ import type {
77
Bip44Account,
88
AccountProvider,
99
} from '@metamask/account-api';
10+
import { BaseController } from '@metamask/base-controller';
1011
import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api';
1112
import { KeyringTypes } from '@metamask/keyring-controller';
1213

1314
import type { MultichainAccountGroup } from './MultichainAccountGroup';
15+
import type { MultichainAccountWalletTransientState } from './MultichainAccountWallet';
1416
import { MultichainAccountWallet } from './MultichainAccountWallet';
1517
import {
1618
AccountProviderWrapper,
1719
isAccountProviderWrapper,
1820
} from './providers/AccountProviderWrapper';
1921
import { EvmAccountProvider } from './providers/EvmAccountProvider';
2022
import { SolAccountProvider } from './providers/SolAccountProvider';
21-
import type { MultichainAccountServiceMessenger } from './types';
23+
import type { TransientStateMetadata } from './transient-state';
24+
import type {
25+
MultichainAccountServiceMessenger,
26+
MultichainAccountServiceTransientState,
27+
} from './types';
2228

2329
export const serviceName = 'MultichainAccountService';
2430

31+
const serviceMetadata: TransientStateMetadata<MultichainAccountServiceTransientState> =
32+
{
33+
accountWalletsTransientState: {
34+
// Transient states are never persisted.
35+
persist: false,
36+
anonymous: false,
37+
},
38+
};
39+
2540
/**
2641
* The options that {@link MultichainAccountService} takes.
2742
*/
@@ -39,7 +54,11 @@ type AccountContext<Account extends Bip44Account<KeyringAccount>> = {
3954
/**
4055
* Service to expose multichain accounts capabilities.
4156
*/
42-
export class MultichainAccountService {
57+
export class MultichainAccountService extends BaseController<
58+
typeof serviceName,
59+
MultichainAccountServiceTransientState,
60+
MultichainAccountServiceMessenger
61+
> {
4362
readonly #messenger: MultichainAccountServiceMessenger;
4463

4564
readonly #providers: AccountProvider<Bip44Account<KeyringAccount>>[];
@@ -69,6 +88,15 @@ export class MultichainAccountService {
6988
* providers.
7089
*/
7190
constructor({ messenger, providers = [] }: MultichainAccountServiceOptions) {
91+
super({
92+
messenger,
93+
name: serviceName,
94+
metadata: serviceMetadata,
95+
state: {
96+
accountWalletsTransientState: {},
97+
},
98+
});
99+
72100
this.#messenger = messenger;
73101
this.#wallets = new Map();
74102
this.#accountIdToContext = new Map();
@@ -150,9 +178,12 @@ export class MultichainAccountService {
150178

151179
// This will automatically "associate" all multichain accounts for that wallet
152180
// (based on the accounts owned by each account providers).
181+
const walletId = toMultichainAccountWalletId(entropySource);
153182
const wallet = new MultichainAccountWallet({
154183
entropySource,
155184
providers: this.#providers,
185+
transientState:
186+
this.#getMultichainAccoultWalletTransientStateProxy(walletId),
156187
});
157188
this.#wallets.set(wallet.id, wallet);
158189

@@ -169,6 +200,54 @@ export class MultichainAccountService {
169200
}
170201
}
171202

203+
#getMultichainAccoultWalletTransientStateProxy(
204+
walletId: MultichainAccountWalletId,
205+
): MultichainAccountWalletTransientState {
206+
// eslint-disable-next-line consistent-this, @typescript-eslint/no-this-alias
207+
const service = this;
208+
209+
const getTransientState = <Return>(
210+
state: MultichainAccountServiceTransientState,
211+
then: (transientState: MultichainAccountWalletTransientState) => Return,
212+
) => {
213+
return then(state.accountWalletsTransientState[walletId]);
214+
};
215+
216+
const setTransientState = (
217+
state: MultichainAccountServiceTransientState,
218+
then: (transientState: MultichainAccountWalletTransientState) => void,
219+
) => {
220+
then(state.accountWalletsTransientState[walletId]);
221+
};
222+
223+
// Create initial transient state for this wallet if it does not exists yet.
224+
if (!service.state.accountWalletsTransientState[walletId]) {
225+
this.update((state) => {
226+
state.accountWalletsTransientState[walletId] = {
227+
isAlignmentInProgress: false,
228+
};
229+
});
230+
}
231+
232+
return {
233+
get isAlignmentInProgress(): boolean {
234+
return getTransientState(
235+
service.state,
236+
(transientState) => transientState.isAlignmentInProgress,
237+
);
238+
},
239+
set isAlignmentInProgress(inProgress: boolean) {
240+
service.update((state) => {
241+
setTransientState(
242+
state,
243+
(transientState) =>
244+
(transientState.isAlignmentInProgress = inProgress),
245+
);
246+
});
247+
},
248+
};
249+
}
250+
172251
#handleOnAccountAdded(account: KeyringAccount): void {
173252
// We completely omit non-BIP-44 accounts!
174253
if (!isBip44Account(account)) {
@@ -177,6 +256,7 @@ export class MultichainAccountService {
177256

178257
let sync = true;
179258

259+
const walletId = toMultichainAccountWalletId(account.options.entropy.id);
180260
let wallet = this.#wallets.get(
181261
toMultichainAccountWalletId(account.options.entropy.id),
182262
);
@@ -185,6 +265,8 @@ export class MultichainAccountService {
185265
wallet = new MultichainAccountWallet({
186266
entropySource: account.options.entropy.id,
187267
providers: this.#providers,
268+
transientState:
269+
this.#getMultichainAccoultWalletTransientStateProxy(walletId),
188270
});
189271
this.#wallets.set(wallet.id, wallet);
190272

packages/multichain-account-service/src/MultichainAccountWallet.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ function setup({
5757
const wallet = new MultichainAccountWallet<Bip44Account<InternalAccount>>({
5858
providers,
5959
entropySource,
60+
transientState: {
61+
isAlignmentInProgress: false,
62+
},
6063
});
6164

6265
return { wallet, providers };

packages/multichain-account-service/src/MultichainAccountWallet.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ import {
1919

2020
import { MultichainAccountGroup } from './MultichainAccountGroup';
2121

22+
/**
23+
* Transient state for a multichain account wallet.
24+
*
25+
* This state WILL NEVER be persisted. It will be shared with the client's UI
26+
* so the UI can reflect th current state of the wallet (aligning, discovering, etc...)
27+
*/
28+
export type MultichainAccountWalletTransientState = {
29+
/** True when the wallet is being aligned (creating missing accounts on each groups). */
30+
isAlignmentInProgress: boolean;
31+
};
32+
2233
/**
2334
* A multichain account wallet that holds multiple multichain accounts (one multichain account per
2435
* group index).
@@ -35,19 +46,22 @@ export class MultichainAccountWallet<
3546

3647
readonly #accountGroups: Map<number, MultichainAccountGroup<Account>>;
3748

38-
#isAlignmentInProgress: boolean = false;
49+
readonly #transientState: MultichainAccountWalletTransientState;
3950

4051
constructor({
4152
providers,
4253
entropySource,
54+
transientState,
4355
}: {
4456
providers: AccountProvider<Account>[];
4557
entropySource: EntropySourceId;
58+
transientState: MultichainAccountWalletTransientState;
4659
}) {
4760
this.#id = toMultichainAccountWalletId(entropySource);
4861
this.#providers = providers;
4962
this.#entropySource = entropySource;
5063
this.#accountGroups = new Map();
64+
this.#transientState = transientState;
5165

5266
// Initial synchronization.
5367
this.sync();
@@ -315,23 +329,23 @@ export class MultichainAccountWallet<
315329
* @returns True if alignment is in progress, false otherwise.
316330
*/
317331
getIsAlignmentInProgress(): boolean {
318-
return this.#isAlignmentInProgress;
332+
return this.#transientState.isAlignmentInProgress;
319333
}
320334

321335
/**
322336
* Align all multichain account groups.
323337
*/
324338
async alignGroups(): Promise<void> {
325-
if (this.#isAlignmentInProgress) {
339+
if (this.#transientState.isAlignmentInProgress) {
326340
return; // Prevent concurrent alignments
327341
}
328342

329-
this.#isAlignmentInProgress = true;
343+
this.#transientState.isAlignmentInProgress = true;
330344
try {
331345
const groups = this.getMultichainAccountGroups();
332346
await Promise.all(groups.map((g) => g.align()));
333347
} finally {
334-
this.#isAlignmentInProgress = false;
348+
this.#transientState.isAlignmentInProgress = false;
335349
}
336350
}
337351

@@ -341,18 +355,18 @@ export class MultichainAccountWallet<
341355
* @param groupIndex - The group index to align.
342356
*/
343357
async alignGroup(groupIndex: number): Promise<void> {
344-
if (this.#isAlignmentInProgress) {
358+
if (this.#transientState.isAlignmentInProgress) {
345359
return; // Prevent concurrent alignments
346360
}
347361

348-
this.#isAlignmentInProgress = true;
362+
this.#transientState.isAlignmentInProgress = true;
349363
try {
350364
const group = this.getMultichainAccountGroup(groupIndex);
351365
if (group) {
352366
await group.align();
353367
}
354368
} finally {
355-
this.#isAlignmentInProgress = false;
369+
this.#transientState.isAlignmentInProgress = false;
356370
}
357371
}
358372
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type {
2+
StateConstraint,
3+
StatePropertyMetadata,
4+
} from '@metamask/base-controller';
5+
6+
/**
7+
* Transient state metadata.
8+
*
9+
* This metadata describes how to get an anonymized representation of the state.
10+
*/
11+
export type TransientStateMetadata<T extends StateConstraint> = {
12+
[P in keyof T]-?: StatePropertyMetadata<T[P]> & {
13+
// Since the state is transient, it cannot be persisted.
14+
persist: false;
15+
};
16+
};

packages/multichain-account-service/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { MultichainAccountWalletId } from '@metamask/account-api';
12
import type {
23
AccountsControllerAccountAddedEvent,
34
AccountsControllerAccountRemovedEvent,
@@ -17,6 +18,15 @@ import type {
1718
MultichainAccountService,
1819
serviceName,
1920
} from './MultichainAccountService';
21+
import type { MultichainAccountWalletTransientState } from './MultichainAccountWallet';
22+
23+
export type MultichainAccountServiceTransientState = {
24+
accountWalletsTransientState: {
25+
[
26+
walletId: MultichainAccountWalletId
27+
]: MultichainAccountWalletTransientState;
28+
};
29+
};
2030

2131
export type MultichainAccountServiceGetMultichainAccountGroupAction = {
2232
type: `${typeof serviceName}:getMultichainAccountGroup`;

0 commit comments

Comments
 (0)