diff --git a/lib/components/transaction/target_tick_dropdown.dart b/lib/components/transaction/target_tick_dropdown.dart index f055a4b4..3c9b7a29 100644 --- a/lib/components/transaction/target_tick_dropdown.dart +++ b/lib/components/transaction/target_tick_dropdown.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:qubic_wallet/di.dart'; import 'package:qubic_wallet/helpers/target_tick.dart'; import 'package:qubic_wallet/l10n/l10n.dart'; +import 'package:qubic_wallet/stores/wallet_content_store.dart'; import 'package:qubic_wallet/styles/text_styles.dart'; import 'package:qubic_wallet/styles/themed_controls.dart'; @@ -19,16 +21,24 @@ class TargetTickDropdown extends StatelessWidget { List> _getTickList(BuildContext context) { final l10n = l10nOf(context); + final walletContentStore = getIt(); return TargetTickTypeEnum.values.map((targetTickType) { + final String label; + switch (targetTickType) { + case TargetTickTypeEnum.manual: + label = l10n.sendItemLabelTargetTickManual; + case TargetTickTypeEnum.automatic: + // Show the offset actually being applied, e.g. "Automatic (+10)". + label = l10n.sendItemLabelTargetTickAutomaticDefault( + walletContentStore.defaultTickOffset); + default: + label = l10n.sendItemLabelTargetTickAutomatic(targetTickType.value); + } + return DropdownMenuItem( value: targetTickType, - child: Text( - targetTickType == TargetTickTypeEnum.manual - ? l10n.sendItemLabelTargetTickManual - : l10n.sendItemLabelTargetTickAutomatic(targetTickType.value), - style: TextStyles.inputBoxSmallStyle, - ), + child: Text(label, style: TextStyles.inputBoxSmallStyle), ); }).toList(); } diff --git a/lib/config.dart b/lib/config.dart index 84fe1d07..44a2f3e9 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -30,6 +30,7 @@ abstract class Config { // Wallet-app specific endpoints static const dapps = "/wallet-app/dapps/dapps.json"; static dappLocale(String locale) => "/wallet-app/dapps/locales/$locale.json"; + static const walletAppConfig = "/wallet-app/config.json"; // General/ecosystem endpoints (shared across Qubic ecosystem) static const smartContracts = "/general/data/smart_contracts.json"; diff --git a/lib/helpers/target_tick.dart b/lib/helpers/target_tick.dart index b306a47d..160a0198 100644 --- a/lib/helpers/target_tick.dart +++ b/lib/helpers/target_tick.dart @@ -1,4 +1,7 @@ enum TargetTickTypeEnum { + /// Default selection: uses the remotely configured offset. Its own [value] is + /// a placeholder — always resolve it through `WalletContentStore.offsetFor`. + automatic(0), autoCurrentPlus5(5), autoCurrentPlus10(10), autoCurrentPlus20(20), @@ -11,5 +14,3 @@ enum TargetTickTypeEnum { // A constructor for the enum const TargetTickTypeEnum(this.value); } - -TargetTickTypeEnum defaultTargetTickType = TargetTickTypeEnum.autoCurrentPlus5; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8c0d299b..3a4977ad 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -337,7 +337,7 @@ "transactionItemLabelConfirmedDate": "Zeitstempel", "transactionItemLabelDetails": "Transaktionsdetails", "sendItemLabelTargetTick": "Ziel-Tick", - "sendItemLabelTargetTickAutomatic": "Automatisch: Aktuell + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Aktuell + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -345,6 +345,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Automatisch (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Manuelles Überschreiben", "sendItemLabelDetermineTargetTick": "Ziel-Tick bestimmen", "sendItemDialogErrorGeneralTitle": "Fehler beim Generieren der Transaktion.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 51da1dae..fce10766 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -382,7 +382,7 @@ "transactionItemLabelConfirmedDate": "Timestamp", "transactionItemLabelDetails": "Transaction Details", "sendItemLabelTargetTick": "Target Tick", - "sendItemLabelTargetTickAutomatic": "Automatically: Current + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Current + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -390,6 +390,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Automatic (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Manual Override", "sendItemLabelDetermineTargetTick": "Determine Target Tick", "sendItemDialogErrorGeneralTitle": "Error while generating transaction", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3f4ab895..8fce1ca8 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -336,7 +336,7 @@ "transactionItemLabelConfirmedDate": "Fecha y hora", "transactionItemLabelDetails": "Detalles de la Transacción", "sendItemLabelTargetTick": "Tick de Ejecución", - "sendItemLabelTargetTickAutomatic": "Automáticamente: Actual + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Actual + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -344,6 +344,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Automático (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Sobreescribir manualmente", "sendItemLabelDetermineTargetTick": "Determinar el Target de Ejecución", "sendItemDialogErrorGeneralTitle": "Error al generar la transacción", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7458640d..3b4099c6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -336,7 +336,7 @@ "transactionItemLabelConfirmedDate": "Horodatage", "transactionItemLabelDetails": "Détails de la Transaction", "sendItemLabelTargetTick": "Tick Cible", - "sendItemLabelTargetTickAutomatic": "Automatiquement: Actuel + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Actuel + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -344,6 +344,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Automatique (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Remplacement Manuel", "sendItemLabelDetermineTargetTick": "Déterminer le Tick Cible", "sendItemDialogErrorGeneralTitle": "Erreur lors de la génération de la transaction", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 91877b3e..7465dd73 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -336,7 +336,7 @@ "transactionItemLabelConfirmedDate": "Метка времени", "transactionItemLabelDetails": "Детали транзакции", "sendItemLabelTargetTick": "На каком тике", - "sendItemLabelTargetTickAutomatic": "Автоматически: этот тик + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Текущий + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -344,6 +344,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Автоматически (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Ручное управление", "sendItemLabelDetermineTargetTick": "Определите целевой тик", "sendItemDialogErrorGeneralTitle": "Ошибка при генерации транзакции", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 1aeb9ee4..64879971 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -337,7 +337,7 @@ "transactionItemLabelConfirmedDate": "Zaman damgası", "transactionItemLabelDetails": "İşlem Detayları", "sendItemLabelTargetTick": "Hedef Tick", - "sendItemLabelTargetTickAutomatic": "Otomatik olarak: Mevcut + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Mevcut + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -345,6 +345,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Otomatik (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Manuel Geçersiz Kılma", "sendItemLabelDetermineTargetTick": "Hedef Tick'i Belirle", "sendItemDialogErrorGeneralTitle": "Transfer oluşturulurken bir hata oluştu", diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 383eebb0..524b9d59 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -376,7 +376,7 @@ "transactionItemLabelConfirmedDate": "Dấu thời gian", "transactionItemLabelDetails": "Chi tiết giao dịch", "sendItemLabelTargetTick": "Tick mục tiêu", - "sendItemLabelTargetTickAutomatic": "Tự động: Hiện tại + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "Hiện tại + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -384,6 +384,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "Tự động (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "Ghi đè thủ công", "sendItemLabelDetermineTargetTick": "Xác định Tick mục tiêu", "sendItemDialogErrorGeneralTitle": "Lỗi khi tạo giao dịch", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b0e46f59..d249b32b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -337,7 +337,7 @@ "transactionItemLabelConfirmedDate": "时间戳", "transactionItemLabelDetails": "交易详情", "sendItemLabelTargetTick": "目标刻度", - "sendItemLabelTargetTickAutomatic": "自动:当前刻度 + {addingTicks}", + "sendItemLabelTargetTickAutomatic": "当前刻度 + {addingTicks}", "@sendItemLabelTargetTickAutomatic": { "placeholders": { "addingTicks": { @@ -345,6 +345,14 @@ } } }, + "sendItemLabelTargetTickAutomaticDefault": "自动 (+{addingTicks})", + "@sendItemLabelTargetTickAutomaticDefault": { + "placeholders": { + "addingTicks": { + "type": "int" + } + } + }, "sendItemLabelTargetTickManual": "手动调整", "sendItemLabelDetermineTargetTick": "确定目标刻度", "sendItemDialogErrorGeneralTitle": "生成交易时出现错误", diff --git a/lib/main.dart b/lib/main.dart index 6fd9c4e8..a8f0604b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -95,6 +95,7 @@ class _WalletAppState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addPostFrameCallback((_) { getIt().showSecurityWarningIfNeeded(); getIt().loadDapps(); + getIt().loadConfig(); }); } diff --git a/lib/models/wallet_app_config_model.dart b/lib/models/wallet_app_config_model.dart new file mode 100644 index 00000000..0df72b2a --- /dev/null +++ b/lib/models/wallet_app_config_model.dart @@ -0,0 +1,13 @@ +class WalletAppConfigResponse { + /// Offset added to the current tick when building a transaction's target + /// tick. Null when the key is absent. + final int? defaultTickOffset; + + WalletAppConfigResponse({this.defaultTickOffset}); + + factory WalletAppConfigResponse.fromJson(Map json) { + return WalletAppConfigResponse( + defaultTickOffset: json['default_tick_offset'], + ); + } +} diff --git a/lib/models/wallet_connect/wallet_connect_modals_controller.dart b/lib/models/wallet_connect/wallet_connect_modals_controller.dart index bd35fb18..b4b63978 100644 --- a/lib/models/wallet_connect/wallet_connect_modals_controller.dart +++ b/lib/models/wallet_connect/wallet_connect_modals_controller.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:qubic_wallet/components/wallet_connect/approve_wc_method_screen.dart'; import 'package:qubic_wallet/di.dart'; import 'package:qubic_wallet/helpers/global_snack_bar.dart'; -import 'package:qubic_wallet/helpers/target_tick.dart'; import 'package:qubic_wallet/helpers/transaction_actions_helpers.dart'; import 'package:qubic_wallet/l10n/l10n.dart'; import 'package:qubic_wallet/models/wallet_connect.dart'; @@ -15,6 +14,7 @@ import 'package:qubic_wallet/models/wallet_connect/request_sign_message_event.da import 'package:qubic_wallet/models/wallet_connect/request_sign_message_result.dart'; import 'package:qubic_wallet/models/wallet_connect/request_sign_transaction_result.dart'; import 'package:qubic_wallet/resources/apis/live/qubic_live_api.dart'; +import 'package:qubic_wallet/stores/wallet_content_store.dart'; import 'package:reown_walletkit/reown_walletkit.dart'; // Provides a unified place to handle WalletConnect modals @@ -183,7 +183,7 @@ class WalletConnectModalsController { } return tick; } else { - return latestTick + defaultTargetTickType.value; + return latestTick + getIt.get().defaultTickOffset; } } catch (e) { throw JsonRpcError( diff --git a/lib/pages/main/wallet_contents/release_transfer_rights.dart b/lib/pages/main/wallet_contents/release_transfer_rights.dart index 7cff5f35..2672ab7d 100644 --- a/lib/pages/main/wallet_contents/release_transfer_rights.dart +++ b/lib/pages/main/wallet_contents/release_transfer_rights.dart @@ -23,6 +23,7 @@ import 'package:qubic_wallet/smart_contracts/release_transfer_rights_info.dart'; import 'package:mobx/mobx.dart'; import 'package:qubic_wallet/stores/application_store.dart'; import 'package:qubic_wallet/stores/qubic_ecosystem_store.dart'; +import 'package:qubic_wallet/stores/wallet_content_store.dart'; import 'package:qubic_wallet/styles/edge_insets.dart'; import 'package:qubic_wallet/styles/input_decorations.dart'; import 'package:qubic_wallet/styles/text_styles.dart'; @@ -48,6 +49,7 @@ class _ReleaseTransferRightsState extends State { final _formKey = GlobalKey(); final ApplicationStore appStore = getIt(); final QubicEcosystemStore ecosystemStore = getIt(); + final WalletContentStore _walletContentStore = getIt(); final _liveApi = getIt(); final TimedController _timedController = getIt(); final GlobalSnackBar _globalSnackBar = getIt(); @@ -58,7 +60,7 @@ class _ReleaseTransferRightsState extends State { ); List expanded = [false]; - TargetTickTypeEnum targetTickType = defaultTargetTickType; + TargetTickTypeEnum targetTickType = TargetTickTypeEnum.automatic; // Selected contract indices int? selectedSourceContractIndex; @@ -684,7 +686,7 @@ class _ReleaseTransferRightsState extends State { } else { // fetch latest tick int latestTick = (await _liveApi.getCurrentTick()).tick; - targetTick = latestTick + targetTickType.value; + targetTick = latestTick + _walletContentStore.offsetFor(targetTickType); } // Get contract address and procedure number from ecosystem store diff --git a/lib/pages/main/wallet_contents/send.dart b/lib/pages/main/wallet_contents/send.dart index d54f47f0..36f866dd 100644 --- a/lib/pages/main/wallet_contents/send.dart +++ b/lib/pages/main/wallet_contents/send.dart @@ -21,6 +21,7 @@ import 'package:qubic_wallet/pages/main/wallet_contents/transfers/transactions_f import 'package:qubic_wallet/resources/apis/live/qubic_live_api.dart'; import 'package:qubic_wallet/resources/qubic_cmd.dart'; import 'package:qubic_wallet/stores/application_store.dart'; +import 'package:qubic_wallet/stores/wallet_content_store.dart'; import 'package:qubic_wallet/styles/edge_insets.dart'; import 'package:qubic_wallet/styles/input_decorations.dart'; import 'package:qubic_wallet/styles/text_styles.dart'; @@ -45,8 +46,9 @@ class _SendState extends State { final QubicCmd qubicCmd = getIt(); final TimedController _timedController = getIt(); final GlobalSnackBar _globalSnackBar = getIt(); + final WalletContentStore _walletContentStore = getIt(); String? transferError; - TargetTickTypeEnum targetTickType = defaultTargetTickType; + TargetTickTypeEnum targetTickType = TargetTickTypeEnum.automatic; final NumberFormat formatter = NumberFormat.decimalPatternDigits( locale: 'en_us', @@ -404,7 +406,7 @@ class _SendState extends State { } else { // fetch latest tick int latestTick = (await _liveApi.getCurrentTick()).tick; - targetTick = latestTick + targetTickType.value; + targetTick = latestTick + _walletContentStore.offsetFor(targetTickType); } if (!mounted) return; SignedTransaction? result = await sendTransactionDialog( diff --git a/lib/pages/main/wallet_contents/transfer_asset.dart b/lib/pages/main/wallet_contents/transfer_asset.dart index 2f67e3e0..3c86619e 100644 --- a/lib/pages/main/wallet_contents/transfer_asset.dart +++ b/lib/pages/main/wallet_contents/transfer_asset.dart @@ -24,6 +24,7 @@ import 'package:qubic_wallet/resources/qubic_cmd.dart'; import 'package:qubic_wallet/smart_contracts/qx_info.dart'; import 'package:qubic_wallet/stores/application_store.dart'; import 'package:qubic_wallet/stores/qubic_ecosystem_store.dart'; +import 'package:qubic_wallet/stores/wallet_content_store.dart'; import 'package:qubic_wallet/styles/edge_insets.dart'; import 'package:qubic_wallet/styles/input_decorations.dart'; import 'package:qubic_wallet/styles/text_styles.dart'; @@ -50,8 +51,9 @@ class _TransferAssetState extends State { final GlobalKey<_TransferAssetState> widgetKey = GlobalKey(); final GlobalSnackBar _globalSnackBar = getIt(); final QubicEcosystemStore _ecosystemStore = getIt(); + final WalletContentStore _walletContentStore = getIt(); String? transferError; - TargetTickTypeEnum targetTickType = defaultTargetTickType; + TargetTickTypeEnum targetTickType = TargetTickTypeEnum.automatic; final NumberFormat formatter = NumberFormat.decimalPatternDigits( locale: 'en_us', @@ -494,7 +496,7 @@ class _TransferAssetState extends State { } else { // fetch latest tick int latestTick = (await _liveApi.getCurrentTick()).tick; - targetTick = latestTick + targetTickType.value; + targetTick = latestTick + _walletContentStore.offsetFor(targetTickType); } if (!mounted) return; var result = await sendAssetTransferTransactionDialog( diff --git a/lib/resources/apis/static/qubic_static_api.dart b/lib/resources/apis/static/qubic_static_api.dart index 40e7403e..4d21b6e0 100644 --- a/lib/resources/apis/static/qubic_static_api.dart +++ b/lib/resources/apis/static/qubic_static_api.dart @@ -5,6 +5,7 @@ import 'package:qubic_wallet/models/exchange_model.dart'; import 'package:qubic_wallet/models/labeled_address_model.dart'; import 'package:qubic_wallet/models/protocol_model.dart'; import 'package:qubic_wallet/models/smart_contracts_response.dart'; +import 'package:qubic_wallet/models/wallet_app_config_model.dart'; import 'package:qubic_wallet/services/dio_client.dart'; class QubicStaticApi { @@ -73,4 +74,13 @@ class QubicStaticApi { throw Exception('Failed to fetch protocol data'); } } + + Future getWalletAppConfig() async { + try { + final response = await _dio.get(Config.walletAppConfig); + return WalletAppConfigResponse.fromJson(response.data); + } catch (error) { + throw Exception('Failed to fetch wallet app config'); + } + } } diff --git a/lib/stores/wallet_content_store.dart b/lib/stores/wallet_content_store.dart index 71c20b28..6fad0fc0 100644 --- a/lib/stores/wallet_content_store.dart +++ b/lib/stores/wallet_content_store.dart @@ -7,6 +7,7 @@ import 'package:qubic_wallet/config.dart'; import 'package:qubic_wallet/di.dart'; import 'package:qubic_wallet/dtos/dapp_dto.dart'; import 'package:qubic_wallet/helpers/app_logger.dart'; +import 'package:qubic_wallet/helpers/target_tick.dart'; import 'package:qubic_wallet/l10n/l10n.dart'; import 'package:qubic_wallet/resources/apis/static/qubic_static_api.dart'; @@ -42,6 +43,27 @@ abstract class WalletContentStoreBase with Store { @observable bool isLoading = false; + /// Raw `default_tick_offset` from the wallet-app config — null until loaded or + /// if absent. Deliberately NOT `@observable`: it is read imperatively in screen + /// `initState`, so no MobX reactivity (and no build_runner regen) is required. + int? remoteDefaultTickOffset; + + /// Effective default tick offset for new transactions: the remote value when + /// present and valid (>= 1), otherwise the historical +5 fallback. + int get defaultTickOffset => + (remoteDefaultTickOffset != null && remoteDefaultTickOffset! >= 1) + ? remoteDefaultTickOffset! + : TargetTickTypeEnum.autoCurrentPlus5.value; + + /// Tick offset a dropdown selection resolves to. + /// + /// [TargetTickTypeEnum.automatic] uses the remotely configured + /// [defaultTickOffset] — the same value the WalletConnect path applies — so a + /// single config value produces the same target tick everywhere. Every other + /// preset uses its own fixed value. + int offsetFor(TargetTickTypeEnum type) => + type == TargetTickTypeEnum.automatic ? defaultTickOffset : type.value; + /// Cached app version for version constraint checks Version? _appVersion; @@ -148,4 +170,18 @@ abstract class WalletContentStoreBase with Store { isLoading = false; } } + + /// Loads wallet-app runtime configuration. Not a MobX `@action`: it only + /// writes a non-observable field, so no reactivity or codegen is involved. + /// Failures are swallowed — callers fall back to compiled defaults. + Future loadConfig() async { + try { + final response = await _staticApi.getWalletAppConfig(); + remoteDefaultTickOffset = response.defaultTickOffset; + appLogger.i( + "Successfully loaded wallet app config (default_tick_offset: ${response.defaultTickOffset})"); + } catch (e) { + appLogger.e("Failed to load wallet app config: ${e.toString()}"); + } + } } diff --git a/test/stores/application_store_set_assets_test.dart b/test/stores/application_store_set_assets_test.dart new file mode 100644 index 00000000..de0e9ffa --- /dev/null +++ b/test/stores/application_store_set_assets_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobx/mobx.dart'; +import 'package:qubic_wallet/models/qubic_asset.dart'; +import 'package:qubic_wallet/models/qubic_list_vm.dart'; +import 'package:qubic_wallet/stores/application_store.dart'; + +const accountId = + 'OWNERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; +const issuerId = 'ISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + +QubicAsset buildAsset(String name, int units, {int tick = 100}) { + return QubicAsset( + ownerIdentity: accountId, + managingContractIndex: 1, + numberOfUnits: units, + issuedAsset: IssuedAsset(issuerIdentity: issuerId, name: name), + info: AssetInfo(tick: tick), + ); +} + +ApplicationStore buildStoreWithAssets(List assets) { + final store = ApplicationStore(); + final account = QubicListVm(accountId, 'Test account', 0, 0, null, false); + account.setAssets(assets); + store.currentQubicIDs = ObservableList.of([account]); + return store; +} + +void main() { + group('ApplicationStore.setAssets change detection', () { + test( + 'flags an account whose asset vanished while another remains, ' + 'reporting the vanished asset with zero units', () { + final store = + buildStoreWithAssets([buildAsset('QX', 5), buildAsset('CFB', 3)]); + + // QX was sold entirely; CFB is still held, unchanged. + final changed = store.setAssets([buildAsset('CFB', 3)]); + + expect(changed, contains(accountId)); + expect(changed[accountId], hasLength(1)); + expect(changed[accountId]!.single.issuedAsset.name, 'QX'); + expect(changed[accountId]!.single.numberOfUnits, 0); + + final remaining = store.currentQubicIDs.first.assets.values; + expect(remaining, hasLength(1)); + expect(remaining.single.issuedAsset.name, 'CFB'); + }); + + test( + 'flags a sold-to-zero account, reporting every vanished asset ' + 'with zero units', () { + final store = + buildStoreWithAssets([buildAsset('QX', 5), buildAsset('CFB', 3)]); + + final changed = store.setAssets([]); + + expect(changed, contains(accountId)); + final reportedNames = + changed[accountId]!.map((a) => a.issuedAsset.name).toSet(); + expect(reportedNames, {'QX', 'CFB'}); + expect(changed[accountId]!.every((a) => a.numberOfUnits == 0), isTrue); + expect(store.currentQubicIDs.first.assets, isEmpty); + }); + + test('reports a unit change with the new amount', () { + final store = buildStoreWithAssets([buildAsset('QX', 5)]); + + final changed = store.setAssets([buildAsset('QX', 2)]); + + expect(changed, contains(accountId)); + expect(changed[accountId]!.single.issuedAsset.name, 'QX'); + expect(changed[accountId]!.single.numberOfUnits, 2); + }); + + test('flags nothing when the fetched assets are unchanged', () { + final store = + buildStoreWithAssets([buildAsset('QX', 5), buildAsset('CFB', 3)]); + + final changed = + store.setAssets([buildAsset('QX', 5), buildAsset('CFB', 3)]); + + expect(changed, isEmpty); + }); + + test( + 'dedupes duplicate fetched records by highest tick, so a stale ' + 'duplicate cannot mask a real change', () { + final store = buildStoreWithAssets([buildAsset('QX', 5)]); + + // The aggregation endpoint can return the same position twice; the + // stale record (same units as before) arrives first. + final changed = store.setAssets([ + buildAsset('QX', 5, tick: 90), + buildAsset('QX', 2, tick: 100), + ]); + + expect(changed, contains(accountId)); + expect(changed[accountId]!.single.numberOfUnits, 2); + expect(store.currentQubicIDs.first.assets.values.single.numberOfUnits, 2); + }); + + test('flags nothing for an account that never had assets', () { + final store = buildStoreWithAssets([]); + + final changed = store.setAssets([]); + + expect(changed, isEmpty); + }); + }); +}