Skip to content

Commit 2bfb7fc

Browse files
authored
feat(account-tree-controller): fallback naming for newly created groups (#6246)
## Explanation **Problem:** When creating new account groups/wallets, naming fails because the rule-based logic depends on existing accounts that don't exist yet for new wallets. **Solution:** Added fallback naming logic that generates "Account 1", "Account 2", etc. within each wallet when rule-based naming returns empty strings. The strategy we agreed upon prescribes "indexes per wallet": - Wallet 1 → Account 1, Account 2 - Wallet 2 → Account 1, Account 2 **Changes:** - Added `#getFallbackAccountGroupName()` method to `AccountTreeController` - Modified `#applyAccountGroupMetadata()` to use fallback when rule-based naming fails - Added test coverage for the fallback logic ## References Fixes naming bug reported by @monte.lai for new account groups/wallets: https://consensys.slack.com/archives/C08R8HGNFDH/p1754062266574059 ## 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 communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent dffe11b commit 2bfb7fc

File tree

11 files changed

+1339
-61
lines changed

11 files changed

+1339
-61
lines changed

packages/account-tree-controller/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Bump `@metamask/base-controller` from `^8.0.1` to `^8.1.0` ([#6284](https://github.com/MetaMask/core/pull/6284))
1313

14+
### Fixed
15+
16+
- Add fallback naming for account groups when rule-based naming fails ([#6246](https://github.com/MetaMask/core/pull/6246))
17+
- Implements "indexes per wallet" strategy (Wallet 1 → Account 1, Account 2; Wallet 2 → Account 1, Account 2)
18+
- Ensures new groups get proper sequential names within each wallet
19+
1420
## [0.8.0]
1521

1622
### Added

packages/account-tree-controller/src/AccountTreeController.test.ts

Lines changed: 224 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import type { InternalAccount } from '@metamask/keyring-internal-api';
2424
import type { GetSnap as SnapControllerGetSnap } from '@metamask/snaps-controllers';
2525

2626
import { AccountTreeController } from './AccountTreeController';
27-
import type { AccountGroupObject } from './group';
28-
import { BaseRule } from './rule';
2927
import { getAccountWalletNameFromKeyringType } from './rules/keyring';
3028
import {
3129
type AccountTreeControllerMessenger,
@@ -1732,38 +1730,6 @@ describe('AccountTreeController', () => {
17321730
});
17331731
});
17341732

1735-
describe('BaseRule', () => {
1736-
it('fallbacks to emptry group name if we cannot get its account', () => {
1737-
const rootMessenger = getRootMessenger();
1738-
const messenger = getAccountTreeControllerMessenger(rootMessenger);
1739-
const rule = new BaseRule(messenger);
1740-
1741-
rootMessenger.registerActionHandler(
1742-
'AccountsController:getAccount',
1743-
() => undefined,
1744-
);
1745-
1746-
const group: AccountGroupObject = {
1747-
id: toMultichainAccountGroupId(
1748-
toMultichainAccountWalletId('test'),
1749-
MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
1750-
),
1751-
type: AccountGroupType.MultichainAccount,
1752-
accounts: [MOCK_HD_ACCOUNT_1.id],
1753-
metadata: {
1754-
name: MOCK_HD_ACCOUNT_1.metadata.name,
1755-
entropy: {
1756-
groupIndex: MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
1757-
},
1758-
pinned: false,
1759-
hidden: false,
1760-
},
1761-
};
1762-
1763-
expect(rule.getDefaultAccountGroupName(group)).toBe('');
1764-
});
1765-
});
1766-
17671733
describe('Persistence - Custom Names', () => {
17681734
it('persists custom account group names across init calls', () => {
17691735
const { controller } = setup({
@@ -2108,4 +2074,228 @@ describe('AccountTreeController', () => {
21082074
).toBeUndefined();
21092075
});
21102076
});
2077+
2078+
describe('Fallback Naming', () => {
2079+
it('detects new groups based on account import time', () => {
2080+
const serviceStartTime = Date.now();
2081+
const mockAccountWithNewImportTime: Bip44Account<InternalAccount> = {
2082+
...MOCK_HD_ACCOUNT_1,
2083+
options: {
2084+
...MOCK_HD_ACCOUNT_1.options,
2085+
entropy: {
2086+
...MOCK_HD_ACCOUNT_1.options.entropy,
2087+
id: MOCK_HD_KEYRING_1.metadata.id,
2088+
groupIndex: 0,
2089+
},
2090+
},
2091+
metadata: {
2092+
...MOCK_HD_ACCOUNT_1.metadata,
2093+
importTime: serviceStartTime + 1000, // Imported after service start
2094+
},
2095+
};
2096+
2097+
const mockAccountWithOldImportTime: Bip44Account<InternalAccount> = {
2098+
...MOCK_HD_ACCOUNT_2,
2099+
options: {
2100+
...MOCK_HD_ACCOUNT_2.options,
2101+
entropy: {
2102+
...MOCK_HD_ACCOUNT_2.options.entropy,
2103+
id: MOCK_HD_KEYRING_1.metadata.id,
2104+
groupIndex: 1,
2105+
},
2106+
},
2107+
metadata: {
2108+
...MOCK_HD_ACCOUNT_2.metadata,
2109+
importTime: serviceStartTime - 1000, // Imported before service start
2110+
},
2111+
};
2112+
2113+
const { controller } = setup({
2114+
accounts: [mockAccountWithOldImportTime, mockAccountWithNewImportTime],
2115+
keyrings: [MOCK_HD_KEYRING_1],
2116+
});
2117+
2118+
controller.init();
2119+
2120+
const expectedWalletId = toMultichainAccountWalletId(
2121+
MOCK_HD_KEYRING_1.metadata.id,
2122+
);
2123+
2124+
const expectedGroupId1 = toMultichainAccountGroupId(
2125+
expectedWalletId,
2126+
mockAccountWithNewImportTime.options.entropy.groupIndex,
2127+
);
2128+
2129+
const expectedGroupId2 = toMultichainAccountGroupId(
2130+
expectedWalletId,
2131+
mockAccountWithOldImportTime.options.entropy.groupIndex,
2132+
);
2133+
2134+
const wallet = controller.state.accountTree.wallets[expectedWalletId];
2135+
const group1 = wallet?.groups[expectedGroupId1];
2136+
const group2 = wallet?.groups[expectedGroupId2];
2137+
2138+
// Groups should be named by index within the wallet
2139+
expect(group1?.metadata.name).toBe('Account 1');
2140+
expect(group2?.metadata.name).toBe('Account 2');
2141+
});
2142+
2143+
it('uses fallback naming when rule-based naming returns empty string', () => {
2144+
// Create accounts with empty names to trigger fallback naming
2145+
const mockAccountWithEmptyName1: Bip44Account<InternalAccount> = {
2146+
...MOCK_HD_ACCOUNT_1,
2147+
id: 'account-1',
2148+
metadata: {
2149+
...MOCK_HD_ACCOUNT_1.metadata,
2150+
name: '', // Empty name will cause rule-based naming to fail
2151+
},
2152+
};
2153+
2154+
const mockAccountWithEmptyName2: Bip44Account<InternalAccount> = {
2155+
...MOCK_HD_ACCOUNT_1,
2156+
id: 'account-2',
2157+
options: {
2158+
...MOCK_HD_ACCOUNT_1.options,
2159+
entropy: {
2160+
...MOCK_HD_ACCOUNT_1.options.entropy,
2161+
groupIndex: 1, // Different group index
2162+
},
2163+
},
2164+
metadata: {
2165+
...MOCK_HD_ACCOUNT_1.metadata,
2166+
name: '', // Empty name will cause rule-based naming to fail
2167+
},
2168+
};
2169+
2170+
const { controller } = setup({
2171+
accounts: [mockAccountWithEmptyName1, mockAccountWithEmptyName2],
2172+
keyrings: [MOCK_HD_KEYRING_1],
2173+
});
2174+
2175+
controller.init();
2176+
2177+
const expectedWalletId = toMultichainAccountWalletId(
2178+
MOCK_HD_KEYRING_1.metadata.id,
2179+
);
2180+
2181+
const expectedGroupId1 = toMultichainAccountGroupId(
2182+
expectedWalletId,
2183+
0, // First group
2184+
);
2185+
2186+
const expectedGroupId2 = toMultichainAccountGroupId(
2187+
expectedWalletId,
2188+
1, // Second group
2189+
);
2190+
2191+
const wallet = controller.state.accountTree.wallets[expectedWalletId];
2192+
const group1 = wallet?.groups[expectedGroupId1];
2193+
const group2 = wallet?.groups[expectedGroupId2];
2194+
2195+
// Verify fallback naming: "Account 1", "Account 2" within the same wallet
2196+
expect(group1?.metadata.name).toBe('Account 1');
2197+
expect(group2?.metadata.name).toBe('Account 2');
2198+
});
2199+
2200+
it('handles adding new accounts to existing groups correctly', () => {
2201+
const serviceStartTime = Date.now();
2202+
// Create an existing account (imported before service start)
2203+
const existingAccount: Bip44Account<InternalAccount> = {
2204+
...MOCK_HD_ACCOUNT_1,
2205+
id: 'existing-account',
2206+
options: {
2207+
...MOCK_HD_ACCOUNT_1.options,
2208+
entropy: {
2209+
...MOCK_HD_ACCOUNT_1.options.entropy,
2210+
id: MOCK_HD_KEYRING_1.metadata.id,
2211+
groupIndex: 0,
2212+
},
2213+
},
2214+
metadata: {
2215+
...MOCK_HD_ACCOUNT_1.metadata,
2216+
name: '', // Empty name to trigger naming logic
2217+
importTime: serviceStartTime - 1000, // Imported before service start
2218+
},
2219+
};
2220+
2221+
// Create a new account (imported after service start) for the same group
2222+
const newAccount: Bip44Account<InternalAccount> = {
2223+
...MOCK_HD_ACCOUNT_1,
2224+
id: 'new-account',
2225+
options: {
2226+
...MOCK_HD_ACCOUNT_1.options,
2227+
entropy: {
2228+
...MOCK_HD_ACCOUNT_1.options.entropy,
2229+
id: MOCK_HD_KEYRING_1.metadata.id,
2230+
groupIndex: 0, // Same group as existing account
2231+
},
2232+
},
2233+
metadata: {
2234+
...MOCK_HD_ACCOUNT_1.metadata,
2235+
name: '', // Empty name to trigger naming logic
2236+
importTime: serviceStartTime + 1000, // Imported after service start
2237+
},
2238+
};
2239+
2240+
const { controller, messenger, mocks } = setup({
2241+
accounts: [existingAccount],
2242+
keyrings: [MOCK_HD_KEYRING_1],
2243+
});
2244+
2245+
controller.init();
2246+
2247+
// Add the new account to the existing group
2248+
mocks.AccountsController.accounts = [existingAccount, newAccount];
2249+
messenger.publish('AccountsController:accountAdded', newAccount);
2250+
2251+
const expectedWalletId = toMultichainAccountWalletId(
2252+
MOCK_HD_KEYRING_1.metadata.id,
2253+
);
2254+
const expectedGroupId = toMultichainAccountGroupId(
2255+
expectedWalletId,
2256+
0, // Same group index
2257+
);
2258+
2259+
const wallet = controller.state.accountTree.wallets[expectedWalletId];
2260+
const group = wallet?.groups[expectedGroupId];
2261+
2262+
// The group should now be treated as "new" and use fallback naming
2263+
expect(group?.metadata.name).toBe('Account 1');
2264+
expect(group?.accounts).toHaveLength(2);
2265+
expect(group?.accounts).toContain(existingAccount.id);
2266+
expect(group?.accounts).toContain(newAccount.id);
2267+
});
2268+
2269+
it('handles groups not in WeakMap (fallback to false)', () => {
2270+
// Create an account with empty name to trigger naming logic
2271+
const mockAccountWithEmptyName: Bip44Account<InternalAccount> = {
2272+
...MOCK_HD_ACCOUNT_1,
2273+
id: 'account-with-empty-name',
2274+
metadata: {
2275+
...MOCK_HD_ACCOUNT_1.metadata,
2276+
name: '', // Empty name will cause rule-based naming to fail
2277+
importTime: Date.now() - 1000, // Old account (not new)
2278+
},
2279+
};
2280+
2281+
const { controller } = setup({
2282+
accounts: [mockAccountWithEmptyName],
2283+
keyrings: [MOCK_HD_KEYRING_1],
2284+
});
2285+
2286+
controller.init();
2287+
2288+
const expectedWalletId = toMultichainAccountWalletId(
2289+
MOCK_HD_KEYRING_1.metadata.id,
2290+
);
2291+
const expectedGroupId = toMultichainAccountGroupId(expectedWalletId, 0);
2292+
2293+
const wallet = controller.state.accountTree.wallets[expectedWalletId];
2294+
const group = wallet?.groups[expectedGroupId];
2295+
2296+
// Should use computed name first since it's not a new group, then fallback to default
2297+
// Since the account has empty name, computed name will be empty, so it falls back to default
2298+
expect(group?.metadata.name).toBe('Account 1');
2299+
});
2300+
});
21112301
});

0 commit comments

Comments
 (0)