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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Komodo Wallet v0.9.1 Release Notes

This is a hotfix release that addresses critical issues with Trezor hardware wallet login functionality.

## 🐛 Bug Fixes

- **Trezor Login Issues** - Fixed critical bugs in the Trezor hardware wallet login flow that were preventing users from accessing their wallets.

**Full Changelog**: [0.9.0...0.9.1](https://github.com/KomodoPlatform/komodo-wallet/compare/0.9.0...0.9.1)

---

# Komodo Wallet v0.9.0 Release Notes

We are excited to announce Komodo Wallet v0.9.0. This release introduces HD wallet functionality, cross-platform fiat on-ramp improvements, a new feedback provider, and numerous bug fixes and dependency upgrades.
Expand Down
2 changes: 1 addition & 1 deletion docs/FLUTTER_VERSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Supported Flutter Version

This project supports Flutter `3.29.2` (latest stable release). We aim to keep the project up-to-date with the most recent stable Flutter versions.
This project supports Flutter `3.29.2`. We aim to keep the project up-to-date with the latest `stable` Flutter release.

## Recommended Approach: Multiple Flutter Versions

Expand Down
2 changes: 1 addition & 1 deletion lib/bloc/app_bloc_root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class AppBlocRoot extends StatelessWidget {
komodoDefiSdk,
)..add(
const PriceChartStarted(
symbols: ['KMD'],
symbols: ['BTC'],
period: Duration(days: 30),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:web_dex/bloc/cex_market_data/sdk_auth_activation_extension.dart'
import 'package:web_dex/mm2/mm2_api/rpc/base.dart';
import 'package:web_dex/model/coin.dart';
import 'package:web_dex/model/text_error.dart';
import 'package:web_dex/shared/utils/extensions/legacy_coin_migration_extensions.dart';

part 'portfolio_growth_event.dart';
part 'portfolio_growth_state.dart';
Expand Down Expand Up @@ -120,10 +121,13 @@ class PortfolioGrowthBloc
await emit.forEach(
// computation is omitted, so null-valued events are emitted on a set
// interval.
Stream<Object?>.periodic(event.updateFrequency)
.asyncMap((_) async => _fetchPortfolioGrowthChart(event)),
Stream<Object?>.periodic(event.updateFrequency).asyncMap((_) async {
// Update prices before fetching chart data
await portfolioGrowthRepository.updatePrices();
return _fetchPortfolioGrowthChart(event);
}),
Comment on lines +124 to +128
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Un-throttled price polling may flood the CEX API
updatePrices() is executed on every tick of Stream.periodic. If event.updateFrequency is small (e.g. a few seconds), the wallet will hammer the price endpoint continuously, even when the UI is backgrounded. Consider one of:

-Stream<Object?>.periodic(event.updateFrequency)…
+Stream<Object?>.periodic(event.updateFrequency.clamp(
+  const Duration(seconds: 30),  // minimum polling interval
+  const Duration(minutes: 5),   // upper bound
+))…

or move price refreshing to a debounced/background service.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart around
lines 124 to 128, the current implementation calls updatePrices() on every tick
of Stream.periodic, which can flood the CEX API if the updateFrequency is small.
To fix this, implement a throttling or debouncing mechanism to limit how often
updatePrices() is called, or move the price refreshing logic to a background
service that respects app lifecycle states, ensuring it does not run
aggressively when the UI is backgrounded.

onData: (data) =>
_handlePortfolioGrowthUpdate(data, event.selectedPeriod),
_handlePortfolioGrowthUpdate(data, event.selectedPeriod, event.coins),
onError: (error, stackTrace) {
_log.shout('Failed to load portfolio growth', error, stackTrace);
return GrowthChartLoadFailure(
Expand Down Expand Up @@ -164,10 +168,21 @@ class PortfolioGrowthBloc
return state;
}

// Fetch prices before calculating total change
// This ensures we have the latest prices in the cache
await portfolioGrowthRepository.updatePrices();

final totalBalance = _calculateTotalBalance(coins);
final totalChange24h = _calculateTotalChange24h(coins);
final percentageChange24h = _calculatePercentageChange24h(coins);

return PortfolioGrowthChartLoadSuccess(
portfolioGrowth: chart,
percentageIncrease: chart.percentageIncrease,
selectedPeriod: event.selectedPeriod,
totalBalance: totalBalance,
totalChange24h: totalChange24h,
percentageChange24h: percentageChange24h,
);
}

Expand Down Expand Up @@ -205,20 +220,71 @@ class PortfolioGrowthBloc
PortfolioGrowthState _handlePortfolioGrowthUpdate(
ChartData growthChart,
Duration selectedPeriod,
List<Coin> coins,
) {
if (growthChart.isEmpty && state is PortfolioGrowthChartLoadSuccess) {
return state;
}

final percentageIncrease = growthChart.percentageIncrease;

// TODO? Include the center value in the bloc state instead of
// calculating it in the UI
final totalBalance = _calculateTotalBalance(coins);
final totalChange24h = _calculateTotalChange24h(coins);
final percentageChange24h = _calculatePercentageChange24h(coins);

return PortfolioGrowthChartLoadSuccess(
portfolioGrowth: growthChart,
percentageIncrease: percentageIncrease,
selectedPeriod: selectedPeriod,
totalBalance: totalBalance,
totalChange24h: totalChange24h,
percentageChange24h: percentageChange24h,
);
}

/// Calculate the total balance of all coins in USD
double _calculateTotalBalance(List<Coin> coins) {
double total = coins.fold(
0,
(prev, coin) => prev + (coin.lastKnownUsdBalance(sdk) ?? 0),
);

// Return at least 0.01 if total is positive but very small
if (total > 0 && total < 0.01) {
return 0.01;
}

return total;
}

/// Calculate the total 24h change in USD value
double _calculateTotalChange24h(List<Coin> coins) {
// Calculate the 24h change by summing the change percentage of each coin
// multiplied by its USD balance and divided by 100 (to convert percentage to decimal)
return coins.fold(
0.0,
(sum, coin) {
// Use the price change from the CexPrice if available
final usdBalance = coin.lastKnownUsdBalance(sdk) ?? 0.0;
// Get the coin price from the repository's prices cache
final price = portfolioGrowthRepository
.getCachedPrice(coin.id.symbol.configSymbol.toUpperCase());
final change24h = price?.change24h ?? 0.0;
return sum + (change24h * usdBalance / 100);
},
);
}

/// Calculate the percentage change over 24h for the entire portfolio
double _calculatePercentageChange24h(List<Coin> coins) {
final double totalBalance = _calculateTotalBalance(coins);
final double totalChange = _calculateTotalChange24h(coins);

// Avoid division by zero or very small balances
if (totalBalance <= 0.01) {
return 0.0;
}

// Return the percentage change
return (totalChange / totalBalance) * 100;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ class PortfolioGrowthRepository {
/// The graph cache provider to store the portfolio growth graph data.
final PersistenceProvider<String, GraphCache> _graphCache;

final CoinsRepo _coinsRepository;
/// The SDK needed for connecting to blockchain nodes
final KomodoDefiSdk _sdk;

/// The coins repository for detailed coin info
final CoinsRepo _coinsRepository;

final _log = Logger('PortfolioGrowthRepository');

static Future<void> ensureInitialized() async {
Expand Down Expand Up @@ -514,4 +517,38 @@ class PortfolioGrowthRepository {
}

Future<void> clearCache() => _graphCache.deleteAll();

/// Calculate the total 24h change in USD value for a list of coins
///
/// This method fetches the current prices for all coins and calculates
/// the 24h change by multiplying each coin's percentage change by its USD balance
Future<double> calculateTotalChange24h(List<Coin> coins) async {
// Fetch current prices including 24h change data
final prices = await _coinsRepository.fetchCurrentPrices() ?? {};

// Calculate the 24h change by summing the change percentage of each coin
// multiplied by its USD balance and divided by 100 (to convert percentage to decimal)
double totalChange = 0.0;
for (final coin in coins) {
final price = prices[coin.id.symbol.configSymbol.toUpperCase()];
final change24h = price?.change24h ?? 0.0;
final usdBalance = coin.lastKnownUsdBalance(_sdk) ?? 0.0;
totalChange += (change24h * usdBalance / 100);
}
return totalChange;
Comment on lines +525 to +538
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

calculateTotalChange24h triggers an extra network call per invocation.

Every call fetches fresh prices, even though updatePrices() intentionally pre-loads and caches them.
This defeats caching and can hammer the market-data service.

-final prices = await _coinsRepository.fetchCurrentPrices() ?? {};
+final prices = _coinsRepository.getCachedPrices() ?? 
+              await _coinsRepository.fetchCurrentPrices() ?? {};

Alternatively pass the already-cached map from the caller.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<double> calculateTotalChange24h(List<Coin> coins) async {
// Fetch current prices including 24h change data
final prices = await _coinsRepository.fetchCurrentPrices() ?? {};
// Calculate the 24h change by summing the change percentage of each coin
// multiplied by its USD balance and divided by 100 (to convert percentage to decimal)
double totalChange = 0.0;
for (final coin in coins) {
final price = prices[coin.id.symbol.configSymbol.toUpperCase()];
final change24h = price?.change24h ?? 0.0;
final usdBalance = coin.lastKnownUsdBalance(_sdk) ?? 0.0;
totalChange += (change24h * usdBalance / 100);
}
return totalChange;
Future<double> calculateTotalChange24h(List<Coin> coins) async {
// Fetch current prices including 24h change data
- final prices = await _coinsRepository.fetchCurrentPrices() ?? {};
+ final prices = _coinsRepository.getCachedPrices()
+ ?? await _coinsRepository.fetchCurrentPrices() ?? {};
// Calculate the 24h change by summing the change percentage of each coin
// multiplied by its USD balance and divided by 100 (to convert percentage to decimal)
double totalChange = 0.0;
for (final coin in coins) {
final price = prices[coin.id.symbol.configSymbol.toUpperCase()];
final change24h = price?.change24h ?? 0.0;
final usdBalance = coin.lastKnownUsdBalance(_sdk) ?? 0.0;
totalChange += (change24h * usdBalance / 100);
}
return totalChange;
}
🤖 Prompt for AI Agents
In lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart
around lines 525 to 538, the calculateTotalChange24h method fetches current
prices on every call, causing unnecessary network requests and bypassing the
existing caching done by updatePrices(). To fix this, modify the method to
accept the cached prices map as a parameter from the caller instead of fetching
it internally, and use this passed-in map to calculate the total 24h change,
thereby avoiding redundant network calls and leveraging the cached data.

}

/// Get the cached price for a given coin symbol
///
/// This is used to avoid fetching prices for every calculation
CexPrice? getCachedPrice(String symbol) {
return _coinsRepository.getCachedPrice(symbol);
}

/// Update prices for all coins by fetching from market data
///
/// This method ensures we have up-to-date price data before calculations
Future<void> updatePrices() async {
await _coinsRepository.fetchCurrentPrices();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@ final class PortfolioGrowthChartLoadSuccess extends PortfolioGrowthState {
required this.portfolioGrowth,
required this.percentageIncrease,
required super.selectedPeriod,
required this.totalBalance,
required this.totalChange24h,
required this.percentageChange24h,
});

final ChartData portfolioGrowth;
final double percentageIncrease;
final double totalBalance;
final double totalChange24h;
final double percentageChange24h;

Comment on lines +22 to 32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid storing money in double.

double introduces rounding errors that quickly surface in financial UIs (e.g. $0.01 sometimes renders as 0.009999).
If practical, migrate these three monetary fields to a fixed-precision type (Decimal, BigInt cents, or a dedicated Money class) and format at the edge.

🤖 Prompt for AI Agents
In lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_state.dart between
lines 22 and 32, the fields totalBalance, totalChange24h, and
percentageChange24h are currently typed as double, which can cause rounding
errors in financial calculations. To fix this, change these fields to use a
fixed-precision type such as Decimal, BigInt representing cents, or a dedicated
Money class. Update all related code to handle this new type and ensure
formatting to string for display happens only at the UI layer.

@override
List<Object> get props => <Object>[
portfolioGrowth,
percentageIncrease,
selectedPeriod,
totalBalance,
totalChange24h,
percentageChange24h,
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ enum PriceChartPeriod {
}
}

String get intervalString {
// TODO: Localize this
String get formatted {
switch (this) {
case PriceChartPeriod.oneHour:
return '1h';
Expand Down
3 changes: 3 additions & 0 deletions lib/bloc/cex_market_data/price_chart/models/time_period.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ enum TimePeriod {
}
}

// TODO: Localize
String formatted() => name;

Duration get duration {
switch (this) {
case TimePeriod.oneHour:
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 @@ -658,4 +658,11 @@ class CoinsRepo {
result: withdrawDetails,
);
}

/// Get a cached price for a given coin symbol
///
/// This returns the price from the cache without fetching new data
CexPrice? getCachedPrice(String symbol) {
return _pricesCache[symbol];
}
}
53 changes: 46 additions & 7 deletions lib/bloc/trezor_init_bloc/trezor_init_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:async';

import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:web_dex/app_config/app_config.dart';
import 'package:web_dex/shared/utils/password.dart';
import 'package:web_dex/bloc/coins_bloc/coins_repo.dart';
import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart';
import 'package:web_dex/generated/codegen_loader.g.dart';
Expand All @@ -18,18 +20,24 @@ import 'package:web_dex/model/kdf_auth_metadata_extension.dart';
import 'package:web_dex/model/text_error.dart';
import 'package:web_dex/model/wallet.dart';
import 'package:web_dex/shared/utils/utils.dart';
import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'
show PrivateKeyPolicy;

part 'trezor_init_event.dart';
part 'trezor_init_state.dart';

const String _trezorPasswordKey = 'trezor_wallet_password';

class TrezorInitBloc extends Bloc<TrezorInitEvent, TrezorInitState> {
TrezorInitBloc({
required KomodoDefiSdk kdfSdk,
required TrezorRepo trezorRepo,
required CoinsRepo coinsRepository,
FlutterSecureStorage? secureStorage,
}) : _trezorRepo = trezorRepo,
_kdfSdk = kdfSdk,
_coinsRepository = coinsRepository,
_secureStorage = secureStorage ?? const FlutterSecureStorage(),
super(TrezorInitState.initial()) {
on<TrezorInitSubscribeStatus>(_onSubscribeStatus);
on<TrezorInit>(_onInit);
Expand All @@ -49,6 +57,7 @@ class TrezorInitBloc extends Bloc<TrezorInitEvent, TrezorInitState> {
final TrezorRepo _trezorRepo;
final KomodoDefiSdk _kdfSdk;
final CoinsRepo _coinsRepository;
final FlutterSecureStorage _secureStorage;
Timer? _statusTimer;

void _unsubscribeStatus() {
Expand Down Expand Up @@ -273,35 +282,65 @@ class TrezorInitBloc extends Bloc<TrezorInitEvent, TrezorInitState> {
/// into a static 'hidden' wallet to init trezor
Future<void> _loginToTrezorWallet({
String walletName = 'My Trezor',
String password = 'hidden-login',
String? password,
AuthOptions authOptions = const AuthOptions(
derivationMethod: DerivationMethod.hdWallet,
privKeyPolicy: PrivateKeyPolicy.trezor,
),
}) async {
try {
password ??= await _secureStorage.read(key: _trezorPasswordKey);
} catch (e, s) {
log(
'Failed to read trezor password from secure storage: $e',
path: 'trezor_init_bloc => _loginToTrezorWallet',
isError: true,
trace: s,
).ignore();
// If reading fails, password will remain null and a new one will be generated
}

if (password == null) {
password = generatePassword();
try {
await _secureStorage.write(key: _trezorPasswordKey, value: password);
} catch (e, s) {
log(
'Failed to write trezor password to secure storage: $e',
path: 'trezor_init_bloc => _loginToTrezorWallet',
isError: true,
trace: s,
).ignore();
// Continue with generated password even if storage write fails
}
}

final bool mm2SignedIn = await _kdfSdk.auth.isSignedIn();
if (state.kdfUser != null && mm2SignedIn) {
return;
}

// final walletName = state.status?.trezorStatus.name ?? 'My Trezor';
// final password =
// state.status?.details.deviceDetails?.deviceId ?? 'hidden-login';
final existingWallets = await _kdfSdk.auth.getUsers();
if (existingWallets.any((wallet) => wallet.walletId.name == walletName)) {
await _kdfSdk.auth.signIn(
walletName: walletName,
password: password,
options: const AuthOptions(derivationMethod: DerivationMethod.iguana),
options: authOptions,
);
await _kdfSdk.setWalletType(WalletType.trezor);
await _kdfSdk.confirmSeedBackup();
await _kdfSdk.addActivatedCoins(enabledByDefaultTrezorCoins);
return;
}

await _kdfSdk.auth.register(
walletName: walletName,
password: password,
options: const AuthOptions(derivationMethod: DerivationMethod.iguana),
options: authOptions,
);
await _kdfSdk.setWalletType(WalletType.trezor);
await _kdfSdk.confirmSeedBackup();
await _kdfSdk.addActivatedCoins(enabledByDefaultTrezorCoins);
}

Future<void> _logout() async {
Expand Down
Loading
Loading