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
14 changes: 13 additions & 1 deletion lib/bloc/coins_bloc/coins_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,20 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
);
}

// Batch-write all asset IDs to wallet metadata in a single call before
// launching parallel activations. This avoids N concurrent read-modify-write
// cycles on the same metadata key which caused last-write-wins data loss.
await _coinsRepo.addAssetsToWalletMetadata(
coinsToActivate.map((asset) => asset.id),
);

final enableFutures = coinsToActivate
.map((asset) => _coinsRepo.activateAssetsSync([asset]))
.map(
(asset) => _coinsRepo.activateAssetsSync(
[asset],
addToWalletMetadata: false,
),
)
.toList();

// Ignore the return type here and let the broadcast handle the state updates as
Expand Down
7 changes: 7 additions & 0 deletions lib/bloc/coins_bloc/coins_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,13 @@ class CoinsRepo {
}
}

/// Adds the given assets (and their parent coins) to wallet metadata.
///
/// This is exposed so callers can batch-write metadata before launching
/// parallel activations with `addToWalletMetadata: false`.
Future<void> addAssetsToWalletMetadata(Iterable<AssetId> assets) =>
_addAssetsToWalletMetdata(assets);

Future<void> _addAssetsToWalletMetdata(Iterable<AssetId> assets) async {
final parentIds = <String>{};
for (final assetId in assets) {
Expand Down
40 changes: 17 additions & 23 deletions lib/model/kdf_auth_metadata_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,41 +98,35 @@ extension KdfAuthMetadataExtension on KomodoDefiSdk {

/// Adds new coin/asset IDs to the current user's activated coins list.
///
/// This method merges the provided [coins] with the existing activated coins,
/// ensuring no duplicates. The merged list is then stored in user metadata.
/// This method atomically merges the provided [coins] with the existing
/// activated coins, ensuring no duplicates and no lost writes under
/// concurrent calls.
///
/// If no user is currently signed in, the operation will complete but have no effect.
/// If no user is currently signed in, the operation will throw.
///
/// [coins] - An iterable of coin/asset configuration IDs to add.
Future<void> addActivatedCoins(Iterable<String> coins) async {
final user = await auth.currentUser;
final existingCoins = user == null
? <String>[]
: user.metadata.valueOrNull<List<String>>('activated_coins') ??
<String>[];

final mergedCoins = <dynamic>{...existingCoins, ...coins}.toList();
await auth.setOrRemoveActiveUserKeyValue('activated_coins', mergedCoins);
await auth.updateActiveUserKeyValue('activated_coins', (current) {
final existing = (current as List<dynamic>?)?.cast<String>() ?? [];
return <String>{...existing, ...coins}.toList();
});
}

/// Removes specified coin/asset IDs from the current user's activated coins list.
///
/// This method removes all occurrences of the provided [coins] from the user's
/// activated coins list and updates the stored metadata.
/// This method atomically removes all occurrences of the provided [coins]
/// from the user's activated coins list, ensuring no lost writes under
/// concurrent calls.
///
/// If no user is currently signed in, the operation will complete but have no effect.
/// If no user is currently signed in, the operation will throw.
///
/// [coins] - A list of coin/asset configuration IDs to remove.
Future<void> removeActivatedCoins(List<String> coins) async {
final user = await auth.currentUser;
final existingCoins = user == null
? <String>[]
: List<String>.from(
user.metadata.valueOrNull<List<String>>('activated_coins') ?? [],
);

existingCoins.removeWhere((coin) => coins.contains(coin));
await auth.setOrRemoveActiveUserKeyValue('activated_coins', existingCoins);
await auth.updateActiveUserKeyValue('activated_coins', (current) {
final existing = (current as List<dynamic>?)?.cast<String>() ?? [];
final updated = existing.where((c) => !coins.contains(c)).toList();
return updated.isEmpty ? null : updated;
});
}

/// Sets the seed backup confirmation status for the current user.
Expand Down
Loading