-
-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathaccount.ts
More file actions
285 lines (249 loc) · 7.67 KB
/
Copy pathaccount.ts
File metadata and controls
285 lines (249 loc) · 7.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import { type KeyringAccount } from '@metamask/keyring-api';
import { isScopeEqualToAny } from '@metamask/keyring-utils';
import type {
MultichainAccountWallet,
MultichainAccountWalletId,
} from './wallet';
import type { Bip44Account } from '../bip44';
import type { AccountGroup } from '../group';
import { AccountGroupType } from '../group';
import type { AccountProvider } from '../provider';
import type { AccountSelector } from '../selector';
import { AccountWalletType } from '../wallet';
const MULTICHAIN_ACCOUNT_ID_REGEX = new RegExp(
`^${AccountWalletType.Entropy}:.*/(?<groupIndex>\\d+)$`,
'u',
);
/**
* Multichain account ID.
*/
export type MultichainAccountId = `${MultichainAccountWalletId}/${number}`; // Use number for the account group index.
/**
* A multichain account that holds multiple accounts.
*/
export class MultichainAccount<Account extends Bip44Account<KeyringAccount>>
implements AccountGroup<Account>
{
readonly #id: MultichainAccountId;
readonly #wallet: MultichainAccountWallet<Account>;
readonly #index: number;
readonly #providers: AccountProvider<Account>[];
readonly #providerToAccounts: Map<AccountProvider<Account>, Account['id'][]>;
readonly #accountToProvider: Map<Account['id'], AccountProvider<Account>>;
constructor({
groupIndex,
wallet,
providers,
}: {
groupIndex: number;
wallet: MultichainAccountWallet<Account>;
providers: AccountProvider<Account>[];
}) {
this.#id = toMultichainAccountId(wallet.id, groupIndex);
this.#index = groupIndex;
this.#wallet = wallet;
this.#providers = providers;
this.#providerToAccounts = new Map();
this.#accountToProvider = new Map();
this.sync();
}
/**
* Force multichain account synchronization.
*
* This can be used if account providers got new accounts that the multichain
* account doesn't know about.
*/
sync(): void {
// Clear reverse mapping and re-construct it entirely based on the refreshed
// list of accounts from each providers.
this.#accountToProvider.clear();
for (const provider of this.#providers) {
// Filter account only for that index.
const accounts = [];
for (const account of provider.getAccounts()) {
if (
account.options.entropy.id === this.wallet.entropySource &&
account.options.entropy.groupIndex === this.index
) {
// We only use IDs to always fetch the latest version of accounts.
accounts.push(account.id);
}
}
this.#providerToAccounts.set(provider, accounts);
// Reverse-mapping for fast indexing.
for (const id of accounts) {
this.#accountToProvider.set(id, provider);
}
}
}
/**
* Gets the multichain account ID.
*
* @returns The multichain account ID.
*/
get id(): MultichainAccountId {
return this.#id;
}
/**
* Gets the multichain account type.
*
* @returns The multichain account type.
*/
get type(): AccountGroupType.MultichainAccount {
return AccountGroupType.MultichainAccount;
}
/**
* Gets the multichain account's wallet reference (parent).
*
* @returns The multichain account's wallet.
*/
get wallet(): MultichainAccountWallet<Account> {
return this.#wallet;
}
/**
* Gets the multichain account group index.
*
* @returns The multichain account group index.
*/
get index(): number {
return this.#index;
}
/**
* Checks if there's any underlying accounts for this multichain accounts.
*
* @returns True if there's any underlying accounts, false otherwise.
*/
hasAccounts(): boolean {
// If there's anything in the reverse-map, it means we have some accounts.
return this.#accountToProvider.size > 0;
}
/**
* Gets the accounts for this multichain account.
*
* @returns The accounts.
*/
getAccounts(): Account[] {
const allAccounts: Account[] = [];
for (const [provider, accounts] of this.#providerToAccounts.entries()) {
for (const id of accounts) {
const account = provider.getAccount(id);
if (account) {
// If for some reason we cannot get this account from the provider, it
// might means it has been deleted or something, so we just filter it
// out.
allAccounts.push(account);
}
}
}
return allAccounts;
}
/**
* Gets the account for a given account ID.
*
* @param id - Account ID.
* @returns The account or undefined if not found.
*/
getAccount(id: Account['id']): Account | undefined {
const provider = this.#accountToProvider.get(id);
// If there's nothing in the map, it means we tried to get an account
// that does not belong to this multichain account.
if (!provider) {
return undefined;
}
return provider.getAccount(id);
}
/**
* Query an account matching the selector.
*
* @param selector - Query selector.
* @returns The account matching the selector or undefined if not matching.
* @throws If multiple accounts match the selector.
*/
get(selector: AccountSelector<Account>): Account | undefined {
const accounts = this.select(selector);
if (accounts.length > 1) {
throw new Error(
`Too many account candidates, expected 1, got: ${accounts.length}`,
);
}
if (accounts.length === 0) {
return undefined;
}
return accounts[0]; // This is safe, see checks above.
}
/**
* Query accounts matching the selector.
*
* @param selector - Query selector.
* @returns The accounts matching the selector.
*/
select(selector: AccountSelector<Account>): Account[] {
return this.getAccounts().filter((account) => {
let selected = true;
if (selector.id) {
selected &&= account.id === selector.id;
}
if (selector.address) {
selected &&= account.address === selector.address;
}
if (selector.type) {
selected &&= account.type === selector.type;
}
if (selector.methods !== undefined) {
selected &&= selector.methods.some((method) =>
account.methods.includes(method),
);
}
if (selector.scopes !== undefined) {
selected &&= selector.scopes.some((scope) => {
return (
// This will cover specific EVM EOA scopes as well.
isScopeEqualToAny(scope, account.scopes)
);
});
}
return selected;
});
}
}
/**
* Gets the multichain account ID from its multichain account wallet ID and its index.
*
* @param walletId - Multichain account wallet ID.
* @param groupIndex - Index of that multichain account.
* @returns The multichain account ID.
*/
export function toMultichainAccountId(
walletId: MultichainAccountWalletId,
groupIndex: number,
): MultichainAccountId {
return `${walletId}/${groupIndex}`;
}
/**
* Checks if the given value is {@link MultichainAccountId}.
*
* @param value - The value to check.
* @returns Whether the value is a {@link MultichainAccountId}.
*/
export function isMultichainAccountId(
value: string,
): value is MultichainAccountId {
return MULTICHAIN_ACCOUNT_ID_REGEX.test(value);
}
/**
* Gets the multichain account index from an account group ID.
*
* @param id - Multichain account ID.
* @returns The multichain account index if extractable, undefined otherwise.
*/
export function getGroupIndexFromMultichainAccountId(
id: MultichainAccountId,
): number {
const matched = id.match(MULTICHAIN_ACCOUNT_ID_REGEX);
if (matched?.groups?.groupIndex === undefined) {
// Unable to extract group index, even though, type wise, this should not
// be possible!
throw new Error('Unable to extract group index');
}
return Number(matched.groups.groupIndex);
}