From 59b0045e9a583f8ca13131fd9b5aac1bf0f17bd1 Mon Sep 17 00:00:00 2001 From: Marcin <84280969+nemoforte@users.noreply.github.com> Date: Wed, 6 Mar 2024 13:53:19 +0100 Subject: [PATCH] Feature: Adaptive default token (#4) This branch introduces adaptive default token queried from the network, and thus specific to each network. Until now, default token was hardcoded, which was removed in this branch. It also prepares a model for the default bech32 prefix, which has analogous purpose and was initially intended to implement together. However, due to work organization purposes, the Adaptive Bech32 Address Prefix feature will be introduced separately. List of changes: - deleted hardcoded "defaultFeeTokenAliasModel" from app_config.dart in order to replace it with queried values - created query_kira_token_aliases_req.dart to enable using query parameters introduced in interx v0.4.43 - pagination and querying by denomination name - updated query_kira_token_aliases_resp.dart with new parameters "defaultDenom" and "bech32Prefix", added in interx v0.4.43 - created token_default_denom_model.dart which is a business model representing default token alias. It also contains default bech32 prefix, which will be used later in the analogous feature Adaptive Bech32 Address Prefix. - created "getTokenDefaultDenomModel()" method in query_kira_token_aliases_service.dart, which is called with every network change and conditions its status - if TokenDefaultDenomModel is missing, network is unhealthy - added "tokenDefaultDenomModel" field to classes extending ANetworkOnlineModel to enable passing the new parameters to the network - replaced every usage of hardcoded default token with the queried values, accessed via NetworkModuleBloc with the variable "tokenDefaultDenomModel" - unrelated with the branch's specific domain: modified token_avatar.dart to handle both .svg and .png logo images --- .../network_module/network_module_bloc.dart | 18 ++++++ lib/config/app_config.dart | 11 +--- lib/generated/intl/messages_en.dart | 2 + lib/generated/l10n.dart | 10 ++++ .../query_kira_tokens_aliases_req.dart | 22 +++++++ .../query_kira_tokens_aliases_resp.dart | 17 +++--- .../repositories/api/api_kira_repository.dart | 6 +- .../api_kira/query_execution_fee_service.dart | 5 +- .../query_kira_tokens_aliases_service.dart | 60 ++++++++++++++++++- .../services/network_module_service.dart | 15 +++++ lib/l10n/intl_en.arb | 1 + .../balances_filter_options.dart | 4 +- .../network/data/interx_warning_model.dart | 13 ++-- .../network/data/interx_warning_type.dart | 1 + .../network/network_properties_model.dart | 9 +-- .../status/online/a_network_online_model.dart | 16 +++-- .../status/online/network_healthy_model.dart | 12 ++-- .../online/network_unhealthy_model.dart | 12 ++-- .../tokens/token_default_denom_model.dart | 23 +++++++ lib/test/mock_api_kira_repository.dart | 4 +- lib/test/mock_app_config.dart | 9 --- .../mock_api_kira_tokens_aliases.dart | 24 ++++---- lib/test/utils/test_utils.dart | 21 +++++++ .../staking_tx_delegate_page.dart | 2 - .../staking_tx_undelegate_page.dart | 2 - lib/views/widgets/generic/token_avatar.dart | 40 +++++++++---- .../network_warning_container.dart | 1 + pubspec.yaml | 2 +- ...uery_kira_tokens_aliases_service_test.dart | 20 +++++++ .../generic/network_module_bloc_test.dart | 15 ++++- .../query_execution_fee_service_test.dart | 5 +- ...uery_kira_tokens_aliases_service_test.dart | 48 ++++++++++++++- .../balances_filter_options_test.dart | 6 +- 33 files changed, 357 insertions(+), 99 deletions(-) create mode 100644 lib/infra/dto/api_kira/query_kira_tokens_aliases/request/query_kira_tokens_aliases_req.dart create mode 100644 lib/shared/models/tokens/token_default_denom_model.dart diff --git a/lib/blocs/generic/network_module/network_module_bloc.dart b/lib/blocs/generic/network_module/network_module_bloc.dart index 2e59b7d1..de2ffb04 100644 --- a/lib/blocs/generic/network_module/network_module_bloc.dart +++ b/lib/blocs/generic/network_module/network_module_bloc.dart @@ -18,6 +18,7 @@ import 'package:miro/shared/models/network/status/a_network_status_model.dart'; import 'package:miro/shared/models/network/status/network_empty_model.dart'; import 'package:miro/shared/models/network/status/network_unknown_model.dart'; import 'package:miro/shared/models/network/status/online/a_network_online_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/shared/utils/network_utils.dart'; class NetworkModuleBloc extends Bloc { @@ -31,6 +32,7 @@ class NetworkModuleBloc extends Bloc { late Timer _timer; bool _refreshingBool = false; + TokenDefaultDenomModel? tokenDefaultDenomModel; NetworkModuleBloc() : super(NetworkModuleState.disconnected()) { on(_mapInitEventToState); @@ -79,8 +81,10 @@ class NetworkModuleBloc extends Bloc { if (networkUnchangedBool) { await _networkCustomSectionCubit.updateNetworks(networkStatusModel); emit(NetworkModuleState.connected(networkStatusModel)); + _refreshTokenDefaultDenomModel(networkStatusModel); } } + await _networkCustomSectionCubit.refreshNetworks(); _refreshingBool = false; @@ -102,6 +106,7 @@ class NetworkModuleBloc extends Bloc { _rpcBrowserUrlController.setRpcAddress(networkStatusModel); await _networkCustomSectionCubit.updateNetworks(networkStatusModel); emit(NetworkModuleState.connected(networkStatusModel)); + _refreshTokenDefaultDenomModel(networkStatusModel); } } @@ -110,6 +115,7 @@ class NetworkModuleBloc extends Bloc { _rpcBrowserUrlController.setRpcAddress(networkOnlineModel); await _networkCustomSectionCubit.updateNetworks(networkOnlineModel); emit(NetworkModuleState.connected(networkOnlineModel)); + _switchTokenDefaultDenomModel(networkOnlineModel); } Future _mapDisconnectEventToState( @@ -135,4 +141,16 @@ class NetworkModuleBloc extends Bloc { ANetworkStatusModel networkStatusModel = await _networkModuleService.getNetworkStatusModel(networkUnknownModel); _networkListCubit.setNetworkStatusModel(networkStatusModel: networkStatusModel); } + + void _refreshTokenDefaultDenomModel(ANetworkStatusModel networkStatusModel) { + if (networkStatusModel is ANetworkOnlineModel) { + tokenDefaultDenomModel ??= networkStatusModel.tokenDefaultDenomModel; + } + } + + void _switchTokenDefaultDenomModel(ANetworkStatusModel networkStatusModel) { + if (networkStatusModel is ANetworkOnlineModel) { + tokenDefaultDenomModel = networkStatusModel.tokenDefaultDenomModel; + } + } } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 59bcaa0a..aba3e144 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,8 +1,6 @@ import 'package:miro/shared/controllers/browser/rpc_browser_url_controller.dart'; import 'package:miro/shared/models/network/data/connection_status_type.dart'; import 'package:miro/shared/models/network/status/network_unknown_model.dart'; -import 'package:miro/shared/models/tokens/token_alias_model.dart'; -import 'package:miro/shared/models/tokens/token_denomination_model.dart'; import 'package:miro/shared/utils/logger/app_logger.dart'; import 'package:miro/shared/utils/logger/log_level.dart'; import 'package:miro/shared/utils/network_utils.dart'; @@ -14,7 +12,6 @@ class AppConfig { final Duration loadingPageTimerDuration; final List supportedInterxVersions; final RpcBrowserUrlController rpcBrowserUrlController; - final TokenAliasModel defaultFeeTokenAliasModel; final int _defaultRefreshIntervalSeconds; final NetworkUnknownModel _defaultNetworkUnknownModel; @@ -30,7 +27,6 @@ class AppConfig { required this.loadingPageTimerDuration, required this.supportedInterxVersions, required this.rpcBrowserUrlController, - required this.defaultFeeTokenAliasModel, required int defaultRefreshIntervalSeconds, required NetworkUnknownModel defaultNetworkUnknownModel, }) : _defaultRefreshIntervalSeconds = defaultRefreshIntervalSeconds, @@ -42,13 +38,8 @@ class AppConfig { defaultApiCacheMaxAge: const Duration(seconds: 60), outdatedBlockDuration: const Duration(minutes: 5), loadingPageTimerDuration: const Duration(seconds: 4), - supportedInterxVersions: ['v0.4.41'], + supportedInterxVersions: ['v0.4.46'], rpcBrowserUrlController: RpcBrowserUrlController(), - defaultFeeTokenAliasModel: const TokenAliasModel( - name: 'Kira', - defaultTokenDenominationModel: TokenDenominationModel(name: 'ukex', decimals: 0), - networkTokenDenominationModel: TokenDenominationModel(name: 'KEX', decimals: 6), - ), defaultRefreshIntervalSeconds: 60, defaultNetworkUnknownModel: NetworkUnknownModel( connectionStatusType: ConnectionStatusType.disconnected, diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index eeb2c75c..1c313b34 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -333,6 +333,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Enable custom address"), "networkWarningIncompatible": MessageLookupByLibrary.simpleMessage( "The application is incompatible with this server. Some views may not work correctly."), + "networkWarningMissingInfo": MessageLookupByLibrary.simpleMessage( + "Connecting a wallet unavailable due to missing essential data from network."), "networkWarningWhenLastBlock": m5, "or": MessageLookupByLibrary.simpleMessage("or "), "paginatedListPageSize": diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index af227730..465bad95 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1390,6 +1390,16 @@ class S { ); } + /// `Connecting a wallet unavailable due to missing essential data from network.` + String get networkWarningMissingInfo { + return Intl.message( + 'Connecting a wallet unavailable due to missing essential data from network.', + name: 'networkWarningMissingInfo', + desc: '', + args: [], + ); + } + /// `The application is incompatible with this server. Some views may not work correctly.` String get networkWarningIncompatible { return Intl.message( diff --git a/lib/infra/dto/api_kira/query_kira_tokens_aliases/request/query_kira_tokens_aliases_req.dart b/lib/infra/dto/api_kira/query_kira_tokens_aliases/request/query_kira_tokens_aliases_req.dart new file mode 100644 index 00000000..bfceca38 --- /dev/null +++ b/lib/infra/dto/api_kira/query_kira_tokens_aliases/request/query_kira_tokens_aliases_req.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; + +class QueryKiraTokensAliasesReq extends Equatable { + final List? tokens; + final int? limit; + final int? offset; + + const QueryKiraTokensAliasesReq({ + this.tokens, + this.limit, + this.offset, + }); + + Map get queryParameters => { + 'tokens': tokens?.join(','), + 'limit': limit, + 'offset': offset, + }; + + @override + List get props => [tokens, limit, offset]; +} diff --git a/lib/infra/dto/api_kira/query_kira_tokens_aliases/response/query_kira_tokens_aliases_resp.dart b/lib/infra/dto/api_kira/query_kira_tokens_aliases/response/query_kira_tokens_aliases_resp.dart index 6dff77c1..3bc4a322 100644 --- a/lib/infra/dto/api_kira/query_kira_tokens_aliases/response/query_kira_tokens_aliases_resp.dart +++ b/lib/infra/dto/api_kira/query_kira_tokens_aliases/response/query_kira_tokens_aliases_resp.dart @@ -3,21 +3,24 @@ import 'package:miro/infra/dto/api_kira/query_kira_tokens_aliases/response/token class QueryKiraTokensAliasesResp extends Equatable { final List tokenAliases; + final String defaultDenom; + final String bech32Prefix; const QueryKiraTokensAliasesResp({ required this.tokenAliases, + required this.defaultDenom, + required this.bech32Prefix, }); - factory QueryKiraTokensAliasesResp.fromJsonList(List jsonList) { + factory QueryKiraTokensAliasesResp.fromJson(Map json) { + List jsonList = json['token_aliases_data'] as List; return QueryKiraTokensAliasesResp( - tokenAliases: jsonList - .map( - (dynamic e) => TokenAlias.fromJson(e as Map), - ) - .toList(), + tokenAliases: jsonList.map((dynamic e) => TokenAlias.fromJson(e as Map)).toList(), + defaultDenom: json['default_denom'] as String, + bech32Prefix: json['bech32_prefix'] as String, ); } @override - List get props => [tokenAliases.hashCode]; + List get props => [tokenAliases.hashCode, defaultDenom, bech32Prefix]; } diff --git a/lib/infra/repositories/api/api_kira_repository.dart b/lib/infra/repositories/api/api_kira_repository.dart index cd7aa0c7..655262ca 100644 --- a/lib/infra/repositories/api/api_kira_repository.dart +++ b/lib/infra/repositories/api/api_kira_repository.dart @@ -6,6 +6,7 @@ import 'package:miro/infra/dto/api_kira/query_delegations/request/query_delegati import 'package:miro/infra/dto/api_kira/query_execution_fee/request/query_execution_fee_request.dart'; import 'package:miro/infra/dto/api_kira/query_identity_record_verify_requests/request/query_identity_record_verify_requests_by_approver_req.dart'; import 'package:miro/infra/dto/api_kira/query_identity_record_verify_requests/request/query_identity_record_verify_requests_by_requester_req.dart'; +import 'package:miro/infra/dto/api_kira/query_kira_tokens_aliases/request/query_kira_tokens_aliases_req.dart'; import 'package:miro/infra/dto/api_kira/query_staking_pool/request/query_staking_pool_req.dart'; import 'package:miro/infra/dto/api_kira/query_undelegations/request/query_undelegations_req.dart'; import 'package:miro/infra/exceptions/dio_connect_exception.dart'; @@ -33,7 +34,7 @@ abstract class IApiKiraRepository { Future> fetchQueryIdentityRecordVerifyRequestsByRequester(ApiRequestModel apiRequestModel); - Future> fetchQueryKiraTokensAliases(ApiRequestModel apiRequestModel); + Future> fetchQueryKiraTokensAliases(ApiRequestModel apiRequestModel); Future> fetchQueryKiraTokensRates(ApiRequestModel apiRequestModel); @@ -195,11 +196,12 @@ class RemoteApiKiraRepository implements IApiKiraRepository { } @override - Future> fetchQueryKiraTokensAliases(ApiRequestModel apiRequestModel) async { + Future> fetchQueryKiraTokensAliases(ApiRequestModel apiRequestModel) async { try { final Response response = await _httpClientManager.get( networkUri: apiRequestModel.networkUri, path: '/api/kira/tokens/aliases', + queryParameters: apiRequestModel.requestData.queryParameters, apiCacheConfigModel: ApiCacheConfigModel(forceRequestBool: apiRequestModel.forceRequestBool), ); return response; diff --git a/lib/infra/services/api_kira/query_execution_fee_service.dart b/lib/infra/services/api_kira/query_execution_fee_service.dart index 9aff4dda..7b61eba5 100644 --- a/lib/infra/services/api_kira/query_execution_fee_service.dart +++ b/lib/infra/services/api_kira/query_execution_fee_service.dart @@ -1,7 +1,6 @@ import 'package:decimal/decimal.dart'; import 'package:dio/dio.dart'; import 'package:miro/blocs/generic/network_module/network_module_bloc.dart'; -import 'package:miro/config/app_config.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/infra/dto/api_kira/query_execution_fee/request/query_execution_fee_request.dart'; import 'package:miro/infra/dto/api_kira/query_execution_fee/response/query_execution_fee_response.dart'; @@ -16,7 +15,6 @@ abstract class _IQueryExecutionFeeService { } class QueryExecutionFeeService implements _IQueryExecutionFeeService { - final AppConfig _appConfig = globalLocator(); final IApiKiraRepository _apiKiraRepository = globalLocator(); final QueryNetworkPropertiesService _queryNetworkPropertiesService = globalLocator(); @@ -33,8 +31,7 @@ class QueryExecutionFeeService implements _IQueryExecutionFeeService { QueryExecutionFeeResponse queryExecutionFeeResponse = QueryExecutionFeeResponse.fromJson(response.data as Map); TokenAmountModel feeTokenAmountModel = TokenAmountModel( defaultDenominationAmount: Decimal.parse(queryExecutionFeeResponse.fee.executionFee), - // tokenAliasModel - interx doesn't return denomination used in QueryExecutionFee endpoint, so we assumed that it's always represented in "ukex" - tokenAliasModel: _appConfig.defaultFeeTokenAliasModel, + tokenAliasModel: globalLocator().tokenDefaultDenomModel!.defaultTokenAliasModel, ); return feeTokenAmountModel; } catch (_) { diff --git a/lib/infra/services/api_kira/query_kira_tokens_aliases_service.dart b/lib/infra/services/api_kira/query_kira_tokens_aliases_service.dart index 3e08a82c..ec07a05e 100644 --- a/lib/infra/services/api_kira/query_kira_tokens_aliases_service.dart +++ b/lib/infra/services/api_kira/query_kira_tokens_aliases_service.dart @@ -1,16 +1,20 @@ import 'package:dio/dio.dart'; import 'package:miro/blocs/generic/network_module/network_module_bloc.dart'; import 'package:miro/config/locator.dart'; +import 'package:miro/infra/dto/api_kira/query_kira_tokens_aliases/request/query_kira_tokens_aliases_req.dart'; import 'package:miro/infra/dto/api_kira/query_kira_tokens_aliases/response/query_kira_tokens_aliases_resp.dart'; import 'package:miro/infra/exceptions/dio_parse_exception.dart'; import 'package:miro/infra/models/api_request_model.dart'; import 'package:miro/infra/repositories/api/api_kira_repository.dart'; import 'package:miro/shared/models/tokens/token_alias_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/shared/utils/logger/app_logger.dart'; import 'package:miro/shared/utils/logger/log_level.dart'; abstract class _IQueryKiraTokensAliasesService { Future> getTokenAliasModels(); + + Future getTokenDefaultDenomModel(Uri networkUri); } class QueryKiraTokensAliasesService implements _IQueryKiraTokensAliasesService { @@ -19,17 +23,67 @@ class QueryKiraTokensAliasesService implements _IQueryKiraTokensAliasesService { @override Future> getTokenAliasModels() async { Uri networkUri = globalLocator().state.networkUri; - Response response = await _apiKiraRepository.fetchQueryKiraTokensAliases(ApiRequestModel( + Response response = await _apiKiraRepository.fetchQueryKiraTokensAliases(ApiRequestModel( networkUri: networkUri, - requestData: null, + requestData: const QueryKiraTokensAliasesReq(), )); try { - QueryKiraTokensAliasesResp queryKiraTokensAliasesResp = QueryKiraTokensAliasesResp.fromJsonList(response.data as List); + QueryKiraTokensAliasesResp queryKiraTokensAliasesResp = QueryKiraTokensAliasesResp.fromJson(response.data as Map); return queryKiraTokensAliasesResp.tokenAliases.map(TokenAliasModel.fromDto).toList(); } catch (e) { AppLogger().log(message: 'QueryKiraTokensAliasesService: Cannot parse getTokenAliasModels() for URI $networkUri ${e}', logLevel: LogLevel.error); throw DioParseException(response: response, error: e); } } + + @override + Future getTokenDefaultDenomModel(Uri networkUri, {bool forceRequestBool = false}) async { + TokenDefaultDenomModel initialTokenDefaultDenomModel = await _getTokenDefaultDenom(networkUri); + try { + TokenAliasModel defaultTokenAliasModel = await _getAliasByTokenName( + initialTokenDefaultDenomModel.defaultTokenAliasModel.name, + networkUri: networkUri, + forceRequestBool: forceRequestBool, + ); + return TokenDefaultDenomModel( + bech32AddressPrefix: initialTokenDefaultDenomModel.bech32AddressPrefix, + defaultTokenAliasModel: defaultTokenAliasModel, + ); + } catch (e) { + return initialTokenDefaultDenomModel; + } + } + + Future _getAliasByTokenName(String tokenName, {Uri? networkUri, bool forceRequestBool = false}) async { + networkUri ??= globalLocator().state.networkUri; + Response response = await _apiKiraRepository.fetchQueryKiraTokensAliases(ApiRequestModel( + networkUri: networkUri, + requestData: QueryKiraTokensAliasesReq(tokens: [tokenName]), + forceRequestBool: forceRequestBool, + )); + + try { + QueryKiraTokensAliasesResp queryKiraTokensAliasesResp = QueryKiraTokensAliasesResp.fromJson(response.data as Map); + return TokenAliasModel.fromDto(queryKiraTokensAliasesResp.tokenAliases.first); + } catch (e) { + AppLogger().log(message: 'QueryKiraTokensAliasesService: Cannot parse getAliasByTokenName() for URI $networkUri ${e}', logLevel: LogLevel.error); + throw DioParseException(response: response, error: e); + } + } + + Future _getTokenDefaultDenom(Uri networkUri) async { + Response response = await _apiKiraRepository.fetchQueryKiraTokensAliases(ApiRequestModel( + networkUri: networkUri, + requestData: const QueryKiraTokensAliasesReq(offset: 0, limit: 0), + )); + + try { + QueryKiraTokensAliasesResp queryKiraTokensAliasesResp = QueryKiraTokensAliasesResp.fromJson(response.data as Map); + return TokenDefaultDenomModel.fromDto(queryKiraTokensAliasesResp); + } catch (e) { + AppLogger().log(message: 'QueryKiraTokensAliasesService: Cannot parse getTokenDefaultDenom() for URI $networkUri ${e}', logLevel: LogLevel.error); + throw DioParseException(response: response, error: e); + } + } } diff --git a/lib/infra/services/network_module_service.dart b/lib/infra/services/network_module_service.dart index 0b6e9cb6..682bd560 100644 --- a/lib/infra/services/network_module_service.dart +++ b/lib/infra/services/network_module_service.dart @@ -3,12 +3,14 @@ import 'package:miro/infra/dto/api/query_interx_status/query_interx_status_resp. import 'package:miro/infra/dto/api/query_validators/response/status.dart'; import 'package:miro/infra/services/api/query_interx_status_service.dart'; import 'package:miro/infra/services/api/query_validators_service.dart'; +import 'package:miro/infra/services/api_kira/query_kira_tokens_aliases_service.dart'; import 'package:miro/shared/models/network/data/connection_status_type.dart'; import 'package:miro/shared/models/network/data/network_info_model.dart'; import 'package:miro/shared/models/network/status/a_network_status_model.dart'; import 'package:miro/shared/models/network/status/network_offline_model.dart'; import 'package:miro/shared/models/network/status/network_unknown_model.dart'; import 'package:miro/shared/models/network/status/online/a_network_online_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/shared/utils/logger/app_logger.dart'; abstract class _INetworkModuleService { @@ -17,14 +19,17 @@ abstract class _INetworkModuleService { class NetworkModuleService implements _INetworkModuleService { final QueryInterxStatusService _queryInterxStatusService = globalLocator(); + final QueryKiraTokensAliasesService _queryKiraTokensAliasesService = globalLocator(); final QueryValidatorsService _queryValidatorsService = globalLocator(); @override Future getNetworkStatusModel(NetworkUnknownModel networkUnknownModel, {NetworkUnknownModel? previousNetworkUnknownModel}) async { try { NetworkInfoModel networkInfoModel = await _getNetworkInfoModel(networkUnknownModel); + TokenDefaultDenomModel? tokenDefaultDenomModel = await _getTokenDefaultDenomModel(networkUnknownModel); return ANetworkOnlineModel.build( networkInfoModel: networkInfoModel, + tokenDefaultDenomModel: tokenDefaultDenomModel, connectionStatusType: ConnectionStatusType.disconnected, uri: networkUnknownModel.uri, name: networkUnknownModel.name, @@ -45,6 +50,16 @@ class NetworkModuleService implements _INetworkModuleService { } } + Future _getTokenDefaultDenomModel(NetworkUnknownModel networkUnknownModel) async { + TokenDefaultDenomModel? tokenDefaultDenomModel; + try { + tokenDefaultDenomModel = await _queryKiraTokensAliasesService.getTokenDefaultDenomModel(networkUnknownModel.uri, forceRequestBool: true); + } catch (e) { + AppLogger().log(message: 'NetworkModuleService: Cannot fetch getTokenDefaultDenomModel() for URI ${networkUnknownModel.uri} $e'); + } + return tokenDefaultDenomModel; + } + Future _getNetworkInfoModel(NetworkUnknownModel networkUnknownModel) async { Status? status; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 466ee996..1ab562e7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -172,6 +172,7 @@ "networkErrorAddressEmpty": "Field can't be empty", "networkErrorAddressInvalid": "Invalid network address", "networkHintCustomAddress": "Custom address", + "networkWarningMissingInfo": "Connecting a wallet unavailable due to missing essential data from network.", "networkWarningIncompatible": "The application is incompatible with this server. Some views may not work correctly.", "networkWarningWhenLastBlock": "The last available block on this interx was created long time ago {latestBlockTime}. The displayed contents may be out of date.", "@networkWarningWhenLastBlock": { diff --git a/lib/shared/controllers/menu/my_account_page/balances_page/balances_filter_options.dart b/lib/shared/controllers/menu/my_account_page/balances_page/balances_filter_options.dart index f2641198..d91f8610 100644 --- a/lib/shared/controllers/menu/my_account_page/balances_page/balances_filter_options.dart +++ b/lib/shared/controllers/menu/my_account_page/balances_page/balances_filter_options.dart @@ -1,7 +1,7 @@ import 'package:decimal/decimal.dart'; +import 'package:miro/blocs/generic/network_module/network_module_bloc.dart'; import 'package:miro/blocs/widgets/kira/kira_list/filters/models/filter_mode.dart'; import 'package:miro/blocs/widgets/kira/kira_list/filters/models/filter_option.dart'; -import 'package:miro/config/app_config.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/shared/models/balances/balance_model.dart'; @@ -16,7 +16,7 @@ class BalancesFilterOptions { static FilterOption filterByDefaultToken = FilterOption( id: 'defaultToken', - filterComparator: (BalanceModel a) => a.tokenAmountModel.tokenAliasModel == globalLocator().defaultFeeTokenAliasModel, + filterComparator: (BalanceModel a) => a.tokenAmountModel.tokenAliasModel == globalLocator().tokenDefaultDenomModel!.defaultTokenAliasModel, filterMode: FilterMode.and, ); diff --git a/lib/shared/models/network/data/interx_warning_model.dart b/lib/shared/models/network/data/interx_warning_model.dart index 720d6346..ca57b3ea 100644 --- a/lib/shared/models/network/data/interx_warning_model.dart +++ b/lib/shared/models/network/data/interx_warning_model.dart @@ -4,20 +4,23 @@ import 'package:miro/config/locator.dart'; import 'package:miro/shared/models/network/data/block_time_model.dart'; import 'package:miro/shared/models/network/data/interx_warning_type.dart'; import 'package:miro/shared/models/network/data/network_info_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; class InterxWarningModel extends Equatable { final List interxWarningTypes; const InterxWarningModel(this.interxWarningTypes); - factory InterxWarningModel.fromNetworkInfoModel(NetworkInfoModel networkInfoModel) { + factory InterxWarningModel.selectWarningType(NetworkInfoModel networkInfoModel, TokenDefaultDenomModel? tokenDefaultDenomModel) { AppConfig appConfig = globalLocator(); - bool versionOutdated = appConfig.isInterxVersionOutdated(networkInfoModel.interxVersion); - bool blockTimeOutdated = BlockTimeModel(networkInfoModel.latestBlockTime).isOutdated(); + bool missingTokenDefaultDenomModelBool = tokenDefaultDenomModel == null; + bool versionOutdatedBool = appConfig.isInterxVersionOutdated(networkInfoModel.interxVersion); + bool blockTimeOutdatedBool = BlockTimeModel(networkInfoModel.latestBlockTime).isOutdated(); List interxWarningTypes = [ - if (versionOutdated) InterxWarningType.versionOutdated, - if (blockTimeOutdated) InterxWarningType.blockTimeOutdated + if (missingTokenDefaultDenomModelBool) InterxWarningType.missingDefaultTokenDenomModel, + if (versionOutdatedBool) InterxWarningType.versionOutdated, + if (blockTimeOutdatedBool) InterxWarningType.blockTimeOutdated ]; return InterxWarningModel(interxWarningTypes); diff --git a/lib/shared/models/network/data/interx_warning_type.dart b/lib/shared/models/network/data/interx_warning_type.dart index 99c4dff7..4b0a6860 100644 --- a/lib/shared/models/network/data/interx_warning_type.dart +++ b/lib/shared/models/network/data/interx_warning_type.dart @@ -1,4 +1,5 @@ enum InterxWarningType { blockTimeOutdated, versionOutdated, + missingDefaultTokenDenomModel, } diff --git a/lib/shared/models/network/network_properties_model.dart b/lib/shared/models/network/network_properties_model.dart index 9c599b47..bb5d1346 100644 --- a/lib/shared/models/network/network_properties_model.dart +++ b/lib/shared/models/network/network_properties_model.dart @@ -1,8 +1,9 @@ import 'package:decimal/decimal.dart'; import 'package:equatable/equatable.dart'; -import 'package:miro/config/app_config.dart'; +import 'package:miro/blocs/generic/network_module/network_module_bloc.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/infra/dto/api_kira/query_network_properties/response/properties.dart'; +import 'package:miro/shared/models/tokens/token_alias_model.dart'; import 'package:miro/shared/models/tokens/token_amount_model.dart'; class NetworkPropertiesModel extends Equatable { @@ -15,16 +16,16 @@ class NetworkPropertiesModel extends Equatable { }); factory NetworkPropertiesModel.fromDto(Properties properties) { - final AppConfig appConfig = globalLocator(); + final TokenAliasModel defaultTokenAliasModel = globalLocator().tokenDefaultDenomModel!.defaultTokenAliasModel; return NetworkPropertiesModel( minTxFee: TokenAmountModel( defaultDenominationAmount: Decimal.parse(properties.minTxFee), - tokenAliasModel: appConfig.defaultFeeTokenAliasModel, + tokenAliasModel: defaultTokenAliasModel, ), minIdentityApprovalTip: TokenAmountModel( defaultDenominationAmount: Decimal.parse(properties.minIdentityApprovalTip), - tokenAliasModel: appConfig.defaultFeeTokenAliasModel, + tokenAliasModel: defaultTokenAliasModel, ), ); } diff --git a/lib/shared/models/network/status/online/a_network_online_model.dart b/lib/shared/models/network/status/online/a_network_online_model.dart index 2204c0d4..801f47cf 100644 --- a/lib/shared/models/network/status/online/a_network_online_model.dart +++ b/lib/shared/models/network/status/online/a_network_online_model.dart @@ -5,13 +5,16 @@ import 'package:miro/shared/models/network/data/network_info_model.dart'; import 'package:miro/shared/models/network/status/a_network_status_model.dart'; import 'package:miro/shared/models/network/status/online/network_healthy_model.dart'; import 'package:miro/shared/models/network/status/online/network_unhealthy_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; abstract class ANetworkOnlineModel extends ANetworkStatusModel { + final TokenDefaultDenomModel? tokenDefaultDenomModel; final NetworkInfoModel networkInfoModel; const ANetworkOnlineModel({ - required Color statusColor, + required this.tokenDefaultDenomModel, required this.networkInfoModel, + required Color statusColor, required ConnectionStatusType connectionStatusType, required Uri uri, String? name, @@ -23,25 +26,28 @@ abstract class ANetworkOnlineModel extends ANetworkStatusModel { ); static ANetworkOnlineModel build({ - required NetworkInfoModel networkInfoModel, required ConnectionStatusType connectionStatusType, + required TokenDefaultDenomModel? tokenDefaultDenomModel, + required NetworkInfoModel networkInfoModel, required Uri uri, required String name, }) { - InterxWarningModel interxWarningModel = InterxWarningModel.fromNetworkInfoModel(networkInfoModel); + InterxWarningModel interxWarningModel = InterxWarningModel.selectWarningType(networkInfoModel, tokenDefaultDenomModel); if (interxWarningModel.hasErrors) { return NetworkUnhealthyModel( interxWarningModel: interxWarningModel, - networkInfoModel: networkInfoModel, connectionStatusType: connectionStatusType, + tokenDefaultDenomModel: tokenDefaultDenomModel, + networkInfoModel: networkInfoModel, uri: uri, name: name, ); } else { return NetworkHealthyModel( - networkInfoModel: networkInfoModel, connectionStatusType: connectionStatusType, + tokenDefaultDenomModel: tokenDefaultDenomModel, + networkInfoModel: networkInfoModel, uri: uri, name: name, ); diff --git a/lib/shared/models/network/status/online/network_healthy_model.dart b/lib/shared/models/network/status/online/network_healthy_model.dart index 20395810..b751c0b5 100644 --- a/lib/shared/models/network/status/online/network_healthy_model.dart +++ b/lib/shared/models/network/status/online/network_healthy_model.dart @@ -2,17 +2,20 @@ import 'package:miro/config/theme/design_colors.dart'; import 'package:miro/shared/models/network/data/connection_status_type.dart'; import 'package:miro/shared/models/network/data/network_info_model.dart'; import 'package:miro/shared/models/network/status/online/a_network_online_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; class NetworkHealthyModel extends ANetworkOnlineModel { const NetworkHealthyModel({ - required NetworkInfoModel networkInfoModel, required ConnectionStatusType connectionStatusType, + required TokenDefaultDenomModel? tokenDefaultDenomModel, + required NetworkInfoModel networkInfoModel, required Uri uri, String? name, }) : super( statusColor: DesignColors.greenStatus1, - networkInfoModel: networkInfoModel, connectionStatusType: connectionStatusType, + tokenDefaultDenomModel: tokenDefaultDenomModel, + networkInfoModel: networkInfoModel, uri: uri, name: name, ); @@ -20,13 +23,14 @@ class NetworkHealthyModel extends ANetworkOnlineModel { @override NetworkHealthyModel copyWith({required ConnectionStatusType connectionStatusType}) { return NetworkHealthyModel( - networkInfoModel: networkInfoModel, connectionStatusType: connectionStatusType, + tokenDefaultDenomModel: tokenDefaultDenomModel, + networkInfoModel: networkInfoModel, uri: uri, name: name, ); } @override - List get props => [runtimeType, networkInfoModel, connectionStatusType, uri.hashCode, name]; + List get props => [runtimeType, connectionStatusType, tokenDefaultDenomModel, networkInfoModel, uri.hashCode, name]; } diff --git a/lib/shared/models/network/status/online/network_unhealthy_model.dart b/lib/shared/models/network/status/online/network_unhealthy_model.dart index d6d9d86d..fb57181f 100644 --- a/lib/shared/models/network/status/online/network_unhealthy_model.dart +++ b/lib/shared/models/network/status/online/network_unhealthy_model.dart @@ -3,20 +3,23 @@ import 'package:miro/shared/models/network/data/connection_status_type.dart'; import 'package:miro/shared/models/network/data/interx_warning_model.dart'; import 'package:miro/shared/models/network/data/network_info_model.dart'; import 'package:miro/shared/models/network/status/online/a_network_online_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; class NetworkUnhealthyModel extends ANetworkOnlineModel { final InterxWarningModel interxWarningModel; const NetworkUnhealthyModel({ required this.interxWarningModel, - required NetworkInfoModel networkInfoModel, required ConnectionStatusType connectionStatusType, + required TokenDefaultDenomModel? tokenDefaultDenomModel, + required NetworkInfoModel networkInfoModel, required Uri uri, String? name, }) : super( statusColor: DesignColors.yellowStatus1, - networkInfoModel: networkInfoModel, connectionStatusType: connectionStatusType, + tokenDefaultDenomModel: tokenDefaultDenomModel, + networkInfoModel: networkInfoModel, uri: uri, name: name, ); @@ -25,13 +28,14 @@ class NetworkUnhealthyModel extends ANetworkOnlineModel { NetworkUnhealthyModel copyWith({required ConnectionStatusType connectionStatusType}) { return NetworkUnhealthyModel( interxWarningModel: interxWarningModel, - networkInfoModel: networkInfoModel, connectionStatusType: connectionStatusType, + tokenDefaultDenomModel: tokenDefaultDenomModel, + networkInfoModel: networkInfoModel, uri: uri, name: name, ); } @override - List get props => [runtimeType, interxWarningModel, networkInfoModel, connectionStatusType, uri.hashCode, name]; + List get props => [runtimeType, interxWarningModel, connectionStatusType, tokenDefaultDenomModel, networkInfoModel, uri.hashCode, name]; } diff --git a/lib/shared/models/tokens/token_default_denom_model.dart b/lib/shared/models/tokens/token_default_denom_model.dart new file mode 100644 index 00000000..312c8051 --- /dev/null +++ b/lib/shared/models/tokens/token_default_denom_model.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; +import 'package:miro/infra/dto/api_kira/query_kira_tokens_aliases/response/query_kira_tokens_aliases_resp.dart'; +import 'package:miro/shared/models/tokens/token_alias_model.dart'; + +class TokenDefaultDenomModel extends Equatable { + final String bech32AddressPrefix; + final TokenAliasModel defaultTokenAliasModel; + + const TokenDefaultDenomModel({ + required this.bech32AddressPrefix, + required this.defaultTokenAliasModel, + }); + + factory TokenDefaultDenomModel.fromDto(QueryKiraTokensAliasesResp queryKiraTokensAliasesResp) { + return TokenDefaultDenomModel( + bech32AddressPrefix: queryKiraTokensAliasesResp.bech32Prefix, + defaultTokenAliasModel: TokenAliasModel.local(queryKiraTokensAliasesResp.defaultDenom), + ); + } + + @override + List get props => [bech32AddressPrefix, defaultTokenAliasModel]; +} diff --git a/lib/test/mock_api_kira_repository.dart b/lib/test/mock_api_kira_repository.dart index 893b3340..82643d39 100644 --- a/lib/test/mock_api_kira_repository.dart +++ b/lib/test/mock_api_kira_repository.dart @@ -272,7 +272,9 @@ class MockApiKiraRepository implements IApiKiraRepository { @override Future> fetchQueryKiraTokensAliases(ApiRequestModel apiRequestModel) async { Uri networkUri = apiRequestModel.networkUri; - bool responseExistsBool = workingEndpoints.contains(networkUri.host); + // 'dynamic.kira.network' handling needs to be included to properly test NetworkModuleBloc + // NetworkModuleBloc uses 'dynamic.kira.network' in tests and implicitly depends on this method + bool responseExistsBool = workingEndpoints.contains(networkUri.host) || networkUri.host == 'dynamic.kira.network'; if (responseExistsBool) { late T response; switch (networkUri.host) { diff --git a/lib/test/mock_app_config.dart b/lib/test/mock_app_config.dart index 569090ed..3292e8d9 100644 --- a/lib/test/mock_app_config.dart +++ b/lib/test/mock_app_config.dart @@ -2,8 +2,6 @@ import 'package:miro/config/app_config.dart'; import 'package:miro/shared/controllers/browser/rpc_browser_url_controller.dart'; import 'package:miro/shared/models/network/data/connection_status_type.dart'; import 'package:miro/shared/models/network/status/network_unknown_model.dart'; -import 'package:miro/shared/models/tokens/token_alias_model.dart'; -import 'package:miro/shared/models/tokens/token_denomination_model.dart'; class MockAppConfig extends AppConfig { MockAppConfig({ @@ -13,7 +11,6 @@ class MockAppConfig extends AppConfig { required Duration loadingPageTimerDuration, required List supportedInterxVersions, required RpcBrowserUrlController rpcBrowserUrlController, - required TokenAliasModel defaultFeeTokenAliasModel, required int defaultRefreshIntervalSeconds, required NetworkUnknownModel defaultNetworkUnknownModel, }) : super( @@ -23,7 +20,6 @@ class MockAppConfig extends AppConfig { loadingPageTimerDuration: loadingPageTimerDuration, supportedInterxVersions: supportedInterxVersions, rpcBrowserUrlController: rpcBrowserUrlController, - defaultFeeTokenAliasModel: defaultFeeTokenAliasModel, defaultRefreshIntervalSeconds: defaultRefreshIntervalSeconds, defaultNetworkUnknownModel: defaultNetworkUnknownModel, ); @@ -36,11 +32,6 @@ class MockAppConfig extends AppConfig { loadingPageTimerDuration: const Duration(seconds: 4), supportedInterxVersions: ['v0.4.22'], rpcBrowserUrlController: RpcBrowserUrlController(), - defaultFeeTokenAliasModel: const TokenAliasModel( - name: 'Kira', - defaultTokenDenominationModel: TokenDenominationModel(name: 'ukex', decimals: 0), - networkTokenDenominationModel: TokenDenominationModel(name: 'KEX', decimals: 6), - ), defaultRefreshIntervalSeconds: 60, defaultNetworkUnknownModel: NetworkUnknownModel( connectionStatusType: ConnectionStatusType.disconnected, diff --git a/lib/test/mocks/api_kira/mock_api_kira_tokens_aliases.dart b/lib/test/mocks/api_kira/mock_api_kira_tokens_aliases.dart index 134fc140..1cd4f1ee 100644 --- a/lib/test/mocks/api_kira/mock_api_kira_tokens_aliases.dart +++ b/lib/test/mocks/api_kira/mock_api_kira_tokens_aliases.dart @@ -1,12 +1,16 @@ class MockApiKiraTokensAliases { - static List> defaultResponse = >[ - { - "decimals": 6, - "denoms": ["ukex", "mkex"], - "name": "Kira", - "symbol": "KEX", - "icon": "", - "amount": "300000000000000" - } - ]; + static Map defaultResponse = { + "token_aliases_data": [ + { + "decimals": 6, + "denoms": ["ukex", "mkex"], + "name": "Kira", + "symbol": "KEX", + "icon": "", + "amount": "300000000000000" + } + ], + "default_denom": "ukex", + "bech32_prefix": "kira" + }; } diff --git a/lib/test/utils/test_utils.dart b/lib/test/utils/test_utils.dart index fd53599d..2f6554e0 100644 --- a/lib/test/utils/test_utils.dart +++ b/lib/test/utils/test_utils.dart @@ -12,6 +12,7 @@ import 'package:miro/shared/models/network/status/network_unknown_model.dart'; import 'package:miro/shared/models/network/status/online/network_healthy_model.dart'; import 'package:miro/shared/models/network/status/online/network_unhealthy_model.dart'; import 'package:miro/shared/models/tokens/token_alias_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/shared/models/tokens/token_denomination_model.dart'; import 'package:miro/shared/models/wallet/mnemonic.dart'; import 'package:miro/shared/models/wallet/wallet.dart'; @@ -76,6 +77,10 @@ class TestUtils { activeValidators: 319, totalValidators: 475, ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TokenAliasModel.local('ukex'), + ), ); static final NetworkUnhealthyModel networkUnhealthyModel = NetworkUnhealthyModel( @@ -92,6 +97,10 @@ class TestUtils { latestBlockHeight: 108843, latestBlockTime: DateTime.parse('2021-11-04 12:42:54.395Z'), ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TokenAliasModel.local('ukex'), + ), ); static final NetworkOfflineModel networkOfflineModel = NetworkOfflineModel( @@ -111,6 +120,10 @@ class TestUtils { activeValidators: 319, totalValidators: 475, ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TokenAliasModel.local('ukex'), + ), ); static final NetworkUnhealthyModel customNetworkUnhealthyModel = NetworkUnhealthyModel( @@ -124,6 +137,10 @@ class TestUtils { activeValidators: 319, totalValidators: 475, ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TokenAliasModel.local('ukex'), + ), interxWarningModel: const InterxWarningModel([ InterxWarningType.versionOutdated, InterxWarningType.blockTimeOutdated, @@ -156,6 +173,10 @@ class TestUtils { latestBlockHeight: 0, latestBlockTime: DateTime.now(), ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TokenAliasModel.local('ukex'), + ), ); globalLocator().add(NetworkModuleConnectEvent(mockNetworkHealthyModel)); diff --git a/lib/views/pages/transactions/tx_send/staking_tx_delegate_page/staking_tx_delegate_page.dart b/lib/views/pages/transactions/tx_send/staking_tx_delegate_page/staking_tx_delegate_page.dart index 033a95bb..ee8e0851 100644 --- a/lib/views/pages/transactions/tx_send/staking_tx_delegate_page/staking_tx_delegate_page.dart +++ b/lib/views/pages/transactions/tx_send/staking_tx_delegate_page/staking_tx_delegate_page.dart @@ -4,7 +4,6 @@ import 'package:miro/blocs/generic/auth/auth_cubit.dart'; import 'package:miro/blocs/pages/transactions/tx_process_cubit/states/tx_process_confirm_state.dart'; import 'package:miro/blocs/pages/transactions/tx_process_cubit/states/tx_process_loaded_state.dart'; import 'package:miro/blocs/pages/transactions/tx_process_cubit/tx_process_cubit.dart'; -import 'package:miro/config/app_config.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/shared/models/balances/balance_model.dart'; import 'package:miro/shared/models/tokens/token_alias_model.dart'; @@ -38,7 +37,6 @@ class _StakingTxDelegatePage extends State { msgFormModel: StakingMsgDelegateFormModel( delegatorWalletAddress: authCubit.state?.address, valoperWalletAddress: WalletAddress(addressBytes: widget.validatorSimplifiedModel.walletAddress.addressBytes, bech32Hrp: 'kiravaloper'), - tokenAliasModel: globalLocator().defaultFeeTokenAliasModel, ), ); diff --git a/lib/views/pages/transactions/tx_send/staking_tx_undelegate_page/staking_tx_undelegate_page.dart b/lib/views/pages/transactions/tx_send/staking_tx_undelegate_page/staking_tx_undelegate_page.dart index 0bf939f2..41e03173 100644 --- a/lib/views/pages/transactions/tx_send/staking_tx_undelegate_page/staking_tx_undelegate_page.dart +++ b/lib/views/pages/transactions/tx_send/staking_tx_undelegate_page/staking_tx_undelegate_page.dart @@ -4,7 +4,6 @@ import 'package:miro/blocs/generic/auth/auth_cubit.dart'; import 'package:miro/blocs/pages/transactions/tx_process_cubit/states/tx_process_confirm_state.dart'; import 'package:miro/blocs/pages/transactions/tx_process_cubit/states/tx_process_loaded_state.dart'; import 'package:miro/blocs/pages/transactions/tx_process_cubit/tx_process_cubit.dart'; -import 'package:miro/config/app_config.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/shared/models/transactions/form_models/staking_msg_undelegate_form_model.dart'; import 'package:miro/shared/models/transactions/messages/tx_msg_type.dart'; @@ -34,7 +33,6 @@ class _StakingTxUndelegatePage extends State { msgFormModel: StakingMsgUndelegateFormModel( delegatorWalletAddress: authCubit.state?.address, valoperWalletAddress: WalletAddress(addressBytes: widget.validatorSimplifiedModel.walletAddress.addressBytes, bech32Hrp: 'kiravaloper'), - tokenAliasModel: globalLocator().defaultFeeTokenAliasModel, ), ); diff --git a/lib/views/widgets/generic/token_avatar.dart b/lib/views/widgets/generic/token_avatar.dart index 85df0e18..5583d029 100644 --- a/lib/views/widgets/generic/token_avatar.dart +++ b/lib/views/widgets/generic/token_avatar.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:miro/config/app_config.dart'; @@ -27,6 +28,30 @@ class TokenAvatar extends StatelessWidget { proxyServerUri: proxyServerUri, appUri: const BrowserUrlController().uri, ); + String networkUri = proxyActiveBool ? '${proxyServerUri}/${iconUrl.toString()}' : iconUrl ?? ''; + bool svgBool = networkUri.endsWith('.svg'); + + Widget placeholderWidget = Padding( + padding: EdgeInsets.all(size - size * 0.75), + child: Image.asset(Assets.assetsLogoSignet), + ); + + Widget imageWidget; + if (iconUrl == null || iconUrl!.isEmpty) { + imageWidget = placeholderWidget; + } + if (svgBool) { + imageWidget = SvgPicture.network( + networkUri, + placeholderBuilder: (_) => placeholderWidget, + ); + } else { + imageWidget = CachedNetworkImage( + imageUrl: networkUri, + errorWidget: (_, __, ___) => placeholderWidget, + ); + } + return Container( width: size, height: size, @@ -38,19 +63,8 @@ class TokenAvatar extends StatelessWidget { child: CircleAvatar( backgroundColor: DesignColors.background, radius: size / 2, - child: iconUrl == null || iconUrl!.isEmpty - ? Padding( - padding: EdgeInsets.all(size - size * 0.75), - child: Image.asset(Assets.assetsLogoSignet), - ) - : SvgPicture.network( - proxyActiveBool ? '${proxyServerUri}/${iconUrl.toString()}' : iconUrl!, - semanticsLabel: 'Token avatar', - placeholderBuilder: (BuildContext context) => const CircularProgressIndicator( - color: DesignColors.accent, - ), - ), + child: imageWidget, ), ); } -} +} \ No newline at end of file diff --git a/lib/views/widgets/network_list/network_warning_container.dart b/lib/views/widgets/network_list/network_warning_container.dart index d1d23fa2..25271122 100644 --- a/lib/views/widgets/network_list/network_warning_container.dart +++ b/lib/views/widgets/network_list/network_warning_container.dart @@ -18,6 +18,7 @@ class NetworkWarningContainer extends StatelessWidget { Widget build(BuildContext context) { TextTheme textTheme = Theme.of(context).textTheme; Map interxWarningMessages = { + InterxWarningType.missingDefaultTokenDenomModel: S.of(context).networkWarningMissingInfo, InterxWarningType.blockTimeOutdated: S.of(context).networkWarningWhenLastBlock(latestBlockTime), InterxWarningType.versionOutdated: S.of(context).networkWarningIncompatible, }; diff --git a/pubspec.yaml b/pubspec.yaml index cd97dd54..ee633519 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.21.4 +version: 1.22.0 environment: sdk: ">=3.1.3" diff --git a/test/integration/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart b/test/integration/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart index d93d929f..d4dac8b3 100644 --- a/test/integration/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart +++ b/test/integration/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart @@ -4,6 +4,7 @@ import 'package:miro/infra/exceptions/dio_connect_exception.dart'; import 'package:miro/infra/exceptions/dio_parse_exception.dart'; import 'package:miro/infra/services/api_kira/query_kira_tokens_aliases_service.dart'; import 'package:miro/shared/models/tokens/token_alias_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/shared/utils/network_utils.dart'; import 'package:miro/test/utils/test_utils.dart'; @@ -37,4 +38,23 @@ Future main() async { } }); }); + + group('Tests of QueryKiraTokensAliasesService.getTokenDefaultDenomModel() method', () { + test('Should return [TokenDefaultDenomModel]', () async { + TestUtils.printInfo('Data request'); + try { + TokenDefaultDenomModel actualTokenDefaultDenomModel = await actualQueryKiraTokensAliasesService.getTokenDefaultDenomModel(networkUri); + + TestUtils.printInfo('Data return'); + print(actualTokenDefaultDenomModel); + print(''); + } on DioConnectException catch (e) { + TestUtils.printError('query_kira_tokens_aliases_service_test.dart: Cannot fetch [TokenDefaultDenomModel] for URI $networkUri: ${e.dioException.message}'); + } on DioParseException catch (e) { + TestUtils.printError('query_kira_tokens_aliases_service_test.dart: Cannot parse [TokenDefaultDenomModel] for URI $networkUri: ${e}'); + } catch (e) { + TestUtils.printError('query_kira_tokens_aliases_service_test.dart: Unknown error for URI $networkUri: ${e}'); + } + }); + }); } diff --git a/test/unit/blocs/generic/network_module_bloc_test.dart b/test/unit/blocs/generic/network_module_bloc_test.dart index 31694d91..91cc1333 100644 --- a/test/unit/blocs/generic/network_module_bloc_test.dart +++ b/test/unit/blocs/generic/network_module_bloc_test.dart @@ -14,6 +14,7 @@ import 'package:miro/shared/models/network/status/network_offline_model.dart'; import 'package:miro/shared/models/network/status/network_unknown_model.dart'; import 'package:miro/shared/models/network/status/online/network_healthy_model.dart'; import 'package:miro/shared/models/network/status/online/network_unhealthy_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/test/mock_locator.dart'; import 'package:miro/test/utils/test_utils.dart'; @@ -198,7 +199,7 @@ Future main() async { connectionStatusType: ConnectionStatusType.disconnected, uri: Uri.parse('http://dynamic.kira.network'), ); - + NetworkHealthyModel dynamicNetworkHealthyModel = NetworkHealthyModel( connectionStatusType: ConnectionStatusType.disconnected, uri: Uri.parse('http://dynamic.kira.network'), @@ -208,8 +209,12 @@ Future main() async { latestBlockHeight: 108843, latestBlockTime: DateTime.now(), ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TestUtils.kexTokenAliasModel, + ), ); - + NetworkUnhealthyModel dynamicNetworkUnhealthyModel = NetworkUnhealthyModel( connectionStatusType: ConnectionStatusType.disconnected, uri: Uri.parse('http://dynamic.kira.network'), @@ -219,12 +224,16 @@ Future main() async { latestBlockHeight: 108843, latestBlockTime: DateTime.parse('2021-11-04T12:42:54.394946399Z'), ), + tokenDefaultDenomModel: TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TestUtils.kexTokenAliasModel, + ), interxWarningModel: const InterxWarningModel([ InterxWarningType.versionOutdated, InterxWarningType.blockTimeOutdated, ]), ); - + NetworkOfflineModel dynamicNetworkOfflineModel = NetworkOfflineModel( connectionStatusType: ConnectionStatusType.disconnected, uri: Uri.parse('http://dynamic.kira.network'), diff --git a/test/unit/infra/services/api_kira/query_execution_fee_service_test.dart b/test/unit/infra/services/api_kira/query_execution_fee_service_test.dart index 6ca20097..34821c44 100644 --- a/test/unit/infra/services/api_kira/query_execution_fee_service_test.dart +++ b/test/unit/infra/services/api_kira/query_execution_fee_service_test.dart @@ -1,6 +1,5 @@ import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:miro/config/app_config.dart'; import 'package:miro/config/locator.dart'; import 'package:miro/infra/exceptions/dio_connect_exception.dart'; import 'package:miro/infra/exceptions/dio_parse_exception.dart'; @@ -16,8 +15,6 @@ Future main() async { await initMockLocator(); final QueryExecutionFeeService queryExecutionFeeService = globalLocator(); - final AppConfig appConfig = globalLocator(); - const String messageType = 'send'; group('Tests of QueryExecutionFeeService.getExecutionFeeForMessage() method', () { @@ -32,7 +29,7 @@ Future main() async { // Assert TokenAmountModel expectedTokenAmountModel = TokenAmountModel( defaultDenominationAmount: Decimal.parse('100'), - tokenAliasModel: appConfig.defaultFeeTokenAliasModel, + tokenAliasModel: TestUtils.kexTokenAliasModel, ); expect(actualTokenAmountModel, expectedTokenAmountModel); diff --git a/test/unit/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart b/test/unit/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart index afb43728..6be2b613 100644 --- a/test/unit/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart +++ b/test/unit/infra/services/api_kira/query_kira_tokens_aliases_service_test.dart @@ -4,6 +4,7 @@ import 'package:miro/infra/exceptions/dio_connect_exception.dart'; import 'package:miro/infra/exceptions/dio_parse_exception.dart'; import 'package:miro/infra/services/api_kira/query_kira_tokens_aliases_service.dart'; import 'package:miro/shared/models/tokens/token_alias_model.dart'; +import 'package:miro/shared/models/tokens/token_default_denom_model.dart'; import 'package:miro/shared/utils/network_utils.dart'; import 'package:miro/test/mock_locator.dart'; import 'package:miro/test/utils/test_utils.dart'; @@ -24,10 +25,10 @@ Future main() async { // Act List actualTokenAliasModelList = await queryKiraTokensAliasesService.getTokenAliasModels(); - + // Assert List expectedTokenAliasModelList = [ - TokenAliasModel.local('ukex'), + TestUtils.kexTokenAliasModel, ]; expect(actualTokenAliasModelList, expectedTokenAliasModelList); @@ -57,4 +58,47 @@ Future main() async { ); }); }); + + group('Tests of QueryKiraTokensAliasesService.getTokenDefaultDenomModel() method', () { + test('Should return [TokenDefaultDenomModel] if [server HEALTHY] and [response data VALID]', () async { + // Arrange + Uri networkUri = NetworkUtils.parseUrlToInterxUri('https://healthy.kira.network/'); + await TestUtils.setupNetworkModel(networkUri: networkUri); + + // Act + TokenDefaultDenomModel actualTokenDefaultDenomModel = await queryKiraTokensAliasesService.getTokenDefaultDenomModel(networkUri); + + // Assert + TokenDefaultDenomModel expectedTokenDefaultDenom = TokenDefaultDenomModel( + bech32AddressPrefix: 'kira', + defaultTokenAliasModel: TestUtils.kexTokenAliasModel, + ); + + expect(actualTokenDefaultDenomModel, expectedTokenDefaultDenom); + }); + + test('Should throw [DioParseException] if [server HEALTHY] and [response data INVALID]', () async { + // Arrange + Uri networkUri = NetworkUtils.parseUrlToInterxUri('https://invalid.kira.network/'); + await TestUtils.setupNetworkModel(networkUri: networkUri); + + // Assert + expect( + queryKiraTokensAliasesService.getTokenDefaultDenomModel(networkUri), + throwsA(isA()), + ); + }); + + test('Should throw [DioConnectException] if [server OFFLINE]', () async { + // Arrange + Uri networkUri = NetworkUtils.parseUrlToInterxUri('https://offline.kira.network/'); + await TestUtils.setupNetworkModel(networkUri: networkUri); + + // Assert + expect( + queryKiraTokensAliasesService.getTokenDefaultDenomModel(networkUri), + throwsA(isA()), + ); + }); + }); } diff --git a/test/unit/shared/controllers/menu/my_acount_page/balances_page/balances_filter_options_test.dart b/test/unit/shared/controllers/menu/my_acount_page/balances_page/balances_filter_options_test.dart index 3d45b8d1..21e89faa 100644 --- a/test/unit/shared/controllers/menu/my_acount_page/balances_page/balances_filter_options_test.dart +++ b/test/unit/shared/controllers/menu/my_acount_page/balances_page/balances_filter_options_test.dart @@ -9,8 +9,10 @@ import 'package:miro/test/utils/test_utils.dart'; // To run this test type in console: // fvm flutter test test/unit/shared/controllers/menu/my_acount_page/balances_page/balances_filter_options_test.dart --platform chrome --null-assertions -void main() { - initMockLocator(); +Future main() async { + await initMockLocator(); + await TestUtils.setupNetworkModel(networkUri: Uri.parse('https://healthy.kira.network/')); + // @formatter:off List balanceModelsList = [ BalanceModel(tokenAmountModel: TokenAmountModel(tokenAliasModel: TestUtils.kexTokenAliasModel, defaultDenominationAmount: Decimal.fromInt(2000000))),