Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
25 changes: 24 additions & 1 deletion lib/bloc/auth_bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,12 @@ class AuthBloc extends Bloc<AuthBlocEvent, AuthBlocState> with TrezorAuthMixin {
await _kdfSdk.confirmSeedBackup(hasBackup: event.wallet.config.hasBackup);
await _kdfSdk.addActivatedCoins(enabledByDefaultCoins);
if (event.wallet.config.activatedCoins.isNotEmpty) {
await _kdfSdk.addActivatedCoins(event.wallet.config.activatedCoins);
// Seed import files and legacy wallets may contain removed or unsupported
// coins, so we filter them out before adding them to the wallet metadata.
final availableWalletCoins = _filterOutUnsupportedCoins(
event.wallet.config.activatedCoins,
);
await _kdfSdk.addActivatedCoins(availableWalletCoins);
}

// Delete legacy wallet on successful restoration & login to avoid
Expand Down Expand Up @@ -372,4 +377,22 @@ class AuthBloc extends Bloc<AuthBlocEvent, AuthBlocState> with TrezorAuthMixin {
add(AuthModeChanged(mode: event, currentUser: user));
});
}

List<String> _filterOutUnsupportedCoins(List<String> coins) {
final unsupportedAssets = coins.where(
(coin) => _kdfSdk.assets.findAssetsByConfigId(coin).isEmpty,
);
_log.warning(
'Skipping import of unsupported assets: '
'${unsupportedAssets.map((coin) => coin).join(', ')}',
);

final supportedAssets = coins
.map((coin) => _kdfSdk.assets.findAssetsByConfigId(coin))
.where((assets) => assets.isNotEmpty)
.map((assets) => assets.single.id.id);
_log.info('Import supported assets: ${supportedAssets.join(', ')}');

return supportedAssets.toList();
}
Comment thread
takenagain marked this conversation as resolved.
}
124 changes: 62 additions & 62 deletions lib/bloc/coins_bloc/coins_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/foundation.dart' show mapEquals;
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';
Expand All @@ -20,10 +20,7 @@ part 'coins_state.dart';

/// Responsible for coin activation, deactivation, syncing, and fiat price
class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
CoinsBloc(
this._kdfSdk,
this._coinsRepo,
) : super(CoinsState.initial()) {
CoinsBloc(this._kdfSdk, this._coinsRepo) : super(CoinsState.initial()) {
on<CoinsStarted>(_onCoinsStarted, transformer: droppable());
// TODO: move auth listener to ui layer: bloclistener should fire auth events
on<CoinsBalanceMonitoringStarted>(_onCoinsBalanceMonitoringStarted);
Expand Down Expand Up @@ -75,14 +72,7 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
final pubkeys = await _kdfSdk.pubkeys.getPubkeys(asset);

// Update state with new pubkeys
emit(
state.copyWith(
pubkeys: {
...state.pubkeys,
event.coinId: pubkeys,
},
),
);
emit(state.copyWith(pubkeys: {...state.pubkeys, event.coinId: pubkeys}));
} catch (e, s) {
_log.shout('Failed to get pubkeys for ${event.coinId}', e, s);
}
Expand Down Expand Up @@ -207,8 +197,9 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
final coinUpdates = _syncIguanaCoinsStates();
await emit.forEach(
coinUpdates,
onData: (coin) => state
.copyWith(walletCoins: {...state.walletCoins, coin.id.id: coin}),
onData: (coin) => state.copyWith(
walletCoins: {...state.walletCoins, coin.id.id: coin},
),
);
}

Expand Down Expand Up @@ -261,8 +252,9 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
Map<String, Coin> currentCoins,
) {
final updatedWalletCoins = Map.fromEntries(
currentWalletCoins.entries
.where((entry) => !coinsToDisable.contains(entry.key)),
currentWalletCoins.entries.where(
(entry) => !coinsToDisable.contains(entry.key),
),
);
final updatedCoins = Map<String, Coin>.of(currentCoins);
for (final assetId in coinsToDisable) {
Expand All @@ -276,38 +268,43 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
CoinsPricesUpdated event,
Emitter<CoinsState> emit,
) async {
final prices = await _coinsRepo.fetchCurrentPrices();
if (prices == null) {
_log.severe('Coin prices list empty/null');
return;
}
final didPricesChange = !mapEquals(state.prices, prices);
if (!didPricesChange) {
_log.info('Coin prices list unchanged');
return;
}
try {
final prices = await _coinsRepo.fetchCurrentPrices();
if (prices == null) {
_log.severe('Coin prices list empty/null');
return;
}
final didPricesChange = !mapEquals(state.prices, prices);
if (!didPricesChange) {
_log.info('Coin prices list unchanged');
return;
}

Map<String, Coin> updateCoinsWithPrices(Map<String, Coin> coins) {
final map = coins.map((key, coin) {
// Use configSymbol to lookup for backwards compatibility with the old,
// string-based price list (and fallback)
final price = prices[coin.id.symbol.configSymbol];
if (price != null) {
return MapEntry(key, coin.copyWith(usdPrice: price));
}
return MapEntry(key, coin);
});
Map<String, Coin> updateCoinsWithPrices(Map<String, Coin> coins) {
final map = coins.map((key, coin) {
// Use configSymbol to lookup for backwards compatibility with the old,
// string-based price list (and fallback)
final price = prices[coin.id.symbol.configSymbol];
if (price != null) {
return MapEntry(key, coin.copyWith(usdPrice: price));
}
return MapEntry(key, coin);
});

// .map already returns a new map, so we don't need to create a new map
return map.unmodifiable();
}

return Map.of(map).unmodifiable();
emit(
state.copyWith(
prices: prices.unmodifiable(),
coins: updateCoinsWithPrices(state.coins),
walletCoins: updateCoinsWithPrices(state.walletCoins),
),
);
} catch (e, s) {
_log.shout('Error on prices updated', e, s);
}

emit(
state.copyWith(
prices: prices.unmodifiable(),
coins: updateCoinsWithPrices(state.coins),
walletCoins: updateCoinsWithPrices(state.walletCoins),
),
);
}

Future<void> _onLogin(
Expand Down Expand Up @@ -364,12 +361,11 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
// activation loops for assets not supported by the SDK.this may happen if the wallet
// has assets that were removed from the SDK or the config has unsupported default
// assets.
final coinsToActivate = coins
// This is also important for coin delistings.
final enableFutures = coins
.map((coin) => _kdfSdk.assets.findAssetsByConfigId(coin))
.where((assetsSet) => assetsSet.isNotEmpty)
.map((assetsSet) => assetsSet.single);

final enableFutures = coinsToActivate
.where((assets) => assets.isNotEmpty)
.map((assets) => assets.single)
.map((asset) => _coinsRepo.activateAssetsSync([asset]))
.toList();

Expand All @@ -382,12 +378,10 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
final knownCoins = _coinsRepo.getKnownCoinsMap();
final activatingCoins = Map<String, Coin>.fromIterable(
coins
.map(
(coin) {
final sdkCoin = knownCoins[coin];
return sdkCoin?.copyWith(state: CoinState.activating);
},
)
.map((coin) {
final sdkCoin = knownCoins[coin];
return sdkCoin?.copyWith(state: CoinState.activating);
})
.where((coin) => coin != null)
.cast<Coin>(),
key: (element) => (element as Coin).id.id,
Expand Down Expand Up @@ -419,17 +413,21 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
}
if (assets.length > 1) {
final assetIds = assets.map((a) => a.id.id).join(', ');
_log.shout('Multiple assets found for activated coin ID: $coinId. '
'Expected single asset, found ${assets.length}: $assetIds. ');
_log.shout(
'Multiple assets found for activated coin ID: $coinId. '
'Expected single asset, found ${assets.length}: $assetIds. ',
);
}

// This is expected to throw if there are multiple assets, to stick
// to the strategy of using `.single` elsewhere in the codebase.
walletAssets.add(assets.single);
}

final coinsToSync =
_getWalletCoinsNotInState(walletAssets, coinsBlocWalletCoinsState);
final coinsToSync = _getWalletCoinsNotInState(
walletAssets,
coinsBlocWalletCoinsState,
);
if (coinsToSync.isNotEmpty) {
_log.info(
'Found ${coinsToSync.length} wallet coins not in state, '
Expand All @@ -440,7 +438,9 @@ class CoinsBloc extends Bloc<CoinsEvent, CoinsState> {
}

List<Coin> _getWalletCoinsNotInState(
List<Asset> walletAssets, Map<String, Coin> coinsBlocWalletCoinsState) {
List<Asset> walletAssets,
Map<String, Coin> coinsBlocWalletCoinsState,
) {
final List<Coin> coinsToSyncToState = [];

final enabledAssetsNotInState = walletAssets
Expand Down
23 changes: 15 additions & 8 deletions lib/bloc/coins_bloc/coins_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:math' show min;

import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' show NetworkImage;
import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'
as kdf_rpc;
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
Expand Down Expand Up @@ -250,7 +250,7 @@ class CoinsRepo {
///
/// **Parameters:**
/// - [assets]: List of assets to activate
/// - [notify]: Whether to broadcast state changes to listeners (default: true)
/// - [notifyListeners]: Whether to broadcast state changes to listeners (default: true)
/// - [addToWalletMetadata]: Whether to add assets to wallet metadata (default: true)
/// - [maxRetryAttempts]: Maximum number of retry attempts (default: 30)
/// - [initialRetryDelay]: Initial delay between retries (default: 500ms)
Expand All @@ -267,7 +267,7 @@ class CoinsRepo {
/// **Note:** Assets are added to wallet metadata even if activation fails.
Future<void> activateAssetsSync(
List<Asset> assets, {
bool notify = true,
bool notifyListeners = true,
bool addToWalletMetadata = true,
int maxRetryAttempts = 30,
Duration initialRetryDelay = const Duration(milliseconds: 500),
Expand All @@ -292,7 +292,9 @@ class CoinsRepo {
for (final asset in assets) {
final coin = _assetToCoinWithoutAddress(asset);
try {
if (notify) _broadcastAsset(coin.copyWith(state: CoinState.activating));
if (notifyListeners) {
_broadcastAsset(coin.copyWith(state: CoinState.activating));
}

// Use retry with exponential backoff for activation
await retry<void>(
Expand Down Expand Up @@ -322,7 +324,7 @@ class CoinsRepo {
);

_log.info('Asset activated: ${asset.id.id}');
if (notify) {
if (notifyListeners) {
_broadcastAsset(coin.copyWith(state: CoinState.active));
if (coin.id.parentId != null) {
final parentCoin = _assetToCoinWithoutAddress(
Expand All @@ -347,7 +349,7 @@ class CoinsRepo {
e,
s,
);
if (notify) {
if (notifyListeners) {
_broadcastAsset(asset.toCoin().copyWith(state: CoinState.suspended));
}
} finally {
Expand Down Expand Up @@ -430,7 +432,7 @@ class CoinsRepo {

return activateAssetsSync(
assets,
notify: notify,
notifyListeners: notify,
addToWalletMetadata: addToWalletMetadata,
maxRetryAttempts: maxRetryAttempts,
initialRetryDelay: initialRetryDelay,
Expand Down Expand Up @@ -550,7 +552,12 @@ class CoinsRepo {
// will hit rate limits and have reduced market metrics functionality.
// This will happen regardless of chunk size. The rate limits are per IP
// per hour.
final activatedAssets = await _kdfSdk.getWalletAssets();
final coinIds = await _kdfSdk.getWalletCoinIds();
final activatedAssets = coinIds
.map((coinId) => _kdfSdk.assets.findAssetsByConfigId(coinId))
.where((assets) => assets.isNotEmpty)
.map((assets) => assets.single)
.toList();
final Iterable<Asset> targetAssets = activatedAssets.isNotEmpty
? activatedAssets
: _kdfSdk.assets.available.values;
Expand Down
Loading
Loading