diff --git a/CHANGELOG.md b/CHANGELOG.md
index 803f698..10eb56a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## 2025-10-22
+
+### Changes
+
+---
+
+Packages with breaking changes:
+
+ - There are no breaking changes in this release.
+
+Packages with other changes:
+
+ - [`nekoton_repository` - `v2.0.3-dev.0`](#nekoton_repository---v203-dev0)
+
+---
+
+#### `nekoton_repository` - `v2.0.3-dev.0`
+
+ - **FIX**(EWM-397): fix tests & create tests for new naming logic. ([81c66a2d](https://github.com/broxus/nekoton_repository/commit/81c66a2dd6bbf2e0e78477af910cac045b910205))
+ - **FIX**(EWM-397): formatting. ([5a26933b](https://github.com/broxus/nekoton_repository/commit/5a26933bb70e622259d2d71196a95430df7604ae))
+
+## 2.0.3-dev.0
+
+ - **FIX**(EWM-397): fix tests & create tests for new naming logic. ([81c66a2d](https://github.com/broxus/nekoton_repository/commit/81c66a2dd6bbf2e0e78477af910cac045b910205))
+ - **FIX**(EWM-397): formatting. ([5a26933b](https://github.com/broxus/nekoton_repository/commit/5a26933bb70e622259d2d71196a95430df7604ae))
+
+
## 2025-10-20
### Changes
diff --git a/lib/src/models/account_list.dart b/lib/src/models/account_list.dart
index 7589c32..21c3eb3 100644
--- a/lib/src/models/account_list.dart
+++ b/lib/src/models/account_list.dart
@@ -36,7 +36,8 @@ class AccountList extends Equatable {
/// Add account to key with [publicKey] and [walletType].
/// [workchain] specify Transport network that should be used for this account
- /// [name] is optional and if not specified, auto-generated name will be used.
+ /// [name] is optional and if not specified, auto-generated name will be used
+ /// in format "Account N.M".
Future
addAccount({
required WalletType walletType,
required int workchain,
@@ -46,6 +47,9 @@ class AccountList extends Equatable {
AccountToAdd(
name:
name ??
+ GetIt.instance().generateDefaultAccountName(
+ publicKey,
+ ) ??
GetIt.instance().currentTransport
.defaultAccountName(walletType),
publicKey: publicKey,
diff --git a/lib/src/nekoton_repository.dart b/lib/src/nekoton_repository.dart
index bc382d9..e1266f5 100644
--- a/lib/src/nekoton_repository.dart
+++ b/lib/src/nekoton_repository.dart
@@ -466,6 +466,61 @@ class NekotonRepository
_keyStore.keysStream.listen((keys) => _hasSeeds.add(keys.isNotEmpty));
}
+ /// Generates default account name in format "Account N.M"
+ /// where N is the key position in the seed (1-based) and M is the account
+ /// position within that key (1-based).
+ ///
+ /// Returns null if the publicKey is not found in any seed.
+ String? generateDefaultAccountName(PublicKey publicKey) {
+ final seed = seedList.findSeedByAnyPublicKey(publicKey);
+ if (seed == null) return null;
+
+ // Find the key within the seed
+ final keyIndex = seed.allKeys.indexWhere(
+ (key) => key.publicKey == publicKey,
+ );
+ if (keyIndex == -1) return null;
+
+ // Key position is 1-based (master key = 1, first derived = 2, etc.)
+ final keyPosition = keyIndex + 1;
+
+ // Get next account number by finding max existing number + 1
+ final accountPosition = _getNextAccountNumber(
+ seed.allKeys[keyIndex].accountList.allAccounts,
+ keyPosition,
+ );
+
+ return 'Account $keyPosition.$accountPosition';
+ }
+
+ /// Gets the next available account number for a specific key position
+ /// by finding the maximum number in existing account names
+ /// (format: "Account N.M") and returning max M + 1 for the given N.
+ /// Returns 1 if no accounts exist or no default names are found.
+ int _getNextAccountNumber(List accounts, int keyPosition) {
+ if (accounts.isEmpty) return 1;
+
+ var maxNumber = 0;
+
+ for (final account in accounts) {
+ final name = account.name;
+
+ // Parse "Account N.M" format
+ final match = RegExp(r'^Account (\d+)\.(\d+)$').firstMatch(name);
+ if (match != null) {
+ final n = int.tryParse(match.group(1) ?? '');
+ final m = int.tryParse(match.group(2) ?? '');
+
+ // Only consider accounts for this specific key position
+ if (n == keyPosition && m != null && m > maxNumber) {
+ maxNumber = m;
+ }
+ }
+ }
+
+ return maxNumber + 1;
+ }
+
void _logHandler(fnb.LogEntry logEntry) {
final logLevel = _toLogLevel(logEntry.level);
diff --git a/lib/src/repositories/account_repository/account_repository_impl.dart b/lib/src/repositories/account_repository/account_repository_impl.dart
index 54c6393..ba2e781 100644
--- a/lib/src/repositories/account_repository/account_repository_impl.dart
+++ b/lib/src/repositories/account_repository/account_repository_impl.dart
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
+import 'package:get_it/get_it.dart';
import 'package:nekoton_repository/nekoton_repository.dart';
/// Implementation of AccountRepository.
@@ -63,6 +64,9 @@ mixin AccountRepositoryImpl on TransportRepository
AccountToAdd(
name:
name ??
+ GetIt.instance().generateDefaultAccountName(
+ publicKey,
+ ) ??
currentTransport.defaultAccountName(
existingWalletInfo.walletType,
),
diff --git a/lib/src/repositories/seed_repository/seed_repository_impl.dart b/lib/src/repositories/seed_repository/seed_repository_impl.dart
index fe4b72f..d0eddc4 100644
--- a/lib/src/repositories/seed_repository/seed_repository_impl.dart
+++ b/lib/src/repositories/seed_repository/seed_repository_impl.dart
@@ -4,6 +4,8 @@ import 'package:get_it/get_it.dart';
import 'package:nekoton_repository/nekoton_repository.dart';
import 'package:rxdart/rxdart.dart';
+const seedPrefix = 'Seed ';
+
/// Implementation of SeedRepository.
/// Usage
/// ```
@@ -159,7 +161,11 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
String? name,
SeedAddType addType = SeedAddType.create,
}) async {
- name = name?.isEmpty ?? true ? null : name;
+ // Generate default seed name if not provided
+ if (name?.isEmpty ?? true) {
+ name = '$seedPrefix${_getNextSeedNumber()}';
+ }
+
mnemonicType ??= phrase.length == 24
? const MnemonicType.legacy()
: const MnemonicType.bip39(
@@ -239,6 +245,11 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
required int workchainId,
String? name,
}) async {
+ // Generate default seed name if not provided
+ if (name?.isEmpty ?? true) {
+ name = '$seedPrefix${_getNextSeedNumber()}';
+ }
+
final publicKey = await keyStore.addKey(
LedgerKeyCreateInput(accountId: accountId, name: name),
);
@@ -266,8 +277,16 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
final transport = currentTransport;
if (transport.transport.disposed) return;
+ // For newly created seeds, this will be the first account on master key
+ // At this point the seed might not be in seedList yet, so we use fallback
+ final accountName =
+ GetIt.instance().generateDefaultAccountName(
+ publicKey,
+ ) ??
+ transport.defaultAccountName(transport.defaultWalletType);
+
final defaultAccount = AccountToAdd(
- name: transport.defaultAccountName(transport.defaultWalletType),
+ name: accountName,
publicKey: publicKey,
contract: transport.defaultWalletType,
workchain: workchainId,
@@ -356,7 +375,10 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
publicKey: a.publicKey,
contract: a.walletType,
workchain: a.address.workchain,
- name: transport.defaultAccountName(a.walletType),
+ name:
+ GetIt.instance()
+ .generateDefaultAccountName(a.publicKey) ??
+ transport.defaultAccountName(a.walletType),
),
),
);
@@ -434,7 +456,10 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
if (activeAccounts.isEmpty) {
accountsToAdd.add(
AccountToAdd(
- name: transport.defaultAccountName(transport.defaultWalletType),
+ name:
+ GetIt.instance()
+ .generateDefaultAccountName(key) ??
+ transport.defaultAccountName(transport.defaultWalletType),
publicKey: key,
contract: transport.defaultWalletType,
workchain: workchainId,
@@ -451,7 +476,10 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
publicKey: a.publicKey,
contract: a.walletType,
workchain: a.address.workchain,
- name: transport.defaultAccountName(a.walletType),
+ name:
+ GetIt.instance()
+ .generateDefaultAccountName(a.publicKey) ??
+ transport.defaultAccountName(a.walletType),
),
),
);
@@ -659,4 +687,30 @@ mixin SeedKeyRepositoryImpl implements SeedKeyRepository {
}
await GetIt.instance().removeAccounts(accountsToRemove);
}
+
+ /// Gets the next available seed number by finding the maximum number
+ /// in existing seed names (format: "Seed N") and returning max + 1.
+ /// Returns 1 if no seeds exist or no default names are found.
+ int _getNextSeedNumber() {
+ final seedMeta = storageRepository.seedMeta;
+ if (seedMeta.isEmpty) return 1;
+
+ var maxNumber = 0;
+
+ for (final metadata in seedMeta.values) {
+ final name = metadata.name;
+ if (name == null) continue;
+
+ // Parse "Seed " format
+ final match = RegExp('^$seedPrefix(\\d+)\$').firstMatch(name);
+ if (match != null) {
+ final number = int.tryParse(match.group(1) ?? '');
+ if (number != null && number > maxNumber) {
+ maxNumber = number;
+ }
+ }
+ }
+
+ return maxNumber + 1;
+ }
}
diff --git a/pubspec.yaml b/pubspec.yaml
index f9dc8e1..1efdebc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
---
name: nekoton_repository
description: Nekoton repository package
-version: 2.0.1
+version: 2.0.3-dev.0
repository: https://github.com/broxus/nekoton_repository
environment:
@@ -49,8 +49,7 @@ flutter_gen:
package_parameter_enabled: true
flutter:
- assets:
- - assets/abi/
+ assets: [assets/abi/]
melos:
useRootAsPackage: true
@@ -61,10 +60,12 @@ melos:
codegen:
description: Generate code for all packages
- exec: "find . -type f -name \"*.gen.dart\" -delete && flutter packages pub run build_runner build --delete-conflicting-outputs && dart format lib/generated/assets.gen.dart lib/nekoton_repository.module.dart"
+ exec: find . -type f -name "*.gen.dart" -delete && flutter packages pub run
+ build_runner build --delete-conflicting-outputs && dart format lib/generated/assets.gen.dart
+ lib/nekoton_repository.module.dart
failFast: true
packageFilters:
- dependsOn: "build_runner"
+ dependsOn: build_runner
analyze:
description: Analyze a specific package in this project.
@@ -82,7 +83,7 @@ melos:
test:
description: Run Flutter tests for a specific package in this project.
- exec: "flutter test test"
+ exec: flutter test test
failFast: true
packageFilters:
flutter: true
@@ -90,7 +91,8 @@ melos:
test:integration:
run: melos exec -c 1 --fail-fast -- "flutter test integration_test"
- description: Run Flutter teintegration teststs for a specific package in this project.
+ description: Run Flutter teintegration teststs for a specific package in this
+ project.
packageFilters:
flutter: true
dirExists: integration_test
@@ -112,4 +114,3 @@ melos:
version:
hooks:
preCommit: melos bs && git add --all
-
diff --git a/test/src/nekoton_repository_account_naming_test.dart b/test/src/nekoton_repository_account_naming_test.dart
new file mode 100644
index 0000000..9fa3d3f
--- /dev/null
+++ b/test/src/nekoton_repository_account_naming_test.dart
@@ -0,0 +1,361 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:nekoton_repository/nekoton_repository.dart';
+import 'package:rxdart/rxdart.dart';
+
+class MockBridge extends Mock implements NekotonBridgeApi {}
+
+/// Testable version of NekotonRepository that allows setting seed list directly
+class TestableNekotonRepository extends NekotonRepository {
+ final _testSeedListSubject = BehaviorSubject();
+
+ void setSeedList(SeedList seedList) {
+ _testSeedListSubject.add(seedList);
+ }
+
+ @override
+ SeedList get seedList => _testSeedListSubject.hasValue
+ ? _testSeedListSubject.value
+ : SeedList(
+ seedMeta: const {},
+ allKeys: const [],
+ mappedAccounts: const {},
+ );
+
+ @override
+ Future dispose() async {
+ await _testSeedListSubject.close();
+ await super.dispose();
+ }
+}
+
+void main() {
+ setUpAll(() {
+ NekotonBridge.initMock(api: MockBridge());
+ });
+
+ group('NekotonRepository.generateDefaultAccountName', () {
+ test('first account on master key gets "Account 1.1"', () {
+ // Arrange
+ final repository = TestableNekotonRepository();
+ const masterKey = PublicKey(publicKey: 'masterkey1');
+ final masterKeyEntry = KeyStoreEntry(
+ name: 'MasterKey',
+ masterKey: masterKey,
+ accountId: 0,
+ publicKey: masterKey,
+ signerName: const KeySigner.derived().name,
+ );
+
+ final seedList = SeedList(
+ seedMeta: {masterKey: const SeedMetadata(name: '${seedPrefix}1')},
+ allKeys: [masterKeyEntry],
+ mappedAccounts: const {},
+ );
+
+ repository.setSeedList(seedList);
+
+ // Act
+ final accountName = repository.generateDefaultAccountName(masterKey);
+
+ // Assert
+ expect(accountName, equals('Account 1.1'));
+ });
+
+ test('sequential accounts get incremented numbers', () {
+ // Arrange
+ final repository = TestableNekotonRepository();
+ const masterKey = PublicKey(publicKey: 'masterkey1');
+ final masterKeyEntry = KeyStoreEntry(
+ name: 'MasterKey',
+ masterKey: masterKey,
+ accountId: 0,
+ publicKey: masterKey,
+ signerName: const KeySigner.derived().name,
+ );
+
+ const account1 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.1',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:account1'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ const account2 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.2',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:account2'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ final seedList = SeedList(
+ seedMeta: {masterKey: const SeedMetadata(name: '${seedPrefix}1')},
+ allKeys: [masterKeyEntry],
+ mappedAccounts: {
+ masterKey: const AccountList(
+ allAccounts: [account1, account2],
+ publicKey: masterKey,
+ ),
+ },
+ );
+
+ repository.setSeedList(seedList);
+
+ // Act
+ final accountName = repository.generateDefaultAccountName(masterKey);
+
+ // Assert
+ expect(accountName, equals('Account 1.3'));
+ });
+
+ test('gaps are not filled - Account 1.1, 1.3 -> next is 1.4', () {
+ // Arrange
+ final repository = TestableNekotonRepository();
+ const masterKey = PublicKey(publicKey: 'masterkey1');
+ final masterKeyEntry = KeyStoreEntry(
+ name: 'MasterKey',
+ masterKey: masterKey,
+ accountId: 0,
+ publicKey: masterKey,
+ signerName: const KeySigner.derived().name,
+ );
+
+ const account1 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.1',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:account1'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ const account3 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.3',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:account3'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ final seedList = SeedList(
+ seedMeta: {masterKey: const SeedMetadata(name: '${seedPrefix}1')},
+ allKeys: [masterKeyEntry],
+ mappedAccounts: {
+ masterKey: const AccountList(
+ allAccounts: [account1, account3],
+ publicKey: masterKey,
+ ),
+ },
+ );
+
+ repository.setSeedList(seedList);
+
+ // Act
+ final accountName = repository.generateDefaultAccountName(masterKey);
+
+ // Assert
+ expect(accountName, equals('Account 1.4'));
+ });
+
+ test('custom names do not affect counter', () {
+ // Arrange
+ final repository = TestableNekotonRepository();
+ const masterKey = PublicKey(publicKey: 'masterkey1');
+ final masterKeyEntry = KeyStoreEntry(
+ name: 'MasterKey',
+ masterKey: masterKey,
+ accountId: 0,
+ publicKey: masterKey,
+ signerName: const KeySigner.derived().name,
+ );
+
+ const account1 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.1',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:account1'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ const customAccount = KeyAccount(
+ account: AssetsList(
+ name: 'My Custom Account',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:custom'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ const account3 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.3',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:account3'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ final seedList = SeedList(
+ seedMeta: {masterKey: const SeedMetadata(name: '${seedPrefix}1')},
+ allKeys: [masterKeyEntry],
+ mappedAccounts: {
+ masterKey: const AccountList(
+ allAccounts: [account1, customAccount, account3],
+ publicKey: masterKey,
+ ),
+ },
+ );
+
+ repository.setSeedList(seedList);
+
+ // Act
+ final accountName = repository.generateDefaultAccountName(masterKey);
+
+ // Assert
+ expect(accountName, equals('Account 1.4'));
+ });
+
+ test('different key positions are independent', () {
+ // Arrange
+ final repository = TestableNekotonRepository();
+ const masterKey = PublicKey(publicKey: 'masterkey1');
+ const subKey = PublicKey(publicKey: 'subkey1');
+
+ final masterKeyEntry = KeyStoreEntry(
+ name: 'MasterKey',
+ masterKey: masterKey,
+ accountId: 0,
+ publicKey: masterKey,
+ signerName: const KeySigner.derived().name,
+ );
+
+ final subKeyEntry = KeyStoreEntry(
+ name: 'SubKey',
+ masterKey: masterKey,
+ accountId: 1,
+ publicKey: subKey,
+ signerName: const KeySigner.derived().name,
+ );
+
+ const masterAccount1 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.1',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:master1'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ const masterAccount2 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 1.2',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: masterKey,
+ address: Address(address: '0:master2'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: masterKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ const subAccount1 = KeyAccount(
+ account: AssetsList(
+ name: 'Account 2.1',
+ additionalAssets: {},
+ tonWallet: TonWalletAsset(
+ publicKey: subKey,
+ address: Address(address: '0:sub1'),
+ contract: WalletType.everWallet(),
+ ),
+ ),
+ publicKey: subKey,
+ isExternal: false,
+ isHidden: false,
+ );
+
+ final seedList = SeedList(
+ seedMeta: {masterKey: const SeedMetadata(name: '${seedPrefix}1')},
+ allKeys: [masterKeyEntry, subKeyEntry],
+ mappedAccounts: {
+ masterKey: const AccountList(
+ allAccounts: [masterAccount1, masterAccount2],
+ publicKey: masterKey,
+ ),
+ subKey: const AccountList(
+ allAccounts: [subAccount1],
+ publicKey: subKey,
+ ),
+ },
+ );
+
+ repository.setSeedList(seedList);
+
+ // Act
+ final masterAccountName = repository.generateDefaultAccountName(
+ masterKey,
+ );
+ final subAccountName = repository.generateDefaultAccountName(subKey);
+
+ // Assert
+ expect(masterAccountName, equals('Account 1.3'));
+ expect(subAccountName, equals('Account 2.2'));
+ });
+ });
+}
diff --git a/test/src/seed_repository_test.dart b/test/src/seed_repository_test.dart
index 88433d0..360a61e 100644
--- a/test/src/seed_repository_test.dart
+++ b/test/src/seed_repository_test.dart
@@ -27,6 +27,11 @@ class MockAccountRepository extends Mock implements AccountRepository {
Future removeAccounts(List accounts) async {}
}
+class MockNekotonRepository extends Mock implements NekotonRepository {
+ @override
+ String? generateDefaultAccountName(PublicKey publicKey) => null;
+}
+
class MockArcTransportBoxTrait extends Mock implements ArcTransportBoxTrait {}
class MockCreateKeyInput extends Mock implements CreateKeyInput {}
@@ -63,6 +68,7 @@ void main() {
late MockAccountsStorage accountsStorage;
late MockNekotonStorageRepository storageRepository;
late MockAccountRepository accountRepository;
+ late MockNekotonRepository nekotonRepository;
late MockTransportStrategy transport;
late MockProtoTransport proto;
late SeedKeyRepositoryTest repository;
@@ -99,6 +105,7 @@ void main() {
accountsStorage = MockAccountsStorage();
storageRepository = MockNekotonStorageRepository();
accountRepository = MockAccountRepository();
+ nekotonRepository = MockNekotonRepository();
transport = MockTransportStrategy();
proto = MockProtoTransport();
@@ -106,6 +113,7 @@ void main() {
registerFallbackValue(box);
GetIt.instance.registerSingleton(accountRepository);
+ GetIt.instance.registerSingleton(nekotonRepository);
repository = SeedKeyRepositoryTest(
transport,
@@ -126,6 +134,7 @@ void main() {
).thenReturn('defaultAccountName');
when(() => proto.disposed).thenReturn(false);
when(() => proto.transportBox).thenReturn(box);
+ when(() => storageRepository.seedMeta).thenReturn({});
});
setUpAll(() {
@@ -328,6 +337,197 @@ void main() {
});
});
+ group('addSeed - default naming', () {
+ test('first seed without name gets "Seed 1"', () async {
+ final phrase = List.generate(12, (i) => 'word$i');
+ const password = 'password';
+ const publicKey = PublicKey(publicKey: 'publickey');
+
+ when(() => keyStore.addKey(any())).thenAnswer((_) async => publicKey);
+ when(() => keyStore.getPublicKeys(any())).thenAnswer((_) async => []);
+ when(() => accountsStorage.accounts).thenReturn([]);
+ when(() => storageRepository.seedMeta).thenReturn({});
+ when(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(named: 'meta'),
+ ),
+ ).thenAnswer((_) async {});
+ when(
+ () => bridge.crateApiMergedTonWalletDartWrapperFindExistingWallets(
+ transport: any(named: 'transport'),
+ publicKey: any(named: 'publicKey'),
+ walletTypes: any(named: 'walletTypes'),
+ workchainId: any(named: 'workchainId'),
+ ),
+ ).thenAnswer((_) => Future.value('[]'));
+
+ await repository.addSeed(
+ phrase: phrase,
+ password: password,
+ workchainId: 0,
+ );
+
+ verify(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(
+ named: 'meta',
+ that: predicate(
+ (meta) => meta.name == '${seedPrefix}1',
+ ),
+ ),
+ ),
+ );
+ });
+
+ test('sequential seeds get incremented numbers', () async {
+ final phrase = List.generate(12, (i) => 'word$i');
+ const password = 'password';
+ const publicKey = PublicKey(publicKey: 'publickey');
+ const pk1 = PublicKey(publicKey: 'pk1');
+ const pk2 = PublicKey(publicKey: 'pk2');
+
+ when(() => keyStore.addKey(any())).thenAnswer((_) async => publicKey);
+ when(() => keyStore.getPublicKeys(any())).thenAnswer((_) async => []);
+ when(() => accountsStorage.accounts).thenReturn([]);
+ when(() => storageRepository.seedMeta).thenReturn({
+ pk1: const SeedMetadata(name: '${seedPrefix}1'),
+ pk2: const SeedMetadata(name: '${seedPrefix}2'),
+ });
+ when(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(named: 'meta'),
+ ),
+ ).thenAnswer((_) async {});
+ when(
+ () => bridge.crateApiMergedTonWalletDartWrapperFindExistingWallets(
+ transport: any(named: 'transport'),
+ publicKey: any(named: 'publicKey'),
+ walletTypes: any(named: 'walletTypes'),
+ workchainId: any(named: 'workchainId'),
+ ),
+ ).thenAnswer((_) => Future.value('[]'));
+
+ await repository.addSeed(
+ phrase: phrase,
+ password: password,
+ workchainId: 0,
+ );
+
+ verify(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(
+ named: 'meta',
+ that: predicate(
+ (meta) => meta.name == '${seedPrefix}3',
+ ),
+ ),
+ ),
+ );
+ });
+
+ test('gaps are not filled - next after Seed 1 and 3 is 4', () async {
+ final phrase = List.generate(12, (i) => 'word$i');
+ const password = 'password';
+ const publicKey = PublicKey(publicKey: 'publickey');
+ const pk1 = PublicKey(publicKey: 'pk1');
+ const pk3 = PublicKey(publicKey: 'pk3');
+
+ when(() => keyStore.addKey(any())).thenAnswer((_) async => publicKey);
+ when(() => keyStore.getPublicKeys(any())).thenAnswer((_) async => []);
+ when(() => accountsStorage.accounts).thenReturn([]);
+ when(() => storageRepository.seedMeta).thenReturn({
+ pk1: const SeedMetadata(name: '${seedPrefix}1'),
+ pk3: const SeedMetadata(name: '${seedPrefix}3'),
+ });
+ when(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(named: 'meta'),
+ ),
+ ).thenAnswer((_) async {});
+ when(
+ () => bridge.crateApiMergedTonWalletDartWrapperFindExistingWallets(
+ transport: any(named: 'transport'),
+ publicKey: any(named: 'publicKey'),
+ walletTypes: any(named: 'walletTypes'),
+ workchainId: any(named: 'workchainId'),
+ ),
+ ).thenAnswer((_) => Future.value('[]'));
+
+ await repository.addSeed(
+ phrase: phrase,
+ password: password,
+ workchainId: 0,
+ );
+
+ verify(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(
+ named: 'meta',
+ that: predicate(
+ (meta) => meta.name == '${seedPrefix}4',
+ ),
+ ),
+ ),
+ );
+ });
+
+ test('custom names do not affect counter', () async {
+ final phrase = List.generate(12, (i) => 'word$i');
+ const password = 'password';
+ const publicKey = PublicKey(publicKey: 'publickey');
+ const pk1 = PublicKey(publicKey: 'pk1');
+ const pk2 = PublicKey(publicKey: 'pk2');
+ const pk5 = PublicKey(publicKey: 'pk5');
+
+ when(() => keyStore.addKey(any())).thenAnswer((_) async => publicKey);
+ when(() => keyStore.getPublicKeys(any())).thenAnswer((_) async => []);
+ when(() => accountsStorage.accounts).thenReturn([]);
+ when(() => storageRepository.seedMeta).thenReturn({
+ pk1: const SeedMetadata(name: '${seedPrefix}1'),
+ pk2: const SeedMetadata(name: 'My Custom Seed'),
+ pk5: const SeedMetadata(name: '${seedPrefix}5'),
+ });
+ when(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(named: 'meta'),
+ ),
+ ).thenAnswer((_) async {});
+ when(
+ () => bridge.crateApiMergedTonWalletDartWrapperFindExistingWallets(
+ transport: any(named: 'transport'),
+ publicKey: any(named: 'publicKey'),
+ walletTypes: any(named: 'walletTypes'),
+ workchainId: any(named: 'workchainId'),
+ ),
+ ).thenAnswer((_) => Future.value('[]'));
+
+ await repository.addSeed(
+ phrase: phrase,
+ password: password,
+ workchainId: 0,
+ );
+
+ verify(
+ () => storageRepository.updateSeedMetadata(
+ masterKey: publicKey,
+ meta: any(
+ named: 'meta',
+ that: predicate(
+ (meta) => meta.name == '${seedPrefix}6',
+ ),
+ ),
+ ),
+ );
+ });
+ });
+
group('removeKeys', () {
test('calls keystore and triggers removing accounts', () async {
const publicKeys = [